@kaleidorg/mind 0.7.0 → 0.8.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/README.md +96 -8
- package/dist/context/compress.d.ts.map +1 -1
- package/dist/context/compress.js +1 -0
- package/dist/context/compress.js.map +1 -1
- package/dist/context/rgb-units.d.ts +18 -0
- package/dist/context/rgb-units.d.ts.map +1 -0
- package/dist/context/rgb-units.js +86 -0
- package/dist/context/rgb-units.js.map +1 -0
- package/dist/engine.d.ts +53 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +179 -17
- package/dist/engine.js.map +1 -1
- package/dist/funnel.d.ts.map +1 -1
- package/dist/funnel.js +23 -4
- package/dist/funnel.js.map +1 -1
- package/dist/guards.d.ts +63 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +284 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +3 -4
- package/dist/kaleidoswap/contract.d.ts.map +1 -1
- package/dist/kaleidoswap/contract.js +3 -17
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/providers/openai.d.ts +64 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +233 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/types.d.ts +20 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/qvac/config.d.ts +1 -1
- package/dist/qvac/config.js +1 -1
- package/dist/qvac/index.d.ts +1 -0
- package/dist/qvac/index.d.ts.map +1 -1
- package/dist/qvac/index.js +1 -0
- package/dist/qvac/index.js.map +1 -1
- package/dist/qvac/models.d.ts +31 -0
- package/dist/qvac/models.d.ts.map +1 -0
- package/dist/qvac/models.js +80 -0
- package/dist/qvac/models.js.map +1 -0
- package/dist/qvac/parse.d.ts +11 -0
- package/dist/qvac/parse.d.ts.map +1 -1
- package/dist/qvac/parse.js +29 -7
- package/dist/qvac/parse.js.map +1 -1
- package/dist/qvac/provider.d.ts +5 -4
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +22 -8
- package/dist/qvac/provider.js.map +1 -1
- package/dist/qvac/stream.d.ts +3 -5
- package/dist/qvac/stream.d.ts.map +1 -1
- package/dist/qvac/stream.js.map +1 -1
- package/dist/qvac/tools.d.ts +5 -0
- package/dist/qvac/tools.d.ts.map +1 -1
- package/dist/qvac/tools.js +10 -0
- package/dist/qvac/tools.js.map +1 -1
- package/dist/recipe/issue-asset.d.ts.map +1 -1
- package/dist/recipe/issue-asset.js +42 -16
- package/dist/recipe/issue-asset.js.map +1 -1
- package/dist/recipe/runner.d.ts.map +1 -1
- package/dist/recipe/runner.js +1 -0
- package/dist/recipe/runner.js.map +1 -1
- package/dist/recipe/submarine-pay.d.ts +17 -0
- package/dist/recipe/submarine-pay.d.ts.map +1 -0
- package/dist/recipe/submarine-pay.js +79 -0
- package/dist/recipe/submarine-pay.js.map +1 -0
- package/dist/skills/registry.js +1 -1
- package/dist/skills/select.d.ts +20 -0
- package/dist/skills/select.d.ts.map +1 -0
- package/dist/skills/select.js +32 -0
- package/dist/skills/select.js.map +1 -0
- package/dist/submarine/contract.d.ts +42 -0
- package/dist/submarine/contract.d.ts.map +1 -0
- package/dist/submarine/contract.js +73 -0
- package/dist/submarine/contract.js.map +1 -0
- package/dist/testing/mock-wallet.d.ts +24 -0
- package/dist/testing/mock-wallet.d.ts.map +1 -1
- package/dist/testing/mock-wallet.js +75 -6
- package/dist/testing/mock-wallet.js.map +1 -1
- package/dist/wallet/confirm.d.ts.map +1 -1
- package/dist/wallet/confirm.js +27 -9
- package/dist/wallet/confirm.js.map +1 -1
- package/package.json +6 -1
- package/skills/README.md +1 -0
- package/skills/kaleido-trading/SKILL.md +22 -28
- package/skills/kaleido-trading/references/api.md +3 -3
- package/skills/rgb-lightning-node/SKILL.md +32 -12
- package/skills/spark-wallet/SKILL.md +1 -0
- package/skills/submarine-swaps/SKILL.md +48 -0
- package/src/context/compress.ts +1 -0
- package/src/context/rgb-units.test.ts +42 -0
- package/src/context/rgb-units.ts +89 -0
- package/src/engine.test.ts +94 -3
- package/src/engine.ts +234 -19
- package/src/funnel.mind.test.ts +4 -2
- package/src/funnel.ts +23 -4
- package/src/guards.test.ts +399 -0
- package/src/guards.ts +299 -0
- package/src/index.ts +37 -1
- package/src/kaleidoswap/contract.test.ts +8 -16
- package/src/kaleidoswap/contract.ts +4 -32
- package/src/providers/openai.test.ts +127 -0
- package/src/providers/openai.ts +282 -0
- package/src/providers/types.ts +22 -0
- package/src/qvac/config.ts +1 -1
- package/src/qvac/index.ts +10 -0
- package/src/qvac/models.ts +98 -0
- package/src/qvac/parse.test.ts +7 -0
- package/src/qvac/parse.ts +33 -1
- package/src/qvac/provider.test.ts +34 -1
- package/src/qvac/provider.ts +30 -13
- package/src/qvac/stream.ts +3 -5
- package/src/qvac/tools.ts +10 -0
- package/src/recipe/issue-asset.test.ts +39 -0
- package/src/recipe/issue-asset.ts +41 -13
- package/src/recipe/runner.ts +1 -0
- package/src/recipe/submarine-pay.test.ts +87 -0
- package/src/recipe/submarine-pay.ts +78 -0
- package/src/skills/registry.ts +1 -1
- package/src/skills/select.ts +41 -0
- package/src/submarine/contract.ts +112 -0
- package/src/testing/mock-wallet.ts +73 -6
- package/src/wallet/confirm.ts +27 -9
|
@@ -5,7 +5,7 @@ tools: rln_get_node_info, rln_get_balances, rln_list_channels, rln_list_assets,
|
|
|
5
5
|
triggers: node, nodeinfo, pubkey, peer, channels, channel capacity, list channels, open channel, close channel, inbound, capacity, asset balance, whitelist, taker, swapstring, swaps, payments, invoice, receive, send asset, send rgb, on-chain address, deposit, rgb invoice, ln invoice, issue, mint, new token, nft, utxos, transfers
|
|
6
6
|
metadata:
|
|
7
7
|
author: kaleidoswap
|
|
8
|
-
version: "0.3.
|
|
8
|
+
version: "0.3.3"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# RGB Lightning Node (taker-side)
|
|
@@ -70,14 +70,26 @@ ASYNCHRONOUSLY (seconds to minutes after payment). If the new channel isn't
|
|
|
70
70
|
listed yet, say it's still opening and suggest checking again — don't claim
|
|
71
71
|
failure.
|
|
72
72
|
|
|
73
|
-
### `rln_list_assets` —
|
|
74
|
-
Lists RGB assets known to the node
|
|
75
|
-
|
|
76
|
-
|
|
73
|
+
### `rln_list_assets` — { schemas? }
|
|
74
|
+
Lists RGB assets known to the node: `asset_id`, `ticker`, `name`, `precision`
|
|
75
|
+
(in-app wallets also return balances). `schemas` is an optional filter — an
|
|
76
|
+
array of `"Nia"`, `"Uda"`, `"Cfa"`; call it with `{}` to list everything.
|
|
77
|
+
Use for "what assets do I hold / what's my USDT balance".
|
|
78
|
+
|
|
79
|
+
Call it **once** per question. An empty list is a real answer: the node holds
|
|
80
|
+
no RGB assets yet — tell the user that (and offer to issue one) instead of
|
|
81
|
+
calling it again.
|
|
82
|
+
|
|
83
|
+
### Tickers are not asset ids
|
|
84
|
+
`asset_id` arguments take the full RGB id (`rgb:…`), never a ticker like
|
|
85
|
+
`USDT` or `HCK`. When the user names an asset by ticker or name, first call
|
|
86
|
+
`rln_list_assets`, find the entry whose `ticker`/`name` matches, and pass its
|
|
87
|
+
`asset_id`. If nothing matches, say the node has no such asset — do not guess
|
|
88
|
+
an id.
|
|
77
89
|
|
|
78
90
|
### `rln_get_asset_balance` — { asset_id }
|
|
79
|
-
Balance for one RGB asset by id.
|
|
80
|
-
|
|
91
|
+
Balance for one RGB asset by id. Resolve a ticker to its `asset_id` with
|
|
92
|
+
`rln_list_assets` first.
|
|
81
93
|
|
|
82
94
|
| Field | Meaning |
|
|
83
95
|
|---|---|
|
|
@@ -105,7 +117,8 @@ the amount.
|
|
|
105
117
|
Sends an RGB asset to the recipient encoded in an RGB invoice. Use the argument
|
|
106
118
|
names of the schema you were given: in-app wallets take `{ asset, amount, to }`
|
|
107
119
|
(ticker or asset_id, units, the invoice); kaleido-mcp takes
|
|
108
|
-
`{ asset_id, amount, recipient_id }
|
|
120
|
+
`{ asset_id, amount, recipient_id }`, where `asset_id` is the `rgb:…` id from
|
|
121
|
+
`rln_list_assets`. Never invent a recipient.
|
|
109
122
|
|
|
110
123
|
### `rln_pay_invoice` — { invoice } — 🔒 confirm-gated
|
|
111
124
|
Pays a BOLT11 Lightning invoice from the node. Pass the full invoice string.
|
|
@@ -144,16 +157,21 @@ Call this **after** `kaleidoswap_atomic_init` and **before**
|
|
|
144
157
|
### `rln_create_ln_invoice` — Lightning invoice for receiving sats
|
|
145
158
|
Args:
|
|
146
159
|
- `amount_sats` (optional) — omit for an amountless invoice.
|
|
160
|
+
- `description` (optional, kaleido-mcp) — memo shown to the payer.
|
|
147
161
|
- `expiry_sec` (default 3600, kaleido-mcp only) — invoice TTL in seconds.
|
|
148
162
|
|
|
163
|
+
Reply with the full `invoice` string from the result — never write an invoice
|
|
164
|
+
yourself.
|
|
165
|
+
|
|
149
166
|
Use when the user wants to **receive** a Lightning payment. Do NOT call inside
|
|
150
167
|
an atomic swap flow unless the user explicitly asked to invoice someone.
|
|
151
168
|
|
|
152
169
|
### `rln_create_rgb_invoice` — on-chain RGB receive invoice
|
|
153
170
|
Args (use the names in your schema):
|
|
154
171
|
- in-app wallets: `asset` (ticker or asset_id) + `amount`.
|
|
155
|
-
- kaleido-mcp: `asset_id` (
|
|
156
|
-
|
|
172
|
+
- kaleido-mcp: `asset_id` (the `rgb:…` id from `rln_list_assets` — never a
|
|
173
|
+
ticker; omit for an any-asset invoice), `amount` (display units),
|
|
174
|
+
`duration_seconds` (default 86400).
|
|
157
175
|
|
|
158
176
|
Reply with the full `invoice` string; the payer needs all of it.
|
|
159
177
|
|
|
@@ -176,8 +194,10 @@ defaults (`num` 5, `fee_rate` 1) are fine; set `up_to: true` to only top up to
|
|
|
176
194
|
### `rln_issue_asset` — { name, ticker?, amount?, precision?, schema?, details? } — 🔒 confirm-gated
|
|
177
195
|
Creates a **new** RGB asset owned by this node. `schema`: `NIA` fungible token
|
|
178
196
|
(default), `CFA` collectible, `UDA` unique asset / NFT (supply 1).
|
|
179
|
-
- `ticker` (uppercase, 1–8 letters/digits) is required for `NIA`
|
|
180
|
-
|
|
197
|
+
- `ticker` (uppercase, 1–8 letters/digits, nothing else) is required for `NIA`
|
|
198
|
+
and `UDA`.
|
|
199
|
+
- `amount` is the total supply in display units (a number), required for `NIA`
|
|
200
|
+
and `CFA` — if the user did not say how many, ask before calling;
|
|
181
201
|
the raw supply is `amount × 10^precision` (`precision` defaults to 0).
|
|
182
202
|
- `details` is an optional description for `CFA` and `UDA`.
|
|
183
203
|
|
|
@@ -3,6 +3,7 @@ name: spark-wallet
|
|
|
3
3
|
description: "Operate the user's Spark BTC wallet on this device — check Spark balance, get a Spark deposit address, create a Spark Lightning invoice to receive, pay any BOLT11 Lightning invoice with Spark, or send BTC on-chain from Spark. Use this when the user names Spark explicitly OR when paying a Lightning invoice on a phone where Spark is the connected layer. Pairs with the bitrefill skill: a Bitrefill purchase that returns a Lightning invoice is paid with `spark_pay_invoice`."
|
|
4
4
|
tools: spark_get_balance, spark_get_address, spark_get_onchain_address, spark_create_invoice, spark_pay_invoice, spark_send, get_price, fiat_to_sats, bitrefill_search, bitrefill_get_product, bitrefill_get_balance, bitrefill_create_invoice, bitrefill_get_invoice, bitrefill_get_order
|
|
5
5
|
triggers: spark, sprak, spakr, spark wallet, pay with spark, send with spark, spark balance, spark address, spark invoice, lightning invoice, pay invoice, bolt11, ln invoice, pay this invoice, on-chain address, onchain address, deposit address, deposit btc, fund spark
|
|
6
|
+
requires-tools: spark_get_balance
|
|
6
7
|
metadata:
|
|
7
8
|
author: kaleidoswap
|
|
8
9
|
version: "1.0.0"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: submarine-swaps
|
|
3
|
+
description: "Pay a Lightning invoice from Liquid funds (L-USDT or L-BTC) through a KaleidoSwap submarine swap on the /v2 maker, and follow its status. Triggers when the user wants to pay a Lightning invoice with Liquid USDT / L-USDT / L-BTC, asks what Liquid assets can pay Lightning, or asks about a submarine swap."
|
|
4
|
+
tools: kaleidoswap_submarine_pairs, kaleidoswap_submarine_create, kaleidoswap_submarine_fund, kaleidoswap_submarine_status
|
|
5
|
+
triggers: l-usdt, liquid usdt, usdt on liquid, l-btc, liquid bitcoin, submarine, submarine swap, pay invoice with liquid, swap status
|
|
6
|
+
metadata:
|
|
7
|
+
author: kaleidoswap
|
|
8
|
+
version: "0.1.0"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# Submarine swaps — pay Lightning from Liquid
|
|
12
|
+
|
|
13
|
+
A submarine swap pays a Lightning (BOLT11) invoice with funds the user locks on
|
|
14
|
+
Liquid. The KaleidoSwap maker pays the invoice, then claims the lockup. It is
|
|
15
|
+
atomic: if the maker can't pay, the lockup is refunded to the user.
|
|
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.
|
|
34
|
+
2. Tell the user the amount and ask to confirm.
|
|
35
|
+
3. `kaleidoswap_submarine_fund { swap_id }`.
|
|
36
|
+
4. `kaleidoswap_submarine_status { swap_id }` — `transaction.claimed` means the
|
|
37
|
+
invoice was paid. `failed: true` on a funded swap means the funds need a refund;
|
|
38
|
+
say so and quote the `refund` field.
|
|
39
|
+
|
|
40
|
+
Use `kaleidoswap_submarine_pairs` when the user asks what can pay a Lightning
|
|
41
|
+
invoice, or for the limits and fees.
|
|
42
|
+
|
|
43
|
+
## Examples
|
|
44
|
+
|
|
45
|
+
- "pay lntbs10u1p… with L-USDT" →
|
|
46
|
+
`kaleidoswap_submarine_create { invoice: "lntbs10u1p…", from_asset: "L-USDT" }`
|
|
47
|
+
- "did my Liquid payment go through? swap sub1" →
|
|
48
|
+
`kaleidoswap_submarine_status { swap_id: "sub1" }`
|
package/src/context/compress.ts
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { annotateRgbBalances, fixRgbBalanceUnits, formatRgbAmount } from './rgb-units.js';
|
|
3
|
+
|
|
4
|
+
const LIST = {
|
|
5
|
+
assets: [
|
|
6
|
+
{ asset_id: 'rgb:usdt', ticker: 'USDT', name: 'Tether USD', precision: 0, balance: { settled: 1000, spendable: 1000 } },
|
|
7
|
+
{ asset_id: 'rgb:xaut', ticker: 'XAUT', name: 'Tether Gold', precision: 0, balance: { spendable: 2 } },
|
|
8
|
+
{ asset_id: 'rgb:dec', ticker: 'DEC', precision: 6, balance: { spendable: 1_500_000 } },
|
|
9
|
+
],
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
describe('RGB balance units', () => {
|
|
13
|
+
it('formats raw amounts with the asset precision', () => {
|
|
14
|
+
expect(formatRgbAmount(1_500_000, 6)).toBe('1.5');
|
|
15
|
+
expect(formatRgbAmount(1_000_000, 0)).toBe('1,000,000');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('adds balance_display next to each asset balance without touching the rest', () => {
|
|
19
|
+
const out = annotateRgbBalances(LIST) as typeof LIST & { assets: Array<{ balance_display: Record<string, string> }> };
|
|
20
|
+
expect(out.assets[0]!.balance_display).toEqual({ settled: '1,000 USDT', spendable: '1,000 USDT' });
|
|
21
|
+
expect(out.assets[2]!.balance_display).toEqual({ spendable: '1.5 DEC' });
|
|
22
|
+
expect(out.assets[0]!.balance).toEqual({ settled: 1000, spendable: 1000 });
|
|
23
|
+
expect(annotateRgbBalances({ vanilla: { spendable: 4277 } })).toEqual({ vanilla: { spendable: 4277 } });
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('relabels the asset balances the 2B model called satoshis', () => {
|
|
27
|
+
const answer =
|
|
28
|
+
'You hold two RGB assets:\n1. **USDT** (Tether USD)\n - Balance: 1,000 satoshis (spendable)\n' +
|
|
29
|
+
'2. **XAUT** (Tether Gold)\n - Balance: 2 satoshis (spendable)';
|
|
30
|
+
expect(fixRgbBalanceUnits(answer, [LIST])).toBe(
|
|
31
|
+
'You hold two RGB assets:\n1. **USDT** (Tether USD)\n - Balance: 1,000 USDT (spendable)\n' +
|
|
32
|
+
'2. **XAUT** (Tether Gold)\n - Balance: 2 XAUT (spendable)',
|
|
33
|
+
);
|
|
34
|
+
expect(fixRgbBalanceUnits('DEC balance: 1,500,000 sats', [LIST])).toBe('DEC balance: 1.5 DEC');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('leaves real sats and unmatched numbers alone', () => {
|
|
38
|
+
const btc = { btc: { vanilla: { spendable: 4277 } } };
|
|
39
|
+
expect(fixRgbBalanceUnits('Your on-chain balance is 4,277 sats.', [btc, LIST])).toBe('Your on-chain balance is 4,277 sats.');
|
|
40
|
+
expect(fixRgbBalanceUnits('USDT channel fee: 300 sats', [LIST])).toBe('USDT channel fee: 300 sats');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RGB asset balances are raw integers in the asset's own unit, scaled by its
|
|
3
|
+
* `precision`. Nothing in the tool result says so, and small models fill the
|
|
4
|
+
* gap with "satoshis" ("USDT — Balance: 1,000 satoshis").
|
|
5
|
+
*
|
|
6
|
+
* `annotateRgbBalances` adds a `balance_display` next to each asset's
|
|
7
|
+
* `balance` before the result reaches the model ("1,000 USDT").
|
|
8
|
+
* `fixRgbBalanceUnits` corrects a final answer that still labels an asset
|
|
9
|
+
* balance as sats: the number must equal one of that asset's balances and the
|
|
10
|
+
* nearest ticker before it must be that asset's.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
type Obj = Record<string, unknown>;
|
|
14
|
+
|
|
15
|
+
interface RgbAsset {
|
|
16
|
+
ticker: string;
|
|
17
|
+
precision: number;
|
|
18
|
+
amounts: number[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const BALANCE_FIELDS = ['settled', 'spendable', 'future'] as const;
|
|
22
|
+
|
|
23
|
+
const isObj = (v: unknown): v is Obj => !!v && typeof v === 'object' && !Array.isArray(v);
|
|
24
|
+
|
|
25
|
+
function asAsset(v: Obj): RgbAsset | null {
|
|
26
|
+
if (typeof v.ticker !== 'string' || !v.ticker || !isObj(v.balance)) return null;
|
|
27
|
+
const amounts = BALANCE_FIELDS.map((k) => (v.balance as Obj)[k]).filter((n): n is number => typeof n === 'number');
|
|
28
|
+
if (!amounts.length) return null;
|
|
29
|
+
const precision = typeof v.precision === 'number' && v.precision > 0 ? v.precision : 0;
|
|
30
|
+
return { ticker: v.ticker, precision, amounts };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A raw RGB amount in display units, e.g. 1500000 at precision 6 → "1.5". */
|
|
34
|
+
export function formatRgbAmount(raw: number, precision: number): string {
|
|
35
|
+
const value = raw / 10 ** precision;
|
|
36
|
+
return value.toLocaleString('en-US', { maximumFractionDigits: precision });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Return a copy of `result` where every RGB asset carries `balance_display`. */
|
|
40
|
+
export function annotateRgbBalances(result: unknown): unknown {
|
|
41
|
+
if (Array.isArray(result)) return result.map(annotateRgbBalances);
|
|
42
|
+
if (!isObj(result)) return result;
|
|
43
|
+
const out: Obj = {};
|
|
44
|
+
for (const [k, v] of Object.entries(result)) out[k] = annotateRgbBalances(v);
|
|
45
|
+
const asset = asAsset(result);
|
|
46
|
+
if (asset) {
|
|
47
|
+
const balance = result.balance as Obj;
|
|
48
|
+
const display: Obj = {};
|
|
49
|
+
for (const k of BALANCE_FIELDS) {
|
|
50
|
+
if (typeof balance[k] === 'number') display[k] = `${formatRgbAmount(balance[k] as number, asset.precision)} ${asset.ticker}`;
|
|
51
|
+
}
|
|
52
|
+
out.balance_display = display;
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function collectAssets(value: unknown, into: RgbAsset[]): void {
|
|
58
|
+
if (Array.isArray(value)) {
|
|
59
|
+
for (const v of value) collectAssets(v, into);
|
|
60
|
+
} else if (isObj(value)) {
|
|
61
|
+
const asset = asAsset(value);
|
|
62
|
+
if (asset) into.push(asset);
|
|
63
|
+
for (const v of Object.values(value)) collectAssets(v, into);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const AMOUNT_IN_SATS = /(\d[\d,]*(?:\.\d+)?)\s*(?:satoshis|sats?)\b/gi;
|
|
68
|
+
const escapeRe = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
69
|
+
|
|
70
|
+
/** Relabel asset balances that the answer calls sats, using the tool results it was built from. */
|
|
71
|
+
export function fixRgbBalanceUnits(text: string, toolResults: unknown[]): string {
|
|
72
|
+
const assets: RgbAsset[] = [];
|
|
73
|
+
for (const r of toolResults) collectAssets(r, assets);
|
|
74
|
+
if (!assets.length) return text;
|
|
75
|
+
const tickerRe = new RegExp(`\\b(${[...new Set(assets.map((a) => escapeRe(a.ticker)))].join('|')})\\b`, 'g');
|
|
76
|
+
|
|
77
|
+
return text.replace(AMOUNT_IN_SATS, (match: string, num: string, offset: number) => {
|
|
78
|
+
let nearest: string | undefined;
|
|
79
|
+
for (const m of text.slice(Math.max(0, offset - 300), offset).matchAll(tickerRe)) nearest = m[1];
|
|
80
|
+
if (!nearest) return match;
|
|
81
|
+
const value = Number(num.replace(/,/g, ''));
|
|
82
|
+
const asset = assets.find(
|
|
83
|
+
(a) => a.ticker === nearest && a.amounts.some((raw) => raw === value || raw / 10 ** a.precision === value),
|
|
84
|
+
);
|
|
85
|
+
if (!asset) return match;
|
|
86
|
+
const raw = asset.amounts.find((r) => r === value) ?? value * 10 ** asset.precision;
|
|
87
|
+
return `${formatRgbAmount(raw, asset.precision)} ${asset.ticker}`;
|
|
88
|
+
});
|
|
89
|
+
}
|
package/src/engine.test.ts
CHANGED
|
@@ -111,8 +111,8 @@ describe('Engine agentic loop', () => {
|
|
|
111
111
|
const res = await engine.runAgentic([{ role: 'user', content: 'pay lnbc1' }], { onConfirm });
|
|
112
112
|
|
|
113
113
|
expect(payTool.handler).not.toHaveBeenCalled();
|
|
114
|
-
expect(res.toolCalls[0].result).toMatchObject({
|
|
115
|
-
expect(res.text).toBe('
|
|
114
|
+
expect(res.toolCalls[0].result).toMatchObject({ status: 'cancelled_by_user', declined_by: 'user', host_reason: 'cancelled' });
|
|
115
|
+
expect(res.text).toBe('Cancelled — you declined: pay invoice. Nothing was sent or changed.');
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
it('chains multiple tool calls across turns', async () => {
|
|
@@ -138,7 +138,9 @@ describe('Engine agentic loop', () => {
|
|
|
138
138
|
it('stops at maxTurns if the model never stops calling tools', async () => {
|
|
139
139
|
const engine = new Engine({
|
|
140
140
|
provider: scriptedProvider([
|
|
141
|
-
{ text: 'loop', toolCalls: [{ name: 'get_balance', arguments: {} }] },
|
|
141
|
+
{ text: 'loop', toolCalls: [{ name: 'get_balance', arguments: { n: 1 } }] },
|
|
142
|
+
{ text: 'loop', toolCalls: [{ name: 'get_balance', arguments: { n: 2 } }] },
|
|
143
|
+
{ text: 'loop', toolCalls: [{ name: 'get_balance', arguments: { n: 3 } }] },
|
|
142
144
|
]),
|
|
143
145
|
tools: freshTools(),
|
|
144
146
|
defaultMaxTurns: 3,
|
|
@@ -205,6 +207,95 @@ describe('Engine agentic loop', () => {
|
|
|
205
207
|
expect(res.toolCalls[0].result).toMatchObject({ error: 'kaboom' });
|
|
206
208
|
expect(res.text).toBe('handled the error');
|
|
207
209
|
});
|
|
210
|
+
|
|
211
|
+
it('sends firstTurnToolChoice on the first call only', async () => {
|
|
212
|
+
const seen: Array<string | undefined> = [];
|
|
213
|
+
const provider: LLMProvider = {
|
|
214
|
+
name: 'rec',
|
|
215
|
+
async runTurn(input) {
|
|
216
|
+
seen.push(input.toolChoice);
|
|
217
|
+
return seen.length === 1
|
|
218
|
+
? { text: '', rawContent: '', toolCalls: [{ name: 'get_balance', arguments: {} }] }
|
|
219
|
+
: { text: 'done', rawContent: 'done', toolCalls: [] };
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
const engine = new Engine({ provider, tools: freshTools() });
|
|
223
|
+
await engine.runAgentic([{ role: 'user', content: 'send 1000 sats' }], { firstTurnToolChoice: 'required' });
|
|
224
|
+
expect(seen).toEqual(['required', undefined]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('feeds an unparseable tool call back to the model once, then gives up cleanly', async () => {
|
|
228
|
+
const histories: number[] = [];
|
|
229
|
+
const provider: LLMProvider = {
|
|
230
|
+
name: 'broken',
|
|
231
|
+
async runTurn(input) {
|
|
232
|
+
histories.push(input.messages.length);
|
|
233
|
+
return {
|
|
234
|
+
text: '<tool_call>{"ticker":"HCK',
|
|
235
|
+
rawContent: '<tool_call>{"ticker":"HCK',
|
|
236
|
+
toolCalls: [],
|
|
237
|
+
toolErrors: [{ code: 'PARSE_ERROR', message: 'unterminated string' }],
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
const engine = new Engine({ provider, tools: freshTools() });
|
|
242
|
+
const res = await engine.runAgentic([{ role: 'user', content: 'issue HCK' }]);
|
|
243
|
+
expect(histories).toEqual([1, 3]);
|
|
244
|
+
expect(res.text).not.toContain('tool_call');
|
|
245
|
+
expect(res.text).toMatch(/couldn't put together a valid request/i);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('does not count the retry against maxTurns and explains a cut-off call', async () => {
|
|
249
|
+
const seen: string[] = [];
|
|
250
|
+
let n = 0;
|
|
251
|
+
const provider: LLMProvider = {
|
|
252
|
+
name: 'cutoff',
|
|
253
|
+
async runTurn(input) {
|
|
254
|
+
n += 1;
|
|
255
|
+
seen.push(input.messages[input.messages.length - 1]!.content);
|
|
256
|
+
if (n === 1) return { text: '', rawContent: '', toolCalls: [{ name: 'get_balance', arguments: {} }] };
|
|
257
|
+
if (n === 2) {
|
|
258
|
+
return {
|
|
259
|
+
text: '', rawContent: '<tool_call>{"name":"get_balance"', toolCalls: [],
|
|
260
|
+
toolErrors: [{ code: 'PARSE_ERROR', message: 'eof' }],
|
|
261
|
+
inference: { durationMs: 1, status: 'truncated' },
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return { text: 'You have 50,000 sats.', rawContent: '', toolCalls: [] };
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
const engine = new Engine({ provider, tools: freshTools() });
|
|
268
|
+
const res = await engine.runAgentic([{ role: 'user', content: 'balance?' }], { maxTurns: 2 });
|
|
269
|
+
expect(res.text).toBe('You have 50,000 sats.');
|
|
270
|
+
expect(seen[2]).toMatch(/cut off/);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('never returns an empty answer', async () => {
|
|
274
|
+
const engine = new Engine({ provider: scriptedProvider([{ text: '' }]), tools: freshTools() });
|
|
275
|
+
const res = await engine.runAgentic([{ role: 'user', content: 'hi' }]);
|
|
276
|
+
expect(res.text).toMatch(/had to stop/);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('recovers when the retried call parses', async () => {
|
|
280
|
+
const engine = new Engine({
|
|
281
|
+
provider: (() => {
|
|
282
|
+
let n = 0;
|
|
283
|
+
return {
|
|
284
|
+
name: 'retry',
|
|
285
|
+
async runTurn(): Promise<TurnOutput> {
|
|
286
|
+
n += 1;
|
|
287
|
+
if (n === 1) return { text: 'x', rawContent: 'x', toolCalls: [], toolErrors: [{ code: 'PARSE_ERROR', message: 'bad' }] };
|
|
288
|
+
if (n === 2) return { text: '', rawContent: '', toolCalls: [{ name: 'get_balance', arguments: {} }] };
|
|
289
|
+
return { text: 'You have 50,000 sats.', rawContent: '', toolCalls: [] };
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
})(),
|
|
293
|
+
tools: freshTools(),
|
|
294
|
+
});
|
|
295
|
+
const res = await engine.runAgentic([{ role: 'user', content: 'balance?' }]);
|
|
296
|
+
expect(balanceTool.handler).toHaveBeenCalledTimes(1);
|
|
297
|
+
expect(res.text).toBe('You have 50,000 sats.');
|
|
298
|
+
});
|
|
208
299
|
});
|
|
209
300
|
|
|
210
301
|
describe('ToolRegistry', () => {
|