@kaleidorg/mind 0.1.0 → 0.3.0

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.
Files changed (182) hide show
  1. package/dist/capabilities.d.ts +38 -0
  2. package/dist/capabilities.d.ts.map +1 -0
  3. package/dist/capabilities.js +41 -0
  4. package/dist/capabilities.js.map +1 -0
  5. package/dist/context/budget.d.ts +29 -0
  6. package/dist/context/budget.d.ts.map +1 -0
  7. package/dist/context/budget.js +36 -0
  8. package/dist/context/budget.js.map +1 -0
  9. package/dist/context/builder.d.ts +39 -0
  10. package/dist/context/builder.d.ts.map +1 -0
  11. package/dist/context/builder.js +77 -0
  12. package/dist/context/builder.js.map +1 -0
  13. package/dist/engine.d.ts +9 -0
  14. package/dist/engine.d.ts.map +1 -1
  15. package/dist/engine.js +1 -0
  16. package/dist/engine.js.map +1 -1
  17. package/dist/fastpath/fastpath.d.ts +38 -0
  18. package/dist/fastpath/fastpath.d.ts.map +1 -0
  19. package/dist/fastpath/fastpath.js +52 -0
  20. package/dist/fastpath/fastpath.js.map +1 -0
  21. package/dist/funnel.d.ts +117 -0
  22. package/dist/funnel.d.ts.map +1 -0
  23. package/dist/funnel.js +195 -0
  24. package/dist/funnel.js.map +1 -0
  25. package/dist/index.d.ts +44 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +36 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/kaleidoswap/contract.d.ts +72 -0
  30. package/dist/kaleidoswap/contract.d.ts.map +1 -0
  31. package/dist/kaleidoswap/contract.js +125 -0
  32. package/dist/kaleidoswap/contract.js.map +1 -0
  33. package/dist/knowledge/bitcoin-copilot.d.ts +11 -0
  34. package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -0
  35. package/dist/knowledge/bitcoin-copilot.js +155 -0
  36. package/dist/knowledge/bitcoin-copilot.js.map +1 -0
  37. package/dist/knowledge/btc-map.d.ts +87 -0
  38. package/dist/knowledge/btc-map.d.ts.map +1 -0
  39. package/dist/knowledge/btc-map.js +365 -0
  40. package/dist/knowledge/btc-map.js.map +1 -0
  41. package/dist/knowledge/merchants.d.ts +24 -0
  42. package/dist/knowledge/merchants.d.ts.map +1 -0
  43. package/dist/knowledge/merchants.js +34 -0
  44. package/dist/knowledge/merchants.js.map +1 -0
  45. package/dist/knowledge/wallet.d.ts +34 -0
  46. package/dist/knowledge/wallet.d.ts.map +1 -0
  47. package/dist/knowledge/wallet.js +63 -0
  48. package/dist/knowledge/wallet.js.map +1 -0
  49. package/dist/lsps1/contract.d.ts +55 -0
  50. package/dist/lsps1/contract.d.ts.map +1 -0
  51. package/dist/lsps1/contract.js +91 -0
  52. package/dist/lsps1/contract.js.map +1 -0
  53. package/dist/memory/store.d.ts +40 -0
  54. package/dist/memory/store.d.ts.map +1 -0
  55. package/dist/memory/store.js +143 -0
  56. package/dist/memory/store.js.map +1 -0
  57. package/dist/memory/tool.d.ts +9 -0
  58. package/dist/memory/tool.d.ts.map +1 -0
  59. package/dist/memory/tool.js +70 -0
  60. package/dist/memory/tool.js.map +1 -0
  61. package/dist/memory/types.d.ts +68 -0
  62. package/dist/memory/types.d.ts.map +1 -0
  63. package/dist/memory/types.js +14 -0
  64. package/dist/memory/types.js.map +1 -0
  65. package/dist/rag/retriever.d.ts +30 -0
  66. package/dist/rag/retriever.d.ts.map +1 -0
  67. package/dist/rag/retriever.js +72 -0
  68. package/dist/rag/retriever.js.map +1 -0
  69. package/dist/rag/tool.d.ts +15 -0
  70. package/dist/rag/tool.d.ts.map +1 -0
  71. package/dist/rag/tool.js +42 -0
  72. package/dist/rag/tool.js.map +1 -0
  73. package/dist/rag/types.d.ts +44 -0
  74. package/dist/rag/types.d.ts.map +1 -0
  75. package/dist/rag/types.js +11 -0
  76. package/dist/rag/types.js.map +1 -0
  77. package/dist/rag/vector-store.d.ts +23 -0
  78. package/dist/rag/vector-store.d.ts.map +1 -0
  79. package/dist/rag/vector-store.js +72 -0
  80. package/dist/rag/vector-store.js.map +1 -0
  81. package/dist/recipe/asset-send.d.ts +15 -0
  82. package/dist/recipe/asset-send.d.ts.map +1 -0
  83. package/dist/recipe/asset-send.js +83 -0
  84. package/dist/recipe/asset-send.js.map +1 -0
  85. package/dist/recipe/kaleidoswap-atomic.d.ts +27 -0
  86. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -0
  87. package/dist/recipe/kaleidoswap-atomic.js +111 -0
  88. package/dist/recipe/kaleidoswap-atomic.js.map +1 -0
  89. package/dist/recipe/payments.d.ts +15 -0
  90. package/dist/recipe/payments.d.ts.map +1 -0
  91. package/dist/recipe/payments.js +119 -0
  92. package/dist/recipe/payments.js.map +1 -0
  93. package/dist/recipe/receive.d.ts +14 -0
  94. package/dist/recipe/receive.d.ts.map +1 -0
  95. package/dist/recipe/receive.js +109 -0
  96. package/dist/recipe/receive.js.map +1 -0
  97. package/dist/recipe/runner.d.ts +42 -0
  98. package/dist/recipe/runner.d.ts.map +1 -0
  99. package/dist/recipe/runner.js +106 -0
  100. package/dist/recipe/runner.js.map +1 -0
  101. package/dist/recipe/swap.d.ts +16 -0
  102. package/dist/recipe/swap.d.ts.map +1 -0
  103. package/dist/recipe/swap.js +73 -0
  104. package/dist/recipe/swap.js.map +1 -0
  105. package/dist/recipe/types.d.ts +71 -0
  106. package/dist/recipe/types.d.ts.map +1 -0
  107. package/dist/recipe/types.js +13 -0
  108. package/dist/recipe/types.js.map +1 -0
  109. package/dist/skills/registry.d.ts.map +1 -1
  110. package/dist/skills/registry.js +20 -2
  111. package/dist/skills/registry.js.map +1 -1
  112. package/dist/tools/cli.d.ts +43 -0
  113. package/dist/tools/cli.d.ts.map +1 -0
  114. package/dist/tools/cli.js +61 -0
  115. package/dist/tools/cli.js.map +1 -0
  116. package/dist/tools/mcp.d.ts +3 -2
  117. package/dist/tools/mcp.d.ts.map +1 -1
  118. package/dist/tools/mcp.js +3 -2
  119. package/dist/tools/mcp.js.map +1 -1
  120. package/dist/wallet/confirm.d.ts +12 -0
  121. package/dist/wallet/confirm.d.ts.map +1 -0
  122. package/dist/wallet/confirm.js +67 -0
  123. package/dist/wallet/confirm.js.map +1 -0
  124. package/dist/wallet/contract.d.ts +57 -0
  125. package/dist/wallet/contract.d.ts.map +1 -0
  126. package/dist/wallet/contract.js +113 -0
  127. package/dist/wallet/contract.js.map +1 -0
  128. package/package.json +10 -5
  129. package/skills/README.md +6 -1
  130. package/skills/kaleido-lsps/SKILL.md +56 -0
  131. package/skills/kaleido-trading/SKILL.md +85 -18
  132. package/skills/merchant-finder/SKILL.md +87 -0
  133. package/skills/paid-data/SKILL.md +12 -0
  134. package/skills/wallet-assistant/SKILL.md +38 -0
  135. package/src/capabilities.ts +79 -0
  136. package/src/context/budget.ts +46 -0
  137. package/src/context/builder.ts +100 -0
  138. package/src/context/context.test.ts +87 -0
  139. package/src/engine.ts +6 -0
  140. package/src/fastpath/fastpath.test.ts +34 -0
  141. package/src/fastpath/fastpath.ts +70 -0
  142. package/src/funnel.test.ts +207 -0
  143. package/src/funnel.ts +285 -0
  144. package/src/index.ts +128 -0
  145. package/src/kaleidoswap/contract.test.ts +147 -0
  146. package/src/kaleidoswap/contract.ts +212 -0
  147. package/src/knowledge/bitcoin-copilot.ts +177 -0
  148. package/src/knowledge/btc-map.test.ts +188 -0
  149. package/src/knowledge/btc-map.ts +446 -0
  150. package/src/knowledge/knowledge.test.ts +63 -0
  151. package/src/knowledge/merchants.ts +49 -0
  152. package/src/knowledge/wallet.ts +84 -0
  153. package/src/lsps1/contract.test.ts +81 -0
  154. package/src/lsps1/contract.ts +132 -0
  155. package/src/memory/memory.test.ts +140 -0
  156. package/src/memory/store.ts +174 -0
  157. package/src/memory/tool.ts +76 -0
  158. package/src/memory/types.ts +76 -0
  159. package/src/rag/rag.test.ts +85 -0
  160. package/src/rag/retriever.ts +94 -0
  161. package/src/rag/tool.ts +55 -0
  162. package/src/rag/types.ts +49 -0
  163. package/src/rag/vector-store.ts +78 -0
  164. package/src/recipe/asset-send.ts +79 -0
  165. package/src/recipe/kaleidoswap-atomic.test.ts +138 -0
  166. package/src/recipe/kaleidoswap-atomic.ts +117 -0
  167. package/src/recipe/payments.ts +116 -0
  168. package/src/recipe/receive.ts +98 -0
  169. package/src/recipe/recipe.test.ts +193 -0
  170. package/src/recipe/runner.ts +134 -0
  171. package/src/recipe/swap.ts +74 -0
  172. package/src/recipe/types.ts +76 -0
  173. package/src/skills/registry.ts +21 -2
  174. package/src/skills/skills.test.ts +42 -0
  175. package/src/tools/cli.test.ts +53 -0
  176. package/src/tools/cli.ts +98 -0
  177. package/src/tools/mcp.ts +3 -2
  178. package/src/wallet/confirm.test.ts +57 -0
  179. package/src/wallet/confirm.ts +74 -0
  180. package/src/wallet/contract.test.ts +89 -0
  181. package/src/wallet/contract.ts +157 -0
  182. package/skills/kaleido-wallet/SKILL.md +0 -28
@@ -0,0 +1,81 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ LSPS1_TOOLS,
4
+ LSPS1_SPEND_TOOLS,
5
+ isLsps1SpendTool,
6
+ getLsps1Tool,
7
+ bindLsps1Tools,
8
+ type Lsps1Handler,
9
+ } from './contract.js';
10
+
11
+ describe('LSPS1_TOOLS — shape invariants', () => {
12
+ it('exposes the expected tool names', () => {
13
+ expect(LSPS1_TOOLS.map((t) => t.name)).toEqual([
14
+ 'lsp_get_info',
15
+ 'lsp_get_network_info',
16
+ 'lsp_estimate_fees',
17
+ 'lsp_create_order',
18
+ 'lsp_get_order',
19
+ ]);
20
+ });
21
+
22
+ it('every tool has an object parameters schema', () => {
23
+ for (const t of LSPS1_TOOLS) {
24
+ expect((t.parameters as any)?.type).toBe('object');
25
+ }
26
+ });
27
+
28
+ it('aligns spend ↔ requiresConfirmation', () => {
29
+ for (const t of LSPS1_TOOLS) {
30
+ expect(!!t.spend).toBe(!!t.requiresConfirmation);
31
+ }
32
+ });
33
+
34
+ it('marks only lsp_create_order as spend', () => {
35
+ expect([...LSPS1_SPEND_TOOLS]).toEqual(['lsp_create_order']);
36
+ expect(isLsps1SpendTool('lsp_create_order')).toBe(true);
37
+ expect(isLsps1SpendTool('lsp_get_info')).toBe(false);
38
+ });
39
+
40
+ it('getLsps1Tool returns by name', () => {
41
+ expect(getLsps1Tool('lsp_estimate_fees')?.name).toBe('lsp_estimate_fees');
42
+ expect(getLsps1Tool('nope')).toBeUndefined();
43
+ });
44
+ });
45
+
46
+ describe('bindLsps1Tools', () => {
47
+ const echoHandlers = (): Record<string, Lsps1Handler> => ({
48
+ lsp_get_info: async () => ({ ok: true, t: 'get_info' }),
49
+ lsp_get_network_info: async () => ({ ok: true, t: 'get_network_info' }),
50
+ lsp_estimate_fees: async (a) => ({ ok: true, t: 'estimate_fees', args: a }),
51
+ lsp_create_order: async (a) => ({ ok: true, t: 'create_order', args: a }),
52
+ lsp_get_order: async (a) => ({ ok: true, t: 'get_order', args: a }),
53
+ });
54
+
55
+ it('binds every tool and preserves the spend gate', () => {
56
+ const src = bindLsps1Tools(echoHandlers());
57
+ expect(src.listTools().length).toBe(5);
58
+ const create = src.listTools().find((t) => t.name === 'lsp_create_order');
59
+ expect(create?.requiresConfirmation).toBe(true);
60
+ const info = src.listTools().find((t) => t.name === 'lsp_get_info');
61
+ expect(info?.requiresConfirmation).toBeFalsy();
62
+ });
63
+
64
+ it('dispatches with args', async () => {
65
+ const src = bindLsps1Tools(echoHandlers());
66
+ const r = await src.execute('lsp_estimate_fees', { lsp_balance_sat: 500_000 });
67
+ expect(r).toMatchObject({ ok: true, t: 'estimate_fees', args: { lsp_balance_sat: 500_000 } });
68
+ });
69
+
70
+ it('throws on a missing handler unless allowMissing', () => {
71
+ const partial = { lsp_get_info: echoHandlers().lsp_get_info };
72
+ expect(() => bindLsps1Tools(partial)).toThrow(/no handler/);
73
+ const src = bindLsps1Tools(partial, { allowMissing: true });
74
+ expect(src.listTools().map((t) => t.name)).toEqual(['lsp_get_info']);
75
+ });
76
+
77
+ it('uses opts.id for the ToolSource id', () => {
78
+ const src = bindLsps1Tools(echoHandlers(), { id: 'lsp-prod' });
79
+ expect(src.id).toBe('lsp-prod');
80
+ });
81
+ });
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Canonical LSPS1 tool contract — Lightning Service Provider channel orders.
3
+ *
4
+ * LSPS1 is a transport-agnostic protocol for buying inbound channel liquidity
5
+ * from a Lightning Service Provider. The maker happens to implement it, but a
6
+ * different LSP could too — so the tool names are LSP-agnostic (`lsp_*`),
7
+ * not `kaleidoswap_*`. The host's binder decides which LSP they reach.
8
+ *
9
+ * Every surface implements THESE EXACT tools, only the transport differs:
10
+ * - mobile → in-process handlers over the WDK LSP adapter
11
+ * - desktop → HTTP / MCP / CLI handlers
12
+ * - eval → stub handlers
13
+ *
14
+ * `lsp_create_order` is a spend → confirmation-gated.
15
+ *
16
+ * Pure data — no deps, no fetch, RN-safe.
17
+ */
18
+
19
+ import type { ToolDef } from '../types.js';
20
+ import { InProcessToolSource } from '../tools/in-process.js';
21
+ import type { InProcessTool } from '../tools/in-process.js';
22
+
23
+ export interface Lsps1ToolDef extends ToolDef {
24
+ /** Moves funds → confirmation-gated. */
25
+ spend?: boolean;
26
+ }
27
+
28
+ type Props = Record<string, { type: string; description?: string; enum?: string[] }>;
29
+
30
+ function t(name: string, description: string, properties: Props = {}, required: string[] = [], spend = false): Lsps1ToolDef {
31
+ return {
32
+ name,
33
+ description,
34
+ spend,
35
+ requiresConfirmation: spend,
36
+ parameters: { type: 'object', properties, required },
37
+ };
38
+ }
39
+
40
+ /**
41
+ * The canonical LSPS1 tool list — agent-facing schemas. Each host's binder
42
+ * translates these args into the LSP's request body (LSPS1 JSON-RPC, the
43
+ * KaleidoSwap maker's REST routes, MCP, or a WDK adapter call).
44
+ */
45
+ export const LSPS1_TOOLS: Lsps1ToolDef[] = [
46
+ t('lsp_get_info',
47
+ "Get the LSP's capabilities: minimum/maximum channel size, supported expiries, fee structure, accepted payment options. Use this before estimating or ordering a channel. No args."),
48
+
49
+ t('lsp_get_network_info',
50
+ "Get the LSP's Lightning network info: pubkey, host, port, connect URI. Useful to display the counterparty or pre-connect a peer. No args."),
51
+
52
+ t('lsp_estimate_fees',
53
+ "Estimate the fee for a channel order BEFORE committing. Returns the total cost in sats plus any LSP routing fee. Re-estimate rather than reusing a stale value.",
54
+ {
55
+ lsp_balance_sat: { type: 'number', description: "Sats the LSP commits on their side (inbound capacity for the user)." },
56
+ client_balance_sat: { type: 'number', description: "Sats the user pre-funds into the channel (push amount). Often 0." },
57
+ channel_expiry_blocks: { type: 'number', description: 'Optional minimum lease in blocks. Defaults to the LSP minimum.' },
58
+ },
59
+ ['lsp_balance_sat']),
60
+
61
+ t('lsp_create_order',
62
+ "Create a channel order. SPEND: confirmation-gated. Returns an order id + a Lightning invoice the user pays to lock the order. The channel opens only after payment.",
63
+ {
64
+ lsp_balance_sat: { type: 'number', description: "Sats the LSP commits on their side (inbound capacity for the user)." },
65
+ client_balance_sat: { type: 'number', description: 'Sats the user pre-funds. Often 0.' },
66
+ channel_expiry_blocks: { type: 'number', description: 'Minimum lease in blocks. Defaults to LSP minimum from lsp_get_info.' },
67
+ refund_onchain_address: { type: 'string', description: 'Optional on-chain refund address if the LSP cannot open the channel.' },
68
+ },
69
+ ['lsp_balance_sat'],
70
+ /* spend */ true),
71
+
72
+ t('lsp_get_order',
73
+ 'Check the status of an LSPS1 order — pending / paid / opening / completed / failed. Poll after creating an order until the channel opens.',
74
+ {
75
+ order_id: { type: 'string', description: 'The order id from lsp_create_order.' },
76
+ },
77
+ ['order_id']),
78
+ ];
79
+
80
+ /** All LSPS1 tool names that move funds (confirmation-gated). */
81
+ export const LSPS1_SPEND_TOOLS: Set<string> = new Set(
82
+ LSPS1_TOOLS.filter((t) => t.spend).map((t) => t.name),
83
+ );
84
+
85
+ export function isLsps1SpendTool(name: string): boolean {
86
+ return LSPS1_SPEND_TOOLS.has(name);
87
+ }
88
+
89
+ export function getLsps1Tool(name: string): Lsps1ToolDef | undefined {
90
+ return LSPS1_TOOLS.find((t) => t.name === name);
91
+ }
92
+
93
+ /** A handler bound to one LSPS1 tool. */
94
+ export type Lsps1Handler = (args: Record<string, unknown>) => Promise<unknown>;
95
+
96
+ export interface BindLsps1Options {
97
+ /** Skip tools without a handler instead of throwing (default false). */
98
+ allowMissing?: boolean;
99
+ /** ToolSource id for the registry (default 'lsps1'). */
100
+ id?: string;
101
+ }
102
+
103
+ /**
104
+ * Bind LSPS1 contract tools to in-process handlers → an InProcessToolSource.
105
+ *
106
+ * const source = bindLsps1Tools({
107
+ * lsp_get_info: async () => makerLsp.getInfo(),
108
+ * lsp_estimate_fees: async (args) => makerLsp.estimateFees(args),
109
+ * lsp_create_order: async (args) => makerLsp.createOrder(args),
110
+ * lsp_get_order: async ({ order_id }) => makerLsp.getOrder(order_id),
111
+ * lsp_get_network_info:async () => makerLsp.networkInfo(),
112
+ * });
113
+ * tools.register(source);
114
+ */
115
+ export function bindLsps1Tools(handlers: Record<string, Lsps1Handler>, opts: BindLsps1Options = {}): InProcessToolSource {
116
+ const bound: InProcessTool[] = [];
117
+ for (const def of LSPS1_TOOLS) {
118
+ const handler = handlers[def.name];
119
+ if (!handler) {
120
+ if (opts.allowMissing) continue;
121
+ throw new Error(`bindLsps1Tools: no handler for "${def.name}"`);
122
+ }
123
+ bound.push({
124
+ name: def.name,
125
+ description: def.description,
126
+ parameters: def.parameters,
127
+ requiresConfirmation: def.requiresConfirmation,
128
+ handler,
129
+ });
130
+ }
131
+ return new InProcessToolSource(opts.id ?? 'lsps1', bound);
132
+ }
@@ -0,0 +1,140 @@
1
+ /** Memory store + tool tests — deterministic, no embeddings needed. */
2
+
3
+ import { describe, it, expect, vi } from 'vitest';
4
+ import { InMemoryMemoryStore } from './store.js';
5
+ import { createMemoryToolSource } from './tool.js';
6
+ import type { MemoryItem, MemoryIO } from './types.js';
7
+
8
+ describe('InMemoryMemoryStore', () => {
9
+ it('adds and recalls by recency when no query text', async () => {
10
+ let t = 1000;
11
+ const store = new InMemoryMemoryStore({ now: () => t++ });
12
+ await store.add({ text: 'first', kind: 'note' });
13
+ await store.add({ text: 'second', kind: 'note' });
14
+ const all = await store.search({ limit: 5 });
15
+ expect(all.map((m) => m.text)).toEqual(['second', 'first']); // newest first
16
+ });
17
+
18
+ it('filters by kind + tags, ranks substring hits first', async () => {
19
+ let t = 0;
20
+ const store = new InMemoryMemoryStore({ now: () => ++t });
21
+ await store.add({ text: 'likes cold brew', kind: 'preference', tags: ['coffee'] });
22
+ await store.add({ text: 'paid rent', kind: 'event' });
23
+ await store.add({ text: 'prefers dark mode', kind: 'preference', tags: ['ui'] });
24
+
25
+ const prefs = await store.search({ kind: 'preference', limit: 5 });
26
+ expect(prefs).toHaveLength(2);
27
+
28
+ const coffee = await store.search({ tags: ['coffee'], limit: 5 });
29
+ expect(coffee.map((m) => m.text)).toEqual(['likes cold brew']);
30
+
31
+ const hit = await store.search({ text: 'dark', limit: 5 });
32
+ expect(hit[0].text).toBe('prefers dark mode');
33
+ });
34
+
35
+ it('persists through injected IO (load + save)', async () => {
36
+ const saved: MemoryItem[] = [];
37
+ const io: MemoryIO = {
38
+ load: vi.fn(async () => [...saved]),
39
+ save: vi.fn(async (items) => {
40
+ saved.length = 0;
41
+ saved.push(...items);
42
+ }),
43
+ };
44
+ const store = new InMemoryMemoryStore({ io, now: () => 1 });
45
+ await store.add({ text: 'remember me', kind: 'fact' });
46
+ expect(io.save).toHaveBeenCalled();
47
+ expect(saved).toHaveLength(1);
48
+
49
+ // A fresh store hydrates from the same IO.
50
+ const store2 = new InMemoryMemoryStore({ io, now: () => 2 });
51
+ expect((await store2.all())[0].text).toBe('remember me');
52
+ });
53
+
54
+ it('semantic recall when an embedder is wired', async () => {
55
+ // 2-dim embeddings: dimension 0 = "wallet"-ness, 1 = "weather"-ness.
56
+ const embed = async (text: string): Promise<number[]> =>
57
+ /balance|wallet|sats/i.test(text) ? [1, 0] : [0, 1];
58
+ const store = new InMemoryMemoryStore({ embed, now: () => 1 });
59
+ await store.add({ text: 'user wallet balance is low', kind: 'fact' });
60
+ await store.add({ text: 'it is sunny today', kind: 'note' });
61
+ const hits = await store.search({ text: 'how many sats do I have', limit: 1 });
62
+ expect(hits[0].text).toMatch(/wallet balance/);
63
+ });
64
+
65
+ // Embedding-only dedup: same vector → near-dup → newer supersedes older. No LLM.
66
+ it('consolidate (dedup): near-duplicates supersede instead of appending', async () => {
67
+ const embed = async (text: string): Promise<number[]> =>
68
+ /eur/i.test(text) ? [1, 0] : [0, 1];
69
+ let t = 0;
70
+ const store = new InMemoryMemoryStore({
71
+ embed,
72
+ consolidate: { threshold: 0.9 },
73
+ now: () => ++t,
74
+ });
75
+ await store.add({ text: 'user prefers EUR', kind: 'preference' });
76
+ await store.add({ text: 'user prefers EUR for fiat display', kind: 'preference' });
77
+
78
+ const all = await store.all();
79
+ expect(all).toHaveLength(1); // folded, not appended
80
+ expect(all[0].text).toBe('user prefers EUR for fiat display'); // newer wins
81
+ });
82
+
83
+ it('consolidate (dedup): distinct facts are kept separate', async () => {
84
+ const embed = async (text: string): Promise<number[]> =>
85
+ /eur/i.test(text) ? [1, 0] : [0, 1];
86
+ const store = new InMemoryMemoryStore({ embed, consolidate: { threshold: 0.9 }, now: () => 1 });
87
+ await store.add({ text: 'user prefers EUR', kind: 'preference' });
88
+ await store.add({ text: 'it is sunny today', kind: 'note' });
89
+ expect(await store.all()).toHaveLength(2);
90
+ });
91
+
92
+ it('consolidate (dedup): different kinds are never merged', async () => {
93
+ const embed = async (): Promise<number[]> => [1, 0]; // identical vectors
94
+ const store = new InMemoryMemoryStore({ embed, consolidate: { threshold: 0.9 }, now: () => 1 });
95
+ await store.add({ text: 'EUR', kind: 'preference' });
96
+ await store.add({ text: 'EUR', kind: 'fact' });
97
+ expect(await store.all()).toHaveLength(2);
98
+ });
99
+
100
+ // LLM merge: injected merger rewrites old + new into one consolidated item, with unioned tags.
101
+ it('consolidate (merge): injected merger folds near-dups into one item', async () => {
102
+ const embed = async (): Promise<number[]> => [1, 0];
103
+ const merge = vi.fn(async (existing: string, incoming: string) => `${existing}; ${incoming}`);
104
+ let t = 0;
105
+ const store = new InMemoryMemoryStore({
106
+ embed,
107
+ consolidate: { threshold: 0.9, merge },
108
+ now: () => ++t,
109
+ });
110
+ await store.add({ text: 'likes EUR', kind: 'preference', tags: ['fiat'] });
111
+ await store.add({ text: 'and CHF', kind: 'preference', tags: ['currency'] });
112
+
113
+ expect(merge).toHaveBeenCalledWith('likes EUR', 'and CHF');
114
+ const all = await store.all();
115
+ expect(all).toHaveLength(1);
116
+ expect(all[0].text).toBe('likes EUR; and CHF');
117
+ expect(all[0].tags).toEqual(['fiat', 'currency']); // unioned
118
+ });
119
+ });
120
+
121
+ describe('memory tool source', () => {
122
+ it('remember saves and recall returns matches', async () => {
123
+ const store = new InMemoryMemoryStore({ now: () => 1 });
124
+ const src = createMemoryToolSource(store);
125
+ expect(src.listTools().map((t) => t.name)).toEqual(['remember', 'recall']);
126
+
127
+ const saved = await src.execute('remember', { text: 'BTC only', kind: 'preference' });
128
+ expect(String(saved)).toMatch(/Remembered \(preference\)/);
129
+
130
+ const recalled = await src.execute('recall', { query: 'BTC' });
131
+ expect(String(recalled)).toMatch(/BTC only/);
132
+ });
133
+
134
+ it('defaults an invalid kind to note', async () => {
135
+ const store = new InMemoryMemoryStore({ now: () => 1 });
136
+ const src = createMemoryToolSource(store);
137
+ await src.execute('remember', { text: 'x', kind: 'banana' });
138
+ expect((await store.all())[0].kind).toBe('note');
139
+ });
140
+ });
@@ -0,0 +1,174 @@
1
+ /**
2
+ * MemoryStore implementation — in-memory, with optional injected persistence
3
+ * and optional semantic ranking. Pure TS, zero deps.
4
+ *
5
+ * const store = new InMemoryMemoryStore(); // ephemeral
6
+ * const store = new InMemoryMemoryStore({ io }); // persisted (RN/Node)
7
+ * const store = new InMemoryMemoryStore({ io, embed }); // + semantic recall
8
+ */
9
+
10
+ import { cosineSimilarity } from '../rag/vector-store.js';
11
+ import type {
12
+ MemoryConsolidation,
13
+ MemoryIO,
14
+ MemoryItem,
15
+ MemoryQuery,
16
+ MemoryStore,
17
+ NewMemory,
18
+ } from './types.js';
19
+
20
+ const DEFAULT_DEDUP_THRESHOLD = 0.92;
21
+
22
+ export interface MemoryStoreOptions {
23
+ /** Persistence (load on first use, save on writes). Omit for ephemeral memory. */
24
+ io?: MemoryIO;
25
+ /** Embed text for semantic recall. Omit to fall back to substring matching. */
26
+ embed?: (text: string) => Promise<number[]>;
27
+ /**
28
+ * Fold near-duplicate writes into one item instead of appending. Needs `embed`.
29
+ * Omit for append-only. See {@link MemoryConsolidation}.
30
+ */
31
+ consolidate?: MemoryConsolidation;
32
+ /** Clock — injectable for deterministic tests. */
33
+ now?: () => number;
34
+ }
35
+
36
+ export class InMemoryMemoryStore implements MemoryStore {
37
+ private items: MemoryItem[] = [];
38
+ private hydrated = false;
39
+ private counter = 0;
40
+ private readonly io?: MemoryIO;
41
+ private readonly embed?: (text: string) => Promise<number[]>;
42
+ private readonly consolidate?: MemoryConsolidation;
43
+ private readonly now: () => number;
44
+
45
+ constructor(opts: MemoryStoreOptions = {}) {
46
+ this.io = opts.io;
47
+ this.embed = opts.embed;
48
+ this.consolidate = opts.consolidate;
49
+ this.now = opts.now ?? (() => Date.now());
50
+ }
51
+
52
+ private async hydrate(): Promise<void> {
53
+ if (this.hydrated) return;
54
+ this.hydrated = true;
55
+ if (this.io) {
56
+ try {
57
+ this.items = await this.io.load();
58
+ this.counter = this.items.length;
59
+ } catch {
60
+ this.items = [];
61
+ }
62
+ }
63
+ }
64
+
65
+ private async persist(): Promise<void> {
66
+ if (this.io) await this.io.save(this.items);
67
+ }
68
+
69
+ async add(item: NewMemory): Promise<MemoryItem> {
70
+ await this.hydrate();
71
+ let text = item.text;
72
+ let embedding =
73
+ item.embedding ?? (this.embed ? await this.embed(text).catch(() => undefined) : undefined);
74
+ let tags = item.tags;
75
+ let supersedeId: string | undefined;
76
+
77
+ // Consolidation: fold a same-kind near-duplicate into this write instead of
78
+ // appending — embedding-only by default, LLM rewrite when `merge` is set.
79
+ if (this.consolidate && embedding) {
80
+ const threshold = this.consolidate.threshold ?? DEFAULT_DEDUP_THRESHOLD;
81
+ let best: { item: MemoryItem; score: number } | undefined;
82
+ for (const m of this.items) {
83
+ if (m.kind !== item.kind || !m.embedding) continue;
84
+ const score = cosineSimilarity(embedding, m.embedding);
85
+ if (!best || score > best.score) best = { item: m, score };
86
+ }
87
+ if (best && best.score >= threshold) {
88
+ supersedeId = best.item.id;
89
+ tags = unionTags(best.item.tags, item.tags);
90
+ if (this.consolidate.merge) {
91
+ const merged = await this.consolidate.merge(best.item.text, text).catch(() => null);
92
+ if (merged && merged.trim()) {
93
+ text = merged.trim();
94
+ if (this.embed) embedding = await this.embed(text).catch(() => embedding);
95
+ }
96
+ }
97
+ // No merger → the incoming (newer) text supersedes the older item as-is.
98
+ }
99
+ }
100
+
101
+ const full: MemoryItem = {
102
+ id: item.id ?? `mem_${this.now()}_${++this.counter}`,
103
+ text,
104
+ kind: item.kind,
105
+ tags,
106
+ createdAt: item.createdAt ?? this.now(),
107
+ ...(embedding ? { embedding } : {}),
108
+ };
109
+ if (supersedeId) this.items = this.items.filter((m) => m.id !== supersedeId);
110
+ this.items.push(full);
111
+ await this.persist();
112
+ return full;
113
+ }
114
+
115
+ async all(): Promise<MemoryItem[]> {
116
+ await this.hydrate();
117
+ return [...this.items];
118
+ }
119
+
120
+ async search(query: MemoryQuery): Promise<MemoryItem[]> {
121
+ await this.hydrate();
122
+ const limit = query.limit ?? 5;
123
+
124
+ let pool = this.items;
125
+ if (query.kind) pool = pool.filter((m) => m.kind === query.kind);
126
+ if (query.tags?.length) {
127
+ pool = pool.filter((m) => query.tags!.every((t) => m.tags?.includes(t)));
128
+ }
129
+
130
+ const text = query.text?.trim();
131
+ if (!text) {
132
+ // No query text → most recent first.
133
+ return [...pool].sort((a, b) => b.createdAt - a.createdAt).slice(0, limit);
134
+ }
135
+
136
+ // Semantic ranking when both the query and items are embedded.
137
+ if (this.embed && pool.some((m) => m.embedding)) {
138
+ const qv = await this.embed(text).catch(() => null);
139
+ if (qv) {
140
+ return [...pool]
141
+ .map((m) => ({ m, score: m.embedding ? cosineSimilarity(qv, m.embedding) : -1 }))
142
+ .sort((a, b) => b.score - a.score)
143
+ .slice(0, limit)
144
+ .map((x) => x.m);
145
+ }
146
+ }
147
+
148
+ // Fallback: substring score + recency.
149
+ const q = text.toLowerCase();
150
+ return [...pool]
151
+ .map((m) => ({ m, hit: m.text.toLowerCase().includes(q) ? 1 : 0 }))
152
+ .sort((a, b) => b.hit - a.hit || b.m.createdAt - a.m.createdAt)
153
+ .slice(0, limit)
154
+ .map((x) => x.m);
155
+ }
156
+
157
+ async remove(id: string): Promise<void> {
158
+ await this.hydrate();
159
+ this.items = this.items.filter((m) => m.id !== id);
160
+ await this.persist();
161
+ }
162
+
163
+ async clear(): Promise<void> {
164
+ await this.hydrate();
165
+ this.items = [];
166
+ await this.persist();
167
+ }
168
+ }
169
+
170
+ /** Merge two optional tag lists, de-duplicated. Returns undefined when both empty. */
171
+ function unionTags(a?: string[], b?: string[]): string[] | undefined {
172
+ if (!a?.length && !b?.length) return undefined;
173
+ return [...new Set([...(a ?? []), ...(b ?? [])])];
174
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Memory tool source — lets the agent persist and recall things across
3
+ * sessions (`remember`, `recall`). Pairs with auto-recall in the
4
+ * ContextBuilder; this is the explicit, agent-driven side.
5
+ */
6
+
7
+ import type { ToolDef } from '../types.js';
8
+ import type { ToolSource } from '../tools/source.js';
9
+ import type { MemoryKind, MemoryStore } from './types.js';
10
+
11
+ const REMEMBER = 'remember';
12
+ const RECALL = 'recall';
13
+ const KINDS: MemoryKind[] = ['fact', 'preference', 'event', 'note'];
14
+
15
+ export function createMemoryToolSource(store: MemoryStore): ToolSource {
16
+ const tools: ToolDef[] = [
17
+ {
18
+ name: REMEMBER,
19
+ description:
20
+ 'Save something to long-term memory so you recall it in future sessions ' +
21
+ '— a user preference, a fact, or an event. Use sparingly for durable info.',
22
+ parameters: {
23
+ type: 'object',
24
+ properties: {
25
+ text: { type: 'string', description: 'What to remember (a short sentence)' },
26
+ kind: { type: 'string', enum: KINDS, description: 'fact | preference | event | note' },
27
+ tags: { type: 'array', items: { type: 'string' }, description: 'Optional tags' },
28
+ },
29
+ required: ['text'],
30
+ },
31
+ },
32
+ {
33
+ name: RECALL,
34
+ description:
35
+ 'Search your long-term memory for what you know about something before answering.',
36
+ parameters: {
37
+ type: 'object',
38
+ properties: {
39
+ query: { type: 'string', description: 'What to recall' },
40
+ limit: { type: 'number', description: 'Max items (default 5)' },
41
+ },
42
+ required: ['query'],
43
+ },
44
+ },
45
+ ];
46
+
47
+ async function execute(name: string, args: Record<string, unknown>): Promise<unknown> {
48
+ if (name === REMEMBER) {
49
+ const text = String(args.text ?? '').trim();
50
+ if (!text) throw new Error('remember: text is required');
51
+ const kind = (KINDS as string[]).includes(String(args.kind))
52
+ ? (args.kind as MemoryKind)
53
+ : 'note';
54
+ const tags = Array.isArray(args.tags) ? args.tags.map(String) : undefined;
55
+ const item = await store.add({ text, kind, tags });
56
+ return `Remembered (${item.kind}): ${item.text}`;
57
+ }
58
+ if (name === RECALL) {
59
+ const query = String(args.query ?? '').trim();
60
+ if (!query) throw new Error('recall: query is required');
61
+ const limit = Number(args.limit) > 0 ? Number(args.limit) : 5;
62
+ const items = await store.search({ text: query, limit });
63
+ if (items.length === 0) return 'Nothing relevant in memory.';
64
+ return items.map((m) => `- (${m.kind}) ${m.text}`).join('\n');
65
+ }
66
+ throw new Error(`memory: unknown tool ${name}`);
67
+ }
68
+
69
+ const names = new Set([REMEMBER, RECALL]);
70
+ return {
71
+ id: 'memory',
72
+ listTools: () => tools,
73
+ has: (name) => names.has(name),
74
+ execute,
75
+ };
76
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Memory — the agent's persistent identity + what it has learned.
3
+ *
4
+ * Two layers, mirroring how nanobot splits SOUL.md / AGENTS.md / memory:
5
+ * - AgentProfile — static identity ("who am I, how do I behave"). Injected.
6
+ * - MemoryStore — durable, growing facts/preferences/events the agent
7
+ * remembers across sessions. Pluggable storage.
8
+ *
9
+ * Pure data + interfaces — no storage or embedding deps. The host injects
10
+ * persistence (AsyncStorage on RN, fs/SQLite on Node) and, optionally, an
11
+ * EmbeddingProvider for semantic recall.
12
+ */
13
+
14
+ /** Static agent identity, composed into the system prompt every turn. */
15
+ export interface AgentProfile {
16
+ /** Display name, e.g. "KaleidoMind". */
17
+ name: string;
18
+ /** Persona / identity — the "soul". Who the agent is, its voice, its values. */
19
+ soul: string;
20
+ /** Operating instructions / house rules (optional). */
21
+ instructions?: string;
22
+ }
23
+
24
+ export type MemoryKind = 'fact' | 'preference' | 'event' | 'note';
25
+
26
+ export interface MemoryItem {
27
+ id: string;
28
+ text: string;
29
+ kind: MemoryKind;
30
+ /** Epoch ms. */
31
+ createdAt: number;
32
+ tags?: string[];
33
+ /** Optional embedding for semantic recall (set when an embedder is wired). */
34
+ embedding?: number[];
35
+ }
36
+
37
+ /** What to add — id/createdAt/embedding are filled in by the store. */
38
+ export type NewMemory = Omit<MemoryItem, 'id' | 'createdAt' | 'embedding'> &
39
+ Partial<Pick<MemoryItem, 'id' | 'createdAt' | 'embedding'>>;
40
+
41
+ export interface MemoryQuery {
42
+ /** Free text to match (semantic if embeddings are available, else substring). */
43
+ text?: string;
44
+ kind?: MemoryKind;
45
+ tags?: string[];
46
+ /** Max items to return (default 5). */
47
+ limit?: number;
48
+ }
49
+
50
+ export interface MemoryStore {
51
+ add(item: NewMemory): Promise<MemoryItem>;
52
+ all(): Promise<MemoryItem[]>;
53
+ /** Best-matching items for the query (recency-ranked, or semantic if embedded). */
54
+ search(query: MemoryQuery): Promise<MemoryItem[]>;
55
+ remove(id: string): Promise<void>;
56
+ clear(): Promise<void>;
57
+ }
58
+
59
+ /** Injected persistence — load once, save on every mutation. RN/Node provide it. */
60
+ export interface MemoryIO {
61
+ load(): Promise<MemoryItem[]>;
62
+ save(items: MemoryItem[]): Promise<void>;
63
+ }
64
+
65
+ /**
66
+ * Consolidation — fold same-kind near-duplicate memories into one item instead
67
+ * of bloating with "user likes EUR" ×5. Needs embeddings (the dup check is
68
+ * cosine). Omit for append-only. Two tiers: embedding-only dedup (zero
69
+ * inference, mobile-safe) and, when `merge` is set, an LLM rewrite.
70
+ */
71
+ export interface MemoryConsolidation {
72
+ /** Cosine threshold above which two same-kind memories are "the same". Default 0.92. */
73
+ threshold?: number;
74
+ /** Optional LLM merger; without it the newer item simply supersedes the older. */
75
+ merge?: (existing: string, incoming: string) => Promise<string>;
76
+ }