@kaleidorg/mind 0.4.0 → 0.5.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 (79) hide show
  1. package/dist/funnel.d.ts +19 -0
  2. package/dist/funnel.d.ts.map +1 -1
  3. package/dist/funnel.js +48 -10
  4. package/dist/funnel.js.map +1 -1
  5. package/dist/index.d.ts +5 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +10 -3
  8. package/dist/index.js.map +1 -1
  9. package/dist/kaleidoswap/contract.d.ts +3 -3
  10. package/dist/kaleidoswap/contract.d.ts.map +1 -1
  11. package/dist/kaleidoswap/contract.js +16 -4
  12. package/dist/kaleidoswap/contract.js.map +1 -1
  13. package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
  14. package/dist/knowledge/bitcoin-copilot.js +102 -0
  15. package/dist/knowledge/bitcoin-copilot.js.map +1 -1
  16. package/dist/knowledge/btc-map.d.ts +14 -17
  17. package/dist/knowledge/btc-map.d.ts.map +1 -1
  18. package/dist/knowledge/btc-map.js +66 -266
  19. package/dist/knowledge/btc-map.js.map +1 -1
  20. package/dist/lsps1/contract.d.ts.map +1 -1
  21. package/dist/lsps1/contract.js +28 -10
  22. package/dist/lsps1/contract.js.map +1 -1
  23. package/dist/recipe/buy-asset-channel.d.ts +26 -0
  24. package/dist/recipe/buy-asset-channel.d.ts.map +1 -0
  25. package/dist/recipe/buy-asset-channel.js +112 -0
  26. package/dist/recipe/buy-asset-channel.js.map +1 -0
  27. package/dist/recipe/kaleidoswap-atomic.d.ts +26 -18
  28. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
  29. package/dist/recipe/kaleidoswap-atomic.js +101 -63
  30. package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
  31. package/dist/recipe/kaleidoswap-channel-order.d.ts +35 -0
  32. package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -0
  33. package/dist/recipe/kaleidoswap-channel-order.js +493 -0
  34. package/dist/recipe/kaleidoswap-channel-order.js.map +1 -0
  35. package/dist/recipe/kaleidoswap-price.d.ts +21 -0
  36. package/dist/recipe/kaleidoswap-price.d.ts.map +1 -0
  37. package/dist/recipe/kaleidoswap-price.js +57 -0
  38. package/dist/recipe/kaleidoswap-price.js.map +1 -0
  39. package/dist/recipe/runner.d.ts +7 -1
  40. package/dist/recipe/runner.d.ts.map +1 -1
  41. package/dist/recipe/runner.js +115 -29
  42. package/dist/recipe/runner.js.map +1 -1
  43. package/dist/recipe/swap.d.ts +26 -1
  44. package/dist/recipe/swap.d.ts.map +1 -1
  45. package/dist/recipe/swap.js +108 -13
  46. package/dist/recipe/swap.js.map +1 -1
  47. package/dist/recipe/types.d.ts +25 -1
  48. package/dist/recipe/types.d.ts.map +1 -1
  49. package/dist/skills/registry.d.ts +33 -1
  50. package/dist/skills/registry.d.ts.map +1 -1
  51. package/dist/skills/registry.js +45 -1
  52. package/dist/skills/registry.js.map +1 -1
  53. package/package.json +1 -1
  54. package/skills/README.md +3 -0
  55. package/skills/kaleido-lsps/SKILL.md +101 -43
  56. package/skills/kaleido-trading/SKILL.md +81 -31
  57. package/skills/merchant-finder/SKILL.md +96 -66
  58. package/skills/rgb-lightning-node/SKILL.md +108 -0
  59. package/skills/wallet-assistant/SKILL.md +32 -21
  60. package/src/funnel.ts +66 -11
  61. package/src/index.ts +14 -2
  62. package/src/kaleidoswap/contract.test.ts +7 -2
  63. package/src/kaleidoswap/contract.ts +27 -5
  64. package/src/knowledge/bitcoin-copilot.ts +111 -0
  65. package/src/knowledge/btc-map.test.ts +53 -96
  66. package/src/knowledge/btc-map.ts +72 -287
  67. package/src/lsps1/contract.ts +32 -14
  68. package/src/recipe/buy-asset-channel.test.ts +148 -0
  69. package/src/recipe/buy-asset-channel.ts +118 -0
  70. package/src/recipe/kaleidoswap-atomic.test.ts +134 -61
  71. package/src/recipe/kaleidoswap-atomic.ts +112 -66
  72. package/src/recipe/kaleidoswap-channel-order.test.ts +333 -0
  73. package/src/recipe/kaleidoswap-channel-order.ts +548 -0
  74. package/src/recipe/kaleidoswap-price.ts +68 -0
  75. package/src/recipe/recipe.test.ts +61 -5
  76. package/src/recipe/runner.ts +128 -31
  77. package/src/recipe/swap.ts +109 -13
  78. package/src/recipe/types.ts +25 -1
  79. package/src/skills/registry.ts +52 -1
@@ -0,0 +1,108 @@
1
+ ---
2
+ name: rgb-lightning-node
3
+ description: "Drive the user's local RGB Lightning Node (RLN) — read its pubkey/status, whitelist a swap, or create Lightning/RGB receive invoices. Triggers when the user asks about the node, needs an invoice, or is mid-atomic-swap and the maker needs the node pubkey or a swapstring whitelisted."
4
+ tools: rln_get_node_info, rln_whitelist_swap, rln_create_ln_invoice, rln_create_rgb_invoice
5
+ triggers: node, nodeinfo, pubkey, peer, channels, whitelist, taker, swapstring, invoice, receive, rgb invoice, ln invoice
6
+ metadata:
7
+ author: kaleidoswap
8
+ version: "0.1.0"
9
+ ---
10
+
11
+ # RGB Lightning Node (taker-side)
12
+
13
+ You drive the **user's own** RGB Lightning Node running locally. In a KaleidoSwap
14
+ atomic swap the **maker** owns init / execute / status (those are
15
+ `kaleidoswap_atomic_*` tools, separate REST endpoints). The node's job in a
16
+ swap is narrow: **expose its pubkey and whitelist the maker's swapstring**.
17
+ The node does NOT init or execute swaps.
18
+
19
+ ## Critical rules
20
+
21
+ You have **no knowledge** of the node's pubkey, channel state, balance, or any
22
+ invoice contents. Every value in your reply MUST come from a tool result
23
+ returned in the CURRENT turn — never invent a pubkey, channel id, invoice
24
+ string, or sats balance. Never reuse a value from a previous turn.
25
+
26
+ **Calling the tool IS the answer.** If the user asks "what's my pubkey?", call
27
+ `rln_get_node_info` — do not describe how to fetch it.
28
+
29
+ ## When to use each tool
30
+
31
+ ### `rln_get_node_info` — no args
32
+ Returns:
33
+ - `pubkey` — the node's identity (32-byte hex).
34
+ - `num_channels` — total channels (may include unusable ones).
35
+ - `num_usable_channels` — subset that can route a payment right now.
36
+ - `local_balance_sat` — **sats YOU own** across all channels. This is your
37
+ **spend** capacity (outbound). It is **NOT** receive capacity, **NOT**
38
+ inbound liquidity, and **NOT** total channel capacity.
39
+ - `pending_outbound_payments_sat` — in-flight, temporarily locked.
40
+ - `num_peers` — currently connected peers.
41
+
42
+ Call this when:
43
+ - The user asks about the node, pubkey, peers, channel count, or how much
44
+ they can **spend**.
45
+ - An atomic swap is in progress and the maker needs `taker_pubkey` —
46
+ fetch the pubkey from this tool's `pubkey` field and pass it to
47
+ `kaleidoswap_atomic_execute`.
48
+
49
+ **Do NOT** use this tool's `local_balance_sat` to answer a question about
50
+ **inbound liquidity / receive capacity** — that is a different quantity
51
+ (the peer's side of each channel, not yours). For "how much can I receive?",
52
+ the LSPS skill answers what's available to BUY (`lsp_get_info`); the current
53
+ remote-balance breakdown isn't exposed by this skill's tools.
54
+
55
+ ### `rln_whitelist_swap` — { swapstring } — 🔒 confirm-gated
56
+ Tell the node "I accept this swap." Args: the `swapstring` returned by
57
+ `kaleidoswap_atomic_init`. The node validates and stores it; **no funds move
58
+ here**, but the user is committing to the swap so the engine pauses for
59
+ confirmation.
60
+
61
+ Call this **after** `kaleidoswap_atomic_init` and **before**
62
+ `kaleidoswap_atomic_execute`. Never call with an empty or invented swapstring
63
+ — the node will reject it.
64
+
65
+ ### `rln_create_ln_invoice` — Lightning invoice for receiving sats
66
+ Args:
67
+ - `amount_sats` (optional) — omit for an amountless invoice.
68
+ - `expiry_sec` (default 3600) — invoice TTL in seconds.
69
+ - `asset_id` + `asset_amount` — optional, for RGB-over-Lightning.
70
+
71
+ Use when the user wants to **receive** a Lightning payment. Do NOT call inside
72
+ an atomic swap flow unless the user explicitly asked to invoice someone.
73
+
74
+ ### `rln_create_rgb_invoice` — on-chain RGB receive invoice
75
+ Args:
76
+ - `min_confirmations` (default 1).
77
+ - `witness` (default false).
78
+ - `asset_id` (optional — omit for an any-asset invoice).
79
+ - `expiration_timestamp` (optional, Unix seconds).
80
+
81
+ Use when the user wants to **receive** an RGB asset directly (not over
82
+ Lightning). Outside the atomic swap flow.
83
+
84
+ ## The maker / node split
85
+
86
+ A user-driven swap on KaleidoSwap is a two-service flow. Keep them straight:
87
+
88
+ | Step | Owner | Tool |
89
+ |------|-------|------|
90
+ | Quote | maker | `kaleidoswap_get_quote` |
91
+ | Init | maker | `kaleidoswap_atomic_init` (returns swapstring + payment_hash) |
92
+ | Pubkey | **node** | `rln_get_node_info` (read `pubkey`) |
93
+ | Whitelist | **node** | `rln_whitelist_swap` (pass the swapstring) |
94
+ | Execute | maker | `kaleidoswap_atomic_execute` (needs swapstring + taker_pubkey + payment_hash) |
95
+ | Status | maker | `kaleidoswap_atomic_status` |
96
+
97
+ The node's two contributions to the swap are the **pubkey** and the
98
+ **whitelist ack** — nothing more. Don't reach for `/makerinit` or
99
+ `/makerexecute`; those are for nodes that act AS the maker, which is not us.
100
+
101
+ ## Reply style
102
+
103
+ - One short sentence built from the tool result.
104
+ - Pubkeys are long hex strings — quote them in monospace if you can, never
105
+ truncate them when the user explicitly asked for them.
106
+ - For `rln_get_node_info`, if the user just said "what's my node status?",
107
+ surface pubkey + num_usable_channels + local_balance_sat. Don't dump the
108
+ whole `details` object.
@@ -1,38 +1,49 @@
1
1
  ---
2
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
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, or quote a swap (e.g. "how many sats is 10 USDT?"). Triggers when the user asks about their balance, wants to receive or send money, pay an invoice, pay a contact, or convert between BTC and supported assets.
4
+ tools: get_balances, resolve_contact, send_payment, rln_pay_invoice, rln_create_ln_invoice, spark_create_invoice, kaleidoswap_get_quote
5
+ triggers: balance, pay, send, receive, address, invoice, transactions, contact, funds, money, sats
6
6
  ---
7
7
 
8
8
  # Wallet assistant
9
9
 
10
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.
11
+ get real data — NEVER invent a balance, address, amount, price, or quote.
12
12
 
13
13
  ## Critical rules
14
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.
15
+ You have no knowledge of balances, addresses, invoices, prices, or quotes.
16
+ Every value in your reply MUST come from a tool result returned in the CURRENT
17
+ turn — do not reuse a number from a previous turn.
18
+
19
+ NEVER mention the exact name of any tool (such as "kaleidoswap_get_quote") in
20
+ your text reply to the user. Only use tools via the proper function call
21
+ format when needed; describe what you are doing in plain language.
18
22
 
19
23
  When a tool returns multiple fields, **report all the load-bearing ones**:
20
- - `get_balances` may return `{confirmed, pending, total}` — when `pending`
24
+ - The balance tool may return `{confirmed, pending, total}` — when `pending`
21
25
  is non-zero, report BOTH. `confirmed` is spendable; `pending` is settling
22
26
  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
- approximationsurface the note.
27
+ - The quote tool returns display strings (e.g. the to amount and fee in
28
+ human readable form). Read these strings verbatim do NOT do unit math
29
+ yourself.
25
30
 
26
31
  ## Rules
27
32
 
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.
33
+ - **Balance / "how much do I have"**use the balance tool, then state the
34
+ number.
35
+ - **Receive / "an invoice for N sats"** → use the invoice creation tool (for
36
+ the appropriate layer) with the amount.
37
+ - **"How many sats is N USDT?" / "What's 0.1 BTC in USDT?" / "convert N X
38
+ to Y"**use the quote/conversion tool and
39
+ read the display fields from the result. Supported pairs: any of
40
+ BTC/USDT/XAUT against each other. Fiat (EUR/USD/GBP) is NOT quoted by
41
+ the maker — if the user asks "how much sats is 10 EUR", say so plainly
42
+ and offer USDT as the closest analogue (USDT is a USD-pegged stablecoin).
43
+ - **Pay a Lightning invoice** use the lightning payment tool.
44
+ - **Send to a person/amount** → first resolve the contact, then use the send
45
+ payment tool with the amount in sats and the recipient. State the amount and
46
+ destination; the app asks the user to confirm before it sends.
47
+
48
+ Keep replies short, but never drop a balance component, a fee, or the
49
+ quote's `rfq_id` when present.
package/src/funnel.ts CHANGED
@@ -31,6 +31,7 @@ import type { Recipe } from './recipe/types.js';
31
31
  import { SkillRegistry } from './skills/registry.js';
32
32
  import type { Skill } from './skills/types.js';
33
33
  import type { LLMProvider } from './providers/types.js';
34
+ import type { Retriever } from './rag/retriever.js';
34
35
  import type { ConfirmDecision, Message, ToolResult } from './types.js';
35
36
 
36
37
  /** Base system prompt for the wallet assistant. Hosts may override. */
@@ -53,6 +54,16 @@ export const DEFAULT_WALLET_SYSTEM = [
53
54
  ' the required field missing.',
54
55
  '5. All BTC amounts are in satoshis. Asset codes are case-insensitive but the',
55
56
  ' canonical forms are BTC, USDT, XAUT — do not silently shorten to USD, XAU.',
57
+ '6. NEVER name a tool, function, endpoint, or argument key in your reply.',
58
+ ' Tools are private plumbing. Bad: "use kaleidoswap_get_quote with amount 1".',
59
+ ' Good: "I can quote that for you — one moment." If you need information',
60
+ ' from the user (like an amount), ASK in plain English without referencing',
61
+ ' how the system will use it.',
62
+ '7. A price/rate question (e.g. "price of USDT", "BTC price", "how much is',
63
+ ' 1 USDT") is a UNIT QUOTE — answer with the value of 1 of the named asset',
64
+ ' in the denomination the user asked for (default: sats when pricing USDT/',
65
+ ' XAUT, USDT when pricing BTC). Never ask the user "how much do you want"',
66
+ ' for a price question.',
56
67
  '',
57
68
  'Keep replies short and friendly. When a tool returns multiple fields, surface',
58
69
  "the ones that matter — never collapse a structured result to a single number",
@@ -98,12 +109,14 @@ export interface FunnelCallbacks {
98
109
  arguments: Record<string, unknown>;
99
110
  result: unknown;
100
111
  }) => void;
101
- onConfirm?: (call: { name: string; arguments: Record<string, unknown> }) => Promise<ConfirmDecision>;
112
+ onConfirm?: (call: { name: string; arguments: Record<string, unknown>; summary?: string }) => Promise<ConfirmDecision>;
102
113
  }
103
114
 
104
115
  export interface FunnelResult {
105
116
  text: string;
106
117
  tier: 'fast' | 'recipe' | 'agentic';
118
+ /** What handled the turn: the intent (fast), recipe name (recipe), or skill name (agentic). */
119
+ route?: string;
107
120
  /** Fast tier only: the matched intent + raw tool result (e.g. for a balance card). */
108
121
  intent?: string;
109
122
  data?: unknown;
@@ -132,6 +145,19 @@ export interface FunnelOptions {
132
145
  renderFast?: (intent: string, result: unknown) => string;
133
146
  /** Diagnostics sink (tier routing, tool calls). Default: silent. */
134
147
  log?: (message: string) => void;
148
+ /**
149
+ * Optional retriever for AUTO-injecting relevant knowledge chunks into the
150
+ * agentic-tier system prompt (T1 only — recipes/fast-path are deterministic
151
+ * and don't need it). When set, the top-`topKRag` chunks for the user's
152
+ * query are prepended as `## Relevant context`. Default: no auto-inject
153
+ * (the `search_knowledge` tool stays available for on-demand lookups).
154
+ *
155
+ * Small models often don't choose to call search_knowledge; auto-inject
156
+ * makes the corpus useful by default without the model having to opt in.
157
+ */
158
+ retriever?: Retriever;
159
+ /** How many chunks to auto-inject when `retriever` is set. Default 3. */
160
+ topKRag?: number;
135
161
  }
136
162
 
137
163
  function defaultRenderFast(intent: string, r: any): string {
@@ -157,6 +183,8 @@ export class Funnel {
157
183
  private readonly getSettings: () => FunnelSettings;
158
184
  private readonly renderFast: (intent: string, result: unknown) => string;
159
185
  private readonly log: (message: string) => void;
186
+ private readonly retriever?: Retriever;
187
+ private readonly topKRag: number;
160
188
 
161
189
  /** Skill registry, rebuilt only when the disabled-skills set changes. */
162
190
  private skillsCache: { key: string; reg: SkillRegistry } | null = null;
@@ -176,6 +204,8 @@ export class Funnel {
176
204
  this.getSettings = opts.getSettings ?? (() => ({}));
177
205
  this.renderFast = opts.renderFast ?? defaultRenderFast;
178
206
  this.log = opts.log ?? (() => {});
207
+ if (opts.retriever) this.retriever = opts.retriever;
208
+ this.topKRag = opts.topKRag ?? 3;
179
209
  }
180
210
 
181
211
  /** Skills currently enabled (e.g. for a skills sheet). */
@@ -205,18 +235,24 @@ export class Funnel {
205
235
  if (fast && (await this.registry.getDef(fast.tool))) {
206
236
  this.log(`tier=fast-path → ${fast.tool}`);
207
237
  const r = await this.registry.execute(fast.tool, fast.args);
208
- return { text: this.renderFast(fast.intent.name, r), tier: 'fast', intent: fast.intent.name, data: r };
238
+ return { text: this.renderFast(fast.intent.name, r), tier: 'fast', route: fast.intent.name, intent: fast.intent.name, data: r };
209
239
  }
210
240
 
211
- // ── T2: recipe multi-step — fires only when the recipe is confident given
212
- // its extracted slots (payments need a recipient; receive always fires),
213
- // and the registry implements the recipe's final action.
241
+ // ── T2: recipe multi-step — fires when:
242
+ // (a) the recipe is confident given its deterministic slots, OR
243
+ // (b) `forceModelExtract` is on — the LLM does the actual extraction
244
+ // inside runRecipe, so we don't gate on the regex result. If the
245
+ // LLM still doesn't yield enough, runRecipe returns status:
246
+ // 'needs-info' with a friendly "please specify X" instead of
247
+ // running steps with bad data.
248
+ // Either way the registry must implement the recipe's final action.
214
249
  const recipe = this.recipes.select(text);
215
250
  const slots = recipe?.extract?.(text) ?? null;
251
+ const deterministicallyConfident =
252
+ !!slots && (recipe?.confident ? recipe.confident(slots) : Object.keys(slots).length > 0);
216
253
  const fires =
217
254
  !!recipe &&
218
- !!slots &&
219
- (recipe.confident ? recipe.confident(slots) : Object.keys(slots).length > 0) &&
255
+ (recipe.forceModelExtract === true || deterministicallyConfident) &&
220
256
  !!(await this.registry.getDef(recipe.final.tool));
221
257
  if (recipe && fires) {
222
258
  this.log(`tier=recipe:${recipe.name} slots=${JSON.stringify(slots)}`);
@@ -229,19 +265,38 @@ export class Funnel {
229
265
  cbs.onStep?.(name);
230
266
  },
231
267
  });
232
- return { text: res.text, tier: 'recipe' };
268
+ return { text: res.text, tier: 'recipe', route: recipe.name };
233
269
  }
234
270
 
235
271
  // ── T1: skill-scoped agentic loop ──
236
272
  const skills = this.skillsFor(settings.disabledSkills);
237
273
  const skill = skills.select(text);
238
- const base = settings.persona ? `${this.system}\n\n## Your persona\n${settings.persona}` : this.system;
274
+ let base = settings.persona ? `${this.system}\n\n## Your persona\n${settings.persona}` : this.system;
275
+
276
+ // Auto-inject relevant knowledge chunks (best-effort — corpus is grounding
277
+ // truth, history is conversational context; both reach the model but the
278
+ // RAG block sits above history so the model treats it as authoritative).
279
+ // Only fires for agentic turns and only when the host opts in via
280
+ // `retriever` AND the user hasn't disabled RAG in settings.
281
+ const ragOn = settings.ragEnabled !== false;
282
+ if (this.retriever && ragOn && this.topKRag > 0) {
283
+ try {
284
+ const hits = await this.retriever.search(text, this.topKRag);
285
+ if (hits.length) {
286
+ const chunks = hits.map((h) => `- ${h.text}`).join('\n');
287
+ base = `${base}\n\n## Relevant context (read first; trust this over conversation history)\n${chunks}`;
288
+ this.log(`rag injected ${hits.length} chunks`);
289
+ }
290
+ } catch (e) {
291
+ this.log(`rag failed: ${(e as Error)?.message ?? e}`);
292
+ }
293
+ }
294
+
239
295
  const { system, allowedTools } = skills.compose(base, skill);
240
296
 
241
297
  // Ambient tools stay available even when a skill narrows the set — gated
242
298
  // by the user's memory/knowledge toggles (default on).
243
299
  const memoryOn = settings.memoryEnabled !== false;
244
- const ragOn = settings.ragEnabled !== false;
245
300
  const ambient = [...(memoryOn ? AMBIENT_MEMORY : []), ...(ragOn ? AMBIENT_RAG : [])];
246
301
  const disabledAmbient = [...(memoryOn ? [] : AMBIENT_MEMORY), ...(ragOn ? [] : AMBIENT_RAG)];
247
302
  let scoped: string[] | undefined;
@@ -280,6 +335,6 @@ export class Funnel {
280
335
  onToolResult: cbs.onToolResult,
281
336
  onConfirm: cbs.onConfirm,
282
337
  });
283
- return { text: res.text ?? '', tier: 'agentic', toolCalls: res.toolCalls, turns: res.turns };
338
+ return { text: res.text ?? '', tier: 'agentic', route: skill?.name, toolCalls: res.toolCalls, turns: res.turns };
284
339
  }
285
340
  }
package/src/index.ts CHANGED
@@ -81,8 +81,19 @@ export type {
81
81
  BindLsps1Options,
82
82
  } from './lsps1/contract.js';
83
83
 
84
- // ── KaleidoSwap atomic-swap recipe (opt-in — register via Funnel.recipes) ──
84
+ // ── KaleidoSwap recipes (opt-in — register via Funnel.recipes) ──
85
+ // price recipe is read-only (quote-only); atomic recipe runs the full swap.
86
+ // Register the price recipe FIRST so phrasings like "BTC price" are answered
87
+ // without firing any spend.
88
+ export { kaleidoswapPriceRecipe } from './recipe/kaleidoswap-price.js';
85
89
  export { kaleidoswapAtomicRecipe } from './recipe/kaleidoswap-atomic.js';
90
+ export {
91
+ kaleidoswapChannelOrderRecipe,
92
+ extractChannelOrder,
93
+ } from './recipe/kaleidoswap-channel-order.js';
94
+
95
+ // ── Buy-an-asset-channel recipe (opt-in — register via Funnel.recipes) ─────
96
+ export { buyAssetChannelRecipe, extractBuyAsset } from './recipe/buy-asset-channel.js';
86
97
 
87
98
  // ── Recipes (mobile multi-step: "recipes, not planning") ───────────────────
88
99
  export { runRecipe, extractSlots, RecipeRegistry } from './recipe/runner.js';
@@ -145,7 +156,7 @@ export { walletHistoryToDocuments, contactsToDocuments } from './knowledge/walle
145
156
  export type { WalletTx, Contact } from './knowledge/wallet.js';
146
157
  export { merchantsToDocuments } from './knowledge/merchants.js';
147
158
  export type { Merchant } from './knowledge/merchants.js';
148
- export { createBtcMapToolSource, BTC_MAP_SAMPLE } from './knowledge/btc-map.js';
159
+ export { createBtcMapToolSource } from './knowledge/btc-map.js';
149
160
  export type {
150
161
  BtcMapToolOptions,
151
162
  BtcMapMerchant,
@@ -165,6 +176,7 @@ export {
165
176
  SkillRegistry,
166
177
  parseSkill,
167
178
  keywordSelector,
179
+ createEmbeddingSkillSelector,
168
180
  READ_REFERENCE_TOOL,
169
181
  } from './skills/registry.js';
170
182
  export { createSkillReferenceToolSource } from './skills/reference-source.js';
@@ -23,12 +23,14 @@ describe('KALEIDOSWAP_TOOLS — shape invariants', () => {
23
23
  'kaleidoswap_atomic_init',
24
24
  'kaleidoswap_atomic_execute',
25
25
  'kaleidoswap_atomic_status',
26
+ 'kaleidoswap_lsp_quote_asset_channel',
27
+ 'kaleidoswap_lsp_create_asset_channel',
26
28
  ]);
27
29
  });
28
30
 
29
31
  it('every tool has a group and a parameters object', () => {
30
32
  for (const t of KALEIDOSWAP_TOOLS) {
31
- expect(['market', 'orders', 'atomic']).toContain(t.group);
33
+ expect(['market', 'orders', 'atomic', 'liquidity']).toContain(t.group);
32
34
  expect(t.parameters).toBeDefined();
33
35
  expect((t.parameters as any).type).toBe('object');
34
36
  }
@@ -43,10 +45,11 @@ describe('KALEIDOSWAP_TOOLS — shape invariants', () => {
43
45
  it('lists every spend tool exactly once in KALEIDOSWAP_SPEND_TOOLS', () => {
44
46
  const expected = KALEIDOSWAP_TOOLS.filter((t) => t.spend).map((t) => t.name).sort();
45
47
  expect([...KALEIDOSWAP_SPEND_TOOLS].sort()).toEqual(expected);
46
- // Sanity: place_order, atomic_init, atomic_execute are spend; the rest aren't.
48
+ // Sanity: place_order, atomic_init/execute, create_asset_channel are spend; the rest aren't.
47
49
  expect(expected).toEqual([
48
50
  'kaleidoswap_atomic_execute',
49
51
  'kaleidoswap_atomic_init',
52
+ 'kaleidoswap_lsp_create_asset_channel',
50
53
  'kaleidoswap_place_order',
51
54
  ]);
52
55
  });
@@ -95,6 +98,8 @@ describe('bindKaleidoswapTools', () => {
95
98
  kaleidoswap_atomic_init: async (a) => ({ ok: true, tool: 'atomic_init', args: a }),
96
99
  kaleidoswap_atomic_execute: async (a) => ({ ok: true, tool: 'atomic_execute', args: a }),
97
100
  kaleidoswap_atomic_status: async (a) => ({ ok: true, tool: 'atomic_status', args: a }),
101
+ kaleidoswap_lsp_quote_asset_channel: async (a) => ({ ok: true, tool: 'lsp_quote_asset_channel', args: a }),
102
+ kaleidoswap_lsp_create_asset_channel: async (a) => ({ ok: true, tool: 'lsp_create_asset_channel', args: a }),
98
103
  });
99
104
 
100
105
  it('binds every tool when all handlers are present', () => {
@@ -9,8 +9,8 @@
9
9
  * - eval → stub handlers, also via `bindKaleidoswapTools`
10
10
  *
11
11
  * Because the schemas are identical everywhere, skills are portable and the
12
- * model comparison is honest. Tools are grouped (`market`, `orders`, `atomic`)
13
- * so a host can expose a read-only subset for sandbox/eval modes.
12
+ * model comparison is honest. Tools are grouped (`market`, `orders`, `atomic`,
13
+ * `liquidity`) so a host can expose a read-only subset for sandbox/eval modes.
14
14
  *
15
15
  * Spend tools (place an order, init/execute an atomic swap) carry
16
16
  * `spend: true` → `requiresConfirmation: true`, so the Engine always pauses
@@ -24,7 +24,7 @@ import { InProcessToolSource } from '../tools/in-process.js';
24
24
  import type { InProcessTool } from '../tools/in-process.js';
25
25
 
26
26
  /** Functional grouping for selective binding (e.g. read-only sandbox). */
27
- export type KaleidoswapGroup = 'market' | 'orders' | 'atomic';
27
+ export type KaleidoswapGroup = 'market' | 'orders' | 'atomic' | 'liquidity';
28
28
 
29
29
  export interface KaleidoswapToolDef extends ToolDef {
30
30
  /** Functional group — lets a host expose a subset. */
@@ -86,7 +86,7 @@ export const KALEIDOSWAP_TOOLS: KaleidoswapToolDef[] = [
86
86
  // ─── orders (orderbook / market-order flow) ─────────────────────────────
87
87
  t('orders',
88
88
  'kaleidoswap_place_order',
89
- 'Place an order using an executable quote. Returns an order id. SPEND: the host pauses for user confirmation before the maker is called. Use only after kaleidoswap_get_quote and only when the user has explicitly approved the amount + destination.',
89
+ 'Place an order using an executable quote. Returns order_id + access_token (save the token — required for get_order_status). SPEND: the host pauses for user confirmation before the maker is called. Use only after kaleidoswap_get_quote and only when the user has explicitly approved the amount + destination.',
90
90
  {
91
91
  quote_id: { type: 'string', description: 'The quote id returned by kaleidoswap_get_quote (must still be valid).' },
92
92
  },
@@ -95,9 +95,10 @@ export const KALEIDOSWAP_TOOLS: KaleidoswapToolDef[] = [
95
95
 
96
96
  t('orders',
97
97
  'kaleidoswap_get_order_status',
98
- 'Check the status of an order by id — pending / settling / completed / failed. Poll this after place_order until the order settles.',
98
+ 'Check the status of an order by id — pending / settling / completed / failed. Poll this after place_order until the order settles. Requires the access_token returned by place_order for authenticated orders.',
99
99
  {
100
100
  order_id: { type: 'string', description: 'The order id returned by kaleidoswap_place_order.' },
101
+ access_token: { type: 'string', description: 'The per-order access token returned by kaleidoswap_place_order. Required for status checks on the order.' },
101
102
  },
102
103
  ['order_id']),
103
104
 
@@ -136,6 +137,27 @@ export const KALEIDOSWAP_TOOLS: KaleidoswapToolDef[] = [
136
137
  atomic_id: { type: 'string', description: 'The atomic id from kaleidoswap_atomic_init.' },
137
138
  },
138
139
  ['atomic_id']),
140
+
141
+ // ─── liquidity (buy a NEW channel pre-loaded with an asset — onboarding) ──
142
+ t('liquidity',
143
+ 'kaleidoswap_lsp_quote_asset_channel',
144
+ 'Quote buying a NEW Lightning channel pre-loaded with an RGB asset (e.g. USDT, XAUT) from the maker LSP. This is the onboarding path for a user who has on-chain BTC but no channel yet and wants to hold an asset — they pay once to receive a channel that already holds the asset. Read-only: returns an rfq_id, the BTC price in sats, the channel/setup fee, the total to pay, and when the quote expires. Re-quote rather than reusing a stale rfq_id.',
145
+ {
146
+ asset: { type: 'string', description: 'RGB asset to receive in the channel, e.g. "USDT" or "XAUT".' },
147
+ asset_amount: { type: 'number', description: 'How much of the asset to load into the channel, in the asset’s display units (e.g. 100 for 100 USDT).' },
148
+ },
149
+ ['asset', 'asset_amount']),
150
+
151
+ t('liquidity',
152
+ 'kaleidoswap_lsp_create_asset_channel',
153
+ 'Order a new Lightning channel pre-loaded with an RGB asset from the maker LSP, using a fresh rfq_id from kaleidoswap_lsp_quote_asset_channel. SPEND: confirmation-gated. Returns an order id and the payment (on-chain address or Lightning invoice) the user pays to open the channel; the channel opens only after the payment confirms. Poll kaleidoswap_lsp_get_order to track it.',
154
+ {
155
+ asset: { type: 'string', description: 'RGB asset to receive (must match the quote).' },
156
+ asset_amount: { type: 'number', description: 'Asset amount in display units (must match the quote).' },
157
+ rfq_id: { type: 'string', description: 'The rfq_id from kaleidoswap_lsp_quote_asset_channel (must still be valid).' },
158
+ },
159
+ ['asset', 'asset_amount', 'rfq_id'],
160
+ /* spend */ true),
139
161
  ];
140
162
 
141
163
  /** All tool names that move funds (confirmation-gated). */
@@ -174,4 +174,115 @@ export const BITCOIN_COPILOT_DOCS: RagDocument[] = [
174
174
  'accumulate BTC or an asset over time without timing the market.',
175
175
  metadata: { topic: 'trading' },
176
176
  },
177
+ {
178
+ id: 'spend-vs-receive-capacity',
179
+ text:
180
+ 'Two completely different numbers, often confused: your SPEND capacity ' +
181
+ '(outbound, local balance — what you can send right now) and your ' +
182
+ 'RECEIVE capacity (inbound, remote balance — what others can pay you ' +
183
+ 'without opening a new channel). Knowing local_balance does NOT tell ' +
184
+ 'you receive capacity, and vice versa. "How much can I spend?" → local ' +
185
+ 'balance. "How much can I receive?" → inbound, derived from channels ' +
186
+ 'or bought from an LSP.',
187
+ metadata: { topic: 'liquidity' },
188
+ },
189
+ {
190
+ id: 'nodeinfo-fields',
191
+ text:
192
+ 'Common RGB Lightning Node fields and what they actually mean: pubkey ' +
193
+ '(your node identity); num_channels (total channels, including unusable ' +
194
+ 'ones); num_usable_channels (subset that can route — what you spend ' +
195
+ 'with); local_balance_sat (sats YOU own across all channels — your ' +
196
+ 'spend / outbound capacity); pending_outbound_payments_sat (in-flight, ' +
197
+ 'temporarily locked); eventual_close_fees_sat (cost if you close every ' +
198
+ 'channel now); num_peers (connected peers). local_balance_sat is NOT ' +
199
+ 'receive capacity and NOT total channel capacity.',
200
+ metadata: { topic: 'lightning' },
201
+ },
202
+ {
203
+ id: 'channel-two-sided',
204
+ text:
205
+ 'Every Lightning channel has TWO balances: your side (local — what you ' +
206
+ 'can spend) and the peer\'s side (remote — what they can spend, which ' +
207
+ 'is what YOU can receive). Total channel capacity = local + remote and ' +
208
+ 'is fixed at open time. Routing a payment moves sats from one side to ' +
209
+ 'the other; it does NOT change total capacity. So if you "have 2 ' +
210
+ 'channels with 1,000,000 sats total capacity", that does NOT mean you ' +
211
+ 'can spend 1M and receive 1M — only the split tells you.',
212
+ metadata: { topic: 'channels' },
213
+ },
214
+ {
215
+ id: 'lsp-info-meaning',
216
+ text:
217
+ 'The LSPS1 `get_info` endpoint returns the LSP\'s OFFER (min/max ' +
218
+ 'channel size you can buy, fees, accepted payment options). It is NOT ' +
219
+ 'your current inbound capacity — it describes what the LSP is willing ' +
220
+ 'to sell you. To learn your CURRENT receive capacity, sum the remote ' +
221
+ 'balance of your existing channels; to BUY MORE, use lsp_get_info and ' +
222
+ 'lsp_create_order.',
223
+ metadata: { topic: 'channels' },
224
+ },
225
+ {
226
+ id: 'asset-channels',
227
+ text:
228
+ 'RGB asset channels (colored channels) carry one specific asset like ' +
229
+ 'USDT or XAUT alongside the BTC funding. Asset capacity is SEPARATE per ' +
230
+ 'asset — having 100,000 sats spendable in BTC channels does not give ' +
231
+ 'you USDT spendable; you need a USDT channel (or buy one via LSPS1). ' +
232
+ 'Likewise, USDT inbound and BTC inbound are independent numbers.',
233
+ metadata: { topic: 'rgb' },
234
+ },
235
+ {
236
+ id: 'swap-vs-payment',
237
+ text:
238
+ 'Swap and payment are different actions. A SWAP trades one asset for ' +
239
+ 'another via the KaleidoSwap maker (quote → init → execute). A PAYMENT ' +
240
+ 'moves an existing balance to a recipient over Lightning or on-chain ' +
241
+ '(no maker). "Send 10 USDT to Alice" is a payment; "swap 10 USDT to ' +
242
+ 'BTC" is a swap. They use different tools, different fees, and a swap ' +
243
+ 'is always between two assets the maker prices.',
244
+ metadata: { topic: 'usage' },
245
+ },
246
+ {
247
+ id: 'asset-channel-prereq',
248
+ text:
249
+ 'Why you must buy a channel BEFORE swapping an RGB asset. An RGB ' +
250
+ 'Lightning swap moves an asset (USDT, XAUT, …) across a channel that ' +
251
+ 'already carries that asset. If you have no USDT channel, the maker ' +
252
+ 'cannot pay you USDT over Lightning — there is no rail to push it ' +
253
+ "down. Open a USDT channel from the LSP first (LSPS1 with `asset_id`, " +
254
+ '`lsp_asset_amount`) — the LSP funds the asset on their side, you get ' +
255
+ 'inbound USDT capacity, and afterwards a BTC→USDT swap can settle into ' +
256
+ 'that channel. Same for XAUT or any other RGB asset. You need ONE ' +
257
+ 'channel per asset you want to receive over Lightning.',
258
+ metadata: { topic: 'rgb-channels' },
259
+ },
260
+ {
261
+ id: 'asset-channel-buy',
262
+ text:
263
+ "Buying a channel that already has an asset inside. Use LSPS1 with " +
264
+ "`asset_id` to ask the LSP to open a channel that's pre-funded on the " +
265
+ "LSP side with a specific RGB asset. `lsp_asset_amount` is the asset " +
266
+ "units the LSP commits on their side (your future inbound capacity " +
267
+ "in that asset). `lsp_balance_sat` is the sats the LSP commits for " +
268
+ "fees/anchor; `client_balance_sat` is what you push in sats. Common " +
269
+ "shape: lsp_balance_sat 5_000_000, client_balance_sat 100_000, " +
270
+ "asset_id <USDT id>, lsp_asset_amount 100_000_000 micro-USDT (= 100 " +
271
+ "USDT). Pay the resulting Lightning invoice and the channel opens " +
272
+ "with the asset pre-loaded.",
273
+ metadata: { topic: 'rgb-channels' },
274
+ },
275
+ {
276
+ id: 'asset-channel-with-push',
277
+ text:
278
+ "Receiving an asset balance ON your side at channel open. Beyond the " +
279
+ "LSP-funded asset, you can request the LSP push some asset balance to " +
280
+ "YOUR side during the open via `client_asset_amount`. This costs sats " +
281
+ "(BTC → asset at the maker rate), so the maker requires a fresh " +
282
+ "`rfq_id` from `kaleidoswap_get_quote(BTC → asset)` to lock the " +
283
+ "price. The order then charges the BTC equivalent on top of the " +
284
+ "channel fee. Use when you want spendable asset balance immediately " +
285
+ "(not just inbound capacity).",
286
+ metadata: { topic: 'rgb-channels' },
287
+ },
177
288
  ];