@kaleidorg/mind 0.2.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 (77) hide show
  1. package/dist/capabilities.d.ts +4 -0
  2. package/dist/capabilities.d.ts.map +1 -1
  3. package/dist/capabilities.js +7 -0
  4. package/dist/capabilities.js.map +1 -1
  5. package/dist/engine.d.ts +9 -0
  6. package/dist/engine.d.ts.map +1 -1
  7. package/dist/engine.js +1 -0
  8. package/dist/engine.js.map +1 -1
  9. package/dist/funnel.d.ts +6 -0
  10. package/dist/funnel.d.ts.map +1 -1
  11. package/dist/funnel.js +26 -6
  12. package/dist/funnel.js.map +1 -1
  13. package/dist/index.d.ts +9 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +8 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/kaleidoswap/contract.d.ts +72 -0
  18. package/dist/kaleidoswap/contract.d.ts.map +1 -0
  19. package/dist/kaleidoswap/contract.js +125 -0
  20. package/dist/kaleidoswap/contract.js.map +1 -0
  21. package/dist/knowledge/btc-map.d.ts +87 -0
  22. package/dist/knowledge/btc-map.d.ts.map +1 -0
  23. package/dist/knowledge/btc-map.js +365 -0
  24. package/dist/knowledge/btc-map.js.map +1 -0
  25. package/dist/lsps1/contract.d.ts +55 -0
  26. package/dist/lsps1/contract.d.ts.map +1 -0
  27. package/dist/lsps1/contract.js +91 -0
  28. package/dist/lsps1/contract.js.map +1 -0
  29. package/dist/memory/store.d.ts +7 -1
  30. package/dist/memory/store.d.ts.map +1 -1
  31. package/dist/memory/store.js +43 -3
  32. package/dist/memory/store.js.map +1 -1
  33. package/dist/memory/types.d.ts +12 -0
  34. package/dist/memory/types.d.ts.map +1 -1
  35. package/dist/recipe/kaleidoswap-atomic.d.ts +27 -0
  36. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -0
  37. package/dist/recipe/kaleidoswap-atomic.js +111 -0
  38. package/dist/recipe/kaleidoswap-atomic.js.map +1 -0
  39. package/dist/recipe/runner.d.ts.map +1 -1
  40. package/dist/recipe/runner.js +13 -1
  41. package/dist/recipe/runner.js.map +1 -1
  42. package/dist/skills/registry.d.ts.map +1 -1
  43. package/dist/skills/registry.js +20 -2
  44. package/dist/skills/registry.js.map +1 -1
  45. package/dist/wallet/confirm.d.ts +12 -0
  46. package/dist/wallet/confirm.d.ts.map +1 -0
  47. package/dist/wallet/confirm.js +67 -0
  48. package/dist/wallet/confirm.js.map +1 -0
  49. package/package.json +2 -1
  50. package/skills/README.md +6 -1
  51. package/skills/kaleido-lsps/SKILL.md +56 -0
  52. package/skills/kaleido-trading/SKILL.md +85 -18
  53. package/skills/merchant-finder/SKILL.md +87 -0
  54. package/skills/paid-data/SKILL.md +12 -0
  55. package/skills/wallet-assistant/SKILL.md +38 -0
  56. package/src/capabilities.ts +12 -0
  57. package/src/context/context.test.ts +6 -2
  58. package/src/engine.ts +6 -0
  59. package/src/funnel.ts +32 -7
  60. package/src/index.ts +43 -0
  61. package/src/kaleidoswap/contract.test.ts +147 -0
  62. package/src/kaleidoswap/contract.ts +212 -0
  63. package/src/knowledge/btc-map.test.ts +188 -0
  64. package/src/knowledge/btc-map.ts +446 -0
  65. package/src/lsps1/contract.test.ts +81 -0
  66. package/src/lsps1/contract.ts +132 -0
  67. package/src/memory/memory.test.ts +55 -0
  68. package/src/memory/store.ts +49 -4
  69. package/src/memory/types.ts +13 -0
  70. package/src/recipe/kaleidoswap-atomic.test.ts +138 -0
  71. package/src/recipe/kaleidoswap-atomic.ts +117 -0
  72. package/src/recipe/runner.ts +13 -1
  73. package/src/skills/registry.ts +21 -2
  74. package/src/skills/skills.test.ts +42 -0
  75. package/src/wallet/confirm.test.ts +57 -0
  76. package/src/wallet/confirm.ts +74 -0
  77. package/skills/kaleido-wallet/SKILL.md +0 -28
@@ -1,31 +1,98 @@
1
1
  ---
2
2
  name: kaleido-trading
3
- description: "Trade and manage a portfolio on KaleidoSwap: get live prices and market data, quote and place atomic swaps between BTC and RGB assets (USDT, XAUT), acquire inbound liquidity from the LSP, and check positions. Triggers when the user wants a price, a quote, to swap or trade assets, to rebalance a portfolio, or to acquire a Lightning channel from the LSP."
4
- tools: get_price, get_market_data, kaleidoswap_get_pairs, kaleidoswap_get_quote, kaleidoswap_place_order, kaleidoswap_get_order_status, kaleidoswap_get_position, kaleidoswap_lsp_get_info, kaleidoswap_lsp_create_order
5
- triggers: price, quote, swap, trade, rebalance, portfolio, market, slippage, liquidity, channel order
3
+ description: "Trade on KaleidoSwap quote and execute swaps between BTC and RGB assets (USDT, XAUT). Get assets and pairs, pull an executable quote, place a market order, or track an atomic swap end-to-end. Triggers when the user wants a price, a quote, to swap or trade assets, or to rebalance between BTC and stablecoins."
4
+ tools: get_price, fiat_to_sats, kaleidoswap_get_assets, kaleidoswap_get_pairs, kaleidoswap_get_quote, kaleidoswap_get_nodeinfo, kaleidoswap_place_order, kaleidoswap_get_order_status, kaleidoswap_get_order_history
5
+ triggers: quote, swap, trade, rebalance, slippage, pair, pairs, usdt, xaut, kaleidoswap, rfq
6
6
  metadata:
7
7
  author: kaleidoswap
8
- version: "0.1.0"
9
- surface: "kaleido-mcp (KaleidoSwap maker + market data)"
8
+ version: "0.3.0"
10
9
  ---
11
10
 
12
11
  # KaleidoSwap trading
13
12
 
14
- Quote, swap, and manage a portfolio through the KaleidoSwap maker and market
15
- data MCP tools.
13
+ Quote and execute swaps on the KaleidoSwap maker. The model picks tools by
14
+ name; the host binds them through whichever transport it runs over (WDK on
15
+ mobile, HTTP/MCP/CLI on desktop).
16
+
17
+ ## Critical rules — these override everything else
18
+
19
+ You have **no knowledge** of any price, quote, fee, pair, or order. Every
20
+ number, pair, or quote id in your reply MUST come from a tool result returned
21
+ in the CURRENT turn:
22
+
23
+ - "What's the BTC price?" → call `get_price` and state the number it returns.
24
+ - "What pairs are listed?" → call `kaleidoswap_get_pairs` and list them.
25
+ - "Quote 100k sats to USDT" → call `kaleidoswap_get_quote(BTC, USDT, 100000)`,
26
+ then state the receive amount + fees from the result.
27
+
28
+ **Calling the tool IS the answer.** Never write "the pairs are listed using
29
+ kaleidoswap_get_pairs" or "the function returns the quote" — just call it.
30
+
31
+ **Never reuse a number across turns.** If the user asks a new question, the
32
+ previous turn's quote, price, or fee is irrelevant — fetch fresh.
33
+
34
+ **Never invent a quote.** Without a `quote_id` and `receive_amount` returned
35
+ this turn, you do not have a quote. Say so and re-quote.
36
+
37
+ ## Asset codes (canonical)
38
+
39
+ Only these codes are accepted:
40
+
41
+ - `BTC` (Bitcoin, amounts always in satoshis)
42
+ - `USDT` (Tether) — **not** `USD`, **not** `tether`
43
+ - `XAUT` (Tether Gold) — **not** `XAU`, **not** `gold`
44
+
45
+ When the user types `USD` they almost always mean `USDT` — confirm before
46
+ quoting. Same for `gold` → `XAUT`. Don't silently substitute.
47
+
48
+ ## Tools
49
+
50
+ ### `kaleidoswap_get_pairs` — no args
51
+ Use when the user asks "what can I trade", "list pairs", "what's available",
52
+ or before quoting an unfamiliar pair.
53
+
54
+ ### `kaleidoswap_get_quote` — REQUIRES `amount`
55
+ Required args: `from_asset` AND `to_asset` AND `amount`. The maker rejects
56
+ calls missing any of these.
57
+
58
+ **If the user didn't give an amount, ASK for it. Do not call the tool with
59
+ from/to alone.**
60
+
61
+ Examples:
62
+ - "Quote 100k sats to USDT" → `{from_asset: "BTC", to_asset: "USDT", amount: 100000}`
63
+ - "What's the USDT/BTC rate?" → ask: "How many USDT do you want to swap?"
64
+ (no amount → no quote possible).
65
+ - "Buy 50 USDT of BTC" → `{from_asset: "USDT", to_asset: "BTC", amount: 50}`
66
+ (USDT is what's being spent).
67
+
68
+ ### `kaleidoswap_place_order(quote_id)` 🔒 spend
69
+ Only after `kaleidoswap_get_quote` returned a `quote_id` THIS turn, and only
70
+ when the user has explicitly approved the amount + direction.
71
+
72
+ ### `kaleidoswap_get_order_status(order_id)`
73
+ Poll after placing an order. Report status plainly — pending, settling,
74
+ completed, failed.
16
75
 
17
76
  ## Flow
18
77
 
19
- 1. **Price first.** Use `get_price` / `get_market_data` for context and
20
- `kaleidoswap_get_quote` for an executable price before proposing a trade.
21
- 2. **Confirm the trade.** Show pair, direction, amount in, expected amount out,
22
- and fees. Wait for explicit approval before `kaleidoswap_place_order`.
23
- 3. **Track it.** After placing, poll `kaleidoswap_get_order_status` until the
24
- swap settles, then report the result and the new `kaleidoswap_get_position`.
25
- 4. **Need inbound liquidity?** If a swap can't route for lack of a channel, use
26
- `kaleidoswap_lsp_get_info` then `kaleidoswap_lsp_create_order` to buy one.
78
+ 1. **Pick a pair** — skip when obvious (`BTC/USDT`, `BTC/XAUT`).
79
+ 2. **Quote** `kaleidoswap_get_quote`. REQUIRES amount.
80
+ 3. **Show + confirm** surface pair, direction, amount in, expected out,
81
+ fees, slippage. **Never hide cost** a small model must not abbreviate
82
+ fees out of the message.
83
+ 4. **Place** spend-gated by the engine. The host pauses for the user.
84
+ 5. **Track** poll `kaleidoswap_get_order_status` until it terminates.
85
+
86
+ ## Don'ts
27
87
 
28
- ## Rules
88
+ - Don't invent prices, quotes, quote_ids, or order_ids.
89
+ - Don't reuse a number from a previous turn.
90
+ - Don't describe how a tool works — call it.
91
+ - Don't call `kaleidoswap_get_quote` with from/to only — ask for the amount.
92
+ - Don't accept `XAU` as `XAUT` or `USD` as `USDT` silently — confirm.
93
+ - Don't retry the same failing tool call in a loop. If a call fails, read the
94
+ error and either ask the user, fix the args, or stop.
29
95
 
30
- - Never trade on a stale quote re-quote if the user hesitates.
31
- - Surface slippage and fees explicitly; small local models must not hide cost.
96
+ For the atomic-swap flow (trust-minimised cross-asset swap that the maker
97
+ can't settle from balance), use the `kaleido-trading` atomic recipe the
98
+ agentic chain is not safe to plan on a 0.6B model.
@@ -0,0 +1,87 @@
1
+ ---
2
+ name: merchant-finder
3
+ description: "Find Bitcoin-accepting merchants near the user using live BTC Map data and the device's real location. Triggers when the user asks where to spend Bitcoin, for a shop, store, restaurant, cafe, bar, or ATM that accepts Bitcoin, or for merchants nearby."
4
+ tools: find_merchant_locations, get_merchant_info
5
+ triggers: merchant, merchants, shop, shops, store, stores, restaurant, restaurants, cafe, cafes, bar, bars, atm, atms, accept, accepts, accepting, nearby, near me, around, place, places, spend, find, pizza, food, coffee, bitcoin map, btcmap
6
+ metadata:
7
+ author: kaleidoswap
8
+ version: "0.2.0"
9
+ homepage: "https://btcmap.org"
10
+ ---
11
+
12
+ # Merchant finder
13
+
14
+ Discover places that accept Bitcoin payments — cafés, restaurants, bars, shops,
15
+ and ATMs. Live BTC Map data when the host injects a fetcher + location;
16
+ otherwise a small offline list keeps the skill answerable.
17
+
18
+ ## Critical rule — never answer from memory
19
+
20
+ You have **no knowledge of any merchant**. Every place, name, address and
21
+ distance in your reply MUST come from a `find_merchant_locations` result
22
+ returned in the CURRENT turn. **Call `find_merchant_locations` for every
23
+ place question**, even if a similar question was answered earlier — do NOT
24
+ reuse or adapt a previous answer. Never invent a merchant.
25
+
26
+ ## How to call the tools
27
+
28
+ 1. **Start with `find_merchant_locations`.** Pass ONLY the fields the user
29
+ actually named — do not invent constraints:
30
+
31
+ - `query` — a specific thing the user named (e.g. `"tapas"`, `"coffee"`,
32
+ `"pizza"`). Omit when they only said "near me" or only named a place.
33
+
34
+ - `category` — must be EXACTLY one of: `restaurant`, `cafe`, `bar`, `shop`,
35
+ `grocery`, `lodging`, `atm`. **Anything else is invalid — leave it empty.**
36
+ The words "merchant", "merchants", "place", "places", "store", "stores"
37
+ are NOT categories — they're the generic noun for what you're searching
38
+ for, so they belong in `query` at best, never in `category`.
39
+
40
+ - `near_address` — a city, neighbourhood, or address (e.g. `"Milan"`,
41
+ `"Bitcoin Beach, El Salvador"`). Use this any time the user names a
42
+ location instead of "near me".
43
+
44
+ - `radius_km` — **omit unless the user names a specific number.** The
45
+ default (5 km) is a sensible search radius for a city. Don't pick a
46
+ small radius (1, 2, 3) yourself — city-wide searches need 5+.
47
+
48
+ - `limit` — 1–20, default 10. Omit unless the user names a count.
49
+
50
+ Examples (positive):
51
+ - "where can I spend btc near me" → `find_merchant_locations({})`
52
+ - "find merchants in Milan" → `find_merchant_locations({ near_address: "Milan" })`
53
+ ↑ no `category` — "merchants" is NOT a category.
54
+ - "cafes in Lisbon" → `find_merchant_locations({ category: "cafe", near_address: "Lisbon" })`
55
+ - "pizza places in Switzerland that take bitcoin" →
56
+ `find_merchant_locations({ query: "pizza", near_address: "Switzerland" })`
57
+ ↑ "places" is NOT a category — pizza goes in `query`.
58
+ - "lightning bars in NYC, within 2 km" →
59
+ `find_merchant_locations({ category: "bar", near_address: "New York", radius_km: 2 })`
60
+ ↑ user explicitly said "2 km" → set radius_km.
61
+
62
+ Examples (anti — do NOT do these):
63
+ - ❌ `category: "merchant"` (not a category)
64
+ - ❌ `category: "place"` (not a category)
65
+ - ❌ `radius_km: 2` when the user didn't say "2 km" — you're picking a
66
+ too-small radius and the result will be empty.
67
+
68
+ 2. **Present the results.** Each row carries:
69
+ - `name`, `category`, `address`
70
+ - `distance_m` when present — show in metres or km
71
+ - `accepts_bitcoin` / `accepts_lightning` — relevant because Lightning is
72
+ fastest for small payments
73
+ - `phone`, `website`, `opening_hours` when present — surface if asked
74
+
75
+ 3. **Use `get_merchant_info` only when** the user asks for more detail on one
76
+ specific result. Pass `merchant_id` (preferred) or `merchant_name`.
77
+
78
+ ## Reply style
79
+
80
+ - Be concise. One line per merchant works:
81
+ `Name — category, address (X m away, accepts: lightning, onchain)`.
82
+ - If the result `source` is `offline`, say so plainly — it means the live
83
+ BTC Map fetch wasn't available, so the list is limited.
84
+ - If `find_merchant_locations` returns zero merchants, say so — don't invent
85
+ places. Suggest widening `radius_km` or trying `near_address`.
86
+ - When the user says "near me" and `precise_location` is false, mention which
87
+ fallback location was used so they know it's not their actual GPS.
@@ -0,0 +1,12 @@
1
+ ---
2
+ name: paid-data
3
+ description: Fetch premium or paywalled data that requires a small Lightning (L402) payment — paid feeds, gated APIs, unlockable resources. Triggers when the user wants premium, paid, or unlockable data behind an L402 paywall.
4
+ tools: fetch_paid_resource
5
+ triggers: premium, paid, l402, feed, subscription, unlock, paywall
6
+ ---
7
+
8
+ # Paid data
9
+
10
+ Fetch L402-paywalled resources, paying small Lightning invoices automatically
11
+ with `fetch_paid_resource`. Small amounts pay without prompting (capped);
12
+ anything larger is declined. Tell the user what was paid and what was returned.
@@ -0,0 +1,38 @@
1
+ ---
2
+ name: wallet-assistant
3
+ description: Everyday wallet tasks on this phone — check the BTC/asset balance, create an invoice to receive, send a payment, look up a contact, get the BTC price, or convert a fiat amount to sats. Triggers when the user asks about their balance, wants to receive or send money, pay an invoice, or pay a contact.
4
+ tools: get_balances, get_price, fiat_to_sats, resolve_contact, send_payment, rln_pay_invoice, rln_create_ln_invoice, spark_create_invoice
5
+ triggers: balance, pay, send, receive, address, invoice, transactions, contact, funds, money, price, sats, eur, gbp
6
+ ---
7
+
8
+ # Wallet assistant
9
+
10
+ You operate the user's on-device multi-L2 Bitcoin wallet. ALWAYS use a tool to
11
+ get real data — NEVER invent a balance, address, amount, price, or result.
12
+
13
+ ## Critical rules
14
+
15
+ You have no knowledge of balances, prices, addresses, or invoices. Every value
16
+ in your reply MUST come from a tool result returned in the CURRENT turn — do
17
+ not reuse a number from a previous turn.
18
+
19
+ When a tool returns multiple fields, **report all the load-bearing ones**:
20
+ - `get_balances` may return `{confirmed, pending, total}` — when `pending`
21
+ is non-zero, report BOTH. `confirmed` is spendable; `pending` is settling
22
+ and is NOT spendable yet. The user needs to know the difference.
23
+ - `fiat_to_sats` returns `{sats}` plus a `note` when the currency was an
24
+ approximation — surface the note.
25
+
26
+ ## Rules
27
+
28
+ - Balance / "how much do I have" → call `get_balances`, then state the number.
29
+ - Receive / "an invoice for N sats" → call `rln_create_ln_invoice` (or
30
+ `spark_create_invoice`) with the amount.
31
+ - Price → `get_price`. "How many sats is 3 EUR" → `fiat_to_sats`.
32
+ - Pay a Lightning invoice → `rln_pay_invoice`.
33
+ - Send to a person/amount → first `resolve_contact` (and `fiat_to_sats` if the
34
+ amount is in fiat), then `send_payment` with the amount in sats and the
35
+ recipient. State the amount and destination; the app asks the user to confirm
36
+ before it sends.
37
+
38
+ Keep replies short, but never drop a balance component or a fee.
@@ -25,6 +25,10 @@ export interface MindCapabilities {
25
25
  memory: boolean;
26
26
  /** Semantic recall for memory (needs embeddings). */
27
27
  semanticMemory: boolean;
28
+ /** Embedding-only dedup of near-duplicate memories (zero inference — mobile-safe). */
29
+ dedupeMemory: boolean;
30
+ /** LLM merge of near-duplicate memories (an extra inference — capable/delegated only). */
31
+ mergeMemory: boolean;
28
32
  /** Retrieval-augmented generation (needs embeddings + enough RAM/context). */
29
33
  rag: boolean;
30
34
  /** Token budget for injected system context. */
@@ -48,6 +52,12 @@ export function capabilityProfile(input: CapabilityInput): MindCapabilities {
48
52
  const memory = budget >= 256;
49
53
  const semanticMemory = memory && hasEmb;
50
54
 
55
+ // Consolidation: embedding-only dedup is cheap (no inference) — on wherever
56
+ // semantic memory is. The LLM merge costs an extra inference, so reserve it
57
+ // for delegated or roomy on-device setups; never run it on a tiny phone model.
58
+ const dedupeMemory = semanticMemory;
59
+ const mergeMemory = dedupeMemory && (input.delegated || (ramGb >= 4 && ctx >= 4096));
60
+
51
61
  // RAG is the expensive one: needs embeddings, a non-tiny context window, and
52
62
  // (on-device) enough RAM to hold an embedding model + index.
53
63
  const rag = hasEmb && ctx >= 4096 && (input.delegated || ramGb >= 3);
@@ -59,6 +69,8 @@ export function capabilityProfile(input: CapabilityInput): MindCapabilities {
59
69
  return {
60
70
  memory,
61
71
  semanticMemory,
72
+ dedupeMemory,
73
+ mergeMemory,
62
74
  rag,
63
75
  contextBudgetTokens: budget,
64
76
  topKMemory,
@@ -60,18 +60,22 @@ describe('ContextBuilder', () => {
60
60
  });
61
61
 
62
62
  describe('capabilityProfile', () => {
63
- it('low-end phone: memory yes, RAG no', () => {
63
+ it('low-end phone: memory yes, RAG no, dedup yes but no LLM merge', () => {
64
64
  const c = capabilityProfile({ ramBytes: 2 * 1024 ** 3, modelCtxTokens: 2048, hasEmbeddings: true });
65
65
  expect(c.memory).toBe(true);
66
66
  expect(c.rag).toBe(false); // ctx too small + low RAM
67
67
  expect(c.topKRag).toBe(0);
68
+ expect(c.dedupeMemory).toBe(true); // embedding-only dedup is mobile-safe
69
+ expect(c.mergeMemory).toBe(false); // never run merge inference on a tiny phone
68
70
  });
69
71
 
70
- it('desktop / delegated: RAG on', () => {
72
+ it('desktop / delegated: RAG on, memory merge on', () => {
71
73
  const c = capabilityProfile({ modelCtxTokens: 8192, hasEmbeddings: true, delegated: true });
72
74
  expect(c.rag).toBe(true);
73
75
  expect(c.topKRag).toBeGreaterThan(0);
74
76
  expect(c.semanticMemory).toBe(true);
77
+ expect(c.dedupeMemory).toBe(true);
78
+ expect(c.mergeMemory).toBe(true);
75
79
  });
76
80
 
77
81
  it('no embeddings → no RAG, no semantic memory', () => {
package/src/engine.ts CHANGED
@@ -35,6 +35,11 @@ export interface AgenticOptions {
35
35
  onStart?: (requestId: string, turn: number) => void;
36
36
  /** Fired when the model requests a tool, before it executes. */
37
37
  onToolCall?: (call: { name: string; arguments: Record<string, unknown> }, turn: number) => void;
38
+ /**
39
+ * Fired after a tool returns (success OR error — errors arrive as `{error}`).
40
+ * Useful for surfacing the raw response back to the user in a debug UI.
41
+ */
42
+ onToolResult?: (event: { name: string; arguments: Record<string, unknown>; result: unknown }, turn: number) => void;
38
43
  /** Human-in-the-loop gate for tools flagged requiresConfirmation. */
39
44
  onConfirm?: (call: { name: string; arguments: Record<string, unknown> }) => Promise<ConfirmDecision>;
40
45
  /**
@@ -127,6 +132,7 @@ export class Engine {
127
132
  }
128
133
 
129
134
  executed.push({ name: call.name, arguments: call.arguments, result });
135
+ opts.onToolResult?.({ name: call.name, arguments: call.arguments, result }, turn);
130
136
  history.push({
131
137
  role: 'tool',
132
138
  content: typeof result === 'string' ? result : JSON.stringify(result),
package/src/funnel.ts CHANGED
@@ -34,13 +34,31 @@ import type { LLMProvider } from './providers/types.js';
34
34
  import type { ConfirmDecision, Message, ToolResult } from './types.js';
35
35
 
36
36
  /** Base system prompt for the wallet assistant. Hosts may override. */
37
- export const DEFAULT_WALLET_SYSTEM =
38
- 'You are KaleidoSwap, a concise, privacy-first assistant running inside a ' +
39
- 'non-custodial Bitcoin, Lightning and RGB wallet. Use the provided tools to ' +
40
- 'take actions: pay invoices and contacts, create invoices, check balances. ' +
41
- 'Never invent a balance, address, amount or result — always call the ' +
42
- 'relevant tool and report what it returns. All BTC amounts are in satoshis. ' +
43
- 'Keep replies short and friendly.';
37
+ export const DEFAULT_WALLET_SYSTEM = [
38
+ 'You are KaleidoSwap, a concise, privacy-first assistant running inside a',
39
+ 'non-custodial Bitcoin, Lightning and RGB wallet.',
40
+ '',
41
+ 'CORE RULES (these override every skill instruction):',
42
+ "1. If a tool can answer the user's question, CALL IT. Never describe how a",
43
+ " tool works (\"the pairs are listed using kaleidoswap_get_pairs\") — calling",
44
+ ' the tool IS the answer.',
45
+ '2. Never invent a balance, address, amount, price, quote, fee, pair, or any',
46
+ " other value. Every number or identifier in your reply MUST come from a tool",
47
+ ' result returned in the CURRENT turn.',
48
+ '3. Never reuse a number, name, or detail from a previous turn unless the user',
49
+ ' is explicitly asking about that earlier result. Each new question gets a',
50
+ ' fresh tool call.',
51
+ '4. If a tool needs a required argument the user did not give (e.g. an amount',
52
+ " for a quote), ASK for it. Do not invent values. Do not call the tool with",
53
+ ' the required field missing.',
54
+ '5. All BTC amounts are in satoshis. Asset codes are case-insensitive but the',
55
+ ' canonical forms are BTC, USDT, XAUT — do not silently shorten to USD, XAU.',
56
+ '',
57
+ 'Keep replies short and friendly. When a tool returns multiple fields, surface',
58
+ "the ones that matter — never collapse a structured result to a single number",
59
+ 'when other fields are non-zero or safety-relevant (e.g. pending balances,',
60
+ 'fees, slippage).',
61
+ ].join('\n');
44
62
 
45
63
  /** Tools that stay available even when a skill narrows the set. */
46
64
  const AMBIENT_MEMORY = ['remember', 'recall'];
@@ -74,6 +92,12 @@ export interface FunnelCallbacks {
74
92
  call: { name: string; arguments: Record<string, unknown> },
75
93
  info: { requiresConfirmation: boolean },
76
94
  ) => void;
95
+ /** A tool returned a result (agentic tier). Errors arrive as `{error}`. */
96
+ onToolResult?: (event: {
97
+ name: string;
98
+ arguments: Record<string, unknown>;
99
+ result: unknown;
100
+ }) => void;
77
101
  onConfirm?: (call: { name: string; arguments: Record<string, unknown> }) => Promise<ConfirmDecision>;
78
102
  }
79
103
 
@@ -253,6 +277,7 @@ export class Funnel {
253
277
  .then((def) => cbs.onToolCall?.(call, { requiresConfirmation: !!def?.requiresConfirmation }))
254
278
  .catch(() => cbs.onToolCall?.(call, { requiresConfirmation: false }));
255
279
  },
280
+ onToolResult: cbs.onToolResult,
256
281
  onConfirm: cbs.onConfirm,
257
282
  });
258
283
  return { text: res.text ?? '', tier: 'agentic', toolCalls: res.toolCalls, turns: res.turns };
package/src/index.ts CHANGED
@@ -49,6 +49,40 @@ export type {
49
49
  WalletHandler,
50
50
  BindWalletOptions,
51
51
  } from './wallet/contract.js';
52
+ export { confirmReadback } from './wallet/confirm.js';
53
+
54
+ // ── KaleidoSwap maker tool contract (single source of truth) ────────────────
55
+ export {
56
+ KALEIDOSWAP_TOOLS,
57
+ KALEIDOSWAP_SPEND_TOOLS,
58
+ isKaleidoswapSpendTool,
59
+ getKaleidoswapTool,
60
+ kaleidoswapTools,
61
+ bindKaleidoswapTools,
62
+ } from './kaleidoswap/contract.js';
63
+ export type {
64
+ KaleidoswapGroup,
65
+ KaleidoswapToolDef,
66
+ KaleidoswapHandler,
67
+ BindKaleidoswapOptions,
68
+ } from './kaleidoswap/contract.js';
69
+
70
+ // ── LSPS1 (Lightning Service Provider channel orders) ───────────────────────
71
+ export {
72
+ LSPS1_TOOLS,
73
+ LSPS1_SPEND_TOOLS,
74
+ isLsps1SpendTool,
75
+ getLsps1Tool,
76
+ bindLsps1Tools,
77
+ } from './lsps1/contract.js';
78
+ export type {
79
+ Lsps1ToolDef,
80
+ Lsps1Handler,
81
+ BindLsps1Options,
82
+ } from './lsps1/contract.js';
83
+
84
+ // ── KaleidoSwap atomic-swap recipe (opt-in — register via Funnel.recipes) ──
85
+ export { kaleidoswapAtomicRecipe } from './recipe/kaleidoswap-atomic.js';
52
86
 
53
87
  // ── Recipes (mobile multi-step: "recipes, not planning") ───────────────────
54
88
  export { runRecipe, extractSlots, RecipeRegistry } from './recipe/runner.js';
@@ -69,6 +103,7 @@ export type { MemoryStoreOptions } from './memory/store.js';
69
103
  export { createMemoryToolSource } from './memory/tool.js';
70
104
  export type {
71
105
  AgentProfile,
106
+ MemoryConsolidation,
72
107
  MemoryItem,
73
108
  MemoryKind,
74
109
  MemoryQuery,
@@ -110,6 +145,14 @@ export { walletHistoryToDocuments, contactsToDocuments } from './knowledge/walle
110
145
  export type { WalletTx, Contact } from './knowledge/wallet.js';
111
146
  export { merchantsToDocuments } from './knowledge/merchants.js';
112
147
  export type { Merchant } from './knowledge/merchants.js';
148
+ export { createBtcMapToolSource, BTC_MAP_SAMPLE } from './knowledge/btc-map.js';
149
+ export type {
150
+ BtcMapToolOptions,
151
+ BtcMapMerchant,
152
+ BtcMapFetch,
153
+ LocationProvider,
154
+ LatLng,
155
+ } from './knowledge/btc-map.js';
113
156
 
114
157
  export { Engine } from './engine.js';
115
158
  export type { EngineOptions, AgenticOptions, AgenticResult } from './engine.js';
@@ -0,0 +1,147 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ KALEIDOSWAP_TOOLS,
4
+ KALEIDOSWAP_SPEND_TOOLS,
5
+ isKaleidoswapSpendTool,
6
+ getKaleidoswapTool,
7
+ kaleidoswapTools,
8
+ bindKaleidoswapTools,
9
+ type KaleidoswapHandler,
10
+ } from './contract.js';
11
+
12
+ describe('KALEIDOSWAP_TOOLS — shape invariants', () => {
13
+ it('exposes the expected tool names', () => {
14
+ const names = KALEIDOSWAP_TOOLS.map((t) => t.name);
15
+ expect(names).toEqual([
16
+ 'kaleidoswap_get_assets',
17
+ 'kaleidoswap_get_pairs',
18
+ 'kaleidoswap_get_quote',
19
+ 'kaleidoswap_get_nodeinfo',
20
+ 'kaleidoswap_place_order',
21
+ 'kaleidoswap_get_order_status',
22
+ 'kaleidoswap_get_order_history',
23
+ 'kaleidoswap_atomic_init',
24
+ 'kaleidoswap_atomic_execute',
25
+ 'kaleidoswap_atomic_status',
26
+ ]);
27
+ });
28
+
29
+ it('every tool has a group and a parameters object', () => {
30
+ for (const t of KALEIDOSWAP_TOOLS) {
31
+ expect(['market', 'orders', 'atomic']).toContain(t.group);
32
+ expect(t.parameters).toBeDefined();
33
+ expect((t.parameters as any).type).toBe('object');
34
+ }
35
+ });
36
+
37
+ it('marks every spend tool as requiresConfirmation', () => {
38
+ for (const t of KALEIDOSWAP_TOOLS) {
39
+ expect(!!t.spend).toBe(!!t.requiresConfirmation);
40
+ }
41
+ });
42
+
43
+ it('lists every spend tool exactly once in KALEIDOSWAP_SPEND_TOOLS', () => {
44
+ const expected = KALEIDOSWAP_TOOLS.filter((t) => t.spend).map((t) => t.name).sort();
45
+ expect([...KALEIDOSWAP_SPEND_TOOLS].sort()).toEqual(expected);
46
+ // Sanity: place_order, atomic_init, atomic_execute are spend; the rest aren't.
47
+ expect(expected).toEqual([
48
+ 'kaleidoswap_atomic_execute',
49
+ 'kaleidoswap_atomic_init',
50
+ 'kaleidoswap_place_order',
51
+ ]);
52
+ });
53
+
54
+ it('isKaleidoswapSpendTool agrees with the set', () => {
55
+ expect(isKaleidoswapSpendTool('kaleidoswap_place_order')).toBe(true);
56
+ expect(isKaleidoswapSpendTool('kaleidoswap_get_pairs')).toBe(false);
57
+ expect(isKaleidoswapSpendTool('not_a_tool')).toBe(false);
58
+ });
59
+
60
+ it('getKaleidoswapTool returns by name', () => {
61
+ expect(getKaleidoswapTool('kaleidoswap_get_quote')?.group).toBe('market');
62
+ expect(getKaleidoswapTool('nope')).toBeUndefined();
63
+ });
64
+ });
65
+
66
+ describe('kaleidoswapTools(groups)', () => {
67
+ it('returns all tools when no group filter', () => {
68
+ expect(kaleidoswapTools().length).toBe(KALEIDOSWAP_TOOLS.length);
69
+ });
70
+
71
+ it('filters by group', () => {
72
+ const market = kaleidoswapTools({ groups: ['market'] });
73
+ expect(market.every((t) => t.group === 'market')).toBe(true);
74
+ expect(market.map((t) => t.name)).toContain('kaleidoswap_get_quote');
75
+ expect(market.map((t) => t.name)).not.toContain('kaleidoswap_place_order');
76
+ });
77
+
78
+ it('combines multiple groups', () => {
79
+ const readPlusOrders = kaleidoswapTools({ groups: ['market', 'orders'] });
80
+ expect(readPlusOrders.some((t) => t.name === 'kaleidoswap_atomic_init')).toBe(false);
81
+ expect(readPlusOrders.some((t) => t.name === 'kaleidoswap_place_order')).toBe(true);
82
+ });
83
+ });
84
+
85
+ describe('bindKaleidoswapTools', () => {
86
+ // Handlers that just echo their args so we can verify wiring.
87
+ const echoHandlers = (): Record<string, KaleidoswapHandler> => ({
88
+ kaleidoswap_get_assets: async () => ({ ok: true, tool: 'get_assets' }),
89
+ kaleidoswap_get_pairs: async () => ({ ok: true, tool: 'get_pairs' }),
90
+ kaleidoswap_get_quote: async (a) => ({ ok: true, tool: 'get_quote', args: a }),
91
+ kaleidoswap_get_nodeinfo: async () => ({ ok: true, tool: 'get_nodeinfo' }),
92
+ kaleidoswap_place_order: async (a) => ({ ok: true, tool: 'place_order', args: a }),
93
+ kaleidoswap_get_order_status: async (a) => ({ ok: true, tool: 'get_order_status', args: a }),
94
+ kaleidoswap_get_order_history: async (a) => ({ ok: true, tool: 'get_order_history', args: a }),
95
+ kaleidoswap_atomic_init: async (a) => ({ ok: true, tool: 'atomic_init', args: a }),
96
+ kaleidoswap_atomic_execute: async (a) => ({ ok: true, tool: 'atomic_execute', args: a }),
97
+ kaleidoswap_atomic_status: async (a) => ({ ok: true, tool: 'atomic_status', args: a }),
98
+ });
99
+
100
+ it('binds every tool when all handlers are present', () => {
101
+ const src = bindKaleidoswapTools(echoHandlers());
102
+ const tools = src.listTools();
103
+ expect(tools.length).toBe(KALEIDOSWAP_TOOLS.length);
104
+ });
105
+
106
+ it('preserves descriptions and the spend gate (requiresConfirmation)', () => {
107
+ const src = bindKaleidoswapTools(echoHandlers());
108
+ const place = src.listTools().find((t) => t.name === 'kaleidoswap_place_order');
109
+ const pairs = src.listTools().find((t) => t.name === 'kaleidoswap_get_pairs');
110
+ expect(place?.requiresConfirmation).toBe(true);
111
+ expect(pairs?.requiresConfirmation).toBeFalsy();
112
+ });
113
+
114
+ it('dispatches execute() to the right handler with args', async () => {
115
+ const src = bindKaleidoswapTools(echoHandlers());
116
+ const r = await src.execute('kaleidoswap_get_quote', { from_asset: 'BTC', to_asset: 'USDT', amount: 100_000 });
117
+ expect(r).toMatchObject({ ok: true, tool: 'get_quote', args: { from_asset: 'BTC', amount: 100_000 } });
118
+ });
119
+
120
+ it('throws when a handler is missing and allowMissing is false', () => {
121
+ const handlers = { kaleidoswap_get_pairs: echoHandlers().kaleidoswap_get_pairs };
122
+ expect(() => bindKaleidoswapTools(handlers)).toThrow(/no handler/);
123
+ });
124
+
125
+ it('skips missing handlers when allowMissing is true', () => {
126
+ const handlers: Record<string, KaleidoswapHandler> = {
127
+ kaleidoswap_get_pairs: async () => ({ ok: true }),
128
+ kaleidoswap_get_quote: async () => ({ ok: true }),
129
+ };
130
+ const src = bindKaleidoswapTools(handlers, { allowMissing: true });
131
+ const names = src.listTools().map((t) => t.name);
132
+ expect(names).toEqual(['kaleidoswap_get_pairs', 'kaleidoswap_get_quote']);
133
+ });
134
+
135
+ it('respects the groups filter when binding', () => {
136
+ const src = bindKaleidoswapTools(echoHandlers(), { groups: ['market'] });
137
+ const names = src.listTools().map((t) => t.name);
138
+ expect(names).toContain('kaleidoswap_get_quote');
139
+ expect(names).not.toContain('kaleidoswap_place_order');
140
+ expect(names).not.toContain('kaleidoswap_atomic_init');
141
+ });
142
+
143
+ it('uses opts.id for the ToolSource id', () => {
144
+ const src = bindKaleidoswapTools(echoHandlers(), { id: 'maker-prod' });
145
+ expect(src.id).toBe('maker-prod');
146
+ });
147
+ });