@kaleidorg/mind 0.5.1 → 0.6.1
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/autonomy/index.d.ts +21 -0
- package/dist/autonomy/index.d.ts.map +1 -0
- package/dist/autonomy/index.js +16 -0
- package/dist/autonomy/index.js.map +1 -0
- package/dist/autonomy/prompt.d.ts +21 -0
- package/dist/autonomy/prompt.d.ts.map +1 -0
- package/dist/autonomy/prompt.js +37 -0
- package/dist/autonomy/prompt.js.map +1 -0
- package/dist/autonomy/risk.d.ts +53 -0
- package/dist/autonomy/risk.d.ts.map +1 -0
- package/dist/autonomy/risk.js +74 -0
- package/dist/autonomy/risk.js.map +1 -0
- package/dist/autonomy/run-state.d.ts +39 -0
- package/dist/autonomy/run-state.d.ts.map +1 -0
- package/dist/autonomy/run-state.js +118 -0
- package/dist/autonomy/run-state.js.map +1 -0
- package/dist/autonomy/scheduler.d.ts +18 -0
- package/dist/autonomy/scheduler.d.ts.map +1 -0
- package/dist/autonomy/scheduler.js +113 -0
- package/dist/autonomy/scheduler.js.map +1 -0
- package/dist/autonomy/task-store.d.ts +44 -0
- package/dist/autonomy/task-store.d.ts.map +1 -0
- package/dist/autonomy/task-store.js +139 -0
- package/dist/autonomy/task-store.js.map +1 -0
- package/dist/autonomy/types.d.ts +164 -0
- package/dist/autonomy/types.d.ts.map +1 -0
- package/dist/autonomy/types.js +20 -0
- package/dist/autonomy/types.js.map +1 -0
- package/dist/bitrefill/contract.d.ts +60 -0
- package/dist/bitrefill/contract.d.ts.map +1 -0
- package/dist/bitrefill/contract.js +119 -0
- package/dist/bitrefill/contract.js.map +1 -0
- package/dist/context/compress.d.ts +65 -0
- package/dist/context/compress.d.ts.map +1 -0
- package/dist/context/compress.js +181 -0
- package/dist/context/compress.js.map +1 -0
- package/dist/engine.d.ts +20 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +23 -4
- package/dist/engine.js.map +1 -1
- package/dist/evidence.d.ts +62 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +47 -0
- package/dist/evidence.js.map +1 -0
- package/dist/flashnet/contract.d.ts +56 -0
- package/dist/flashnet/contract.d.ts.map +1 -0
- package/dist/flashnet/contract.js +100 -0
- package/dist/flashnet/contract.js.map +1 -0
- package/dist/funnel.d.ts +11 -0
- package/dist/funnel.d.ts.map +1 -1
- package/dist/funnel.js +62 -7
- package/dist/funnel.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.js +1 -1
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
- package/dist/knowledge/bitcoin-copilot.js +85 -2
- package/dist/knowledge/bitcoin-copilot.js.map +1 -1
- package/dist/providers/types.d.ts +17 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/qvac/index.d.ts +1 -1
- package/dist/qvac/index.d.ts.map +1 -1
- package/dist/qvac/index.js.map +1 -1
- package/dist/qvac/parse.d.ts +18 -0
- package/dist/qvac/parse.d.ts.map +1 -1
- package/dist/qvac/parse.js +1 -0
- package/dist/qvac/parse.js.map +1 -1
- package/dist/qvac/provider.d.ts +16 -0
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +40 -1
- package/dist/qvac/provider.js.map +1 -1
- package/dist/qvac/stream.d.ts +22 -0
- package/dist/qvac/stream.d.ts.map +1 -1
- package/dist/qvac/stream.js +33 -1
- package/dist/qvac/stream.js.map +1 -1
- package/dist/recipe/buy-asset-channel.d.ts +1 -1
- package/dist/recipe/buy-asset-channel.d.ts.map +1 -1
- package/dist/recipe/buy-asset-channel.js +4 -3
- package/dist/recipe/buy-asset-channel.js.map +1 -1
- package/dist/recipe/flashnet-swap.d.ts +35 -0
- package/dist/recipe/flashnet-swap.d.ts.map +1 -0
- package/dist/recipe/flashnet-swap.js +239 -0
- package/dist/recipe/flashnet-swap.js.map +1 -0
- package/dist/recipe/kaleidoswap-atomic.d.ts +1 -1
- package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-atomic.js +42 -20
- package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
- package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-channel-order.js +31 -10
- package/dist/recipe/kaleidoswap-channel-order.js.map +1 -1
- package/dist/recipe/kaleidoswap-price.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-price.js +7 -1
- package/dist/recipe/kaleidoswap-price.js.map +1 -1
- package/dist/recipe/runner.d.ts.map +1 -1
- package/dist/recipe/runner.js +43 -3
- package/dist/recipe/runner.js.map +1 -1
- package/dist/recipe/swap.d.ts.map +1 -1
- package/dist/recipe/swap.js +14 -1
- package/dist/recipe/swap.js.map +1 -1
- package/dist/tools/mcp.d.ts +19 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mcp.js +51 -9
- package/dist/tools/mcp.js.map +1 -1
- package/dist/wallet/confirm.d.ts.map +1 -1
- package/dist/wallet/confirm.js +1 -0
- package/dist/wallet/confirm.js.map +1 -1
- package/dist/wallet/contract.d.ts.map +1 -1
- package/dist/wallet/contract.js +20 -4
- package/dist/wallet/contract.js.map +1 -1
- package/package.json +5 -4
- package/skills/bitrefill/SKILL.md +152 -52
- package/skills/channel-manager/SKILL.md +59 -0
- package/skills/dca/SKILL.md +48 -0
- package/skills/flashnet-swaps/SKILL.md +158 -0
- package/skills/kaleido-lsps/SKILL.md +34 -17
- package/skills/kaleido-trading/SKILL.md +37 -13
- package/skills/liquidity-optimizer/SKILL.md +91 -0
- package/skills/merchant-finder/SKILL.md +2 -2
- package/skills/portfolio-manager/SKILL.md +67 -0
- package/skills/rgb-lightning-node/SKILL.md +38 -11
- package/skills/spark-wallet/SKILL.md +235 -0
- package/skills/wallet-assistant/SKILL.md +2 -2
- package/src/autonomy/autonomy.test.ts +348 -0
- package/src/autonomy/index.ts +50 -0
- package/src/autonomy/prompt.ts +48 -0
- package/src/autonomy/risk.ts +139 -0
- package/src/autonomy/run-state.ts +144 -0
- package/src/autonomy/scheduler.ts +120 -0
- package/src/autonomy/task-store.ts +167 -0
- package/src/autonomy/types.ts +186 -0
- package/src/bitrefill/contract.test.ts +89 -0
- package/src/bitrefill/contract.ts +190 -0
- package/src/context/compress.test.ts +120 -0
- package/src/context/compress.ts +230 -0
- package/src/engine.test.ts +34 -0
- package/src/engine.ts +35 -4
- package/src/evidence.test.ts +80 -0
- package/src/evidence.ts +114 -0
- package/src/flashnet/contract.test.ts +101 -0
- package/src/flashnet/contract.ts +164 -0
- package/src/funnel.mind.test.ts +390 -0
- package/src/funnel.ts +73 -8
- package/src/index.ts +92 -1
- package/src/kaleidoswap/contract.ts +1 -1
- package/src/knowledge/bitcoin-copilot.ts +96 -2
- package/src/providers/types.ts +18 -0
- package/src/qvac/index.ts +1 -0
- package/src/qvac/parse.ts +20 -0
- package/src/qvac/provider.test.ts +17 -0
- package/src/qvac/provider.ts +62 -2
- package/src/qvac/stream.test.ts +36 -0
- package/src/qvac/stream.ts +54 -1
- package/src/recipe/buy-asset-channel.test.ts +5 -0
- package/src/recipe/buy-asset-channel.ts +6 -3
- package/src/recipe/flashnet-swap.test.ts +114 -0
- package/src/recipe/flashnet-swap.ts +266 -0
- package/src/recipe/kaleidoswap-atomic.test.ts +24 -3
- package/src/recipe/kaleidoswap-atomic.ts +39 -20
- package/src/recipe/kaleidoswap-channel-order.test.ts +38 -0
- package/src/recipe/kaleidoswap-channel-order.ts +27 -9
- package/src/recipe/kaleidoswap-price.ts +7 -1
- package/src/recipe/recipe.test.ts +21 -0
- package/src/recipe/runner.ts +46 -3
- package/src/recipe/swap.ts +16 -1
- package/src/tools/mcp.live.test.ts +116 -0
- package/src/tools/mcp.parse.test.ts +37 -0
- package/src/tools/mcp.ts +55 -9
- package/src/wallet/confirm.test.ts +8 -0
- package/src/wallet/confirm.ts +1 -0
- package/src/wallet/contract.test.ts +10 -0
- package/src/wallet/contract.ts +26 -4
package/src/qvac/stream.ts
CHANGED
|
@@ -27,10 +27,37 @@ export interface StreamHandlers {
|
|
|
27
27
|
onToken?: (token: string) => void;
|
|
28
28
|
/** The model's `<think>` reasoning, streamed separately. */
|
|
29
29
|
onThinking?: (token: string) => void;
|
|
30
|
+
/**
|
|
31
|
+
* Cap the `<think>` reasoning at this many tokens. The cap is on TOKENS, not
|
|
32
|
+
* wall-clock seconds — tok/s varies by model and hardware, so a time budget is
|
|
33
|
+
* unreliable; the SDK has no numeric reasoning budget (`reasoning_budget` is
|
|
34
|
+
* only on/off), so we count thinking tokens and stop the run once they exceed
|
|
35
|
+
* this. Omit for unlimited reasoning.
|
|
36
|
+
*/
|
|
37
|
+
maxThinkingTokens?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Fires once, the moment the thinking budget is exceeded, so the host can
|
|
40
|
+
* cancel the in-flight run (the SDK keeps generating otherwise). consumeRun
|
|
41
|
+
* stops forwarding deltas after this.
|
|
42
|
+
*/
|
|
43
|
+
onThinkingBudgetExceeded?: () => void;
|
|
44
|
+
/** Injectable monotonic-ish wall clock for deterministic timing tests. */
|
|
45
|
+
now?: () => number;
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
export interface ConsumedTurn extends ParsedTurn {
|
|
33
49
|
requestId: string;
|
|
50
|
+
/** True when the run was stopped because `<think>` hit `maxThinkingTokens`. */
|
|
51
|
+
thinkingBudgetExceeded?: boolean;
|
|
52
|
+
timing: {
|
|
53
|
+
ttftMs?: number;
|
|
54
|
+
durationMs: number;
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Rough token estimate (~4 chars/token) — same heuristic the context budget uses. */
|
|
59
|
+
function approxTokens(chars: number): number {
|
|
60
|
+
return Math.ceil(chars / 4);
|
|
34
61
|
}
|
|
35
62
|
|
|
36
63
|
/**
|
|
@@ -42,15 +69,41 @@ export async function consumeRun(
|
|
|
42
69
|
run: CompletionRunLike,
|
|
43
70
|
handlers: StreamHandlers = {},
|
|
44
71
|
): Promise<ConsumedTurn> {
|
|
72
|
+
const now = handlers.now ?? Date.now;
|
|
73
|
+
const startedAt = now();
|
|
74
|
+
let firstTokenAt: number | undefined;
|
|
45
75
|
let streamed = '';
|
|
76
|
+
let thinkingChars = 0;
|
|
77
|
+
let budgetExceeded = false;
|
|
46
78
|
for await (const event of run.events) {
|
|
47
79
|
if (event.type === 'contentDelta' && typeof event.text === 'string') {
|
|
80
|
+
if (firstTokenAt === undefined && event.text.length > 0) firstTokenAt = now();
|
|
48
81
|
streamed += event.text;
|
|
49
82
|
handlers.onToken?.(event.text);
|
|
50
83
|
} else if (event.type === 'thinkingDelta' && typeof event.text === 'string') {
|
|
84
|
+
if (firstTokenAt === undefined && event.text.length > 0) firstTokenAt = now();
|
|
51
85
|
handlers.onThinking?.(event.text);
|
|
86
|
+
if (handlers.maxThinkingTokens !== undefined && !budgetExceeded) {
|
|
87
|
+
thinkingChars += event.text.length;
|
|
88
|
+
if (approxTokens(thinkingChars) >= handlers.maxThinkingTokens) {
|
|
89
|
+
budgetExceeded = true;
|
|
90
|
+
handlers.onThinkingBudgetExceeded?.();
|
|
91
|
+
// Stop forwarding; the host cancels the run, so `final` resolves
|
|
92
|
+
// (stopReason 'cancelled') with whatever was produced so far.
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
52
96
|
}
|
|
53
97
|
}
|
|
54
98
|
const final = await run.final;
|
|
55
|
-
|
|
99
|
+
const finishedAt = now();
|
|
100
|
+
return {
|
|
101
|
+
...finalToTurn(final, streamed),
|
|
102
|
+
requestId: run.requestId,
|
|
103
|
+
thinkingBudgetExceeded: budgetExceeded,
|
|
104
|
+
timing: {
|
|
105
|
+
...(firstTokenAt === undefined ? {} : { ttftMs: Math.max(0, firstTokenAt - startedAt) }),
|
|
106
|
+
durationMs: Math.max(0, finishedAt - startedAt),
|
|
107
|
+
},
|
|
108
|
+
};
|
|
56
109
|
}
|
|
@@ -54,6 +54,11 @@ describe('extractBuyAsset (deterministic Tier-0)', () => {
|
|
|
54
54
|
it('handles comma grouping in the amount', () => {
|
|
55
55
|
expect(extractBuyAsset('buy 1,000 usdt')).toEqual({ asset: 'USDT', asset_amount: 1000 });
|
|
56
56
|
});
|
|
57
|
+
it('parses an article/filler between the verb and amount ("buy a 100 usdt channel")', () => {
|
|
58
|
+
expect(extractBuyAsset('buy a 100 usdt channel')).toEqual({ asset: 'USDT', asset_amount: 100 });
|
|
59
|
+
expect(extractBuyAsset('get a 100 usdt inbound channel')).toEqual({ asset: 'USDT', asset_amount: 100 });
|
|
60
|
+
expect(extractBuyAsset('buy and sell 100 usdt')).toBeNull(); // "and" is not filler
|
|
61
|
+
});
|
|
57
62
|
it('null for a swap (a named source asset ⇒ swap owns it)', () => {
|
|
58
63
|
expect(extractBuyAsset('buy 0.001 btc with usdt')).toBeNull();
|
|
59
64
|
expect(extractBuyAsset('swap 10 usdt for btc')).toBeNull();
|
|
@@ -48,13 +48,16 @@ const num = (s?: string): number | undefined => {
|
|
|
48
48
|
/** Thousands separators, locale-independent (deterministic for tests). */
|
|
49
49
|
const commas = (n: number): string => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
50
50
|
|
|
51
|
-
/** "buy 100 usdt" / "get me 50 xaut" / "
|
|
51
|
+
/** "buy 100 usdt" / "get me 50 xaut" / "buy a 100 usdt channel" / "purchase 10 xaut". */
|
|
52
52
|
export function extractBuyAsset(text: string): Record<string, unknown> | null {
|
|
53
53
|
const t = text.trim();
|
|
54
54
|
if (NOT_BUY.test(t) || HAS_SOURCE.test(t)) return null;
|
|
55
55
|
if (!RGB_ASSET.test(t)) return null;
|
|
56
|
-
// buy/get/want/acquire/purchase [me] <amount> <asset>
|
|
57
|
-
|
|
56
|
+
// buy/get/want/acquire/purchase [me|a|an|some|new]* <amount> <asset>
|
|
57
|
+
// Filler words (the article in "buy A 100 usdt channel") must not break extraction.
|
|
58
|
+
const m = t.match(
|
|
59
|
+
/\b(?:buy|get|acquire|want|purchase|onboard|need)\b(?:\s+(?:me|a|an|some|new)\b)*\s+([\d.,]+)\s*([a-z]+)/i,
|
|
60
|
+
);
|
|
58
61
|
if (!m) return null;
|
|
59
62
|
const asset = normAsset(m[2]);
|
|
60
63
|
const amount = num(m[1]);
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { flashnetSwapRecipe } from './flashnet-swap.js';
|
|
3
|
+
|
|
4
|
+
describe('flashnetSwapRecipe — selection', () => {
|
|
5
|
+
it('claims swap/buy/sell phrasings with a Flashnet/Spark cue', () => {
|
|
6
|
+
expect(flashnetSwapRecipe.match!('swap 10000 sats with usdb')).toBe(true);
|
|
7
|
+
expect(flashnetSwapRecipe.match!('swap 5000 sats to usdb on flashnet')).toBe(true);
|
|
8
|
+
expect(flashnetSwapRecipe.match!('buy usdb with 1000 sats')).toBe(true);
|
|
9
|
+
expect(flashnetSwapRecipe.match!('exchange btc for usdb on spark')).toBe(true);
|
|
10
|
+
expect(flashnetSwapRecipe.match!('sell 5 usdb')).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('does NOT claim RGB swaps (USDT / XAUT belong to kaleidoswap-atomic)', () => {
|
|
14
|
+
expect(flashnetSwapRecipe.match!('swap 100k sats to usdt')).toBe(false);
|
|
15
|
+
expect(flashnetSwapRecipe.match!('convert btc to xaut')).toBe(false);
|
|
16
|
+
expect(flashnetSwapRecipe.match!('sell 10 usdt for sats')).toBe(false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('does NOT claim a bare swap without a Flashnet/Spark cue', () => {
|
|
20
|
+
expect(flashnetSwapRecipe.match!('swap 100000 sats')).toBe(false);
|
|
21
|
+
expect(flashnetSwapRecipe.match!('exchange some bitcoin')).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('does NOT claim commerce / receive / channel phrasings', () => {
|
|
25
|
+
expect(flashnetSwapRecipe.match!('buy a gift card with btc')).toBe(false);
|
|
26
|
+
expect(flashnetSwapRecipe.match!('create an invoice for 1000 sats')).toBe(false);
|
|
27
|
+
expect(flashnetSwapRecipe.match!('buy a usdb channel on flashnet')).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does NOT claim educational questions', () => {
|
|
31
|
+
expect(flashnetSwapRecipe.match!('how does a flashnet swap work?')).toBe(false);
|
|
32
|
+
expect(flashnetSwapRecipe.match!('what is usdb?')).toBe(false);
|
|
33
|
+
expect(flashnetSwapRecipe.match!('explain flashnet')).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('flashnetSwapRecipe — shape', () => {
|
|
38
|
+
it('extracts the 4 swap slots', () => {
|
|
39
|
+
expect(flashnetSwapRecipe.slots.map((s) => s.name).sort()).toEqual([
|
|
40
|
+
'amount', 'amount_side', 'from_asset', 'to_asset',
|
|
41
|
+
]);
|
|
42
|
+
const required = flashnetSwapRecipe.slots.filter((s) => s.required).map((s) => s.name).sort();
|
|
43
|
+
expect(required).toEqual(['from_asset', 'to_asset']);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('forces the model to do slot extraction (not the deterministic regex)', () => {
|
|
47
|
+
expect(flashnetSwapRecipe.forceModelExtract).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('runs the canonical 2-step + final chain (list_pools → simulate → execute)', () => {
|
|
51
|
+
expect(flashnetSwapRecipe.steps.map((s) => s.tool)).toEqual([
|
|
52
|
+
'flashnet_list_pools',
|
|
53
|
+
'flashnet_simulate_swap',
|
|
54
|
+
]);
|
|
55
|
+
expect(flashnetSwapRecipe.final.tool).toBe('flashnet_execute_swap');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('confident only when both assets + amount are extracted', () => {
|
|
59
|
+
expect(flashnetSwapRecipe.confident!({})).toBe(false);
|
|
60
|
+
expect(flashnetSwapRecipe.confident!({ from_asset: 'BTC' })).toBe(false);
|
|
61
|
+
expect(flashnetSwapRecipe.confident!({ from_asset: 'BTC', to_asset: 'USDB' })).toBeFalsy();
|
|
62
|
+
expect(flashnetSwapRecipe.confident!({ from_asset: 'BTC', to_asset: 'USDB', amount: 1000 })).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('has a recipe-level confirm gate (single approval covers the chain)', () => {
|
|
66
|
+
expect(typeof flashnetSwapRecipe.confirm).toBe('function');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('flashnetSwapRecipe — direction is LITERAL', () => {
|
|
71
|
+
it('simulate step uses from_asset as asset_in (the spent leg) by construction', () => {
|
|
72
|
+
const simStep = flashnetSwapRecipe.steps.find((s) => s.tool === 'flashnet_simulate_swap')!;
|
|
73
|
+
// Build a minimal ctx as runRecipe would: pools result with a BTC/USDB
|
|
74
|
+
// pool, slots saying from=BTC, to=USDB, amount=10000. The args function
|
|
75
|
+
// must put BTC on asset_in and amount=10000 on amount_in — the previous
|
|
76
|
+
// model-driven inversion bug becomes impossible.
|
|
77
|
+
const ctx: any = {
|
|
78
|
+
slots: { from_asset: 'BTC', to_asset: 'USDB', amount: 10000 },
|
|
79
|
+
results: {
|
|
80
|
+
pools: {
|
|
81
|
+
pools: [{
|
|
82
|
+
pool_id: 'pool-xyz',
|
|
83
|
+
asset_a_address: '0e6354aaaa', asset_a_symbol: 'USDB',
|
|
84
|
+
asset_b_address: '020202bbbb', asset_b_symbol: 'BTC',
|
|
85
|
+
}],
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
const args = simStep.args(ctx) as any;
|
|
90
|
+
expect(args.pool_id).toBe('pool-xyz');
|
|
91
|
+
// The pool already stores BTC's address (the SDK constant `020202…`);
|
|
92
|
+
// the recipe passes it through directly so the adapter doesn't have to
|
|
93
|
+
// re-resolve the "BTC" ticker.
|
|
94
|
+
expect(args.asset_in_address).toBe('020202bbbb'); // BTC, from the pool
|
|
95
|
+
expect(args.asset_out_address).toBe('0e6354aaaa'); // USDB by symbol
|
|
96
|
+
expect(args.amount_in).toBe('10000');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('execute step computes min_amount_out from the simulated value (not the raw value)', () => {
|
|
100
|
+
const ctx: any = {
|
|
101
|
+
slots: { from_asset: 'BTC', to_asset: 'USDB', amount: 10000 },
|
|
102
|
+
results: {
|
|
103
|
+
pools: { pools: [{ pool_id: 'p', asset_a_address: 'a', asset_a_symbol: 'USDB', asset_b_address: 'b', asset_b_symbol: 'BTC' }] },
|
|
104
|
+
sim: { amount_out: '1472', execution_price: '0.1472' },
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const args = flashnetSwapRecipe.final.args(ctx) as any;
|
|
108
|
+
// Default 50 bps = 0.5% slippage tolerance → floor(1472 * 0.995) = 1464.
|
|
109
|
+
expect(args.min_amount_out).toBe('1464');
|
|
110
|
+
expect(args.max_slippage_bps).toBe(50);
|
|
111
|
+
// BTC (from_asset) → pool's BTC-side address `b`.
|
|
112
|
+
expect(args.asset_in_address).toBe('b');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in "swap on Flashnet" recipe — Spark-native AMM, deterministic chain.
|
|
3
|
+
*
|
|
4
|
+
* A Flashnet swap on a small model was fragile when left to the agentic loop:
|
|
5
|
+
* the model had to discover the pool, get the asset addresses right, pick the
|
|
6
|
+
* correct simulate DIRECTION (asset_in = what the user spends), compute
|
|
7
|
+
* `min_amount_out` from slippage tolerance, and then thread all of that into
|
|
8
|
+
* execute. With Qwen3-1.7B that often produced an inverted simulate (token →
|
|
9
|
+
* BTC instead of BTC → token), and on the follow-up "yes" the skill context
|
|
10
|
+
* was lost and the model called `flashnet_simulate_swap({})` with no args.
|
|
11
|
+
*
|
|
12
|
+
* "swap 10000 sats with usdb"
|
|
13
|
+
* ↓ 1 model inference (slot extraction: from/to/amount/amount_side)
|
|
14
|
+
* flashnet_list_pools ← discover the right pool deterministically
|
|
15
|
+
* ↓
|
|
16
|
+
* flashnet_simulate_swap ← quote (read-only)
|
|
17
|
+
* ↓ [ONE confirmation gate — shows the real quote in plain English]
|
|
18
|
+
* flashnet_execute_swap ← settle (the spend)
|
|
19
|
+
*
|
|
20
|
+
* The whole chain runs with 1 LLM inference total (slot extraction), same
|
|
21
|
+
* pattern as `kaleidoswapAtomicRecipe`. The host's `flashnet_list_pools` is
|
|
22
|
+
* side-agnostic and labels symbols, so picking the right pool from the
|
|
23
|
+
* extracted asset pair is cheap and reliable.
|
|
24
|
+
*
|
|
25
|
+
* Slippage: default `max_slippage_bps = 50` (0.5%). The runner computes
|
|
26
|
+
* `min_amount_out = floor(amount_out × (1 − bps/10000))` from the simulate
|
|
27
|
+
* result before execute — never trusts the simulated value as-is.
|
|
28
|
+
*
|
|
29
|
+
* Asset taxonomy guard: matches ONLY when a Flashnet cue is present
|
|
30
|
+
* (flashnet/usdb/spark), so RGB swaps (USDT/XAUT) still go to the
|
|
31
|
+
* KaleidoSwap atomic recipe. The two recipes are disjoint.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { Recipe, RecipeContext } from './types.js';
|
|
35
|
+
import { extractSwap } from './swap.js';
|
|
36
|
+
|
|
37
|
+
// Flashnet cue — same set the kaleidoswap-atomic recipe defers ON, so the
|
|
38
|
+
// two are disjoint by construction.
|
|
39
|
+
const FLASHNET_CUE = /\b(flashnet|usdb|spark)\b/i;
|
|
40
|
+
// Spark-token tickers that imply a Flashnet swap even without "flashnet" in
|
|
41
|
+
// the text. Keep this list small and Spark-specific — never includes RGB
|
|
42
|
+
// assets (USDT, XAUT, gold).
|
|
43
|
+
const SPARK_TOKEN = /\b(usdb)\b/i;
|
|
44
|
+
// Generic swap-intent verbs that pair with an asset name. "buy/sell/get" join
|
|
45
|
+
// the swap intent only when an ASSET is named (so "buy a gift card" doesn't
|
|
46
|
+
// route here — that's bitrefill territory).
|
|
47
|
+
const SWAP_VERB = /\b(swap|exchange|convert|trade)\b/i;
|
|
48
|
+
const BUY_VERB = /\b(buy|sell|get|purchase|acquire)\b/i;
|
|
49
|
+
const ASSET = /\b(btc|bitcoin|sats?|usdb)\b/i;
|
|
50
|
+
const NON_SWAP = /\b(gift\s?card|top-?up|esim|voucher|invoice|address|channel|inbound|liquidity|lsps?\b)\b/i;
|
|
51
|
+
|
|
52
|
+
const FLASHNET_INTENT = (t: string) => {
|
|
53
|
+
// Educational questions go to the agentic skill (which can call list_pools
|
|
54
|
+
// for an honest read).
|
|
55
|
+
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;
|
|
56
|
+
// Must have a Flashnet/Spark cue OR name a Spark-native token like USDB,
|
|
57
|
+
// so this never grabs an RGB swap (those go to kaleidoswap-atomic).
|
|
58
|
+
if (!FLASHNET_CUE.test(t) && !SPARK_TOKEN.test(t)) return false;
|
|
59
|
+
if (NON_SWAP.test(t)) return false;
|
|
60
|
+
if (SWAP_VERB.test(t)) return true;
|
|
61
|
+
if (BUY_VERB.test(t) && ASSET.test(t)) return true;
|
|
62
|
+
return false;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// ── Shape helpers (decoupled from the SDK's exact field names; the host
|
|
66
|
+
// adapter normalizes responses to these). ────────────────────────────────
|
|
67
|
+
interface PoolRow {
|
|
68
|
+
pool_id: string;
|
|
69
|
+
asset_a_address: string;
|
|
70
|
+
asset_b_address: string;
|
|
71
|
+
asset_a_symbol?: string;
|
|
72
|
+
asset_b_symbol?: string;
|
|
73
|
+
curve_type?: string;
|
|
74
|
+
tvl_asset_b?: string;
|
|
75
|
+
fee_bps?: number;
|
|
76
|
+
}
|
|
77
|
+
interface ListPoolsResult { pools: PoolRow[]; total_count?: number }
|
|
78
|
+
interface SimulateResult {
|
|
79
|
+
amount_out?: string;
|
|
80
|
+
execution_price?: string;
|
|
81
|
+
price_impact_pct?: string;
|
|
82
|
+
fee_paid_asset_in?: string;
|
|
83
|
+
warning?: string;
|
|
84
|
+
}
|
|
85
|
+
interface ExecuteResult {
|
|
86
|
+
accepted?: boolean;
|
|
87
|
+
request_id?: string;
|
|
88
|
+
amount_out?: string;
|
|
89
|
+
execution_price?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Resolve the asset address pair from a pool row + the user's requested
|
|
94
|
+
* (from_asset, to_asset) symbols. The pool stores ONE order (typically the
|
|
95
|
+
* non-BTC token on side A, BTC on side B), but the user can phrase the swap
|
|
96
|
+
* either direction.
|
|
97
|
+
*
|
|
98
|
+
* Strategy — deduce the unknown side from the known one:
|
|
99
|
+
* 1. Match each leg to a pool side by symbol or by the BTC-ticker family.
|
|
100
|
+
* 2. If one leg resolves to a side, the other leg MUST be the opposite side
|
|
101
|
+
* (a pool only has two assets). This is the case that prevented an
|
|
102
|
+
* earlier "asset_in == asset_out" bug on regtest, where the non-BTC
|
|
103
|
+
* side carries no symbol.
|
|
104
|
+
* 3. Only as a last resort fall back to the "BTC on side B, token on side
|
|
105
|
+
* A" default — true for almost every regtest pool we've seen.
|
|
106
|
+
*/
|
|
107
|
+
function isBtcTicker(s: string): boolean {
|
|
108
|
+
return s === 'BTC' || s === 'SATS' || s === 'BITCOIN';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveLegAddresses(pool: PoolRow, from: string, to: string): { fromAddr: string; toAddr: string } {
|
|
112
|
+
const f = (from ?? '').toUpperCase();
|
|
113
|
+
const t = (to ?? '').toUpperCase();
|
|
114
|
+
const aSym = (pool.asset_a_symbol ?? '').toUpperCase();
|
|
115
|
+
const bSym = (pool.asset_b_symbol ?? '').toUpperCase();
|
|
116
|
+
|
|
117
|
+
// Which side (if any) does each leg map to?
|
|
118
|
+
const sideOf = (sym: string): 'a' | 'b' | undefined => {
|
|
119
|
+
if (sym === aSym && aSym) return 'a';
|
|
120
|
+
if (sym === bSym && bSym) return 'b';
|
|
121
|
+
return undefined;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let fSide = sideOf(f);
|
|
125
|
+
let tSide = sideOf(t);
|
|
126
|
+
|
|
127
|
+
// 2. Deduce the unknown side from the known one.
|
|
128
|
+
if (!fSide && tSide) fSide = tSide === 'a' ? 'b' : 'a';
|
|
129
|
+
if (!tSide && fSide) tSide = fSide === 'a' ? 'b' : 'a';
|
|
130
|
+
|
|
131
|
+
// 3. Last-resort default — only when NEITHER side resolved. BTC on side B
|
|
132
|
+
// is the canonical layout for the Spark/Flashnet pools we've observed.
|
|
133
|
+
if (!fSide && !tSide) {
|
|
134
|
+
fSide = isBtcTicker(f) ? 'b' : 'a';
|
|
135
|
+
tSide = fSide === 'a' ? 'b' : 'a';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const addr = (side: 'a' | 'b'): string => (side === 'a' ? pool.asset_a_address : pool.asset_b_address);
|
|
139
|
+
return { fromAddr: addr(fSide!), toAddr: addr(tSide!) };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Compute min_amount_out = floor(amount_out × (1 − bps/10000)). String-safe. */
|
|
143
|
+
function computeMinAmountOut(amountOut: string | undefined, slippageBps: number): string {
|
|
144
|
+
if (!amountOut) return '0';
|
|
145
|
+
try {
|
|
146
|
+
const out = BigInt(String(amountOut).replace(/[^\d]/g, ''));
|
|
147
|
+
const num = BigInt(10_000 - Math.max(0, Math.min(5_000, slippageBps)));
|
|
148
|
+
const min = (out * num) / 10_000n;
|
|
149
|
+
return min.toString();
|
|
150
|
+
} catch {
|
|
151
|
+
return '0';
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const DEFAULT_SLIPPAGE_BPS = 50;
|
|
156
|
+
|
|
157
|
+
export const flashnetSwapRecipe: Recipe = {
|
|
158
|
+
name: 'flashnet-swap',
|
|
159
|
+
description:
|
|
160
|
+
"Swap on Flashnet (Spark-native AMM): list pool → simulate → confirm once → execute. The user's Spark wallet IS the swap account.",
|
|
161
|
+
match: (t) => FLASHNET_INTENT(t),
|
|
162
|
+
triggers: ['flashnet', 'usdb', 'swap', 'exchange', 'convert', 'trade'],
|
|
163
|
+
slots: [
|
|
164
|
+
{ name: 'from_asset', type: 'string', description: 'Asset the user SPENDS (BTC / USDB). "swap 10000 sats with usdb" → from_asset=BTC. "sell 1 usdb" → from_asset=USDB.', required: true },
|
|
165
|
+
{ name: 'to_asset', type: 'string', description: 'Asset the user GETS (BTC / USDB). "swap 10000 sats with usdb" → to_asset=USDB. "buy USDB with sats" → to_asset=USDB.', required: true },
|
|
166
|
+
{ name: 'amount', type: 'number', description: 'The numeric amount the user named. e.g. "swap 10000 sats with usdb" → amount=10000. Always in the asset on `amount_side`.' },
|
|
167
|
+
{ name: 'amount_side', type: 'string', description: "Which leg the amount is denominated in: 'from' (the spent asset) or 'to' (the received asset). Default 'from'. 'buy 10 usdb with sats' → 'to'." },
|
|
168
|
+
],
|
|
169
|
+
// extractSwap is the same regex extractor the KaleidoSwap atomic recipe
|
|
170
|
+
// uses; it returns {from_asset, to_asset, amount, amount_side?}. With
|
|
171
|
+
// forceModelExtract=true the runner ignores the det result and always asks
|
|
172
|
+
// the model — but the det extraction still feeds the Funnel's pre-filter so
|
|
173
|
+
// bare "buy/sell" with a Spark asset routes here even before the model runs.
|
|
174
|
+
extract: extractSwap,
|
|
175
|
+
forceModelExtract: true,
|
|
176
|
+
confident: (s) => !!s.from_asset && !!s.to_asset && !!s.amount,
|
|
177
|
+
steps: [
|
|
178
|
+
// 1. Discover the pool. Side-agnostic on the host side; one row is enough.
|
|
179
|
+
{
|
|
180
|
+
tool: 'flashnet_list_pools',
|
|
181
|
+
as: 'pools',
|
|
182
|
+
args: (ctx) => ({
|
|
183
|
+
asset_a: String(ctx.slots.from_asset ?? '').toUpperCase(),
|
|
184
|
+
asset_b: String(ctx.slots.to_asset ?? '').toUpperCase(),
|
|
185
|
+
sort: 'TVL_DESC',
|
|
186
|
+
limit: 5,
|
|
187
|
+
}),
|
|
188
|
+
},
|
|
189
|
+
// 2. Quote the swap. Direction is LITERAL: asset_in = what the user
|
|
190
|
+
// spends (`from_asset`), asset_out = what they get (`to_asset`). The
|
|
191
|
+
// runner — not the model — assembles this, so the inverted-direction
|
|
192
|
+
// bug is impossible by construction.
|
|
193
|
+
{
|
|
194
|
+
tool: 'flashnet_simulate_swap',
|
|
195
|
+
as: 'sim',
|
|
196
|
+
args: (ctx) => {
|
|
197
|
+
const r = ctx.results.pools as ListPoolsResult | undefined;
|
|
198
|
+
const pool = r?.pools?.[0];
|
|
199
|
+
if (!pool) throw new Error(`No Flashnet pool found for ${ctx.slots.from_asset} ↔ ${ctx.slots.to_asset}.`);
|
|
200
|
+
const { fromAddr, toAddr } = resolveLegAddresses(pool, String(ctx.slots.from_asset ?? ''), String(ctx.slots.to_asset ?? ''));
|
|
201
|
+
return {
|
|
202
|
+
pool_id: pool.pool_id,
|
|
203
|
+
asset_in_address: fromAddr,
|
|
204
|
+
asset_out_address: toAddr,
|
|
205
|
+
amount_in: String(ctx.slots.amount ?? ''),
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
// 3. Settle. Confirmation-gated at the recipe level (see runner.ts).
|
|
211
|
+
// min_amount_out is computed here — never the raw simulated value — so a
|
|
212
|
+
// moving pool can't fill at a worse price than `slippage_bps` allows.
|
|
213
|
+
final: {
|
|
214
|
+
tool: 'flashnet_execute_swap',
|
|
215
|
+
as: 'exec',
|
|
216
|
+
args: (ctx) => {
|
|
217
|
+
const r = ctx.results.pools as ListPoolsResult | undefined;
|
|
218
|
+
const pool = r?.pools?.[0];
|
|
219
|
+
const sim = ctx.results.sim as SimulateResult | undefined;
|
|
220
|
+
if (!pool || !sim) throw new Error('Flashnet swap: missing pool or simulation result.');
|
|
221
|
+
const { fromAddr, toAddr } = resolveLegAddresses(pool, String(ctx.slots.from_asset ?? ''), String(ctx.slots.to_asset ?? ''));
|
|
222
|
+
return {
|
|
223
|
+
pool_id: pool.pool_id,
|
|
224
|
+
asset_in_address: fromAddr,
|
|
225
|
+
asset_out_address: toAddr,
|
|
226
|
+
amount_in: String(ctx.slots.amount ?? ''),
|
|
227
|
+
min_amount_out: computeMinAmountOut(sim.amount_out, DEFAULT_SLIPPAGE_BPS),
|
|
228
|
+
max_slippage_bps: DEFAULT_SLIPPAGE_BPS,
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
// ONE confirmation, fired after simulate / before execute, with real numbers.
|
|
233
|
+
confirm: (ctx: RecipeContext) => {
|
|
234
|
+
const sim = ctx.results.sim as SimulateResult | undefined;
|
|
235
|
+
const from = fmtAmount(ctx.slots.amount, String(ctx.slots.from_asset ?? ''));
|
|
236
|
+
const to = fmtAmount(sim?.amount_out, String(ctx.slots.to_asset ?? ''));
|
|
237
|
+
const impact = sim?.price_impact_pct ? ` · ${sim.price_impact_pct} price impact` : '';
|
|
238
|
+
const warn = sim?.warning ? ` · ${sim.warning}` : '';
|
|
239
|
+
return `Swap ${from} → ~${to} on Flashnet (slippage cap ${DEFAULT_SLIPPAGE_BPS / 100}%)${impact}${warn}. Proceed?`;
|
|
240
|
+
},
|
|
241
|
+
summary: (ctx) => {
|
|
242
|
+
const sim = ctx.results.sim as SimulateResult | undefined;
|
|
243
|
+
const exec = ctx.results.exec as ExecuteResult | undefined;
|
|
244
|
+
const from = fmtAmount(ctx.slots.amount, String(ctx.slots.from_asset ?? ''));
|
|
245
|
+
const got = fmtAmount(exec?.amount_out ?? sim?.amount_out, String(ctx.slots.to_asset ?? ''));
|
|
246
|
+
if (exec?.accepted === false) {
|
|
247
|
+
return `Flashnet swap rejected: ${(exec as any)?.error ?? 'unknown error'}.`;
|
|
248
|
+
}
|
|
249
|
+
return `Flashnet swap submitted: ${from} → ${got}. request_id=${exec?.request_id ?? '?'}.`;
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Format an amount + asset for user display. BTC is rendered as "X,XXX sats"
|
|
255
|
+
* (BTC is the asset, sats is the unit; the on-the-wire amount is already in
|
|
256
|
+
* sats so no conversion is needed). Anything else is rendered as
|
|
257
|
+
* "N TICKER" with thousand-separators.
|
|
258
|
+
*/
|
|
259
|
+
function fmtAmount(amount: unknown, asset: string): string {
|
|
260
|
+
const t = (asset ?? '').toUpperCase();
|
|
261
|
+
const n = Number(amount);
|
|
262
|
+
if (!Number.isFinite(n)) return `${amount ?? '?'} ${t}`;
|
|
263
|
+
const sep = n.toLocaleString('en-US');
|
|
264
|
+
if (t === 'BTC' || t === 'SATS' || t === 'BITCOIN') return `${sep} sats`;
|
|
265
|
+
return `${sep} ${t}`;
|
|
266
|
+
}
|
|
@@ -44,7 +44,7 @@ function buildStubs(captured: { name: string; args: any }[]) {
|
|
|
44
44
|
]),
|
|
45
45
|
new InProcessToolSource('rln', [
|
|
46
46
|
tool('rln_get_node_info', { pubkey: '03c31dae' }),
|
|
47
|
-
tool('
|
|
47
|
+
tool('rln_atomic_taker', { ok: true }, /* spend */ true),
|
|
48
48
|
]),
|
|
49
49
|
]);
|
|
50
50
|
}
|
|
@@ -66,6 +66,27 @@ describe('kaleidoswapAtomicRecipe — selection', () => {
|
|
|
66
66
|
it('does not trigger on a balance question', () => {
|
|
67
67
|
expect(kaleidoswapAtomicRecipe.match!('what is my balance')).toBe(false);
|
|
68
68
|
});
|
|
69
|
+
it('DEFERS to Flashnet when a Flashnet/Spark cue is present (venue split)', () => {
|
|
70
|
+
// These belong to the agentic flashnet-swaps skill, not the KaleidoSwap
|
|
71
|
+
// maker recipe — so the recipe must NOT claim them.
|
|
72
|
+
expect(kaleidoswapAtomicRecipe.match!('swap 10000 sats with asset of your choice in flashnet')).toBe(false);
|
|
73
|
+
expect(kaleidoswapAtomicRecipe.match!('swap 5000 sats to usdb')).toBe(false);
|
|
74
|
+
expect(kaleidoswapAtomicRecipe.match!('swap btc to usdb on spark')).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
it('does NOT claim a bare swap with no venue/asset cue (falls to agentic)', () => {
|
|
77
|
+
// Ambiguous — a swap always needs a target asset; let the skill tier ask
|
|
78
|
+
// or pick the connected venue rather than grabbing it for the maker.
|
|
79
|
+
expect(kaleidoswapAtomicRecipe.match!('swap 100000 sats')).toBe(false);
|
|
80
|
+
expect(kaleidoswapAtomicRecipe.match!('exchange some bitcoin')).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
it('still claims swaps that name an RGB/maker asset', () => {
|
|
83
|
+
expect(kaleidoswapAtomicRecipe.match!('swap 100000 sats to usdt')).toBe(true);
|
|
84
|
+
expect(kaleidoswapAtomicRecipe.match!('convert btc to xaut')).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
it('does not turn portfolio review or explicit no-trade language into a swap', () => {
|
|
87
|
+
expect(kaleidoswapAtomicRecipe.match!('review my portfolio allocation but do not trade')).toBe(false);
|
|
88
|
+
expect(kaleidoswapAtomicRecipe.match!('analyze my holdings without trading')).toBe(false);
|
|
89
|
+
});
|
|
69
90
|
});
|
|
70
91
|
|
|
71
92
|
describe('kaleidoswapAtomicRecipe — forceModelExtract (less deterministic slot parsing)', () => {
|
|
@@ -130,7 +151,7 @@ describe('kaleidoswapAtomicRecipe — full chain', () => {
|
|
|
130
151
|
'kaleidoswap_get_quote',
|
|
131
152
|
'kaleidoswap_atomic_init',
|
|
132
153
|
'rln_get_node_info',
|
|
133
|
-
'
|
|
154
|
+
'rln_atomic_taker',
|
|
134
155
|
'kaleidoswap_atomic_execute',
|
|
135
156
|
]);
|
|
136
157
|
});
|
|
@@ -157,7 +178,7 @@ describe('kaleidoswapAtomicRecipe — full chain', () => {
|
|
|
157
178
|
provider: refusingProvider, tools, onConfirm: async () => ({ approved: true }),
|
|
158
179
|
slots: { from_asset: 'USDT', to_asset: 'BTC', amount: 10, amount_side: 'from' },
|
|
159
180
|
});
|
|
160
|
-
const whitelist = captured.find((c) => c.name === '
|
|
181
|
+
const whitelist = captured.find((c) => c.name === 'rln_atomic_taker')!;
|
|
161
182
|
expect(whitelist.args).toEqual({ swapstring: 'SWAP/abc/def' });
|
|
162
183
|
const exe = captured.find((c) => c.name === 'kaleidoswap_atomic_execute')!;
|
|
163
184
|
expect(exe.args).toEqual({
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* ↓ [ONE confirmation gate — shows the real quote numbers]
|
|
13
13
|
* kaleidoswap_atomic_init ← MAKER locks the swap → swapstring, payment_hash
|
|
14
14
|
* rln_get_node_info ← NODE read pubkey (= taker_pubkey)
|
|
15
|
-
*
|
|
15
|
+
* rln_atomic_taker ← NODE whitelist the swapstring (taker accepts)
|
|
16
16
|
* kaleidoswap_atomic_execute ← MAKER settle (final)
|
|
17
17
|
*
|
|
18
18
|
* `forceModelExtract` ensures the model is always consulted for slot parsing
|
|
@@ -34,23 +34,38 @@
|
|
|
34
34
|
import type { Recipe, RecipeContext } from './types.js';
|
|
35
35
|
import { extractSwap } from './swap.js';
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
|
|
37
|
+
// KaleidoSwap is a BTC↔RGB ATOMIC swap venue (maker + RLN node). It is NOT the
|
|
38
|
+
// only swap venue anymore — Flashnet (Spark-native AMM, BTC↔Spark tokens like
|
|
39
|
+
// USDB) is a sibling, handled by the agentic `flashnet-swaps` skill. The Funnel
|
|
40
|
+
// runs recipes BEFORE skills, so a greedy "any swap word" match here would
|
|
41
|
+
// monopolize every swap and starve Flashnet. To let both coexist, this recipe
|
|
42
|
+
// only claims swaps that point at ITS venue:
|
|
43
|
+
// - names an RGB/maker asset or the venue itself (RGB_CUE), AND
|
|
44
|
+
// - does NOT name a Flashnet/Spark cue (FLASHNET_CUE → defer to the skill).
|
|
45
|
+
// A bare "swap" with no venue cue falls through to the agentic tier, where the
|
|
46
|
+
// skill selector disambiguates (or the model asks).
|
|
47
|
+
const RGB_CUE = /\b(usdt|tether|xaut|gold|rgb|kaleidoswap|kaleido|atomic)\b/i;
|
|
48
|
+
const FLASHNET_CUE = /\b(flashnet|usdb|spark)\b/i;
|
|
42
49
|
const SWAP_INTENT = (t: string) => {
|
|
43
50
|
// Explanatory / educational questions → route to RAG-backed agentic answer,
|
|
44
51
|
// not the deterministic spend chain.
|
|
45
52
|
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
|
-
|
|
47
|
-
if (
|
|
53
|
+
// Flashnet owns its venue — defer to the flashnet-swaps skill.
|
|
54
|
+
if (FLASHNET_CUE.test(t)) return false;
|
|
55
|
+
// Portfolio analysis belongs to the portfolio skill, which reads balances
|
|
56
|
+
// and targets before suggesting any action. A mention of "trade" in a review
|
|
57
|
+
// request—especially "do not trade"—must never become an immediate swap.
|
|
58
|
+
if (/\b(portfolio|allocation|holdings|rebalance|rebalancing)\b/i.test(t)) return false;
|
|
59
|
+
if (/\b(?:do\s+not|don't|without|no)\s+(?:place\s+(?:a\s+)?)?(?:trade|swap|buy|sell|trading)\b/i.test(t)) return false;
|
|
60
|
+
const swapVerb = /\b(swap|exchange|convert|trade)\b/i.test(t);
|
|
61
|
+
const buyVerb =
|
|
48
62
|
/\b(buy|sell|get|purchase|acquire)\b/i.test(t) &&
|
|
49
|
-
ASSET.test(t) &&
|
|
50
63
|
// Exclude commerce / receive / LSPS1 channel-order phrasings that share
|
|
51
64
|
// 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
|
-
)
|
|
65
|
+
!/\b(gift\s?card|top-?up|esim|voucher|invoice|address|channel|inbound|liquidity|lsps?\b)\b/i.test(t);
|
|
66
|
+
// Only claim the swap when an RGB/maker asset (or the venue) is named, so a
|
|
67
|
+
// bare/ambiguous "swap" or a Flashnet-asset swap doesn't get grabbed here.
|
|
68
|
+
if (swapVerb || buyVerb) return RGB_CUE.test(t);
|
|
54
69
|
return false;
|
|
55
70
|
};
|
|
56
71
|
|
|
@@ -62,7 +77,7 @@ interface QuoteResult {
|
|
|
62
77
|
to_amount_display?: string;
|
|
63
78
|
fee_display?: string;
|
|
64
79
|
}
|
|
65
|
-
interface InitResult { swapstring?: string; payment_hash?: string }
|
|
80
|
+
interface InitResult { swapstring?: string; payment_hash?: string; atomic_id?: string }
|
|
66
81
|
interface NodeInfo { pubkey?: string }
|
|
67
82
|
|
|
68
83
|
export const kaleidoswapAtomicRecipe: Recipe = {
|
|
@@ -72,10 +87,10 @@ export const kaleidoswapAtomicRecipe: Recipe = {
|
|
|
72
87
|
match: (t) => SWAP_INTENT(t),
|
|
73
88
|
triggers: ['swap', 'exchange', 'convert', 'trade', 'buy', 'sell'],
|
|
74
89
|
slots: [
|
|
75
|
-
{ name: 'from_asset', type: 'string', description: 'Asset to spend (BTC / USDT / XAUT)', required: true },
|
|
76
|
-
{ name: 'to_asset', type: 'string', description: 'Asset to receive (BTC / USDT / XAUT)', required: true },
|
|
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)" },
|
|
90
|
+
{ name: 'from_asset', type: 'string', description: 'Asset to spend (BTC / USDT / XAUT). Example: "swap 10 usdt to btc" → from_asset=USDT', required: true },
|
|
91
|
+
{ name: 'to_asset', type: 'string', description: 'Asset to receive (BTC / USDT / XAUT). Example: "buy 1 usdt" → to_asset=USDT', required: true },
|
|
92
|
+
{ name: 'amount', type: 'number', description: 'The amount the user named (in from_asset units for sell, to_asset for buy). E.g. "buy 1 usdt" amount=1; "swap 100000 sats" amount=100000' },
|
|
93
|
+
{ name: 'amount_side', type: 'string', description: "Which leg the amount is on: 'from' (sell/swap) or 'to' (buy). Use examples in descriptions and 'buy X Y' means to_asset." },
|
|
79
94
|
],
|
|
80
95
|
// Keep the fast `extract` for the Funnel's cheap pre-filter (so "buy 1 usdt"
|
|
81
96
|
// reliably enters the recipe branch instead of falling to free agentic).
|
|
@@ -122,10 +137,11 @@ export const kaleidoswapAtomicRecipe: Recipe = {
|
|
|
122
137
|
as: 'node',
|
|
123
138
|
args: () => ({}),
|
|
124
139
|
},
|
|
125
|
-
// 4. NODE:
|
|
126
|
-
//
|
|
140
|
+
// 4. NODE: the taker whitelists the maker's swapstring (accept the swap).
|
|
141
|
+
// Exposed by kaleido-mcp as `rln_atomic_taker` (calls rln.whitelistSwap).
|
|
142
|
+
// Ungated — covered by the single confirm above.
|
|
127
143
|
{
|
|
128
|
-
tool: '
|
|
144
|
+
tool: 'rln_atomic_taker',
|
|
129
145
|
as: 'whitelist',
|
|
130
146
|
args: (ctx) => {
|
|
131
147
|
const init = ctx.results.init as InitResult | undefined;
|
|
@@ -158,6 +174,9 @@ export const kaleidoswapAtomicRecipe: Recipe = {
|
|
|
158
174
|
const q = ctx.results.quote as QuoteResult | undefined;
|
|
159
175
|
const from = q?.from_amount_display ?? `${ctx.slots.amount} ${ctx.slots.from_asset}`;
|
|
160
176
|
const to = q?.to_amount_display ?? String(ctx.slots.to_asset);
|
|
161
|
-
|
|
177
|
+
const init = ctx.results.init as InitResult | undefined;
|
|
178
|
+
const id = init?.atomic_id || init?.payment_hash || '?';
|
|
179
|
+
return `remember: atomic swap atomic_id=${id} (for later kaleidoswap_atomic_status checks).
|
|
180
|
+
Swap submitted: ${from} → ${to}. To check status later, call: kaleidoswap_atomic_status(atomic_id=${id}). Say "check my swap status" and I will recall + poll automatically.`;
|
|
162
181
|
},
|
|
163
182
|
};
|