@kaleidorg/mind 0.3.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.
- package/dist/funnel.d.ts +19 -0
- package/dist/funnel.d.ts.map +1 -1
- package/dist/funnel.js +48 -10
- package/dist/funnel.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -3
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +3 -3
- package/dist/kaleidoswap/contract.d.ts.map +1 -1
- package/dist/kaleidoswap/contract.js +16 -4
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
- package/dist/knowledge/bitcoin-copilot.js +102 -0
- package/dist/knowledge/bitcoin-copilot.js.map +1 -1
- package/dist/knowledge/btc-map.d.ts +14 -17
- package/dist/knowledge/btc-map.d.ts.map +1 -1
- package/dist/knowledge/btc-map.js +66 -266
- package/dist/knowledge/btc-map.js.map +1 -1
- package/dist/lsps1/contract.d.ts.map +1 -1
- package/dist/lsps1/contract.js +28 -10
- package/dist/lsps1/contract.js.map +1 -1
- package/dist/qvac/assistant.d.ts +73 -0
- package/dist/qvac/assistant.d.ts.map +1 -0
- package/dist/qvac/assistant.js +97 -0
- package/dist/qvac/assistant.js.map +1 -0
- package/dist/qvac/config.d.ts +64 -0
- package/dist/qvac/config.d.ts.map +1 -0
- package/dist/qvac/config.js +71 -0
- package/dist/qvac/config.js.map +1 -0
- package/dist/qvac/delegate.d.ts +48 -0
- package/dist/qvac/delegate.d.ts.map +1 -0
- package/dist/qvac/delegate.js +51 -0
- package/dist/qvac/delegate.js.map +1 -0
- package/dist/qvac/index.d.ts +19 -0
- package/dist/qvac/index.d.ts.map +1 -0
- package/dist/qvac/index.js +19 -0
- package/dist/qvac/index.js.map +1 -0
- package/dist/qvac/parse.d.ts +44 -0
- package/dist/qvac/parse.d.ts.map +1 -0
- package/dist/qvac/parse.js +28 -0
- package/dist/qvac/parse.js.map +1 -0
- package/dist/qvac/provider.d.ts +49 -0
- package/dist/qvac/provider.d.ts.map +1 -0
- package/dist/qvac/provider.js +68 -0
- package/dist/qvac/provider.js.map +1 -0
- package/dist/qvac/stream.d.ts +37 -0
- package/dist/qvac/stream.d.ts.map +1 -0
- package/dist/qvac/stream.js +29 -0
- package/dist/qvac/stream.js.map +1 -0
- package/dist/qvac/text.d.ts +19 -0
- package/dist/qvac/text.d.ts.map +1 -0
- package/dist/qvac/text.js +56 -0
- package/dist/qvac/text.js.map +1 -0
- package/dist/qvac/voice.d.ts +69 -0
- package/dist/qvac/voice.d.ts.map +1 -0
- package/dist/qvac/voice.js +51 -0
- package/dist/qvac/voice.js.map +1 -0
- package/dist/recipe/buy-asset-channel.d.ts +26 -0
- package/dist/recipe/buy-asset-channel.d.ts.map +1 -0
- package/dist/recipe/buy-asset-channel.js +112 -0
- package/dist/recipe/buy-asset-channel.js.map +1 -0
- package/dist/recipe/kaleidoswap-atomic.d.ts +26 -18
- package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-atomic.js +101 -63
- package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
- package/dist/recipe/kaleidoswap-channel-order.d.ts +35 -0
- package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -0
- package/dist/recipe/kaleidoswap-channel-order.js +493 -0
- package/dist/recipe/kaleidoswap-channel-order.js.map +1 -0
- package/dist/recipe/kaleidoswap-price.d.ts +21 -0
- package/dist/recipe/kaleidoswap-price.d.ts.map +1 -0
- package/dist/recipe/kaleidoswap-price.js +57 -0
- package/dist/recipe/kaleidoswap-price.js.map +1 -0
- package/dist/recipe/runner.d.ts +7 -1
- package/dist/recipe/runner.d.ts.map +1 -1
- package/dist/recipe/runner.js +115 -29
- package/dist/recipe/runner.js.map +1 -1
- package/dist/recipe/swap.d.ts +26 -1
- package/dist/recipe/swap.d.ts.map +1 -1
- package/dist/recipe/swap.js +108 -13
- package/dist/recipe/swap.js.map +1 -1
- package/dist/recipe/types.d.ts +25 -1
- package/dist/recipe/types.d.ts.map +1 -1
- package/dist/skills/registry.d.ts +33 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +45 -1
- package/dist/skills/registry.js.map +1 -1
- package/package.json +15 -1
- package/skills/README.md +3 -0
- package/skills/kaleido-lsps/SKILL.md +101 -43
- package/skills/kaleido-trading/SKILL.md +81 -31
- package/skills/merchant-finder/SKILL.md +96 -66
- package/skills/rgb-lightning-node/SKILL.md +108 -0
- package/skills/wallet-assistant/SKILL.md +32 -21
- package/src/funnel.ts +66 -11
- package/src/index.ts +14 -2
- package/src/kaleidoswap/contract.test.ts +7 -2
- package/src/kaleidoswap/contract.ts +27 -5
- package/src/knowledge/bitcoin-copilot.ts +111 -0
- package/src/knowledge/btc-map.test.ts +53 -96
- package/src/knowledge/btc-map.ts +72 -287
- package/src/lsps1/contract.ts +32 -14
- package/src/qvac/assistant.test.ts +132 -0
- package/src/qvac/assistant.ts +146 -0
- package/src/qvac/config.test.ts +44 -0
- package/src/qvac/config.ts +76 -0
- package/src/qvac/delegate.test.ts +68 -0
- package/src/qvac/delegate.ts +71 -0
- package/src/qvac/index.ts +72 -0
- package/src/qvac/parse.test.ts +52 -0
- package/src/qvac/parse.ts +57 -0
- package/src/qvac/provider.test.ts +107 -0
- package/src/qvac/provider.ts +124 -0
- package/src/qvac/stream.test.ts +79 -0
- package/src/qvac/stream.ts +56 -0
- package/src/qvac/text.test.ts +70 -0
- package/src/qvac/text.ts +60 -0
- package/src/qvac/voice.test.ts +151 -0
- package/src/qvac/voice.ts +122 -0
- package/src/recipe/buy-asset-channel.test.ts +148 -0
- package/src/recipe/buy-asset-channel.ts +118 -0
- package/src/recipe/kaleidoswap-atomic.test.ts +134 -61
- package/src/recipe/kaleidoswap-atomic.ts +112 -66
- package/src/recipe/kaleidoswap-channel-order.test.ts +333 -0
- package/src/recipe/kaleidoswap-channel-order.ts +548 -0
- package/src/recipe/kaleidoswap-price.ts +68 -0
- package/src/recipe/recipe.test.ts +61 -5
- package/src/recipe/runner.ts +128 -31
- package/src/recipe/swap.ts +109 -13
- package/src/recipe/types.ts +25 -1
- package/src/skills/registry.ts +52 -1
|
@@ -5,15 +5,19 @@ import type { LLMProvider } from '../providers/types.js';
|
|
|
5
5
|
import { runRecipe } from './runner.js';
|
|
6
6
|
import { kaleidoswapAtomicRecipe } from './kaleidoswap-atomic.js';
|
|
7
7
|
|
|
8
|
-
// LLM provider that should never be called when slots are
|
|
8
|
+
// LLM provider that should never be called when slots are pre-supplied to runRecipe
|
|
9
|
+
// (or when a recipe is not using forceModelExtract).
|
|
9
10
|
const refusingProvider: LLMProvider = {
|
|
10
11
|
name: 'refusing',
|
|
11
12
|
runTurn: async () => {
|
|
12
|
-
throw new Error('provider should NOT be called
|
|
13
|
+
throw new Error('provider should NOT be called (slots pre-supplied or det path)');
|
|
13
14
|
},
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Stub the maker + node tools. The quote echoes full asset specs (asset_id +
|
|
19
|
+
* maker-unit amount) the way the real maker does, so init can source them.
|
|
20
|
+
*/
|
|
17
21
|
function buildStubs(captured: { name: string; args: any }[]) {
|
|
18
22
|
const tool = (name: string, response: any, spend = false) => ({
|
|
19
23
|
name,
|
|
@@ -27,112 +31,181 @@ function buildStubs(captured: { name: string; args: any }[]) {
|
|
|
27
31
|
});
|
|
28
32
|
return new ToolRegistry([
|
|
29
33
|
new InProcessToolSource('kaleidoswap', [
|
|
30
|
-
tool('kaleidoswap_get_quote', {
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
tool('kaleidoswap_get_quote', {
|
|
35
|
+
rfq_id: 'rfq-1',
|
|
36
|
+
from_asset: { asset_id: 'USDT', ticker: 'USDT', amount: 10_000_000 },
|
|
37
|
+
to_asset: { asset_id: 'BTC', ticker: 'BTC', amount: 15_250_000 },
|
|
38
|
+
from_amount_display: '10 USDT',
|
|
39
|
+
to_amount_display: '15,250 sats',
|
|
40
|
+
fee_display: '154 sats',
|
|
41
|
+
}),
|
|
42
|
+
tool('kaleidoswap_atomic_init', { swapstring: 'SWAP/abc/def', payment_hash: 'ph-1' }, /* spend */ true),
|
|
43
|
+
tool('kaleidoswap_atomic_execute', { status: 200, message: 'Swap executed successfully.' }, /* spend */ true),
|
|
33
44
|
]),
|
|
34
45
|
new InProcessToolSource('rln', [
|
|
35
|
-
tool('
|
|
36
|
-
tool('
|
|
37
|
-
tool('rln_pay_invoice', { status: 'SUCCESS', payment_hash: 'h' }, /* spend */ true),
|
|
46
|
+
tool('rln_get_node_info', { pubkey: '03c31dae' }),
|
|
47
|
+
tool('rln_whitelist_swap', { ok: true }, /* spend */ true),
|
|
38
48
|
]),
|
|
39
49
|
]);
|
|
40
50
|
}
|
|
41
51
|
|
|
42
|
-
describe('kaleidoswapAtomicRecipe — selection
|
|
43
|
-
it('triggers on
|
|
44
|
-
expect(kaleidoswapAtomicRecipe.match!('
|
|
45
|
-
expect(kaleidoswapAtomicRecipe.match!('
|
|
46
|
-
expect(kaleidoswapAtomicRecipe.match!('
|
|
52
|
+
describe('kaleidoswapAtomicRecipe — selection', () => {
|
|
53
|
+
it('triggers on swap phrasings', () => {
|
|
54
|
+
expect(kaleidoswapAtomicRecipe.match!('swap 10 usdt to btc')).toBe(true);
|
|
55
|
+
expect(kaleidoswapAtomicRecipe.match!('exchange 100000 sats for usdt')).toBe(true);
|
|
56
|
+
expect(kaleidoswapAtomicRecipe.match!('convert btc to usdt')).toBe(true);
|
|
47
57
|
});
|
|
58
|
+
it('triggers on buy/sell of a crypto asset (the reported bug)', () => {
|
|
59
|
+
expect(kaleidoswapAtomicRecipe.match!('buy one usdt from kaleido')).toBe(true);
|
|
60
|
+
expect(kaleidoswapAtomicRecipe.match!('sell 100 usdt')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
it('does NOT trigger on buying a gift card (that is commerce, not a swap)', () => {
|
|
63
|
+
expect(kaleidoswapAtomicRecipe.match!('buy a gift card')).toBe(false);
|
|
64
|
+
expect(kaleidoswapAtomicRecipe.match!('buy an amazon voucher')).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it('does not trigger on a balance question', () => {
|
|
67
|
+
expect(kaleidoswapAtomicRecipe.match!('what is my balance')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('kaleidoswapAtomicRecipe — forceModelExtract (less deterministic slot parsing)', () => {
|
|
72
|
+
it('always uses 1 LLM inference for slots even when a det extract would succeed (model does the NL understanding)', async () => {
|
|
73
|
+
const captured: { name: string; args: any }[] = [];
|
|
74
|
+
const tools = buildStubs(captured);
|
|
75
|
+
|
|
76
|
+
// Provider that handles the synthetic extract_request tool the runner builds.
|
|
77
|
+
const modelExtractProvider: LLMProvider = {
|
|
78
|
+
name: 'model-extract',
|
|
79
|
+
runTurn: async (input) => {
|
|
80
|
+
// The runner sends a single-turn request with the extract tool.
|
|
81
|
+
const call = input.tools?.find((t) => t.name === 'extract_request');
|
|
82
|
+
if (call && input.messages.some((m) => m.role === 'user' && /usdt/i.test(m.content || ''))) {
|
|
83
|
+
// Simulate the model correctly parsing a natural "buy" phrasing.
|
|
84
|
+
return {
|
|
85
|
+
text: '',
|
|
86
|
+
rawContent: '',
|
|
87
|
+
toolCalls: [{
|
|
88
|
+
id: 'ex1',
|
|
89
|
+
name: 'extract_request',
|
|
90
|
+
arguments: { from_asset: 'BTC', to_asset: 'USDT', amount: 1, amount_side: 'to' },
|
|
91
|
+
}],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { text: '', rawContent: '', toolCalls: [] };
|
|
95
|
+
},
|
|
96
|
+
};
|
|
48
97
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
98
|
+
const res = await runRecipe(kaleidoswapAtomicRecipe, 'buy 1 usdt', {
|
|
99
|
+
provider: modelExtractProvider,
|
|
100
|
+
tools,
|
|
101
|
+
onConfirm: async () => ({ approved: true }),
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(res.status).toBe('done');
|
|
105
|
+
expect(res.inferences).toBe(1); // forced through the model
|
|
106
|
+
// The execution still used the model-provided slots (from_asset came from the "model" not regex default).
|
|
107
|
+
// (The stub quote in the test is for USDT→BTC, but the point is the inference count + that it ran.)
|
|
108
|
+
expect(captured[0].name).toBe('kaleidoswap_get_quote');
|
|
52
109
|
});
|
|
53
110
|
});
|
|
54
111
|
|
|
55
|
-
describe('kaleidoswapAtomicRecipe —
|
|
56
|
-
it('runs quote →
|
|
112
|
+
describe('kaleidoswapAtomicRecipe — full chain', () => {
|
|
113
|
+
it('runs quote → init → nodeinfo → whitelist → execute in order (one inference)', async () => {
|
|
57
114
|
const captured: { name: string; args: any }[] = [];
|
|
58
115
|
const tools = buildStubs(captured);
|
|
59
116
|
|
|
60
|
-
const res = await runRecipe(kaleidoswapAtomicRecipe, '
|
|
117
|
+
const res = await runRecipe(kaleidoswapAtomicRecipe, 'swap 10 usdt to btc', {
|
|
61
118
|
provider: refusingProvider,
|
|
62
119
|
tools,
|
|
63
120
|
onConfirm: async () => ({ approved: true }),
|
|
121
|
+
// Pre-supply slots so refusingProvider is not hit. This simulates a
|
|
122
|
+
// successful prior extraction (the normal fast path for most recipes,
|
|
123
|
+
// or the early Funnel heuristic for forceModelExtract recipes).
|
|
124
|
+
slots: { from_asset: 'USDT', to_asset: 'BTC', amount: 10, amount_side: 'from' },
|
|
64
125
|
});
|
|
65
126
|
|
|
66
127
|
expect(res.status).toBe('done');
|
|
67
|
-
expect(res.inferences).toBe(0);
|
|
68
|
-
|
|
69
|
-
// The chain: quote → rgb_invoice → atomic_init → pay → atomic_execute (5 calls).
|
|
128
|
+
expect(res.inferences).toBe(0);
|
|
70
129
|
expect(captured.map((c) => c.name)).toEqual([
|
|
71
130
|
'kaleidoswap_get_quote',
|
|
72
|
-
'rln_create_rgb_invoice',
|
|
73
131
|
'kaleidoswap_atomic_init',
|
|
74
|
-
'
|
|
132
|
+
'rln_get_node_info',
|
|
133
|
+
'rln_whitelist_swap',
|
|
75
134
|
'kaleidoswap_atomic_execute',
|
|
76
135
|
]);
|
|
136
|
+
});
|
|
77
137
|
|
|
78
|
-
|
|
138
|
+
it('threads quote → init args (flat asset ids + maker-unit amounts)', async () => {
|
|
139
|
+
const captured: { name: string; args: any }[] = [];
|
|
140
|
+
const tools = buildStubs(captured);
|
|
141
|
+
await runRecipe(kaleidoswapAtomicRecipe, 'swap 10 usdt to btc', {
|
|
142
|
+
provider: refusingProvider, tools, onConfirm: async () => ({ approved: true }),
|
|
143
|
+
slots: { from_asset: 'USDT', to_asset: 'BTC', amount: 10, amount_side: 'from' },
|
|
144
|
+
});
|
|
79
145
|
const init = captured.find((c) => c.name === 'kaleidoswap_atomic_init')!;
|
|
80
|
-
expect(init.args).toEqual({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
146
|
+
expect(init.args).toEqual({
|
|
147
|
+
rfq_id: 'rfq-1',
|
|
148
|
+
from_asset: 'USDT', from_amount: 10_000_000,
|
|
149
|
+
to_asset: 'BTC', to_amount: 15_250_000,
|
|
150
|
+
});
|
|
151
|
+
});
|
|
85
152
|
|
|
86
|
-
|
|
153
|
+
it('threads init.swapstring + node.pubkey + init.payment_hash → execute', async () => {
|
|
154
|
+
const captured: { name: string; args: any }[] = [];
|
|
155
|
+
const tools = buildStubs(captured);
|
|
156
|
+
await runRecipe(kaleidoswapAtomicRecipe, 'swap 10 usdt to btc', {
|
|
157
|
+
provider: refusingProvider, tools, onConfirm: async () => ({ approved: true }),
|
|
158
|
+
slots: { from_asset: 'USDT', to_asset: 'BTC', amount: 10, amount_side: 'from' },
|
|
159
|
+
});
|
|
160
|
+
const whitelist = captured.find((c) => c.name === 'rln_whitelist_swap')!;
|
|
161
|
+
expect(whitelist.args).toEqual({ swapstring: 'SWAP/abc/def' });
|
|
87
162
|
const exe = captured.find((c) => c.name === 'kaleidoswap_atomic_execute')!;
|
|
88
|
-
expect(exe.args).toEqual({
|
|
163
|
+
expect(exe.args).toEqual({
|
|
164
|
+
swapstring: 'SWAP/abc/def',
|
|
165
|
+
taker_pubkey: '03c31dae',
|
|
166
|
+
payment_hash: 'ph-1',
|
|
167
|
+
});
|
|
89
168
|
});
|
|
90
169
|
});
|
|
91
170
|
|
|
92
|
-
describe('kaleidoswapAtomicRecipe —
|
|
93
|
-
it('
|
|
171
|
+
describe('kaleidoswapAtomicRecipe — single confirmation', () => {
|
|
172
|
+
it('fires ONE gate (before init), showing the quote summary, then runs ungated', async () => {
|
|
94
173
|
const captured: { name: string; args: any }[] = [];
|
|
95
174
|
const tools = buildStubs(captured);
|
|
175
|
+
const confirms: { name: string; summary?: string }[] = [];
|
|
96
176
|
|
|
97
|
-
const res = await runRecipe(kaleidoswapAtomicRecipe, '
|
|
177
|
+
const res = await runRecipe(kaleidoswapAtomicRecipe, 'swap 10 usdt to btc', {
|
|
98
178
|
provider: refusingProvider,
|
|
99
179
|
tools,
|
|
100
|
-
onConfirm: async () =>
|
|
180
|
+
onConfirm: async (call) => {
|
|
181
|
+
confirms.push({ name: call.name, summary: (call as any).summary });
|
|
182
|
+
return { approved: true };
|
|
183
|
+
},
|
|
184
|
+
slots: { from_asset: 'USDT', to_asset: 'BTC', amount: 10, amount_side: 'from' },
|
|
101
185
|
});
|
|
102
186
|
|
|
103
187
|
expect(res.status).toBe('done');
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
]);
|
|
111
|
-
const init = captured.find((c) => c.name === 'kaleidoswap_atomic_init')!;
|
|
112
|
-
expect(init.args.receive_invoice).toBe('lnbc1user');
|
|
188
|
+
// Exactly one confirm, on the first spend step (init), with the rich summary.
|
|
189
|
+
expect(confirms).toHaveLength(1);
|
|
190
|
+
expect(confirms[0]!.name).toBe('kaleidoswap_atomic_init');
|
|
191
|
+
expect(confirms[0]!.summary).toContain('10 USDT');
|
|
192
|
+
expect(confirms[0]!.summary).toContain('15,250 sats');
|
|
193
|
+
expect(confirms[0]!.summary).toContain('154 sats');
|
|
113
194
|
});
|
|
114
|
-
});
|
|
115
195
|
|
|
116
|
-
|
|
117
|
-
it('cancels the chain when the user declines a spend gate', async () => {
|
|
196
|
+
it('declining the single gate cancels the whole chain before any spend', async () => {
|
|
118
197
|
const captured: { name: string; args: any }[] = [];
|
|
119
198
|
const tools = buildStubs(captured);
|
|
120
|
-
let firstSpendSeen = false;
|
|
121
199
|
|
|
122
|
-
const res = await runRecipe(kaleidoswapAtomicRecipe, '
|
|
200
|
+
const res = await runRecipe(kaleidoswapAtomicRecipe, 'swap 10 usdt to btc', {
|
|
123
201
|
provider: refusingProvider,
|
|
124
202
|
tools,
|
|
125
|
-
onConfirm: async () => {
|
|
126
|
-
|
|
127
|
-
firstSpendSeen = true;
|
|
128
|
-
return { approved: false, reason: 'user said no' };
|
|
129
|
-
},
|
|
203
|
+
onConfirm: async () => ({ approved: false, reason: 'user said no' }),
|
|
204
|
+
slots: { from_asset: 'USDT', to_asset: 'BTC', amount: 10, amount_side: 'from' },
|
|
130
205
|
});
|
|
131
206
|
|
|
132
|
-
expect(res.status).
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
expect(captured.some((c) => c.name === 'rln_pay_invoice')).toBe(false);
|
|
136
|
-
expect(captured.some((c) => c.name === 'kaleidoswap_atomic_execute')).toBe(false);
|
|
207
|
+
expect(res.status).toBe('cancelled');
|
|
208
|
+
// Quote ran (read-only), but NOTHING after the declined gate.
|
|
209
|
+
expect(captured.map((c) => c.name)).toEqual(['kaleidoswap_get_quote']);
|
|
137
210
|
});
|
|
138
211
|
});
|
|
@@ -1,53 +1,92 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Built-in "
|
|
2
|
+
* Built-in "swap on KaleidoSwap" recipe — the real atomic-swap chain.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* user pays the maker's Lightning invoice, and the maker releases.
|
|
4
|
+
* A swap (especially the full maker + RLN atomic) is a 6-step, two-service flow
|
|
5
|
+
* no small model can plan reliably, so the recipe carries the plan. The model
|
|
6
|
+
* is used for natural-language understanding of the request (slot extraction).
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* "buy 1 usdt" (or "swap 10 usdt to btc")
|
|
9
|
+
* ↓ heuristic pre-filter (0 inf) decides to enter the reliable recipe branch
|
|
10
|
+
* ↓ 1 model inference (forced LLM slot extraction — the model parses intent)
|
|
11
|
+
* kaleidoswap_get_quote ← MAKER prices the swap (read-only)
|
|
12
|
+
* ↓ [ONE confirmation gate — shows the real quote numbers]
|
|
13
|
+
* kaleidoswap_atomic_init ← MAKER locks the swap → swapstring, payment_hash
|
|
14
|
+
* rln_get_node_info ← NODE read pubkey (= taker_pubkey)
|
|
15
|
+
* rln_whitelist_swap ← NODE accept the swapstring
|
|
16
|
+
* kaleidoswap_atomic_execute ← MAKER settle (final)
|
|
12
17
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* kaleidoswap_atomic_init 🔒 ← maker locks the swap, returns its invoice
|
|
19
|
-
* rln_pay_invoice 🔒 ← user pays the maker
|
|
20
|
-
* kaleidoswap_atomic_execute 🔒 ← (final) maker releases the asset
|
|
18
|
+
* `forceModelExtract` ensures the model is always consulted for slot parsing
|
|
19
|
+
* (1 inference) so natural language like "buy 1 usdt" is interpreted by the LLM.
|
|
20
|
+
* A safety fallback in the runner uses the deterministic extractor if the model
|
|
21
|
+
* returns incomplete slots. The execution sequence + single-confirm gate remain
|
|
22
|
+
* fully deterministic and reliable.
|
|
21
23
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
+
* Status is NOT polled here — settlement takes seconds-to-minutes and blocking
|
|
25
|
+
* the chat is bad UX. The recipe reports "submitted, settling"; the user (or a
|
|
26
|
+
* follow-up turn) calls `kaleidoswap_atomic_status` on demand.
|
|
27
|
+
*
|
|
28
|
+
* Confirmation: the single decision a user makes is "given this quote, proceed?"
|
|
29
|
+
* — so the recipe declares ONE `confirm(ctx)` summary, fired after the quote and
|
|
30
|
+
* before init. init/whitelist/execute then run as one approved unit. (The
|
|
31
|
+
* runner's recipe-level confirm path handles this; see recipe/runner.ts.)
|
|
24
32
|
*/
|
|
25
33
|
|
|
26
|
-
import type { Recipe } from './types.js';
|
|
34
|
+
import type { Recipe, RecipeContext } from './types.js';
|
|
27
35
|
import { extractSwap } from './swap.js';
|
|
28
36
|
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
// Fire on swap intent — "swap/exchange/convert/trade", or "buy/sell/get" when a
|
|
38
|
+
// crypto asset is named (so "buy one usdt" routes here, but "buy a gift card"
|
|
39
|
+
// does not). PRICE / rate / "how much" questions are read-only and go to
|
|
40
|
+
// `kaleidoswapPriceRecipe` instead — keep them out of this match.
|
|
41
|
+
const ASSET = /\b(btc|bitcoin|sats?|usdt|tether|xaut|gold)\b/i;
|
|
42
|
+
const SWAP_INTENT = (t: string) => {
|
|
43
|
+
// Explanatory / educational questions → route to RAG-backed agentic answer,
|
|
44
|
+
// not the deterministic spend chain.
|
|
45
|
+
if (/\b(why|how|what|when|explain|tell\s+me|do\s+I\s+need|should\s+I|can\s+I)\b/i.test(t)) return false;
|
|
46
|
+
if (/\b(swap|exchange|convert|trade)\b/i.test(t)) return true;
|
|
47
|
+
if (
|
|
48
|
+
/\b(buy|sell|get|purchase|acquire)\b/i.test(t) &&
|
|
49
|
+
ASSET.test(t) &&
|
|
50
|
+
// Exclude commerce / receive / LSPS1 channel-order phrasings that share
|
|
51
|
+
// the buy/get verb. "Buy a USDT channel" is a channel order, not a swap.
|
|
52
|
+
!/\b(gift\s?card|top-?up|esim|voucher|invoice|address|channel|inbound|liquidity|lsps?\b)\b/i.test(t)
|
|
53
|
+
) return true;
|
|
54
|
+
return false;
|
|
55
|
+
};
|
|
31
56
|
|
|
32
|
-
|
|
33
|
-
|
|
57
|
+
interface QuoteResult {
|
|
58
|
+
rfq_id?: string;
|
|
59
|
+
from_asset?: { asset_id?: string; ticker?: string; amount?: number };
|
|
60
|
+
to_asset?: { asset_id?: string; ticker?: string; amount?: number };
|
|
61
|
+
from_amount_display?: string;
|
|
62
|
+
to_amount_display?: string;
|
|
63
|
+
fee_display?: string;
|
|
34
64
|
}
|
|
65
|
+
interface InitResult { swapstring?: string; payment_hash?: string }
|
|
66
|
+
interface NodeInfo { pubkey?: string }
|
|
35
67
|
|
|
36
68
|
export const kaleidoswapAtomicRecipe: Recipe = {
|
|
37
69
|
name: 'kaleidoswap-atomic',
|
|
38
70
|
description:
|
|
39
|
-
'
|
|
40
|
-
match: (t) =>
|
|
41
|
-
triggers: ['
|
|
71
|
+
'Swap between BTC and an RGB asset on KaleidoSwap: quote, confirm once, then init (maker) → whitelist (node) → execute (maker).',
|
|
72
|
+
match: (t) => SWAP_INTENT(t),
|
|
73
|
+
triggers: ['swap', 'exchange', 'convert', 'trade', 'buy', 'sell'],
|
|
42
74
|
slots: [
|
|
43
75
|
{ name: 'from_asset', type: 'string', description: 'Asset to spend (BTC / USDT / XAUT)', required: true },
|
|
44
76
|
{ name: 'to_asset', type: 'string', description: 'Asset to receive (BTC / USDT / XAUT)', required: true },
|
|
45
|
-
{ name: 'amount', type: 'number', description: '
|
|
77
|
+
{ name: 'amount', type: 'number', description: 'The amount the user named' },
|
|
78
|
+
{ name: 'amount_side', type: 'string', description: "Which leg the amount is on: 'from' (sell/swap) or 'to' (buy)" },
|
|
46
79
|
],
|
|
80
|
+
// Keep the fast `extract` for the Funnel's cheap pre-filter (so "buy 1 usdt"
|
|
81
|
+
// reliably enters the recipe branch instead of falling to free agentic).
|
|
82
|
+
// `forceModelExtract` makes runRecipe ignore the deterministic result and
|
|
83
|
+
// always ask the model to produce the actual slots used for execution.
|
|
47
84
|
extract: extractSwap,
|
|
85
|
+
forceModelExtract: true,
|
|
48
86
|
confident: (s) => !!s.from_asset && !!s.to_asset && !!s.amount,
|
|
49
87
|
steps: [
|
|
50
|
-
// 1.
|
|
88
|
+
// 1. MAKER quotes the swap (read-only). Returns rfq_id + full asset specs
|
|
89
|
+
// (echoes the rgb: asset ids and maker-unit amounts) + *_display strings.
|
|
51
90
|
{
|
|
52
91
|
tool: 'kaleidoswap_get_quote',
|
|
53
92
|
as: 'quote',
|
|
@@ -55,63 +94,70 @@ export const kaleidoswapAtomicRecipe: Recipe = {
|
|
|
55
94
|
from_asset: ctx.slots.from_asset,
|
|
56
95
|
to_asset: ctx.slots.to_asset,
|
|
57
96
|
amount: ctx.slots.amount,
|
|
97
|
+
// 'to' for buy ("buy 1 USDT" → amount is what you RECEIVE); default
|
|
98
|
+
// 'from' for sell/swap. The host puts the amount on the right leg.
|
|
99
|
+
amount_side: ctx.slots.amount_side ?? 'from',
|
|
58
100
|
}),
|
|
59
101
|
},
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
as: 'receive_rgb',
|
|
64
|
-
args: (ctx) => {
|
|
65
|
-
const q = ctx.results.quote as { receive_amount?: number } | undefined;
|
|
66
|
-
return { asset: ctx.slots.to_asset, amount: q?.receive_amount };
|
|
67
|
-
},
|
|
68
|
-
skipIf: (ctx) => isBtc(ctx.slots.to_asset),
|
|
69
|
-
},
|
|
70
|
-
// 2b. User's node creates an LN receive invoice (when to_asset is BTC).
|
|
71
|
-
{
|
|
72
|
-
tool: 'rln_create_ln_invoice',
|
|
73
|
-
as: 'receive_ln',
|
|
74
|
-
args: (ctx) => {
|
|
75
|
-
const q = ctx.results.quote as { receive_amount?: number } | undefined;
|
|
76
|
-
return { amount_sats: q?.receive_amount };
|
|
77
|
-
},
|
|
78
|
-
skipIf: (ctx) => !isBtc(ctx.slots.to_asset),
|
|
79
|
-
},
|
|
80
|
-
// 3. Maker locks the swap. Returns { atomic_id, maker_invoice }. Spend-gated.
|
|
102
|
+
// 2. MAKER locks the swap. SwapRequest is flat (asset ids + maker-unit
|
|
103
|
+
// amounts) — sourced straight from the quote result, no re-scaling.
|
|
104
|
+
// First spend step → the recipe-level confirm gate fires just before it.
|
|
81
105
|
{
|
|
82
106
|
tool: 'kaleidoswap_atomic_init',
|
|
83
|
-
as: '
|
|
107
|
+
as: 'init',
|
|
84
108
|
args: (ctx) => {
|
|
85
|
-
const
|
|
86
|
-
const ln = ctx.results.receive_ln as { invoice?: string } | undefined;
|
|
87
|
-
const q = ctx.results.quote as { quote_id?: string } | undefined;
|
|
109
|
+
const q = ctx.results.quote as QuoteResult | undefined;
|
|
88
110
|
return {
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
rfq_id: q?.rfq_id,
|
|
112
|
+
from_asset: q?.from_asset?.asset_id,
|
|
113
|
+
from_amount: q?.from_asset?.amount,
|
|
114
|
+
to_asset: q?.to_asset?.asset_id,
|
|
115
|
+
to_amount: q?.to_asset?.amount,
|
|
91
116
|
};
|
|
92
117
|
},
|
|
93
118
|
},
|
|
94
|
-
//
|
|
119
|
+
// 3. NODE: read our pubkey — the maker needs it as taker_pubkey for execute.
|
|
95
120
|
{
|
|
96
|
-
tool: '
|
|
97
|
-
as: '
|
|
121
|
+
tool: 'rln_get_node_info',
|
|
122
|
+
as: 'node',
|
|
123
|
+
args: () => ({}),
|
|
124
|
+
},
|
|
125
|
+
// 4. NODE: whitelist the maker's swapstring (accept the swap). Ungated —
|
|
126
|
+
// covered by the single confirm above.
|
|
127
|
+
{
|
|
128
|
+
tool: 'rln_whitelist_swap',
|
|
129
|
+
as: 'whitelist',
|
|
98
130
|
args: (ctx) => {
|
|
99
|
-
const
|
|
100
|
-
return {
|
|
131
|
+
const init = ctx.results.init as InitResult | undefined;
|
|
132
|
+
return { swapstring: init?.swapstring };
|
|
101
133
|
},
|
|
102
134
|
},
|
|
103
135
|
],
|
|
104
|
-
// 5.
|
|
136
|
+
// 5. MAKER settles the swap. Needs swapstring + taker_pubkey + payment_hash.
|
|
105
137
|
final: {
|
|
106
138
|
tool: 'kaleidoswap_atomic_execute',
|
|
107
139
|
args: (ctx) => {
|
|
108
|
-
const
|
|
109
|
-
|
|
140
|
+
const init = ctx.results.init as InitResult | undefined;
|
|
141
|
+
const node = ctx.results.node as NodeInfo | undefined;
|
|
142
|
+
return {
|
|
143
|
+
swapstring: init?.swapstring,
|
|
144
|
+
taker_pubkey: node?.pubkey,
|
|
145
|
+
payment_hash: init?.payment_hash,
|
|
146
|
+
};
|
|
110
147
|
},
|
|
111
148
|
},
|
|
149
|
+
// ONE confirmation, fired after the quote / before init, with the real numbers.
|
|
150
|
+
confirm: (ctx: RecipeContext) => {
|
|
151
|
+
const q = ctx.results.quote as QuoteResult | undefined;
|
|
152
|
+
const from = q?.from_amount_display ?? `${ctx.slots.amount} ${ctx.slots.from_asset}`;
|
|
153
|
+
const to = q?.to_amount_display ?? String(ctx.slots.to_asset);
|
|
154
|
+
const fee = q?.fee_display ? ` · fee ${q.fee_display}` : '';
|
|
155
|
+
return `Swap ${from} → ${to}${fee} on KaleidoSwap. Proceed?`;
|
|
156
|
+
},
|
|
112
157
|
summary: (ctx) => {
|
|
113
|
-
const q = ctx.results.quote as
|
|
114
|
-
const
|
|
115
|
-
|
|
158
|
+
const q = ctx.results.quote as QuoteResult | undefined;
|
|
159
|
+
const from = q?.from_amount_display ?? `${ctx.slots.amount} ${ctx.slots.from_asset}`;
|
|
160
|
+
const to = q?.to_amount_display ?? String(ctx.slots.to_asset);
|
|
161
|
+
return `Swap submitted: ${from} → ${to}. Settling now — ask me to check the status.`;
|
|
116
162
|
},
|
|
117
163
|
};
|