@kaleidorg/mind 0.9.0 → 0.10.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/guards.d.ts.map +1 -1
- package/dist/guards.js +20 -0
- package/dist/guards.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +7 -0
- package/dist/kaleidoswap/contract.d.ts.map +1 -1
- package/dist/kaleidoswap/contract.js +71 -17
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/kaleidoswap/index.d.ts +1 -1
- package/dist/kaleidoswap/index.d.ts.map +1 -1
- package/dist/kaleidoswap/index.js +1 -1
- package/dist/kaleidoswap/index.js.map +1 -1
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +115 -96
- package/dist/qvac/provider.js.map +1 -1
- package/dist/recipe/asset-send.js +1 -1
- package/dist/recipe/asset-send.js.map +1 -1
- package/dist/testing/mock-wallet.d.ts.map +1 -1
- package/dist/testing/mock-wallet.js +6 -0
- package/dist/testing/mock-wallet.js.map +1 -1
- package/dist/wallet/contract.d.ts +5 -0
- package/dist/wallet/contract.d.ts.map +1 -1
- package/dist/wallet/contract.js +39 -6
- package/dist/wallet/contract.js.map +1 -1
- package/package.json +1 -1
- package/scripts/snapshot-mcp-tools.mjs +38 -0
- package/skills/README.md +98 -64
- package/skills/bitrefill/SKILL.md +30 -157
- package/skills/channel-manager/SKILL.md +31 -52
- package/skills/flashnet-swaps/SKILL.md +24 -150
- package/skills/kaleido-node/SKILL.md +25 -55
- package/skills/kaleido-trading/SKILL.md +28 -172
- package/skills/kaleido-trading/references/assets.md +4 -4
- package/skills/kaleido-trading/references/atomic.md +5 -7
- package/skills/merchant-finder/SKILL.md +25 -108
- package/skills/paid-data/SKILL.md +25 -58
- package/skills/portfolio-manager/SKILL.md +26 -60
- package/skills/rgb-lightning-node/SKILL.md +37 -255
- package/skills/rgb-lightning-node/references/channels.md +34 -0
- package/skills/spark-wallet/SKILL.md +26 -228
- package/skills/submarine-swaps/SKILL.md +19 -37
- package/skills/wallet-assistant/SKILL.md +26 -44
- package/src/funnel.mind.test.ts +6 -5
- package/src/guards.test.ts +12 -0
- package/src/guards.ts +20 -0
- package/src/index.ts +1 -0
- package/src/kaleidoswap/contract.test.ts +32 -3
- package/src/kaleidoswap/contract.ts +65 -17
- package/src/kaleidoswap/index.ts +1 -0
- package/src/qvac/provider.test.ts +17 -0
- package/src/qvac/provider.ts +23 -4
- package/src/recipe/asset-send.ts +1 -1
- package/src/recipe/recipe.test.ts +1 -1
- package/src/skills/catalog.test.ts +215 -0
- package/src/skills/mcp-tools.snapshot.json +1938 -0
- package/src/testing/mock-wallet.ts +6 -0
- package/src/wallet/contract.test.ts +20 -1
- package/src/wallet/contract.ts +36 -6
- package/skills/dca/SKILL.md +0 -48
- package/skills/kaleido-lsps/SKILL.md +0 -131
- package/skills/liquidity-optimizer/SKILL.md +0 -91
|
@@ -1,48 +1,30 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: submarine-swaps
|
|
3
|
-
description: "Pay a Lightning invoice from Liquid funds (L-USDT or L-BTC) through a KaleidoSwap submarine swap
|
|
3
|
+
description: "Pay a Lightning invoice from Liquid funds (L-USDT or L-BTC) through a KaleidoSwap submarine swap, and follow its status."
|
|
4
4
|
tools: kaleidoswap_submarine_pairs, kaleidoswap_submarine_create, kaleidoswap_submarine_fund, kaleidoswap_submarine_status
|
|
5
|
-
|
|
5
|
+
requires-tools: kaleidoswap_submarine_create
|
|
6
|
+
triggers: l-usdt, liquid usdt, usdt on liquid, l-btc, liquid bitcoin, submarine, submarine swap, pay invoice with liquid
|
|
6
7
|
metadata:
|
|
7
8
|
author: kaleidoswap
|
|
8
|
-
version: "0.
|
|
9
|
+
version: "0.2.0"
|
|
9
10
|
---
|
|
11
|
+
# Submarine swaps
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
The user locks Liquid funds; the maker pays the Lightning invoice, then claims
|
|
14
|
+
the lockup. If the maker can't pay, the lockup is refundable. Plain "USDT" is
|
|
15
|
+
RGB USDT (`kaleido-trading`), not this skill.
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## Critical rules
|
|
18
|
-
|
|
19
|
-
- Every amount, fee and status in your reply MUST come from a tool result in the
|
|
20
|
-
CURRENT turn. Never guess the amount to lock — `kaleidoswap_submarine_create`
|
|
21
|
-
returns it.
|
|
22
|
-
- `kaleidoswap_submarine_fund` moves money. Call it ONLY after the user has
|
|
23
|
-
confirmed the exact `expected_amount` returned by `kaleidoswap_submarine_create`.
|
|
24
|
-
- `kaleidoswap_submarine_fund` takes only `swap_id`. Never try to pass an amount
|
|
25
|
-
or an address.
|
|
26
|
-
- A bare "USDT" means RGB USDT on the Lightning node, which uses the atomic swap
|
|
27
|
-
tools, not this skill. Use this skill only for Liquid assets (L-USDT, L-BTC).
|
|
28
|
-
|
|
29
|
-
## Flow
|
|
30
|
-
|
|
31
|
-
1. `kaleidoswap_submarine_create { invoice, from_asset }` — from_asset is
|
|
32
|
-
`L-USDT` (default) or `L-BTC`. Returns `swap_id`, `expected_amount` (smallest
|
|
33
|
-
unit: L-USDT has 8 decimals, L-BTC is sats) and the fees.
|
|
17
|
+
## Do
|
|
18
|
+
1. `kaleidoswap_submarine_create` (`invoice`, `from_asset`) — `from_asset` is
|
|
19
|
+
`L-USDT` (default) or `L-BTC`. Returns `swap_id`, `expected_amount`
|
|
20
|
+
(smallest unit, 8 decimals for both) and fees. Nothing moves yet.
|
|
34
21
|
2. Tell the user the amount and ask to confirm.
|
|
35
|
-
3. `kaleidoswap_submarine_fund
|
|
36
|
-
4. `kaleidoswap_submarine_status
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
Use `kaleidoswap_submarine_pairs` when the user asks what can pay a Lightning
|
|
41
|
-
invoice, or for the limits and fees.
|
|
22
|
+
3. `kaleidoswap_submarine_fund` (`swap_id` only; confirm-gated).
|
|
23
|
+
4. `kaleidoswap_submarine_status` (`swap_id`) — `transaction.claimed` means paid;
|
|
24
|
+
`failed: true` on a funded swap needs a refund: quote the `refund` field.
|
|
25
|
+
- Limits and fees: `kaleidoswap_submarine_pairs {}`.
|
|
42
26
|
|
|
43
27
|
## Examples
|
|
44
|
-
|
|
45
|
-
- "
|
|
46
|
-
|
|
47
|
-
- "did my Liquid payment go through? swap sub1" →
|
|
48
|
-
`kaleidoswap_submarine_status { swap_id: "sub1" }`
|
|
28
|
+
- "Pay lntbs10u1p… with L-USDT" → `kaleidoswap_submarine_create {"invoice":"lntbs10u1p…","from_asset":"L-USDT"}`
|
|
29
|
+
- "Did swap sub1 go through?" → `kaleidoswap_submarine_status {"swap_id":"sub1"}`
|
|
30
|
+
- "What can pay Lightning from Liquid?" → `kaleidoswap_submarine_pairs {}`
|
|
@@ -1,49 +1,31 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wallet-assistant
|
|
3
|
-
description: Everyday wallet tasks
|
|
4
|
-
tools: get_balances,
|
|
5
|
-
|
|
3
|
+
description: "Everyday wallet tasks in the app across all layers: balance, receive (invoice or address), send or pay, contacts, BTC price and fiat conversion."
|
|
4
|
+
tools: get_balances, create_invoice, send_payment, resolve_contact, get_price, fiat_to_sats, rln_list_assets, rln_pay_invoice, spark_pay_invoice
|
|
5
|
+
requires-tools: get_balances
|
|
6
|
+
triggers: balance, balances, pay, send, receive, address, invoice, contact, funds, money, price, how much
|
|
7
|
+
metadata:
|
|
8
|
+
author: kaleidoswap
|
|
9
|
+
version: "0.3.0"
|
|
6
10
|
---
|
|
7
|
-
|
|
8
11
|
# Wallet assistant
|
|
9
12
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
##
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
yourself.
|
|
30
|
-
|
|
31
|
-
## Rules
|
|
32
|
-
|
|
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.
|
|
13
|
+
The app routes these tools to the right layer (Spark, RLN, Arkade, Liquid).
|
|
14
|
+
`amount_sats` is satoshis; other amounts are display units of the asset.
|
|
15
|
+
|
|
16
|
+
## Do
|
|
17
|
+
- Balance: `get_balances` (add `layer` for one layer). Report every component;
|
|
18
|
+
`pending` is not spendable yet.
|
|
19
|
+
- Receive: `create_invoice` with `asset` and optional `amount`; reply with the
|
|
20
|
+
full invoice or address returned.
|
|
21
|
+
- Send to a name: `resolve_contact` first, then `send_payment`. A BOLT11
|
|
22
|
+
invoice the user pasted: `rln_pay_invoice` or `spark_pay_invoice`. Every
|
|
23
|
+
send is confirm-gated; state amount and destination.
|
|
24
|
+
- Fiat: `fiat_to_sats` converts; `get_price` gives the BTC price.
|
|
25
|
+
- Swaps go to `kaleido-trading` (RGB) or `flashnet-swaps` (Spark tokens).
|
|
26
|
+
|
|
27
|
+
## Examples
|
|
28
|
+
- "What's my balance?" → `get_balances {}`
|
|
29
|
+
- "Invoice for 2000 sats" → `create_invoice {"asset":"BTC","amount":2000}`
|
|
30
|
+
- "Send 5000 sats to bob" → `resolve_contact {"name":"bob"}` then `send_payment {"to":"<bob's address>","amount_sats":5000}`
|
|
31
|
+
- "How many sats is 10 EUR?" → `fiat_to_sats {"amount":10,"currency":"EUR"}`
|
package/src/funnel.mind.test.ts
CHANGED
|
@@ -318,10 +318,11 @@ describe('desktop mind — skill scoping (real skills)', () => {
|
|
|
318
318
|
);
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
-
it('wallet-assistant
|
|
321
|
+
it('wallet-assistant is the in-app router; the node skill carries the rln_* balance tools', () => {
|
|
322
322
|
const wallet = SKILLS.find((s) => s.name === 'wallet-assistant')!;
|
|
323
|
-
expect(wallet.tools).
|
|
324
|
-
|
|
323
|
+
expect(wallet.metadata?.['requires-tools']).toBe('get_balances');
|
|
324
|
+
const node = SKILLS.find((s) => s.name === 'rgb-lightning-node')!;
|
|
325
|
+
expect(node.tools).toEqual(expect.arrayContaining(['rln_get_balances', 'rln_get_address', 'rln_send_btc', 'rln_create_ln_invoice']));
|
|
325
326
|
});
|
|
326
327
|
|
|
327
328
|
it('rgb-lightning-node (triggers on "channels") exposes only canonical rln_* tools', () => {
|
|
@@ -361,9 +362,9 @@ describe('desktop mind — skill scoping (real skills)', () => {
|
|
|
361
362
|
const res = await funnel.runTurn("what's my balance?");
|
|
362
363
|
|
|
363
364
|
expect(res.tier).toBe('agentic');
|
|
364
|
-
//
|
|
365
|
+
// Node-only host: the RGB node skill is selected AND rln_get_balances survives its scoping…
|
|
365
366
|
const agenticLine = logs.find((l) => l.startsWith('tier=agentic'));
|
|
366
|
-
expect(agenticLine).toMatch(/skill=
|
|
367
|
+
expect(agenticLine).toMatch(/skill=rgb-lightning-node/);
|
|
367
368
|
expect(agenticLine).toMatch(/rln_get_balances/);
|
|
368
369
|
// …and the tool actually executes (not narrated).
|
|
369
370
|
expect(calls.map((c) => c.name)).toContain('rln_get_balances');
|
package/src/guards.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { InProcessToolSource } from './tools/in-process.js';
|
|
|
6
6
|
import { parseSkill, SkillRegistry } from './skills/registry.js';
|
|
7
7
|
import { scriptedProvider } from './testing/scripted-provider.js';
|
|
8
8
|
import { confirmReadback } from './wallet/confirm.js';
|
|
9
|
+
import { getKaleidoswapTool } from './kaleidoswap/contract.js';
|
|
9
10
|
import {
|
|
10
11
|
DECLINED_TOOL_MESSAGE,
|
|
11
12
|
detectWalletAction,
|
|
@@ -31,7 +32,18 @@ const ISSUE_SCHEMA = {
|
|
|
31
32
|
|
|
32
33
|
const INVOICE = 'lnbcrt50u1pn9xyzpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq';
|
|
33
34
|
|
|
35
|
+
const QUOTE = { name: 'kaleidoswap_get_quote', parameters: getKaleidoswapTool('kaleidoswap_get_quote')!.parameters };
|
|
36
|
+
|
|
34
37
|
describe('validateToolArgs', () => {
|
|
38
|
+
it('rejects sats passed as a BTC display amount and names the BTC value', () => {
|
|
39
|
+
const r = validateToolArgs(QUOTE, { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 50_000 });
|
|
40
|
+
expect(r.ok).toBe(false);
|
|
41
|
+
expect(r.errors.join(' ')).toMatch(/from_amount: 0\.0005/);
|
|
42
|
+
expect(validateToolArgs(QUOTE, { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.0005 }).ok).toBe(true);
|
|
43
|
+
expect(validateToolArgs(QUOTE, { from_asset_id: 'USDT', to_asset_id: 'BTC', from_amount: 5000 }).ok).toBe(true);
|
|
44
|
+
expect(validateToolArgs(QUOTE, { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 1, to_amount: 2 }).ok).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
35
47
|
it('accepts valid args and coerces numeric strings', () => {
|
|
36
48
|
const r = validateToolArgs({ name: 'rln_issue_asset', parameters: ISSUE_SCHEMA }, { name: 'Hack', ticker: 'HCK', amount: '1000' });
|
|
37
49
|
expect(r.ok).toBe(true);
|
package/src/guards.ts
CHANGED
|
@@ -78,6 +78,26 @@ const SEMANTIC_RULES: Record<string, (a: Record<string, unknown>) => string[]> =
|
|
|
78
78
|
};
|
|
79
79
|
SEMANTIC_RULES.wdk_issue_asset = SEMANTIC_RULES.rln_issue_asset!;
|
|
80
80
|
|
|
81
|
+
/** Above this, a BTC amount in display units is almost certainly sats. */
|
|
82
|
+
const MAX_PLAUSIBLE_BTC = 1000;
|
|
83
|
+
|
|
84
|
+
/** KaleidoSwap quote amounts are display units; reject sats passed as BTC. */
|
|
85
|
+
function btcDisplayAmounts(a: Record<string, unknown>): string[] {
|
|
86
|
+
const errors: string[] = [];
|
|
87
|
+
const legs: Array<[string, unknown]> = [['from_amount', a.from_asset_id], ['to_amount', a.to_asset_id]];
|
|
88
|
+
if (a.from_amount != null && a.to_amount != null) errors.push('give only one of "from_amount" or "to_amount"');
|
|
89
|
+
for (const [key, asset] of legs) {
|
|
90
|
+
const v = a[key];
|
|
91
|
+
if (typeof v !== 'number' || !/^btc$/i.test(String(asset ?? '').trim())) continue;
|
|
92
|
+
if (v >= MAX_PLAUSIBLE_BTC) {
|
|
93
|
+
errors.push(`"${key}" is in BTC, not sats: ${v} sats is ${+(v / 1e8).toFixed(8)} BTC — pass ${key}: ${+(v / 1e8).toFixed(8)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return errors;
|
|
97
|
+
}
|
|
98
|
+
SEMANTIC_RULES.kaleidoswap_get_quote = btcDisplayAmounts;
|
|
99
|
+
SEMANTIC_RULES.kaleidoswap_get_spreads = btcDisplayAmounts;
|
|
100
|
+
|
|
81
101
|
/**
|
|
82
102
|
* Validate (and lightly coerce) a tool call's arguments against its schema.
|
|
83
103
|
* Unknown schema shapes pass through unchanged.
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
getKaleidoswapTool,
|
|
7
7
|
kaleidoswapTools,
|
|
8
8
|
bindKaleidoswapTools,
|
|
9
|
+
normalizeKaleidoswapArgs,
|
|
9
10
|
type KaleidoswapHandler,
|
|
10
11
|
} from './contract.js';
|
|
11
12
|
|
|
@@ -109,10 +110,14 @@ describe('bindKaleidoswapTools', () => {
|
|
|
109
110
|
expect(pairs?.requiresConfirmation).toBeFalsy();
|
|
110
111
|
});
|
|
111
112
|
|
|
112
|
-
it('dispatches execute()
|
|
113
|
+
it('dispatches execute() with kaleido-mcp args plus the legacy aliases', async () => {
|
|
113
114
|
const src = bindKaleidoswapTools(echoHandlers());
|
|
114
|
-
const r = await src.execute('kaleidoswap_get_quote', {
|
|
115
|
-
expect(r).toMatchObject({
|
|
115
|
+
const r = await src.execute('kaleidoswap_get_quote', { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.001 });
|
|
116
|
+
expect(r).toMatchObject({
|
|
117
|
+
ok: true,
|
|
118
|
+
tool: 'get_quote',
|
|
119
|
+
args: { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.001, from_asset: 'BTC', to_asset: 'USDT', amount: 100_000 },
|
|
120
|
+
});
|
|
116
121
|
});
|
|
117
122
|
|
|
118
123
|
it('throws when a handler is missing and allowMissing is false', () => {
|
|
@@ -142,3 +147,27 @@ describe('bindKaleidoswapTools', () => {
|
|
|
142
147
|
expect(src.id).toBe('maker-prod');
|
|
143
148
|
});
|
|
144
149
|
});
|
|
150
|
+
|
|
151
|
+
describe('normalizeKaleidoswapArgs', () => {
|
|
152
|
+
it('maps a display-unit quote to the legacy sats amount', () => {
|
|
153
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_get_quote', { from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.0005 }))
|
|
154
|
+
.toMatchObject({ from_asset: 'BTC', to_asset: 'USDT', amount: 50_000 });
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('keeps RGB amounts in display units and marks a buy-side amount', () => {
|
|
158
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_get_quote', { from_asset_id: 'BTC', to_asset_id: 'USDT', to_amount: 10 }))
|
|
159
|
+
.toMatchObject({ amount: 10, amount_side: 'to' });
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('maps a legacy sats quote to display units', () => {
|
|
163
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_get_quote', { from_asset: 'BTC', to_asset: 'USDT', amount: 100_000 }))
|
|
164
|
+
.toMatchObject({ from_asset_id: 'BTC', to_asset_id: 'USDT', from_amount: 0.001 });
|
|
165
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_get_quote', { from_asset: 'USDT', to_asset: 'BTC', amount: 25 }))
|
|
166
|
+
.toMatchObject({ from_amount: 25 });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('aliases rfq_id/quote_id and payment_hash/atomic_id', () => {
|
|
170
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_atomic_init', { rfq_id: 'r1' })).toMatchObject({ quote_id: 'r1' });
|
|
171
|
+
expect(normalizeKaleidoswapArgs('kaleidoswap_atomic_status', { atomic_id: 'h1' })).toMatchObject({ payment_hash: 'h1' });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -70,14 +70,16 @@ export const KALEIDOSWAP_TOOLS: KaleidoswapToolDef[] = [
|
|
|
70
70
|
|
|
71
71
|
t('market',
|
|
72
72
|
'kaleidoswap_get_quote',
|
|
73
|
-
'Get an executable quote for
|
|
73
|
+
'Get an executable quote for one pair. Amounts are DISPLAY units (0.0005 = 0.0005 BTC = 50,000 sats). Give exactly one of from_amount (sell a fixed input) or to_amount (buy a fixed output). Returns rfq_id, from_asset/to_asset with amount_display and amount_raw, price and expires_at (~60s).',
|
|
74
74
|
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
from_asset_id: { type: 'string', description: "Asset to sell: ticker ('BTC', 'USDT') or RGB id ('rgb:…')." },
|
|
76
|
+
to_asset_id: { type: 'string', description: "Asset to buy: ticker ('USDT', 'BTC') or RGB id ('rgb:…')." },
|
|
77
|
+
from_layer: { type: 'string', description: "Optional: 'BTC_LN', 'RGB_LN', 'BTC_SPARK'. Derived from the asset when omitted." },
|
|
78
|
+
to_layer: { type: 'string', description: "Optional: 'RGB_LN', 'BTC_LN', 'BTC_SPARK'. Derived from the asset when omitted." },
|
|
79
|
+
from_amount: { type: 'number', description: 'Amount to SELL in display units (e.g. 0.001 BTC, 10 USDT).' },
|
|
80
|
+
to_amount: { type: 'number', description: 'Amount to BUY in display units (e.g. 10 USDT).' },
|
|
79
81
|
},
|
|
80
|
-
['
|
|
82
|
+
['from_asset_id', 'to_asset_id']),
|
|
81
83
|
|
|
82
84
|
t('market',
|
|
83
85
|
'kaleidoswap_get_nodeinfo',
|
|
@@ -86,30 +88,36 @@ export const KALEIDOSWAP_TOOLS: KaleidoswapToolDef[] = [
|
|
|
86
88
|
// ─── atomic (the trust-minimised swap chain — used by the recipe) ───────
|
|
87
89
|
t('atomic',
|
|
88
90
|
'kaleidoswap_atomic_init',
|
|
89
|
-
|
|
91
|
+
'Start an atomic swap from a fresh quote. SPEND: confirmation-gated. Pass the quote rfq_id, both asset ids and the quote legs\' amount_raw values unchanged. Returns swapstring, payment_hash and access_token (keep it for status).',
|
|
90
92
|
{
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
rfq_id: { type: 'string', description: 'rfq_id from kaleidoswap_get_quote.' },
|
|
94
|
+
from_asset_id: { type: 'string', description: 'from_asset.asset_id from the quote.' },
|
|
95
|
+
from_amount_raw: { type: 'integer', description: 'from_asset.amount_raw from the quote, unchanged.' },
|
|
96
|
+
to_asset_id: { type: 'string', description: 'to_asset.asset_id from the quote.' },
|
|
97
|
+
to_amount_raw: { type: 'integer', description: 'to_asset.amount_raw from the quote, unchanged.' },
|
|
93
98
|
},
|
|
94
|
-
['
|
|
99
|
+
['rfq_id', 'from_asset_id', 'from_amount_raw', 'to_asset_id', 'to_amount_raw'],
|
|
95
100
|
/* spend */ true),
|
|
96
101
|
|
|
97
102
|
t('atomic',
|
|
98
103
|
'kaleidoswap_atomic_execute',
|
|
99
|
-
|
|
104
|
+
'Confirm the swap after rln_atomic_taker whitelisted the swapstring. SPEND: confirmation-gated. taker_pubkey is the pubkey from rln_get_node_info.',
|
|
100
105
|
{
|
|
101
|
-
|
|
106
|
+
swapstring: { type: 'string', description: 'swapstring from kaleidoswap_atomic_init.' },
|
|
107
|
+
taker_pubkey: { type: 'string', description: 'Node pubkey from rln_get_node_info.' },
|
|
108
|
+
payment_hash: { type: 'string', description: 'payment_hash from kaleidoswap_atomic_init.' },
|
|
102
109
|
},
|
|
103
|
-
['
|
|
110
|
+
['swapstring', 'taker_pubkey', 'payment_hash'],
|
|
104
111
|
/* spend */ true),
|
|
105
112
|
|
|
106
113
|
t('atomic',
|
|
107
114
|
'kaleidoswap_atomic_status',
|
|
108
|
-
'Poll
|
|
115
|
+
'Poll an atomic swap by payment_hash: Waiting → Pending → Succeeded | Expired | Failed.',
|
|
109
116
|
{
|
|
110
|
-
|
|
117
|
+
payment_hash: { type: 'string', description: 'payment_hash from kaleidoswap_atomic_init.' },
|
|
118
|
+
access_token: { type: 'string', description: 'access_token from kaleidoswap_atomic_init, when the host needs it.' },
|
|
111
119
|
},
|
|
112
|
-
['
|
|
120
|
+
['payment_hash']),
|
|
113
121
|
|
|
114
122
|
// ─── liquidity (buy a NEW channel pre-loaded with an asset — onboarding) ──
|
|
115
123
|
t('liquidity',
|
|
@@ -157,6 +165,46 @@ export function kaleidoswapTools(
|
|
|
157
165
|
return KALEIDOSWAP_TOOLS.filter((x) => groups.has(x.group));
|
|
158
166
|
}
|
|
159
167
|
|
|
168
|
+
const isBtc = (asset: unknown) => typeof asset === 'string' && /^(btc|sats?)$/i.test(asset.trim());
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Map kaleido-mcp argument names onto the pre-0.9 contract names (and back), so
|
|
172
|
+
* one call shape works on every surface and older host handlers keep working.
|
|
173
|
+
* Canonical names win; legacy names are only filled in when absent. The legacy
|
|
174
|
+
* quote `amount` is sats for BTC and display units otherwise.
|
|
175
|
+
*/
|
|
176
|
+
export function normalizeKaleidoswapArgs(name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
177
|
+
const a: Record<string, unknown> = { ...args };
|
|
178
|
+
const alias = (canonical: string, legacy: string) => {
|
|
179
|
+
if (a[canonical] == null && a[legacy] != null) a[canonical] = a[legacy];
|
|
180
|
+
if (a[legacy] == null && a[canonical] != null) a[legacy] = a[canonical];
|
|
181
|
+
};
|
|
182
|
+
if (name === 'kaleidoswap_get_quote') {
|
|
183
|
+
if (a.from_asset_id == null && a.from_asset != null) a.from_asset_id = a.from_asset;
|
|
184
|
+
if (a.to_asset_id == null && a.to_asset != null) a.to_asset_id = a.to_asset;
|
|
185
|
+
if (a.from_amount == null && a.to_amount == null && a.amount != null) {
|
|
186
|
+
const side = a.amount_side === 'to' || a.side === 'buy' ? 'to' : 'from';
|
|
187
|
+
const asset = side === 'to' ? a.to_asset_id : a.from_asset_id;
|
|
188
|
+
const display = isBtc(asset) ? Number(a.amount) / 1e8 : Number(a.amount);
|
|
189
|
+
a[side === 'to' ? 'to_amount' : 'from_amount'] = display;
|
|
190
|
+
}
|
|
191
|
+
a.from_asset ??= a.from_asset_id;
|
|
192
|
+
a.to_asset ??= a.to_asset_id;
|
|
193
|
+
if (a.amount == null) {
|
|
194
|
+
const toSide = a.from_amount == null && a.to_amount != null;
|
|
195
|
+
const display = Number(toSide ? a.to_amount : a.from_amount);
|
|
196
|
+
const asset = toSide ? a.to_asset_id : a.from_asset_id;
|
|
197
|
+
if (Number.isFinite(display)) a.amount = isBtc(asset) ? Math.round(display * 1e8) : display;
|
|
198
|
+
if (toSide) a.amount_side = 'to';
|
|
199
|
+
}
|
|
200
|
+
} else if (name === 'kaleidoswap_atomic_init') {
|
|
201
|
+
alias('rfq_id', 'quote_id');
|
|
202
|
+
} else if (name === 'kaleidoswap_atomic_execute' || name === 'kaleidoswap_atomic_status') {
|
|
203
|
+
alias('payment_hash', 'atomic_id');
|
|
204
|
+
}
|
|
205
|
+
return a;
|
|
206
|
+
}
|
|
207
|
+
|
|
160
208
|
/** A handler bound to one contract tool. Args validated by JSON schema upstream. */
|
|
161
209
|
export type KaleidoswapHandler = (args: Record<string, unknown>) => Promise<unknown>;
|
|
162
210
|
|
|
@@ -199,7 +247,7 @@ export function bindKaleidoswapTools(
|
|
|
199
247
|
description: def.description,
|
|
200
248
|
parameters: def.parameters,
|
|
201
249
|
requiresConfirmation: def.requiresConfirmation,
|
|
202
|
-
handler,
|
|
250
|
+
handler: (args) => handler(normalizeKaleidoswapArgs(def.name, args)),
|
|
203
251
|
});
|
|
204
252
|
}
|
|
205
253
|
return new InProcessToolSource(opts.id ?? 'kaleidoswap', bound);
|
package/src/kaleidoswap/index.ts
CHANGED
|
@@ -123,6 +123,23 @@ describe('createQvacProvider.runTurn', () => {
|
|
|
123
123
|
expect(deleted).toEqual([{ kvCacheKey: 'run-1' }]);
|
|
124
124
|
});
|
|
125
125
|
|
|
126
|
+
it('retries a turn without reasoning when the tool grammar rejects the inserted </think>', async () => {
|
|
127
|
+
const calls: any[] = [];
|
|
128
|
+
const fn = (params: any) => {
|
|
129
|
+
calls.push(params);
|
|
130
|
+
if (calls.length === 1) throw new Error('Unexpected empty grammar stack after accepting piece: </think> (248069)');
|
|
131
|
+
return { requestId: 'req-2', events: (async function* () {})(), final: Promise.resolve({ contentText: 'ok', toolCalls: [], raw: { fullText: 'ok' } }) };
|
|
132
|
+
};
|
|
133
|
+
const p = createQvacProvider({ completion: fn as any, cancel: noopCancel, getModelId: () => 'm1', maxThinkingTokens: 128 });
|
|
134
|
+
const out = await p.runTurn({ messages: [{ role: 'user', content: 'x' }], tools: [] });
|
|
135
|
+
expect(out.text).toBe('ok');
|
|
136
|
+
expect(calls.map((c) => c.generationParams?.reasoning_budget)).toEqual([128, 0]);
|
|
137
|
+
await expect(
|
|
138
|
+
createQvacProvider({ completion: (() => { throw new Error('boom'); }) as any, cancel: noopCancel, getModelId: () => 'm1' })
|
|
139
|
+
.runTurn({ messages: [{ role: 'user', content: 'x' }], tools: [] }),
|
|
140
|
+
).rejects.toThrow('boom');
|
|
141
|
+
});
|
|
142
|
+
|
|
126
143
|
it('keeps the reasoning budget below the output cap', async () => {
|
|
127
144
|
const { fn, calls } = fakeCompletion({ contentText: 'ok', toolCalls: [], raw: { fullText: 'ok' } });
|
|
128
145
|
const p = createQvacProvider({ completion: fn as any, cancel: noopCancel, getModelId: () => 'm1', defaultMaxTokens: 512, maxThinkingTokens: 512 });
|
package/src/qvac/provider.ts
CHANGED
|
@@ -76,11 +76,17 @@ export interface QvacTurnInput extends TurnInput {
|
|
|
76
76
|
onStats?: (stats: QvacTurnStats) => void;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
/**
|
|
80
|
+
* llama.cpp's tool-call grammar can reject the `</think>` it inserts when a
|
|
81
|
+
* reasoning budget runs out ("Unexpected empty grammar stack after accepting
|
|
82
|
+
* piece: </think>"), which aborts the turn.
|
|
83
|
+
*/
|
|
84
|
+
function isReasoningGrammarError(err: unknown): boolean {
|
|
85
|
+
return /empty grammar stack/i.test(err instanceof Error ? err.message : String(err));
|
|
86
|
+
}
|
|
82
87
|
|
|
83
|
-
|
|
88
|
+
export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
89
|
+
const runTurnOnce = async (input: QvacTurnInput): Promise<TurnOutput> => {
|
|
84
90
|
const modelId = options.getModelId();
|
|
85
91
|
if (!modelId) throw new Error('QVAC model not loaded');
|
|
86
92
|
|
|
@@ -192,6 +198,19 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
|
192
198
|
inference,
|
|
193
199
|
...(incomplete ? { incomplete: true } : {}),
|
|
194
200
|
};
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
name: 'qvac',
|
|
205
|
+
|
|
206
|
+
async runTurn(input: QvacTurnInput): Promise<TurnOutput> {
|
|
207
|
+
try {
|
|
208
|
+
return await runTurnOnce(input);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
// Retry once without reasoning: no budget, so nothing to insert.
|
|
211
|
+
if (input.thinking === 'off' || !isReasoningGrammarError(err)) throw err;
|
|
212
|
+
return runTurnOnce({ ...input, thinking: 'off' });
|
|
213
|
+
}
|
|
195
214
|
},
|
|
196
215
|
|
|
197
216
|
async endSession(sessionKey: string): Promise<void> {
|
package/src/recipe/asset-send.ts
CHANGED
|
@@ -69,7 +69,7 @@ export const assetSendRecipe: Recipe = {
|
|
|
69
69
|
tool: 'rln_send_asset',
|
|
70
70
|
args: (ctx: RecipeContext) => {
|
|
71
71
|
const contact = ctx.results.contact as { ln_address?: string } | undefined;
|
|
72
|
-
return {
|
|
72
|
+
return { asset_id: ctx.slots.asset, amount: ctx.slots.amount, recipient_id: contact?.ln_address ?? ctx.slots.recipient };
|
|
73
73
|
},
|
|
74
74
|
},
|
|
75
75
|
summary: (ctx) => {
|
|
@@ -257,7 +257,7 @@ describe('runRecipe — asset send', () => {
|
|
|
257
257
|
const res = await runRecipe(assetSendRecipe, 'send 10 usdt to bob', { provider: approve, tools, onConfirm });
|
|
258
258
|
expect(res.status).toBe('done');
|
|
259
259
|
expect(onConfirm).toHaveBeenCalledOnce();
|
|
260
|
-
expect(sent[0]).toEqual({
|
|
260
|
+
expect(sent[0]).toEqual({ asset_id: 'USDT', amount: 10, recipient_id: 'bob@x.com' });
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
|