@render-harness/cap-memory-pg 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,18 @@
1
+ ## @render-harness/cap-memory-pg
2
+
3
+ Postgres-backed long-term memory for any harness entry:
4
+
5
+ ```yaml
6
+ capabilities:
7
+ - pack: "@render-harness/cap-memory-pg"
8
+ config:
9
+ namespace: my-agent # optional; defaults to the entry name
10
+ ```
11
+
12
+ The pack contributes:
13
+
14
+ - `cap-memory-pg.write({ key, value, tags? })` — durable upsert keyed on `(namespace, key)`.
15
+ - `cap-memory-pg.search({ query, limit?, tags? })` — trigram fuzzy search.
16
+ - A skill (`memory`) telling the agent when to use these tools.
17
+
18
+ The Postgres schema (an `agent_memory` table + `pg_trgm` extension + GIN indexes) is bootstrapped lazily on first call. No env vars required — the pack uses the existing `DATABASE_URL` that every harness service already has.
@@ -0,0 +1,28 @@
1
+ import * as _render_harness_registry from '@render-harness/registry';
2
+
3
+ /**
4
+ * cap-memory-pg — long-term memory backed by Postgres.
5
+ *
6
+ * Adds two LocalToolHandlers an agent can use to write durable notes
7
+ * across runs and search them with fuzzy text matching.
8
+ *
9
+ * - memory.write { key, value, tags? } → stores a note.
10
+ * - memory.search { query, limit?, tags? } → returns ranked matches.
11
+ *
12
+ * The pack uses Postgres `pg_trgm` for fuzzy similarity. We don't pull
13
+ * in pgvector for v1 — keeps the dependency surface tiny and avoids
14
+ * extension issues on Render's managed Postgres.
15
+ *
16
+ * Usage in render-harness.yaml:
17
+ *
18
+ * capabilities:
19
+ * - pack: "@render-harness/cap-memory-pg"
20
+ * config:
21
+ * namespace: "support" # optional; default: entry.name
22
+ *
23
+ * The Postgres table is bootstrapped lazily on first tool call:
24
+ * `CREATE TABLE IF NOT EXISTS` and `CREATE EXTENSION IF NOT EXISTS pg_trgm`.
25
+ */
26
+ declare const pack: _render_harness_registry.CapabilityPack;
27
+
28
+ export { pack as default };
package/dist/index.js ADDED
@@ -0,0 +1,166 @@
1
+ import { dirname, join } from 'path';
2
+ import { fileURLToPath } from 'url';
3
+ import { getPool } from '@render-harness/core';
4
+ import { definePack } from '@render-harness/registry';
5
+
6
+ // src/index.ts
7
+ var HERE = dirname(fileURLToPath(import.meta.url));
8
+ var SKILLS_DIR = join(HERE, "..", "skills");
9
+ var SCHEMA_BOOTSTRAP_SQL = `
10
+ CREATE EXTENSION IF NOT EXISTS pg_trgm;
11
+
12
+ CREATE TABLE IF NOT EXISTS agent_memory (
13
+ id BIGSERIAL PRIMARY KEY,
14
+ namespace TEXT NOT NULL,
15
+ key TEXT NOT NULL,
16
+ value TEXT NOT NULL,
17
+ tags TEXT[] NOT NULL DEFAULT '{}',
18
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
19
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
20
+ CONSTRAINT agent_memory_ns_key UNIQUE (namespace, key)
21
+ );
22
+ CREATE INDEX IF NOT EXISTS agent_memory_ns_idx ON agent_memory (namespace);
23
+ CREATE INDEX IF NOT EXISTS agent_memory_value_trgm
24
+ ON agent_memory USING gin (value gin_trgm_ops);
25
+ CREATE INDEX IF NOT EXISTS agent_memory_tags_idx ON agent_memory USING gin (tags);
26
+ `;
27
+ var bootstrapped = false;
28
+ async function ensureSchema() {
29
+ if (bootstrapped) return;
30
+ const pool = getPool();
31
+ await pool.query(SCHEMA_BOOTSTRAP_SQL);
32
+ bootstrapped = true;
33
+ }
34
+ var pack = definePack({
35
+ name: "cap-memory-pg",
36
+ version: "0.1.0",
37
+ localTools(ctx) {
38
+ const cfg = ctx.config;
39
+ const namespace = cfg.namespace ?? ctx.entryName;
40
+ const writeMemory = {
41
+ definition: {
42
+ name: "write",
43
+ description: "Store a durable note in long-term memory. Notes survive across runs of this agent. Use for facts the user wants you to remember (preferences, names, decisions). The (key) is unique per namespace; calling write twice with the same key updates the value.",
44
+ source: "pack:cap-memory-pg",
45
+ inputSchema: {
46
+ type: "object",
47
+ additionalProperties: false,
48
+ properties: {
49
+ key: {
50
+ type: "string",
51
+ description: "Stable identifier. Reuse the same key to update.",
52
+ minLength: 1,
53
+ maxLength: 256
54
+ },
55
+ value: {
56
+ type: "string",
57
+ description: "The note body.",
58
+ minLength: 1,
59
+ maxLength: 1e4
60
+ },
61
+ tags: {
62
+ type: "array",
63
+ items: { type: "string", minLength: 1, maxLength: 64 },
64
+ description: "Optional category tags for filtering on search.",
65
+ maxItems: 16
66
+ }
67
+ },
68
+ required: ["key", "value"]
69
+ }
70
+ },
71
+ async handler({ input }) {
72
+ const args = input ?? {};
73
+ if (!args.key || !args.value) {
74
+ return { content: "memory.write: key and value are required", isError: true };
75
+ }
76
+ await ensureSchema();
77
+ const pool = getPool();
78
+ const result = await pool.query(
79
+ `INSERT INTO agent_memory (namespace, key, value, tags)
80
+ VALUES ($1, $2, $3, COALESCE($4::text[], '{}'::text[]))
81
+ ON CONFLICT (namespace, key) DO UPDATE SET
82
+ value = EXCLUDED.value,
83
+ tags = EXCLUDED.tags,
84
+ updated_at = now()
85
+ RETURNING id, (xmax::text::int > 0) AS updated`,
86
+ [namespace, args.key, args.value, args.tags ?? null]
87
+ );
88
+ const row = result.rows[0];
89
+ return {
90
+ content: row ? `memory.write: ${row.updated ? "updated" : "created"} id=${row.id} key="${args.key}"` : "memory.write: stored"
91
+ };
92
+ }
93
+ };
94
+ const searchMemory = {
95
+ definition: {
96
+ name: "search",
97
+ description: "Search long-term memory using fuzzy text similarity. Returns the top matches ranked by trigram similarity. Use this before answering when the user asks about something they may have told you before.",
98
+ source: "pack:cap-memory-pg",
99
+ inputSchema: {
100
+ type: "object",
101
+ additionalProperties: false,
102
+ properties: {
103
+ query: { type: "string", description: "Free-form search text.", minLength: 1 },
104
+ limit: {
105
+ type: "integer",
106
+ description: "Max rows to return. Default 10, max 50.",
107
+ minimum: 1,
108
+ maximum: 50
109
+ },
110
+ tags: {
111
+ type: "array",
112
+ items: { type: "string" },
113
+ description: "Optional tag filter; row must have at least one of these."
114
+ }
115
+ },
116
+ required: ["query"]
117
+ }
118
+ },
119
+ async handler({ input }) {
120
+ const args = input ?? {};
121
+ if (!args.query) {
122
+ return { content: "memory.search: query is required", isError: true };
123
+ }
124
+ await ensureSchema();
125
+ const pool = getPool();
126
+ const limit = Math.min(args.limit ?? 10, 50);
127
+ const rows = await pool.query(
128
+ `SELECT key, value, tags, similarity(value, $2) AS score, updated_at
129
+ FROM agent_memory
130
+ WHERE namespace = $1
131
+ AND ($3::text[] IS NULL OR tags && $3::text[])
132
+ AND value ILIKE '%' || $2 || '%' OR similarity(value, $2) > 0.1
133
+ ORDER BY score DESC, updated_at DESC
134
+ LIMIT $4`,
135
+ [namespace, args.query, args.tags ?? null, limit]
136
+ );
137
+ if (rows.rows.length === 0) {
138
+ return {
139
+ content: `memory.search: no matches for "${args.query}" in namespace "${namespace}"`
140
+ };
141
+ }
142
+ const lines = rows.rows.map(
143
+ (r, i) => `${i + 1}. [${r.key}] (score=${r.score.toFixed(2)}, tags=${r.tags.join(",") || "\u2014"})
144
+ ${r.value.slice(0, 600)}`
145
+ );
146
+ return { content: lines.join("\n\n") };
147
+ }
148
+ };
149
+ return [writeMemory, searchMemory];
150
+ },
151
+ skills(_ctx) {
152
+ return [
153
+ {
154
+ name: "memory",
155
+ description: "Use long-term memory to remember facts across runs.",
156
+ whenToUse: "When the user asks you to remember something, OR when they ask about something they may have told you in a prior run.",
157
+ contentPath: join(SKILLS_DIR, "memory.md")
158
+ }
159
+ ];
160
+ }
161
+ });
162
+ var src_default = pack;
163
+
164
+ export { src_default as default };
165
+ //# sourceMappingURL=index.js.map
166
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * cap-memory-pg — long-term memory backed by Postgres.\n *\n * Adds two LocalToolHandlers an agent can use to write durable notes\n * across runs and search them with fuzzy text matching.\n *\n * - memory.write { key, value, tags? } → stores a note.\n * - memory.search { query, limit?, tags? } → returns ranked matches.\n *\n * The pack uses Postgres `pg_trgm` for fuzzy similarity. We don't pull\n * in pgvector for v1 — keeps the dependency surface tiny and avoids\n * extension issues on Render's managed Postgres.\n *\n * Usage in render-harness.yaml:\n *\n * capabilities:\n * - pack: \"@render-harness/cap-memory-pg\"\n * config:\n * namespace: \"support\" # optional; default: entry.name\n *\n * The Postgres table is bootstrapped lazily on first tool call:\n * `CREATE TABLE IF NOT EXISTS` and `CREATE EXTENSION IF NOT EXISTS pg_trgm`.\n */\n\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { getPool, type LocalToolHandler, type SkillMetadata } from \"@render-harness/core\";\nimport { definePack, type PackContext } from \"@render-harness/registry\";\n\nconst HERE = dirname(fileURLToPath(import.meta.url));\nconst SKILLS_DIR = join(HERE, \"..\", \"skills\");\n\ninterface MemoryConfig {\n namespace?: string;\n}\n\nconst SCHEMA_BOOTSTRAP_SQL = `\nCREATE EXTENSION IF NOT EXISTS pg_trgm;\n\nCREATE TABLE IF NOT EXISTS agent_memory (\n id BIGSERIAL PRIMARY KEY,\n namespace TEXT NOT NULL,\n key TEXT NOT NULL,\n value TEXT NOT NULL,\n tags TEXT[] NOT NULL DEFAULT '{}',\n created_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),\n CONSTRAINT agent_memory_ns_key UNIQUE (namespace, key)\n);\nCREATE INDEX IF NOT EXISTS agent_memory_ns_idx ON agent_memory (namespace);\nCREATE INDEX IF NOT EXISTS agent_memory_value_trgm\n ON agent_memory USING gin (value gin_trgm_ops);\nCREATE INDEX IF NOT EXISTS agent_memory_tags_idx ON agent_memory USING gin (tags);\n`;\n\nlet bootstrapped = false;\nasync function ensureSchema(): Promise<void> {\n if (bootstrapped) return;\n const pool = getPool();\n await pool.query(SCHEMA_BOOTSTRAP_SQL);\n bootstrapped = true;\n}\n\nconst pack = definePack({\n name: \"cap-memory-pg\",\n version: \"0.1.0\",\n localTools(ctx: PackContext): LocalToolHandler[] {\n const cfg = ctx.config as MemoryConfig;\n const namespace = cfg.namespace ?? ctx.entryName;\n\n const writeMemory: LocalToolHandler = {\n definition: {\n name: \"write\",\n description:\n \"Store a durable note in long-term memory. Notes survive across runs of this agent. Use for facts the user wants you to remember (preferences, names, decisions). The (key) is unique per namespace; calling write twice with the same key updates the value.\",\n source: \"pack:cap-memory-pg\",\n inputSchema: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n key: {\n type: \"string\",\n description: \"Stable identifier. Reuse the same key to update.\",\n minLength: 1,\n maxLength: 256,\n },\n value: {\n type: \"string\",\n description: \"The note body.\",\n minLength: 1,\n maxLength: 10_000,\n },\n tags: {\n type: \"array\",\n items: { type: \"string\", minLength: 1, maxLength: 64 },\n description: \"Optional category tags for filtering on search.\",\n maxItems: 16,\n },\n },\n required: [\"key\", \"value\"],\n },\n },\n async handler({ input }) {\n const args = (input ?? {}) as { key?: string; value?: string; tags?: string[] };\n if (!args.key || !args.value) {\n return { content: \"memory.write: key and value are required\", isError: true };\n }\n await ensureSchema();\n const pool = getPool();\n const result = await pool.query<{ id: string; updated: boolean }>(\n `INSERT INTO agent_memory (namespace, key, value, tags)\n VALUES ($1, $2, $3, COALESCE($4::text[], '{}'::text[]))\n ON CONFLICT (namespace, key) DO UPDATE SET\n value = EXCLUDED.value,\n tags = EXCLUDED.tags,\n updated_at = now()\n RETURNING id, (xmax::text::int > 0) AS updated`,\n [namespace, args.key, args.value, args.tags ?? null],\n );\n const row = result.rows[0];\n return {\n content: row\n ? `memory.write: ${row.updated ? \"updated\" : \"created\"} id=${row.id} key=\"${args.key}\"`\n : \"memory.write: stored\",\n };\n },\n };\n\n const searchMemory: LocalToolHandler = {\n definition: {\n name: \"search\",\n description:\n \"Search long-term memory using fuzzy text similarity. Returns the top matches ranked by trigram similarity. Use this before answering when the user asks about something they may have told you before.\",\n source: \"pack:cap-memory-pg\",\n inputSchema: {\n type: \"object\",\n additionalProperties: false,\n properties: {\n query: { type: \"string\", description: \"Free-form search text.\", minLength: 1 },\n limit: {\n type: \"integer\",\n description: \"Max rows to return. Default 10, max 50.\",\n minimum: 1,\n maximum: 50,\n },\n tags: {\n type: \"array\",\n items: { type: \"string\" },\n description: \"Optional tag filter; row must have at least one of these.\",\n },\n },\n required: [\"query\"],\n },\n },\n async handler({ input }) {\n const args = (input ?? {}) as { query?: string; limit?: number; tags?: string[] };\n if (!args.query) {\n return { content: \"memory.search: query is required\", isError: true };\n }\n await ensureSchema();\n const pool = getPool();\n const limit = Math.min(args.limit ?? 10, 50);\n const rows = await pool.query<{\n key: string;\n value: string;\n tags: string[];\n score: number;\n updated_at: Date;\n }>(\n `SELECT key, value, tags, similarity(value, $2) AS score, updated_at\n FROM agent_memory\n WHERE namespace = $1\n AND ($3::text[] IS NULL OR tags && $3::text[])\n AND value ILIKE '%' || $2 || '%' OR similarity(value, $2) > 0.1\n ORDER BY score DESC, updated_at DESC\n LIMIT $4`,\n [namespace, args.query, args.tags ?? null, limit],\n );\n if (rows.rows.length === 0) {\n return {\n content: `memory.search: no matches for \"${args.query}\" in namespace \"${namespace}\"`,\n };\n }\n const lines = rows.rows.map(\n (r, i) =>\n `${i + 1}. [${r.key}] (score=${r.score.toFixed(2)}, tags=${r.tags.join(\",\") || \"—\"})\\n ${r.value.slice(0, 600)}`,\n );\n return { content: lines.join(\"\\n\\n\") };\n },\n };\n\n return [writeMemory, searchMemory];\n },\n skills(_ctx: PackContext): SkillMetadata[] {\n return [\n {\n name: \"memory\",\n description: \"Use long-term memory to remember facts across runs.\",\n whenToUse:\n \"When the user asks you to remember something, OR when they ask about something they may have told you in a prior run.\",\n contentPath: join(SKILLS_DIR, \"memory.md\"),\n },\n ];\n },\n});\n\nexport default pack;\n"]}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@render-harness/cap-memory-pg",
3
+ "version": "0.1.1",
4
+ "description": "Postgres-backed long-term memory for the Render agent harness.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist",
17
+ "skills"
18
+ ],
19
+ "keywords": [
20
+ "render-harness-cap",
21
+ "render-harness",
22
+ "memory",
23
+ "postgres"
24
+ ],
25
+ "renderHarness": {
26
+ "gallery": {
27
+ "label": "Postgres long-term memory",
28
+ "envHint": "DATABASE_URL"
29
+ }
30
+ },
31
+ "scripts": {
32
+ "build": "tsup",
33
+ "typecheck": "tsc --noEmit",
34
+ "test": "vitest run --passWithNoTests"
35
+ },
36
+ "dependencies": {
37
+ "@render-harness/core": "workspace:*",
38
+ "@render-harness/registry": "workspace:*"
39
+ },
40
+ "devDependencies": {
41
+ "@types/node": "^25.6.2",
42
+ "tsup": "^8.5.1",
43
+ "typescript": "^6.0.3",
44
+ "vitest": "^4.1.5"
45
+ }
46
+ }
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: memory
3
+ description: Use long-term memory to remember facts across runs.
4
+ when_to_use: When the user asks you to remember something, OR when they ask about something they may have told you in a prior run.
5
+ ---
6
+
7
+ # Long-term memory
8
+
9
+ Two tools store and retrieve durable notes:
10
+
11
+ - `cap-memory-pg.write({ key, value, tags? })` — store a note. The `(namespace, key)` pair is unique; reusing a key updates the row.
12
+ - `cap-memory-pg.search({ query, limit?, tags? })` — fuzzy search by trigram similarity. Returns the top matches.
13
+
14
+ The namespace defaults to the entry name; multiple agents in the same Postgres won't collide.
15
+
16
+ ## Workflow
17
+
18
+ 1. Before answering, search memory for terms in the user's question.
19
+ 2. If the user gives you a fact to remember (preferences, deadlines, names), write it with a stable, descriptive key.
20
+ 3. Tags are optional but help filter later; suggested tags: `pref`, `decision`, `fact`, `plan`.
21
+
22
+ ## Hard rules
23
+
24
+ - Never silently overwrite. If a search hit shows the user previously stated something and they're now contradicting it, surface the conflict in your reply before writing the new value.
25
+ - Keys must be stable across runs. Don't include timestamps or run ids.
26
+ - Don't store secrets, API keys, or any sensitive personal data.