@kaleidorg/mind 0.6.0 → 0.6.2

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 (109) hide show
  1. package/dist/bitrefill/contract.d.ts +60 -0
  2. package/dist/bitrefill/contract.d.ts.map +1 -0
  3. package/dist/bitrefill/contract.js +119 -0
  4. package/dist/bitrefill/contract.js.map +1 -0
  5. package/dist/context/compress.d.ts +65 -0
  6. package/dist/context/compress.d.ts.map +1 -0
  7. package/dist/context/compress.js +181 -0
  8. package/dist/context/compress.js.map +1 -0
  9. package/dist/engine.d.ts +20 -0
  10. package/dist/engine.d.ts.map +1 -1
  11. package/dist/engine.js +23 -4
  12. package/dist/engine.js.map +1 -1
  13. package/dist/evidence.d.ts +62 -0
  14. package/dist/evidence.d.ts.map +1 -0
  15. package/dist/evidence.js +47 -0
  16. package/dist/evidence.js.map +1 -0
  17. package/dist/flashnet/contract.d.ts +56 -0
  18. package/dist/flashnet/contract.d.ts.map +1 -0
  19. package/dist/flashnet/contract.js +100 -0
  20. package/dist/flashnet/contract.js.map +1 -0
  21. package/dist/funnel.d.ts +11 -0
  22. package/dist/funnel.d.ts.map +1 -1
  23. package/dist/funnel.js +50 -7
  24. package/dist/funnel.js.map +1 -1
  25. package/dist/index.d.ts +10 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +7 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/kaleidoswap/contract.js +1 -1
  30. package/dist/kaleidoswap/contract.js.map +1 -1
  31. package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
  32. package/dist/knowledge/bitcoin-copilot.js +83 -0
  33. package/dist/knowledge/bitcoin-copilot.js.map +1 -1
  34. package/dist/providers/types.d.ts +17 -0
  35. package/dist/providers/types.d.ts.map +1 -1
  36. package/dist/qvac/provider.d.ts.map +1 -1
  37. package/dist/qvac/provider.js +23 -0
  38. package/dist/qvac/provider.js.map +1 -1
  39. package/dist/qvac/stream.d.ts +6 -0
  40. package/dist/qvac/stream.d.ts.map +1 -1
  41. package/dist/qvac/stream.js +12 -0
  42. package/dist/qvac/stream.js.map +1 -1
  43. package/dist/recipe/flashnet-swap.d.ts +35 -0
  44. package/dist/recipe/flashnet-swap.d.ts.map +1 -0
  45. package/dist/recipe/flashnet-swap.js +239 -0
  46. package/dist/recipe/flashnet-swap.js.map +1 -0
  47. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
  48. package/dist/recipe/kaleidoswap-atomic.js +66 -32
  49. package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
  50. package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -1
  51. package/dist/recipe/kaleidoswap-channel-order.js +31 -10
  52. package/dist/recipe/kaleidoswap-channel-order.js.map +1 -1
  53. package/dist/recipe/kaleidoswap-price.d.ts.map +1 -1
  54. package/dist/recipe/kaleidoswap-price.js +7 -1
  55. package/dist/recipe/kaleidoswap-price.js.map +1 -1
  56. package/dist/recipe/runner.d.ts.map +1 -1
  57. package/dist/recipe/runner.js +5 -3
  58. package/dist/recipe/runner.js.map +1 -1
  59. package/dist/recipe/swap.d.ts.map +1 -1
  60. package/dist/recipe/swap.js +14 -1
  61. package/dist/recipe/swap.js.map +1 -1
  62. package/dist/wallet/confirm.d.ts.map +1 -1
  63. package/dist/wallet/confirm.js +1 -0
  64. package/dist/wallet/confirm.js.map +1 -1
  65. package/dist/wallet/contract.d.ts.map +1 -1
  66. package/dist/wallet/contract.js +20 -4
  67. package/dist/wallet/contract.js.map +1 -1
  68. package/package.json +4 -4
  69. package/skills/bitrefill/SKILL.md +152 -52
  70. package/skills/flashnet-swaps/SKILL.md +158 -0
  71. package/skills/kaleido-lsps/SKILL.md +25 -8
  72. package/skills/kaleido-trading/SKILL.md +36 -12
  73. package/skills/merchant-finder/SKILL.md +1 -1
  74. package/skills/rgb-lightning-node/SKILL.md +35 -8
  75. package/skills/spark-wallet/SKILL.md +235 -0
  76. package/skills/wallet-assistant/SKILL.md +2 -2
  77. package/src/bitrefill/contract.test.ts +89 -0
  78. package/src/bitrefill/contract.ts +190 -0
  79. package/src/context/compress.test.ts +120 -0
  80. package/src/context/compress.ts +230 -0
  81. package/src/engine.test.ts +34 -0
  82. package/src/engine.ts +35 -4
  83. package/src/evidence.test.ts +80 -0
  84. package/src/evidence.ts +114 -0
  85. package/src/flashnet/contract.test.ts +101 -0
  86. package/src/flashnet/contract.ts +164 -0
  87. package/src/funnel.mind.test.ts +3 -5
  88. package/src/funnel.ts +59 -8
  89. package/src/index.ts +51 -1
  90. package/src/kaleidoswap/contract.ts +1 -1
  91. package/src/knowledge/bitcoin-copilot.ts +94 -0
  92. package/src/providers/types.ts +18 -0
  93. package/src/qvac/provider.ts +25 -1
  94. package/src/qvac/stream.test.ts +11 -0
  95. package/src/qvac/stream.ts +16 -0
  96. package/src/recipe/flashnet-swap.test.ts +114 -0
  97. package/src/recipe/flashnet-swap.ts +266 -0
  98. package/src/recipe/kaleidoswap-atomic.test.ts +52 -6
  99. package/src/recipe/kaleidoswap-atomic.ts +71 -34
  100. package/src/recipe/kaleidoswap-channel-order.test.ts +38 -0
  101. package/src/recipe/kaleidoswap-channel-order.ts +27 -9
  102. package/src/recipe/kaleidoswap-price.ts +7 -1
  103. package/src/recipe/recipe.test.ts +5 -0
  104. package/src/recipe/runner.ts +5 -3
  105. package/src/recipe/swap.ts +16 -1
  106. package/src/wallet/confirm.test.ts +8 -0
  107. package/src/wallet/confirm.ts +1 -0
  108. package/src/wallet/contract.test.ts +10 -0
  109. package/src/wallet/contract.ts +26 -4
@@ -0,0 +1,101 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ FLASHNET_TOOLS,
4
+ FLASHNET_SPEND_TOOLS,
5
+ isFlashnetSpendTool,
6
+ getFlashnetTool,
7
+ bindFlashnetTools,
8
+ type FlashnetHandler,
9
+ } from './contract.js';
10
+
11
+ describe('FLASHNET_TOOLS — shape invariants', () => {
12
+ it('exposes the expected tool names in order', () => {
13
+ expect(FLASHNET_TOOLS.map((t) => t.name)).toEqual([
14
+ 'flashnet_list_pools',
15
+ 'flashnet_get_pool',
16
+ 'flashnet_simulate_swap',
17
+ 'flashnet_execute_swap',
18
+ 'flashnet_get_balance',
19
+ ]);
20
+ });
21
+
22
+ it('every tool has an object parameters schema', () => {
23
+ for (const t of FLASHNET_TOOLS) {
24
+ expect((t.parameters as any)?.type).toBe('object');
25
+ }
26
+ });
27
+
28
+ it('aligns spend ↔ requiresConfirmation', () => {
29
+ for (const t of FLASHNET_TOOLS) {
30
+ expect(!!t.spend).toBe(!!t.requiresConfirmation);
31
+ }
32
+ });
33
+
34
+ it('marks only flashnet_execute_swap as spend', () => {
35
+ expect([...FLASHNET_SPEND_TOOLS]).toEqual(['flashnet_execute_swap']);
36
+ expect(isFlashnetSpendTool('flashnet_execute_swap')).toBe(true);
37
+ expect(isFlashnetSpendTool('flashnet_simulate_swap')).toBe(false);
38
+ expect(isFlashnetSpendTool('flashnet_list_pools')).toBe(false);
39
+ });
40
+
41
+ it('getFlashnetTool returns by name', () => {
42
+ expect(getFlashnetTool('flashnet_simulate_swap')?.name).toBe('flashnet_simulate_swap');
43
+ expect(getFlashnetTool('nope')).toBeUndefined();
44
+ });
45
+
46
+ it('execute_swap requires the canonical 5 fields', () => {
47
+ const def = getFlashnetTool('flashnet_execute_swap')!;
48
+ expect((def.parameters as any).required).toEqual([
49
+ 'pool_id', 'asset_in_address', 'asset_out_address', 'amount_in', 'min_amount_out',
50
+ ]);
51
+ });
52
+
53
+ it('simulate_swap requires pool + assets + amount but no slippage', () => {
54
+ const def = getFlashnetTool('flashnet_simulate_swap')!;
55
+ expect((def.parameters as any).required).toEqual([
56
+ 'pool_id', 'asset_in_address', 'asset_out_address', 'amount_in',
57
+ ]);
58
+ });
59
+ });
60
+
61
+ describe('bindFlashnetTools', () => {
62
+ const echoHandlers = (): Record<string, FlashnetHandler> => ({
63
+ flashnet_list_pools: async (a) => ({ ok: true, t: 'list_pools', args: a }),
64
+ flashnet_get_pool: async (a) => ({ ok: true, t: 'get_pool', args: a }),
65
+ flashnet_simulate_swap: async (a) => ({ ok: true, t: 'simulate_swap', args: a }),
66
+ flashnet_execute_swap: async (a) => ({ ok: true, t: 'execute_swap', args: a }),
67
+ flashnet_get_balance: async () => ({ btc_sats: 100000, tokens: [] }),
68
+ });
69
+
70
+ it('binds every tool and preserves the spend gate', () => {
71
+ const src = bindFlashnetTools(echoHandlers());
72
+ expect(src.listTools().length).toBe(5);
73
+ const exec = src.listTools().find((t) => t.name === 'flashnet_execute_swap');
74
+ expect(exec?.requiresConfirmation).toBe(true);
75
+ const sim = src.listTools().find((t) => t.name === 'flashnet_simulate_swap');
76
+ expect(sim?.requiresConfirmation).toBeFalsy();
77
+ });
78
+
79
+ it('dispatches with args', async () => {
80
+ const src = bindFlashnetTools(echoHandlers());
81
+ const r = await src.execute('flashnet_simulate_swap', {
82
+ pool_id: 'p1',
83
+ asset_in_address: 'btc',
84
+ asset_out_address: 'usdb',
85
+ amount_in: '100000',
86
+ });
87
+ expect(r).toMatchObject({ ok: true, t: 'simulate_swap' });
88
+ });
89
+
90
+ it('throws on a missing handler unless allowMissing', () => {
91
+ const partial = { flashnet_list_pools: echoHandlers().flashnet_list_pools };
92
+ expect(() => bindFlashnetTools(partial)).toThrow(/no handler/);
93
+ const src = bindFlashnetTools(partial, { allowMissing: true });
94
+ expect(src.listTools().map((t) => t.name)).toEqual(['flashnet_list_pools']);
95
+ });
96
+
97
+ it('uses opts.id for the ToolSource id', () => {
98
+ const src = bindFlashnetTools(echoHandlers(), { id: 'flashnet-regtest' });
99
+ expect(src.id).toBe('flashnet-regtest');
100
+ });
101
+ });
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Canonical Flashnet tool contract — AMM swaps on Spark.
3
+ *
4
+ * Flashnet is a Spark-native AMM (constant-product + V3 concentrated-liquidity
5
+ * pools). The agent surface is small and intent-aligned:
6
+ *
7
+ * flashnet_list_pools — discover pools by asset pair (read)
8
+ * flashnet_get_pool — pool details + reserves (read)
9
+ * flashnet_simulate_swap — quote a swap, no funds move (read)
10
+ * flashnet_execute_swap — SPEND, confirmation-gated (the swap itself)
11
+ * flashnet_get_balance — Spark wallet balance (BTC + tokens) as the
12
+ * AMM client sees it
13
+ *
14
+ * The model picks a pool, simulates to see the rate/output, optionally shows
15
+ * the user the quote, and then executes. The host's `FlashnetClient` (built
16
+ * over a `SparkWallet`) does the actual signing.
17
+ *
18
+ * Asset addresses on Flashnet:
19
+ * - BTC is a constant pubkey: `BTC_ASSET_PUBKEY` per network.
20
+ * - Tokens are Spark Bech32m token identifiers (or hex; the client coerces).
21
+ *
22
+ * Pure data — no deps, RN-safe.
23
+ */
24
+
25
+ import type { ToolDef } from '../types.js';
26
+ import { InProcessToolSource } from '../tools/in-process.js';
27
+ import type { InProcessTool } from '../tools/in-process.js';
28
+
29
+ export interface FlashnetToolDef extends ToolDef {
30
+ /** Moves funds → confirmation-gated. */
31
+ spend?: boolean;
32
+ }
33
+
34
+ type Props = Record<
35
+ string,
36
+ { type: string; description?: string; enum?: string[]; items?: unknown }
37
+ >;
38
+
39
+ function t(
40
+ name: string,
41
+ description: string,
42
+ properties: Props = {},
43
+ required: string[] = [],
44
+ spend = false,
45
+ ): FlashnetToolDef {
46
+ return {
47
+ name,
48
+ description,
49
+ spend,
50
+ requiresConfirmation: spend,
51
+ parameters: { type: 'object', properties, required },
52
+ };
53
+ }
54
+
55
+ /** Canonical Flashnet tools. */
56
+ export const FLASHNET_TOOLS: FlashnetToolDef[] = [
57
+ t(
58
+ 'flashnet_list_pools',
59
+ "List Flashnet AMM pools. Filter by asset pair to find a venue for a swap (e.g. asset_a=BTC, asset_b=<USDB address> → pools that swap between them). Returns pools sorted by TVL by default. Use this BEFORE simulate_swap to pick a `pool_id`.",
60
+ {
61
+ asset_a: { type: 'string', description: 'OPTIONAL — first asset address (BTC pubkey or Spark token id). Filters pools.' },
62
+ asset_b: { type: 'string', description: 'OPTIONAL — second asset address. Filters pools containing both assets.' },
63
+ sort: { type: 'string', description: 'OPTIONAL — sort order. Default TVL_DESC.', enum: ['TVL_DESC', 'TVL_ASC', 'VOLUME24H_DESC', 'VOLUME24H_ASC', 'CREATED_AT_DESC', 'CREATED_AT_ASC'] },
64
+ limit: { type: 'number', description: 'OPTIONAL — max pools (1–50, default 10).' },
65
+ },
66
+ ),
67
+
68
+ t(
69
+ 'flashnet_get_pool',
70
+ "Get a single pool's details — reserves, fees, current price, TVL. Use after flashnet_list_pools when the user wants to inspect a pool before swapping.",
71
+ {
72
+ pool_id: { type: 'string', description: 'Pool id (LP pubkey) from flashnet_list_pools.' },
73
+ },
74
+ ['pool_id'],
75
+ ),
76
+
77
+ t(
78
+ 'flashnet_simulate_swap',
79
+ "Simulate a swap WITHOUT executing — returns `amount_out`, `execution_price`, `price_impact_pct`, `fee_paid`. Read-only. Use this to quote the user before they confirm. The `amount_in` is in smallest units of `asset_in` (sats for BTC, smallest unit of the token).",
80
+ {
81
+ pool_id: { type: 'string', description: 'Pool id from flashnet_list_pools.' },
82
+ asset_in_address: { type: 'string', description: 'Address of the asset the user is selling (BTC pubkey or Spark token id).' },
83
+ asset_out_address: { type: 'string', description: 'Address of the asset the user is buying.' },
84
+ amount_in: { type: 'string', description: 'Amount to swap, in smallest units of asset_in. Strings (BigInt-safe). e.g. "100000" for 100k sats.' },
85
+ },
86
+ ['pool_id', 'asset_in_address', 'asset_out_address', 'amount_in'],
87
+ ),
88
+
89
+ t(
90
+ 'flashnet_execute_swap',
91
+ "SPEND: confirmation-gated. Execute a swap quoted by flashnet_simulate_swap. `min_amount_out` and `max_slippage_bps` cap the worst-case fill (basis points: 100 = 1%, 50 = 0.5%). Returns the swap request id and the amount actually received.",
92
+ {
93
+ pool_id: { type: 'string', description: 'Pool id from flashnet_list_pools.' },
94
+ asset_in_address: { type: 'string' },
95
+ asset_out_address: { type: 'string' },
96
+ amount_in: { type: 'string', description: 'Amount to swap, smallest units.' },
97
+ min_amount_out: { type: 'string', description: 'Minimum acceptable output, smallest units. Calculate from simulate_swap.amount_out × (1 − max_slippage_bps/10000) and pass that — never trust the simulated value as-is.' },
98
+ max_slippage_bps: { type: 'number', description: 'Maximum slippage in basis points (default 50 = 0.5%). 100 = 1%, 500 = 5%.' },
99
+ },
100
+ ['pool_id', 'asset_in_address', 'asset_out_address', 'amount_in', 'min_amount_out'],
101
+ /* spend */ true,
102
+ ),
103
+
104
+ t(
105
+ 'flashnet_get_balance',
106
+ "Get the Spark wallet's BTC + token balances as the Flashnet client sees them. Useful to verify the user has enough of asset_in before quoting or executing a swap. Returns `{ btc_sats, tokens: [{ address, balance, symbol?, decimals? }] }`.",
107
+ ),
108
+ ];
109
+
110
+ /** All Flashnet tool names that move funds (confirmation-gated). */
111
+ export const FLASHNET_SPEND_TOOLS: Set<string> = new Set(
112
+ FLASHNET_TOOLS.filter((t) => t.spend).map((t) => t.name),
113
+ );
114
+
115
+ export function isFlashnetSpendTool(name: string): boolean {
116
+ return FLASHNET_SPEND_TOOLS.has(name);
117
+ }
118
+
119
+ export function getFlashnetTool(name: string): FlashnetToolDef | undefined {
120
+ return FLASHNET_TOOLS.find((t) => t.name === name);
121
+ }
122
+
123
+ /** A handler bound to one Flashnet tool. */
124
+ export type FlashnetHandler = (args: Record<string, unknown>) => Promise<unknown>;
125
+
126
+ export interface BindFlashnetOptions {
127
+ /** Skip tools without a handler instead of throwing (default false). */
128
+ allowMissing?: boolean;
129
+ /** ToolSource id for the registry (default 'flashnet'). */
130
+ id?: string;
131
+ }
132
+
133
+ /**
134
+ * Bind Flashnet contract tools to in-process handlers → an InProcessToolSource.
135
+ *
136
+ * const source = bindFlashnetTools({
137
+ * flashnet_list_pools: async (a) => client.listPools(a),
138
+ * flashnet_get_pool: async ({ pool_id }) => client.getPoolDetails(pool_id),
139
+ * flashnet_simulate_swap: async (a) => client.simulateSwap(a),
140
+ * flashnet_execute_swap: async (a) => client.executeSwap(a),
141
+ * flashnet_get_balance: async () => client.getBalance(),
142
+ * });
143
+ */
144
+ export function bindFlashnetTools(
145
+ handlers: Record<string, FlashnetHandler>,
146
+ opts: BindFlashnetOptions = {},
147
+ ): InProcessToolSource {
148
+ const bound: InProcessTool[] = [];
149
+ for (const def of FLASHNET_TOOLS) {
150
+ const handler = handlers[def.name];
151
+ if (!handler) {
152
+ if (opts.allowMissing) continue;
153
+ throw new Error(`bindFlashnetTools: no handler for "${def.name}"`);
154
+ }
155
+ bound.push({
156
+ name: def.name,
157
+ description: def.description,
158
+ parameters: def.parameters,
159
+ requiresConfirmation: def.requiresConfirmation,
160
+ handler,
161
+ });
162
+ }
163
+ return new InProcessToolSource(opts.id ?? 'flashnet', bound);
164
+ }
@@ -110,10 +110,8 @@ function buildMind(
110
110
  // atomic-swap chain (quote read; init/whitelist/execute are spends)
111
111
  tool('kaleidoswap_get_quote', {
112
112
  rfq_id: 'rfq-1',
113
- from_asset: { asset_id: 'BTC', ticker: 'BTC', amount: 100_000 },
114
- to_asset: { asset_id: 'rgb:USDT', ticker: 'USDT', amount: 1_000_000 },
115
- from_amount_display: '100,000 sats',
116
- to_amount_display: '1 USDT',
113
+ from_asset: { asset_id: 'BTC', ticker: 'BTC', layer: 'BTC_LN', amount_raw: 100_000, amount_display: '100,000 sats' },
114
+ to_asset: { asset_id: 'rgb:USDT', ticker: 'USDT', layer: 'RGB_LN', amount_raw: 1_000_000, amount_display: '1 USDT' },
117
115
  fee_display: '154 sats',
118
116
  }),
119
117
  tool('kaleidoswap_atomic_init', { swapstring: 'SWAP/abc/def', payment_hash: 'ph-1' }, /* spend */ true),
@@ -226,7 +224,7 @@ describe('desktop mind — buy assets via atomic swap', () => {
226
224
  ]);
227
225
  // init sources the asset ids + maker-unit amounts straight from the quote.
228
226
  const init = calls.find((c) => c.name === 'kaleidoswap_atomic_init')!;
229
- expect(init.args).toMatchObject({ rfq_id: 'rfq-1', from_asset: 'BTC', to_asset: 'rgb:USDT' });
227
+ expect(init.args).toMatchObject({ rfq_id: 'rfq-1', from_asset_id: 'BTC', to_asset_id: 'rgb:USDT' });
230
228
  // execute carries the node pubkey as taker_pubkey + the maker's payment_hash.
231
229
  const exec = calls.find((c) => c.name === 'kaleidoswap_atomic_execute')!;
232
230
  expect(exec.args).toMatchObject({ swapstring: 'SWAP/abc/def', taker_pubkey: '030637ec', payment_hash: 'ph-1' });
package/src/funnel.ts CHANGED
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { Engine } from './engine.js';
23
+ import type { ToolCrushOptions } from './context/compress.js';
23
24
  import type { ToolRegistry } from './tools/registry.js';
24
25
  import { FastPath, WALLET_FAST_INTENTS } from './fastpath/fastpath.js';
25
26
  import type { FastIntent } from './fastpath/fastpath.js';
@@ -31,6 +32,7 @@ import type { Recipe } from './recipe/types.js';
31
32
  import { SkillRegistry } from './skills/registry.js';
32
33
  import type { Skill } from './skills/types.js';
33
34
  import type { LLMProvider } from './providers/types.js';
35
+ import type { InferenceMetrics } from './providers/types.js';
34
36
  import type { Retriever } from './rag/retriever.js';
35
37
  import type { ConfirmDecision, Message, ToolResult } from './types.js';
36
38
 
@@ -123,6 +125,8 @@ export interface FunnelResult {
123
125
  /** Agentic tier only: executed tool calls + reasoning turns. */
124
126
  toolCalls?: ToolResult[];
125
127
  turns?: number;
128
+ /** Agentic tier only: one local-inference receipt per model call. */
129
+ inference?: InferenceMetrics[];
126
130
  }
127
131
 
128
132
  export interface FunnelOptions {
@@ -139,6 +143,13 @@ export interface FunnelOptions {
139
143
  system?: string;
140
144
  /** Max reasoning↔tool rounds in the agentic tier. Default 5. */
141
145
  maxTurns?: number;
146
+ /**
147
+ * Crush verbose tool results before they re-enter the agentic loop's history,
148
+ * so a tiny on-device model's window isn't drowned in repetitive JSON. `true`
149
+ * uses safe defaults (amounts/addresses/invoices preserved); pass options to
150
+ * tune. Off by default. See compressToolResult.
151
+ */
152
+ compressToolOutput?: boolean | ToolCrushOptions;
142
153
  /** User settings, read fresh each turn. */
143
154
  getSettings?: () => FunnelSettings;
144
155
  /** Render a fast-path tool result as user-facing text. Default: built-in. */
@@ -196,6 +207,7 @@ export class Funnel {
196
207
  provider: opts.provider,
197
208
  tools: opts.tools,
198
209
  defaultMaxTurns: opts.maxTurns ?? 5,
210
+ compressToolOutput: opts.compressToolOutput,
199
211
  });
200
212
  this.fastPath = new FastPath(opts.fastIntents ?? WALLET_FAST_INTENTS);
201
213
  this.recipes = new RecipeRegistry(opts.recipes ?? [assetSendRecipe, paymentsRecipe, receiveRecipe]);
@@ -226,6 +238,8 @@ export class Funnel {
226
238
 
227
239
  async runTurn(text: string, cbs: FunnelCallbacks = {}): Promise<FunnelResult> {
228
240
  const settings = this.getSettings();
241
+ const memoryOn = settings.memoryEnabled !== false;
242
+ const ragOn = settings.ragEnabled !== false;
229
243
 
230
244
  // ── T0: deterministic fast-path (no LLM) ──
231
245
  // Only fires when the host's registry actually implements the intent's
@@ -247,15 +261,27 @@ export class Funnel {
247
261
  // running steps with bad data.
248
262
  // Either way the registry must implement the recipe's final action.
249
263
  const recipe = this.recipes.select(text);
250
- const slots = recipe?.extract?.(text) ?? null;
251
- const deterministicallyConfident =
252
- !!slots && (recipe?.confident ? recipe.confident(slots) : Object.keys(slots).length > 0);
264
+ // For forceModelExtract recipes (channel-order, atomic) the det extractor is
265
+ // de-emphasized: only used inside runRecipe as a backfill safety net; firing
266
+ // decision + log do not depend on brittle regex for varied NL.
267
+ let slotsForLog: any = null;
268
+ let detConfident = false;
269
+ if (recipe) {
270
+ if (recipe.forceModelExtract === true) {
271
+ slotsForLog = { forceModelExtract: true };
272
+ detConfident = true; // force path handles via LLM inside; prefilter only needs tool presence
273
+ } else {
274
+ const d = recipe.extract?.(text) ?? null;
275
+ slotsForLog = d;
276
+ detConfident = !!d && (recipe.confident ? recipe.confident(d) : Object.keys(d).length > 0);
277
+ }
278
+ }
253
279
  const fires =
254
280
  !!recipe &&
255
- (recipe.forceModelExtract === true || deterministicallyConfident) &&
281
+ (recipe.forceModelExtract === true || detConfident) &&
256
282
  !!(await this.registry.getDef(recipe.final.tool));
257
283
  if (recipe && fires) {
258
- this.log(`tier=recipe:${recipe.name} slots=${JSON.stringify(slots)}`);
284
+ this.log(`tier=recipe:${recipe.name} slots=${JSON.stringify(slotsForLog)}`);
259
285
  const res = await runRecipe(recipe, text, {
260
286
  provider: this.provider,
261
287
  tools: this.registry,
@@ -265,6 +291,26 @@ export class Funnel {
265
291
  cbs.onStep?.(name);
266
292
  },
267
293
  });
294
+ // Auto-remember ids/tokens from recipe summaries (the "remember: ..." lines)
295
+ // via the tool so status follow-ups can reliably recall even cross-session.
296
+ if (res.status === 'done' && memoryOn) {
297
+ try {
298
+ const hasRemember = await this.registry.getDef('remember');
299
+ if (hasRemember) {
300
+ const text = res.text || '';
301
+ const lines = text.split(/\n+/).filter((l) => /^\s*remember:/i.test(l));
302
+ for (const line of lines) {
303
+ const clean = line.trim();
304
+ if (clean.length > 8) {
305
+ void this.registry
306
+ .execute('remember', { text: clean, kind: 'event', tags: ['recipe', 'order', 'status'] })
307
+ .catch(() => {});
308
+ this.log(`auto-remembered: ${clean.slice(0, 80)}`);
309
+ }
310
+ }
311
+ }
312
+ } catch {}
313
+ }
268
314
  return { text: res.text, tier: 'recipe', route: recipe.name };
269
315
  }
270
316
 
@@ -278,7 +324,6 @@ export class Funnel {
278
324
  // RAG block sits above history so the model treats it as authoritative).
279
325
  // Only fires for agentic turns and only when the host opts in via
280
326
  // `retriever` AND the user hasn't disabled RAG in settings.
281
- const ragOn = settings.ragEnabled !== false;
282
327
  if (this.retriever && ragOn && this.topKRag > 0) {
283
328
  try {
284
329
  const hits = await this.retriever.search(text, this.topKRag);
@@ -296,7 +341,6 @@ export class Funnel {
296
341
 
297
342
  // Ambient tools stay available even when a skill narrows the set — gated
298
343
  // by the user's memory/knowledge toggles (default on).
299
- const memoryOn = settings.memoryEnabled !== false;
300
344
  const ambient = [...(memoryOn ? AMBIENT_MEMORY : []), ...(ragOn ? AMBIENT_RAG : [])];
301
345
  const disabledAmbient = [...(memoryOn ? [] : AMBIENT_MEMORY), ...(ragOn ? [] : AMBIENT_RAG)];
302
346
  let scoped: string[] | undefined;
@@ -349,6 +393,13 @@ export class Funnel {
349
393
  onToolResult: cbs.onToolResult,
350
394
  onConfirm: cbs.onConfirm,
351
395
  });
352
- return { text: res.text ?? '', tier: 'agentic', route: skill?.name, toolCalls: res.toolCalls, turns: res.turns };
396
+ return {
397
+ text: res.text ?? '',
398
+ tier: 'agentic',
399
+ route: skill?.name,
400
+ toolCalls: res.toolCalls,
401
+ turns: res.turns,
402
+ inference: res.inference,
403
+ };
353
404
  }
354
405
  }
package/src/index.ts CHANGED
@@ -17,7 +17,12 @@ export type {
17
17
  ConfirmDecision,
18
18
  } from './types.js';
19
19
 
20
- export type { LLMProvider, TurnInput, TurnOutput } from './providers/types.js';
20
+ export type {
21
+ InferenceMetrics,
22
+ LLMProvider,
23
+ TurnInput,
24
+ TurnOutput,
25
+ } from './providers/types.js';
21
26
 
22
27
  export type { ToolSource } from './tools/source.js';
23
28
  export { InProcessToolSource } from './tools/in-process.js';
@@ -81,12 +86,41 @@ export type {
81
86
  BindLsps1Options,
82
87
  } from './lsps1/contract.js';
83
88
 
89
+ // ── Bitrefill (gift cards / mobile top-ups / eSIMs) ─────────────────────────
90
+ export {
91
+ BITREFILL_TOOLS,
92
+ BITREFILL_SPEND_TOOLS,
93
+ isBitrefillSpendTool,
94
+ getBitrefillTool,
95
+ bindBitrefillTools,
96
+ } from './bitrefill/contract.js';
97
+ export type {
98
+ BitrefillToolDef,
99
+ BitrefillHandler,
100
+ BindBitrefillOptions,
101
+ } from './bitrefill/contract.js';
102
+
103
+ // ── Flashnet (Spark-native AMM — swaps over Spark) ──────────────────────────
104
+ export {
105
+ FLASHNET_TOOLS,
106
+ FLASHNET_SPEND_TOOLS,
107
+ isFlashnetSpendTool,
108
+ getFlashnetTool,
109
+ bindFlashnetTools,
110
+ } from './flashnet/contract.js';
111
+ export type {
112
+ FlashnetToolDef,
113
+ FlashnetHandler,
114
+ BindFlashnetOptions,
115
+ } from './flashnet/contract.js';
116
+
84
117
  // ── KaleidoSwap recipes (opt-in — register via Funnel.recipes) ──
85
118
  // price recipe is read-only (quote-only); atomic recipe runs the full swap.
86
119
  // Register the price recipe FIRST so phrasings like "BTC price" are answered
87
120
  // without firing any spend.
88
121
  export { kaleidoswapPriceRecipe } from './recipe/kaleidoswap-price.js';
89
122
  export { kaleidoswapAtomicRecipe } from './recipe/kaleidoswap-atomic.js';
123
+ export { flashnetSwapRecipe } from './recipe/flashnet-swap.js';
90
124
  export {
91
125
  kaleidoswapChannelOrderRecipe,
92
126
  extractChannelOrder,
@@ -147,6 +181,8 @@ export {
147
181
  contextBudgetTokens,
148
182
  } from './context/budget.js';
149
183
  export type { BudgetReserves } from './context/budget.js';
184
+ export { compressToolResult, DEFAULT_PRESERVE_KEYS } from './context/compress.js';
185
+ export type { ToolCrushOptions, CrushResult } from './context/compress.js';
150
186
  export { capabilityProfile } from './capabilities.js';
151
187
  export type { CapabilityInput, MindCapabilities } from './capabilities.js';
152
188
 
@@ -187,6 +223,20 @@ export type { Skill, SkillReference, SkillSelector } from './skills/types.js';
187
223
  export { TurnLogger, defaultMask } from './logger.js';
188
224
  export type { TurnLog, Device, LoggerIO, LoggerOptions } from './logger.js';
189
225
 
226
+ export {
227
+ EVIDENCE_SCHEMA,
228
+ EvidenceRecorder,
229
+ sanitizeEvidenceEvent,
230
+ } from './evidence.js';
231
+ export type {
232
+ EvidenceEvent,
233
+ EvidenceEventType,
234
+ EvidenceInput,
235
+ EvidenceIO,
236
+ EvidenceRecorderOptions,
237
+ EvidenceSurface,
238
+ } from './evidence.js';
239
+
190
240
  // ── Autonomy (the task brain: scheduled tasks + run history + spend guardrails)
191
241
  // The operational half of the agent's memory — the state nanobot kept in
192
242
  // tasks.json + cron + run history, lifted into core (storage/timers injected).
@@ -100,7 +100,7 @@ export const KALEIDOSWAP_TOOLS: KaleidoswapToolDef[] = [
100
100
  order_id: { type: 'string', description: 'The order id returned by kaleidoswap_place_order.' },
101
101
  access_token: { type: 'string', description: 'The per-order access token returned by kaleidoswap_place_order. Required for status checks on the order.' },
102
102
  },
103
- ['order_id']),
103
+ ['order_id', 'access_token']),
104
104
 
105
105
  t('orders',
106
106
  'kaleidoswap_get_order_history',
@@ -285,4 +285,98 @@ export const BITCOIN_COPILOT_DOCS: RagDocument[] = [
285
285
  "(not just inbound capacity).",
286
286
  metadata: { topic: 'rgb-channels' },
287
287
  },
288
+
289
+ // ── Layer / protocol taxonomy ─────────────────────────────────────────
290
+ // The single biggest source of model confusion is mixing up which assets
291
+ // live on which layer. Small models pattern-match on "USDT" or "Bitcoin"
292
+ // and assume every L2 supports every asset — they don't. Each L2 has its
293
+ // OWN asset family, and assets do not move between them without an
294
+ // explicit cross-layer swap or bridge.
295
+
296
+ {
297
+ id: 'kaleidomind-layers-overview',
298
+ text:
299
+ 'This wallet supports THREE distinct Bitcoin L2s, each with its own ' +
300
+ 'asset family. They are NOT interchangeable: a balance on one layer ' +
301
+ 'cannot be spent on another without an explicit swap. ' +
302
+ '(1) SPARK — an off-chain BTC scaling layer (Lightspark / buildonspark, ' +
303
+ 'Statechains-based). Assets: BTC (sats) + Spark-native tokens like ' +
304
+ 'USDB. Tools: spark_* (balance/address/invoice/pay). Swap venue: ' +
305
+ 'Flashnet AMM (BTC ⇄ Spark tokens). ' +
306
+ '(2) RLN / RGB — a Lightning node that carries RGB assets over ' +
307
+ 'BOLT11 channels (colored channels). Assets: BTC + RGB assets like ' +
308
+ 'USDT, XAUT. Tools: rln_* (nodeinfo/invoice/pay/whitelist). Swap ' +
309
+ 'venue: KaleidoSwap maker (BTC ⇄ RGB assets via atomic HTLC swap). ' +
310
+ '(3) ARKADE — an Ark-based off-chain BTC layer. Assets: BTC. Tools: ' +
311
+ 'arkade_* (balance/address/send). No native non-BTC assets today.',
312
+ metadata: { topic: 'layers' },
313
+ },
314
+
315
+ {
316
+ id: 'spark-layer-assets',
317
+ text:
318
+ 'Spark is an off-chain BTC scaling layer (Lightspark / buildonspark). ' +
319
+ "It holds BTC (sats) and Spark-native tokens. USDB is a Spark token. " +
320
+ 'Spark addresses look like spark1… (or sparkrt1… on regtest). ' +
321
+ 'CRITICAL: Spark does NOT carry RGB assets. USDT and XAUT are RGB ' +
322
+ 'assets that live on the RLN (RGB Lightning Node) layer — not on ' +
323
+ "Spark. A user's USDT balance, if they have one, is on RLN, NOT " +
324
+ 'Spark. Conversely, USDB lives only on Spark (and trades on ' +
325
+ 'Flashnet); it has no presence on RLN/RGB. When asked "what assets ' +
326
+ 'are on Spark / what can I trade on Spark", answer with Spark-native ' +
327
+ 'tokens (BTC + USDB and any other Spark tokens the AMM lists via ' +
328
+ 'flashnet_list_pools), NOT USDT/XAUT/RGB.',
329
+ metadata: { topic: 'layers' },
330
+ },
331
+
332
+ {
333
+ id: 'rln-layer-assets',
334
+ text:
335
+ 'RLN (RGB Lightning Node) is a Lightning node that carries RGB ' +
336
+ 'assets over BOLT11 channels (a.k.a. colored channels). It holds ' +
337
+ 'BTC on standard Lightning channels and RGB assets — USDT, XAUT, ' +
338
+ 'and any other client-side-validated asset issued via RGB — on ' +
339
+ 'asset channels. Each asset needs its own channel. RGB assets do ' +
340
+ 'NOT live on Spark or Arkade; they are RLN-only. Swap venue for ' +
341
+ 'BTC ⇄ RGB asset is the KaleidoSwap maker (atomic HTLC: quote → ' +
342
+ 'init → whitelist → execute). To receive an RGB asset over ' +
343
+ 'Lightning, you first need an LSPS1-opened asset channel.',
344
+ metadata: { topic: 'layers' },
345
+ },
346
+
347
+ {
348
+ id: 'swap-venue-split',
349
+ text:
350
+ "Two swap venues, two asset families — DO NOT confuse them. " +
351
+ "FLASHNET is a Spark-native AMM. It trades between BTC and " +
352
+ "Spark-native tokens (e.g. USDB). It uses the same Spark wallet " +
353
+ "as the user's balance. Tools: flashnet_list_pools, " +
354
+ "flashnet_simulate_swap, flashnet_execute_swap. Skill: " +
355
+ "flashnet-swaps. ASSETS: BTC, USDB, and anything else " +
356
+ "flashnet_list_pools returns. NEVER offer USDT/XAUT on Flashnet. " +
357
+ "KALEIDOSWAP is an atomic HTLC maker. It trades between BTC and " +
358
+ "RGB assets (USDT, XAUT). It uses the RLN node. Tools: " +
359
+ "kaleidoswap_get_quote, kaleidoswap_atomic_init, " +
360
+ "kaleidoswap_atomic_execute. Skill: kaleido-trading. ASSETS: BTC, " +
361
+ "USDT, XAUT, and other RGB assets the maker prices. NEVER offer " +
362
+ "USDB on KaleidoSwap.",
363
+ metadata: { topic: 'venues' },
364
+ },
365
+
366
+ {
367
+ id: 'asset-to-layer-routing',
368
+ text:
369
+ "How to route by asset name. The asset names tell you which layer " +
370
+ "to use — don't guess: " +
371
+ "BTC / sats → all layers (Spark / RLN / Arkade / on-chain) carry " +
372
+ "BTC; pick by user context. " +
373
+ "USDB → Spark only, via Flashnet (flashnet-swaps). " +
374
+ "USDT → RLN/RGB only, via KaleidoSwap (kaleido-trading). " +
375
+ "XAUT (tether-gold) → RLN/RGB only, via KaleidoSwap. " +
376
+ "If a user names an asset you don't recognise, do NOT assume a " +
377
+ "layer — ask, or list pools/assets via the right tool first " +
378
+ "(flashnet_list_pools for Spark-side, kaleidoswap_get_pairs / " +
379
+ "kaleidoswap_get_assets for RGB-side).",
380
+ metadata: { topic: 'venues' },
381
+ },
288
382
  ];
@@ -22,6 +22,22 @@ export interface TurnInput {
22
22
  signal?: AbortSignal;
23
23
  }
24
24
 
25
+ /** Judge-auditable metrics for one provider inference request. */
26
+ export interface InferenceMetrics {
27
+ requestId?: string;
28
+ backendDevice?: 'cpu' | 'gpu';
29
+ promptTokens?: number;
30
+ completionTokens?: number;
31
+ totalTokens?: number;
32
+ /** Milliseconds from completion() start to the first generated delta. */
33
+ ttftMs?: number;
34
+ /** End-to-end completion duration measured by the host. */
35
+ durationMs: number;
36
+ tokensPerSecond?: number;
37
+ stopReason?: string;
38
+ status: 'completed' | 'cancelled' | 'truncated' | 'failed';
39
+ }
40
+
25
41
  export interface TurnOutput {
26
42
  /** Cleaned assistant content for display. */
27
43
  text: string;
@@ -36,6 +52,8 @@ export interface TurnOutput {
36
52
  toolCalls: ToolCall[];
37
53
  /** Provider request id, for cancellation. */
38
54
  requestId?: string;
55
+ /** Optional local-inference receipt. Hosts may persist this as JSONL evidence. */
56
+ inference?: InferenceMetrics;
39
57
  }
40
58
 
41
59
  export interface LLMProvider {