@kaleidorg/mind 0.6.3 → 0.7.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 +257 -0
- package/dist/autonomy/risk.js.map +1 -1
- package/dist/autonomy/run-state.d.ts.map +1 -1
- package/dist/autonomy/run-state.js.map +1 -1
- package/dist/autonomy/task-store.d.ts.map +1 -1
- package/dist/autonomy/task-store.js.map +1 -1
- package/dist/bitrefill/contract.js.map +1 -1
- package/dist/context/budget.js.map +1 -1
- package/dist/context/builder.d.ts.map +1 -1
- package/dist/context/builder.js.map +1 -1
- package/dist/context/compress.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js.map +1 -1
- package/dist/evidence.d.ts +1 -1
- package/dist/evidence.d.ts.map +1 -1
- package/dist/evidence.js.map +1 -1
- package/dist/fastpath/fastpath.d.ts.map +1 -1
- package/dist/fastpath/fastpath.js.map +1 -1
- package/dist/flashnet/contract.js.map +1 -1
- package/dist/funnel.d.ts +3 -0
- package/dist/funnel.d.ts.map +1 -1
- package/dist/funnel.js +2 -0
- package/dist/funnel.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/knowledge/btc-map.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/lsps1/contract.js.map +1 -1
- package/dist/memory/store.d.ts.map +1 -1
- package/dist/memory/store.js.map +1 -1
- package/dist/qvac/assistant.js.map +1 -1
- package/dist/qvac/config.d.ts +6 -6
- package/dist/qvac/config.d.ts.map +1 -1
- package/dist/qvac/delegate.d.ts +2 -0
- package/dist/qvac/delegate.d.ts.map +1 -1
- package/dist/qvac/delegate.js +2 -0
- package/dist/qvac/delegate.js.map +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/parse.d.ts +5 -2
- package/dist/qvac/parse.d.ts.map +1 -1
- package/dist/qvac/parse.js.map +1 -1
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +21 -13
- package/dist/qvac/provider.js.map +1 -1
- package/dist/qvac/stream.d.ts.map +1 -1
- package/dist/qvac/stream.js +29 -1
- package/dist/qvac/stream.js.map +1 -1
- package/dist/qvac/tools.d.ts +32 -0
- package/dist/qvac/tools.d.ts.map +1 -0
- package/dist/qvac/tools.js +84 -0
- package/dist/qvac/tools.js.map +1 -0
- package/dist/qvac/voice.js.map +1 -1
- package/dist/rag/retriever.d.ts.map +1 -1
- package/dist/rag/tool.js.map +1 -1
- package/dist/rag/vector-store.d.ts.map +1 -1
- package/dist/rag/vector-store.js.map +1 -1
- package/dist/recipe/issue-asset.d.ts +16 -0
- package/dist/recipe/issue-asset.d.ts.map +1 -0
- package/dist/recipe/issue-asset.js +94 -0
- package/dist/recipe/issue-asset.js.map +1 -0
- package/dist/recipe/runner.d.ts +1 -1
- package/dist/recipe/runner.d.ts.map +1 -1
- package/dist/recipe/runner.js +29 -6
- package/dist/recipe/runner.js.map +1 -1
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js.map +1 -1
- package/dist/testing/index.d.ts +14 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +12 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/mock-wallet.d.ts +79 -0
- package/dist/testing/mock-wallet.d.ts.map +1 -0
- package/dist/testing/mock-wallet.js +180 -0
- package/dist/testing/mock-wallet.js.map +1 -0
- package/dist/testing/scripted-provider.d.ts +17 -0
- package/dist/testing/scripted-provider.d.ts.map +1 -0
- package/dist/testing/scripted-provider.js +26 -0
- package/dist/testing/scripted-provider.js.map +1 -0
- package/dist/tools/in-process.d.ts.map +1 -1
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/wallet/confirm.d.ts.map +1 -1
- package/dist/wallet/confirm.js +21 -1
- package/dist/wallet/confirm.js.map +1 -1
- package/dist/wallet/contract.d.ts.map +1 -1
- package/dist/wallet/contract.js +36 -0
- package/dist/wallet/contract.js.map +1 -1
- package/package.json +10 -5
- package/skills/README.md +1 -1
- package/skills/kaleido-node/SKILL.md +63 -0
- package/skills/kaleido-trading/SKILL.md +15 -2
- package/skills/kaleido-trading/references/api.md +61 -0
- package/skills/kaleido-trading/references/assets.md +57 -0
- package/skills/kaleido-trading/references/atomic.md +93 -0
- package/skills/paid-data/SKILL.md +59 -6
- package/skills/rgb-lightning-node/SKILL.md +116 -11
- package/src/funnel.ts +5 -0
- package/src/index.ts +3 -0
- package/src/qvac/delegate.ts +2 -0
- package/src/qvac/index.ts +2 -0
- package/src/qvac/parse.ts +5 -2
- package/src/qvac/provider.test.ts +38 -0
- package/src/qvac/provider.ts +27 -14
- package/src/qvac/stream.test.ts +28 -0
- package/src/qvac/stream.ts +30 -1
- package/src/qvac/tools.test.ts +86 -0
- package/src/qvac/tools.ts +106 -0
- package/src/recipe/issue-asset.test.ts +92 -0
- package/src/recipe/issue-asset.ts +95 -0
- package/src/recipe/kaleidoswap-atomic.test.ts +27 -0
- package/src/recipe/runner.ts +24 -5
- package/src/testing/index.ts +14 -0
- package/src/testing/mock-wallet.ts +211 -0
- package/src/testing/scripted-provider.ts +37 -0
- package/src/wallet/confirm.test.ts +16 -0
- package/src/wallet/confirm.ts +21 -1
- package/src/wallet/contract.test.ts +38 -0
- package/src/wallet/contract.ts +36 -0
package/src/recipe/runner.ts
CHANGED
|
@@ -71,6 +71,7 @@ export async function extractSlots(
|
|
|
71
71
|
provider: LLMProvider,
|
|
72
72
|
recipe: Recipe,
|
|
73
73
|
text: string,
|
|
74
|
+
signal?: AbortSignal,
|
|
74
75
|
): Promise<{ slots: Record<string, unknown>; inferences: number }> {
|
|
75
76
|
const det = recipe.extract?.(text);
|
|
76
77
|
const detValid = det && Object.values(det).some((v) => v !== undefined && v !== null && v !== '');
|
|
@@ -110,12 +111,17 @@ export async function extractSlots(
|
|
|
110
111
|
system,
|
|
111
112
|
messages: [{ role: 'user', content: text }],
|
|
112
113
|
tools: [extractTool],
|
|
114
|
+
signal,
|
|
113
115
|
});
|
|
114
116
|
} catch (err) {
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
|
|
117
|
+
// An explicit user cancel (the stop button aborts the signal) must NOT be
|
|
118
|
+
// papered over with deterministic slots — that would run the action the
|
|
119
|
+
// user just tried to stop. Re-throw so runRecipe reports it as cancelled.
|
|
120
|
+
if (signal?.aborted) throw err;
|
|
121
|
+
// Otherwise the forced extraction inference simply failed — on small
|
|
122
|
+
// on-device models this happens when the model rambles and the request is
|
|
123
|
+
// cancelled/times out. Don't fail a request the deterministic regex already
|
|
124
|
+
// understood: if det produced valid slots, degrade gracefully to them.
|
|
119
125
|
if (detValid) return { slots: det, inferences: 0 };
|
|
120
126
|
throw err;
|
|
121
127
|
}
|
|
@@ -172,12 +178,21 @@ export async function extractSlots(
|
|
|
172
178
|
export async function runRecipe(recipe: Recipe, text: string, opts: RunRecipeOptions): Promise<RecipeResult> {
|
|
173
179
|
const ctx: RecipeContext = { text, slots: opts.slots ?? {}, results: {} };
|
|
174
180
|
let inferences = 0;
|
|
181
|
+
// The user pressed stop (opts.signal aborted) — nothing has been sent yet.
|
|
182
|
+
const stopped = (): RecipeResult => ({
|
|
183
|
+
recipe: recipe.name, slots: ctx.slots, results: ctx.results,
|
|
184
|
+
text: 'Stopped.', status: 'cancelled', inferences,
|
|
185
|
+
});
|
|
175
186
|
try {
|
|
187
|
+
if (opts.signal?.aborted) return stopped();
|
|
176
188
|
if (!opts.slots) {
|
|
177
|
-
const ex = await extractSlots(opts.provider, recipe, text);
|
|
189
|
+
const ex = await extractSlots(opts.provider, recipe, text, opts.signal);
|
|
178
190
|
ctx.slots = ex.slots;
|
|
179
191
|
inferences = ex.inferences;
|
|
180
192
|
}
|
|
193
|
+
// Extraction can be cancelled either by throwing (caught below) or by
|
|
194
|
+
// returning empty when the run was aborted mid-flight — catch the latter.
|
|
195
|
+
if (opts.signal?.aborted) return stopped();
|
|
181
196
|
|
|
182
197
|
// Confidence re-check AFTER extraction (whether deterministic, LLM, or
|
|
183
198
|
// pre-supplied). When the recipe defines `confident()` and the extracted
|
|
@@ -230,6 +245,7 @@ export async function runRecipe(recipe: Recipe, text: string, opts: RunRecipeOpt
|
|
|
230
245
|
};
|
|
231
246
|
|
|
232
247
|
for (const step of recipe.steps) {
|
|
248
|
+
if (opts.signal?.aborted) return stopped();
|
|
233
249
|
if (step.skipIf?.(ctx)) continue;
|
|
234
250
|
const args = step.args(ctx);
|
|
235
251
|
if (!(await passesGate(step.tool, args))) return cancelled();
|
|
@@ -252,6 +268,9 @@ export async function runRecipe(recipe: Recipe, text: string, opts: RunRecipeOpt
|
|
|
252
268
|
const out = recipe.summary?.(ctx, finalResult) ?? 'Done.';
|
|
253
269
|
return { recipe: recipe.name, slots: ctx.slots, results: ctx.results, final: finalResult, text: out, status: 'done', inferences };
|
|
254
270
|
} catch (e) {
|
|
271
|
+
// A stop pressed mid-inference surfaces as a thrown cancel — report it as
|
|
272
|
+
// cancelled ("Stopped."), not as an error.
|
|
273
|
+
if (opts.signal?.aborted) return stopped();
|
|
255
274
|
const msg = (e as Error)?.message ?? String(e);
|
|
256
275
|
return { recipe: recipe.name, slots: ctx.slots, results: ctx.results, text: `Couldn't complete that: ${msg}`, status: 'error', error: msg, inferences };
|
|
257
276
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kaleidorg/mind/testing — build and demo an agent with no node, no funds and
|
|
3
|
+
* no model: a stateful mock wallet bound to the canonical contract, plus a
|
|
4
|
+
* scripted provider.
|
|
5
|
+
*
|
|
6
|
+
* import { MockWallet, scriptedProvider } from '@kaleidorg/mind/testing';
|
|
7
|
+
* const wallet = new MockWallet();
|
|
8
|
+
* const funnel = new Funnel({ provider: scriptedProvider(), tools: wallet.registry(), … });
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export { MockWallet } from './mock-wallet.js';
|
|
12
|
+
export type { MockWalletOptions, MockContact, MockRgbAsset, MockTransfer, SendRecord } from './mock-wallet.js';
|
|
13
|
+
export { scriptedProvider } from './scripted-provider.js';
|
|
14
|
+
export type { ScriptedTurn } from './scripted-provider.js';
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stateful mock wallet — balances per layer, contacts (incl. ambiguous +
|
|
3
|
+
* injectable), a price, validation (insufficient funds, no route, unknown
|
|
4
|
+
* contact, no colored UTXOs), RGB issuance, and a record of what actually got
|
|
5
|
+
* "sent". The canonical contract tools bind to it, so the agent sees real
|
|
6
|
+
* schemas and real failure modes — not trivial canned stubs.
|
|
7
|
+
*
|
|
8
|
+
* Used by the eval harness and exported as `@kaleidorg/mind/testing` so hosts
|
|
9
|
+
* can build and demo an agent with no node, no funds and no network.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { bindWalletTools, type WalletHandler } from '../wallet/contract.js';
|
|
13
|
+
import { ToolRegistry } from '../tools/registry.js';
|
|
14
|
+
|
|
15
|
+
export interface SendRecord {
|
|
16
|
+
tool: string;
|
|
17
|
+
to: string;
|
|
18
|
+
amount_sats?: number;
|
|
19
|
+
asset?: string;
|
|
20
|
+
amount?: number;
|
|
21
|
+
}
|
|
22
|
+
export interface MockContact { name: string; ln_address: string; note?: string }
|
|
23
|
+
export interface MockWalletOptions {
|
|
24
|
+
priceUsd?: number;
|
|
25
|
+
failRoute?: boolean;
|
|
26
|
+
contacts?: MockContact[];
|
|
27
|
+
balances?: { spark: number; rln: number; arkade: number };
|
|
28
|
+
assets?: Record<string, number>;
|
|
29
|
+
/** Free colored UTXOs on the RLN node (issuance / RGB receive consume one). */
|
|
30
|
+
utxos?: number;
|
|
31
|
+
}
|
|
32
|
+
export interface MockRgbAsset {
|
|
33
|
+
asset_id: string;
|
|
34
|
+
ticker: string;
|
|
35
|
+
name: string;
|
|
36
|
+
precision: number;
|
|
37
|
+
schema: 'NIA' | 'CFA' | 'UDA';
|
|
38
|
+
issued_supply: number;
|
|
39
|
+
}
|
|
40
|
+
export interface MockTransfer {
|
|
41
|
+
idx: number;
|
|
42
|
+
asset_id: string;
|
|
43
|
+
kind: 'Issuance' | 'Send' | 'ReceiveBlind';
|
|
44
|
+
status: 'Settled' | 'WaitingCounterparty';
|
|
45
|
+
amount: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class MockWallet {
|
|
49
|
+
priceUsd: number;
|
|
50
|
+
failRoute: boolean;
|
|
51
|
+
balances: { spark: number; rln: number; arkade: number };
|
|
52
|
+
assets: Record<string, number>;
|
|
53
|
+
contacts: MockContact[];
|
|
54
|
+
utxos: number;
|
|
55
|
+
/** RGB assets the node knows about (seeded USDT/XAUT + anything issued). */
|
|
56
|
+
rgbAssets: MockRgbAsset[];
|
|
57
|
+
transfers: MockTransfer[] = [];
|
|
58
|
+
sends: SendRecord[] = [];
|
|
59
|
+
|
|
60
|
+
constructor(o: MockWalletOptions = {}) {
|
|
61
|
+
this.priceUsd = o.priceUsd ?? 65_000;
|
|
62
|
+
this.failRoute = o.failRoute ?? false;
|
|
63
|
+
this.balances = o.balances ?? { spark: 500_000, rln: 300_000, arkade: 200_000 };
|
|
64
|
+
this.assets = o.assets ?? { USDT: 25_000_000, XAUT: 0 };
|
|
65
|
+
this.contacts = o.contacts ?? [
|
|
66
|
+
{ name: 'bob', ln_address: 'bob@kaleidoswap.com' },
|
|
67
|
+
{ name: 'alice', ln_address: 'alice@kaleidoswap.com' },
|
|
68
|
+
{ name: 'john', ln_address: 'john.smith@kaleidoswap.com' },
|
|
69
|
+
{ name: 'john', ln_address: 'john.doe@kaleidoswap.com' }, // ambiguous on purpose
|
|
70
|
+
];
|
|
71
|
+
this.utxos = o.utxos ?? 5;
|
|
72
|
+
this.rgbAssets = Object.keys(this.assets).map((ticker) => ({
|
|
73
|
+
asset_id: mockAssetId(ticker),
|
|
74
|
+
ticker,
|
|
75
|
+
name: ticker === 'USDT' ? 'Tether USD' : ticker === 'XAUT' ? 'Tether Gold' : ticker,
|
|
76
|
+
precision: 0,
|
|
77
|
+
schema: 'NIA' as const,
|
|
78
|
+
issued_supply: this.assets[ticker]!,
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private findAsset(ref: unknown): MockRgbAsset | undefined {
|
|
83
|
+
const q = String(ref ?? '');
|
|
84
|
+
return this.rgbAssets.find((a) => a.asset_id === q || a.ticker.toUpperCase() === q.toUpperCase());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Issue a new RGB asset — mirrors the node: needs a free colored UTXO. */
|
|
88
|
+
issueAsset(args: Record<string, unknown>): MockRgbAsset {
|
|
89
|
+
if (this.utxos < 1) throw new Error('No available colored UTXOs — call rln_create_utxos first.');
|
|
90
|
+
const schema = (['NIA', 'CFA', 'UDA'].includes(String(args.schema)) ? args.schema : 'NIA') as MockRgbAsset['schema'];
|
|
91
|
+
const ticker = String(args.ticker ?? '').toUpperCase();
|
|
92
|
+
if (!/^[A-Z0-9]{1,8}$/.test(ticker)) throw new Error('ticker must be 1-8 uppercase letters/digits.');
|
|
93
|
+
if (this.findAsset(ticker)) throw new Error(`An asset with ticker ${ticker} already exists.`);
|
|
94
|
+
const supply = schema === 'UDA' ? 1 : Number(args.amount);
|
|
95
|
+
if (!(supply > 0)) throw new Error('amount must be positive.');
|
|
96
|
+
const asset: MockRgbAsset = {
|
|
97
|
+
asset_id: mockAssetId(ticker),
|
|
98
|
+
ticker,
|
|
99
|
+
name: String(args.name ?? ticker),
|
|
100
|
+
precision: Number(args.precision ?? 0),
|
|
101
|
+
schema,
|
|
102
|
+
issued_supply: supply,
|
|
103
|
+
};
|
|
104
|
+
this.utxos -= 1;
|
|
105
|
+
this.rgbAssets.push(asset);
|
|
106
|
+
this.assets[ticker] = supply;
|
|
107
|
+
this.transfers.push({ idx: this.transfers.length + 1, asset_id: asset.asset_id, kind: 'Issuance', status: 'Settled', amount: supply });
|
|
108
|
+
this.sends.push({ tool: 'rln_issue_asset', to: 'self', asset: ticker, amount: supply });
|
|
109
|
+
return asset;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
totalSats(): number {
|
|
113
|
+
return this.balances.spark + this.balances.rln + this.balances.arkade;
|
|
114
|
+
}
|
|
115
|
+
reset(): void {
|
|
116
|
+
this.sends = [];
|
|
117
|
+
this.transfers = [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private send(tool: string, rec: { to: string; amount_sats?: number; asset?: string; amount?: number }) {
|
|
121
|
+
if (this.failRoute) throw new Error('No route to destination.');
|
|
122
|
+
if (rec.amount_sats != null && rec.amount_sats > this.totalSats()) {
|
|
123
|
+
throw new Error(`Insufficient funds: have ${this.totalSats()} sats, need ${rec.amount_sats}.`);
|
|
124
|
+
}
|
|
125
|
+
this.sends.push({ tool, ...rec });
|
|
126
|
+
return { status: 'SUCCESS', payment_hash: 'mock' + this.sends.length };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
handlers(): Record<string, WalletHandler> {
|
|
130
|
+
return {
|
|
131
|
+
get_balances: async () => ({
|
|
132
|
+
total_sats: this.totalSats(),
|
|
133
|
+
layers: [
|
|
134
|
+
{ layer: 'spark', btc_sats: this.balances.spark, assets: [] },
|
|
135
|
+
{ layer: 'rln', btc_sats: this.balances.rln, assets: Object.entries(this.assets).filter(([, v]) => v > 0).map(([ticker, amount]) => ({ ticker, amount })) },
|
|
136
|
+
{ layer: 'arkade', btc_sats: this.balances.arkade, assets: [] },
|
|
137
|
+
],
|
|
138
|
+
}),
|
|
139
|
+
spark_get_balance: async () => ({ btc_sats: this.balances.spark }),
|
|
140
|
+
rln_get_balances: async () => ({ btc_sats: this.balances.rln, assets: this.assets }),
|
|
141
|
+
arkade_get_balance: async () => ({ btc_sats: this.balances.arkade }),
|
|
142
|
+
spark_get_address: async () => ({ address: 'bc1qspark0mockreceiveaddr' }),
|
|
143
|
+
arkade_get_address: async () => ({ address: 'ark1q0mockreceiveaddr' }),
|
|
144
|
+
get_price: async ({ fiat }) => ({ asset: 'BTC', price_usd: this.priceUsd, fiat: (fiat as string) ?? 'USD' }),
|
|
145
|
+
fiat_to_sats: async ({ amount }) => ({ sats: Math.round((Number(amount) / this.priceUsd) * 1e8) }),
|
|
146
|
+
resolve_contact: async ({ name }) => {
|
|
147
|
+
const q = String(name).toLowerCase();
|
|
148
|
+
const matches = this.contacts.filter((c) => c.name.toLowerCase() === q);
|
|
149
|
+
if (matches.length === 0) throw new Error(`No contact named "${name}".`);
|
|
150
|
+
if (matches.length > 1) throw new Error(`Ambiguous: ${matches.length} contacts named "${name}" — ask the user which one.`);
|
|
151
|
+
return matches[0]!;
|
|
152
|
+
},
|
|
153
|
+
rln_create_ln_invoice: async ({ amount_sats }) => ({ invoice: `lnbcmock${amount_sats ?? ''}` }),
|
|
154
|
+
rln_create_rgb_invoice: async ({ asset, amount }) => ({ invoice: 'rgb:mockinvoice', asset, amount }),
|
|
155
|
+
send_payment: async ({ to, amount_sats }) => this.send('send_payment', { to: String(to), amount_sats: amount_sats != null ? Number(amount_sats) : undefined }),
|
|
156
|
+
rln_pay_invoice: async ({ invoice }) => this.send('rln_pay_invoice', { to: String(invoice) }),
|
|
157
|
+
rln_send_asset: async ({ asset, amount, to }) => {
|
|
158
|
+
const a = String(asset);
|
|
159
|
+
if ((this.assets[a] ?? 0) < Number(amount)) throw new Error(`Insufficient ${a} balance.`);
|
|
160
|
+
const res = this.send('rln_send_asset', { to: String(to), asset: a, amount: Number(amount) });
|
|
161
|
+
const found = this.findAsset(a);
|
|
162
|
+
if (found) this.transfers.push({ idx: this.transfers.length + 1, asset_id: found.asset_id, kind: 'Send', status: 'Settled', amount: Number(amount) });
|
|
163
|
+
return res;
|
|
164
|
+
},
|
|
165
|
+
rln_list_assets: async () => ({
|
|
166
|
+
assets: this.rgbAssets.map((a) => ({ ...a, balance: { spendable: this.assets[a.ticker] ?? 0 } })),
|
|
167
|
+
}),
|
|
168
|
+
rln_list_transfers: async ({ asset_id, asset }) => {
|
|
169
|
+
const ref = asset_id ?? asset;
|
|
170
|
+
const found = this.findAsset(ref);
|
|
171
|
+
if (!found) throw new Error(`Unknown asset "${String(ref)}".`);
|
|
172
|
+
return { transfers: this.transfers.filter((x) => x.asset_id === found.asset_id) };
|
|
173
|
+
},
|
|
174
|
+
rln_create_utxos: async ({ num }) => {
|
|
175
|
+
const n = Number(num ?? 5);
|
|
176
|
+
this.utxos += n;
|
|
177
|
+
this.sends.push({ tool: 'rln_create_utxos', to: 'self', amount: n });
|
|
178
|
+
return { created: true, num: n, free_utxos: this.utxos };
|
|
179
|
+
},
|
|
180
|
+
rln_issue_asset: async (args) => {
|
|
181
|
+
const a = this.issueAsset(args);
|
|
182
|
+
return { issued: true, asset_id: a.asset_id, ticker: a.ticker, name: a.name, schema: a.schema, issued_supply: a.issued_supply };
|
|
183
|
+
},
|
|
184
|
+
get_swap_quote: async ({ from_asset, to_asset, amount }) => ({
|
|
185
|
+
quote_id: 'quote-mock',
|
|
186
|
+
from_asset,
|
|
187
|
+
to_asset,
|
|
188
|
+
amount: Number(amount),
|
|
189
|
+
// toy rate: 1 USDT ≈ 1538 sats at $65k; otherwise echo.
|
|
190
|
+
receive_amount: String(from_asset).toUpperCase() === 'USDT' ? Math.round(Number(amount) * (1e8 / this.priceUsd)) : Number(amount),
|
|
191
|
+
}),
|
|
192
|
+
execute_swap: async ({ from_asset, to_asset, amount }) => {
|
|
193
|
+
this.sends.push({ tool: 'execute_swap', to: `${from_asset}->${to_asset}`, amount: Number(amount) });
|
|
194
|
+
return { status: 'SUCCESS', swap_id: 'swap' + this.sends.length };
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Bind the contract tools to this wallet (optionally overriding some — e.g. to inject). */
|
|
200
|
+
registry(overrides?: Partial<Record<string, WalletHandler>>): ToolRegistry {
|
|
201
|
+
const h = { ...this.handlers(), ...(overrides ?? {}) } as Record<string, WalletHandler>;
|
|
202
|
+
return new ToolRegistry([bindWalletTools(h, { layers: ['spark', 'rln', 'arkade', 'core'], allowMissing: true })]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Deterministic fake RGB contract id per ticker (stable across runs). */
|
|
207
|
+
function mockAssetId(ticker: string): string {
|
|
208
|
+
let h = 0;
|
|
209
|
+
for (const c of ticker) h = (h * 31 + c.charCodeAt(0)) >>> 0;
|
|
210
|
+
return `rgb:mock-${ticker.toLowerCase()}-${h.toString(36)}`;
|
|
211
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A scripted LLMProvider — replays planned turns instead of running a model.
|
|
3
|
+
*
|
|
4
|
+
* Lets hosts wire the full Engine / Funnel (skills, recipes, confirm gate) in
|
|
5
|
+
* unit tests, CI and demos without loading a QVAC model. Deterministic tiers
|
|
6
|
+
* (fast-path, recipes with regex extraction) never call it at all.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ToolCall } from '../types.js';
|
|
10
|
+
import type { LLMProvider } from '../providers/types.js';
|
|
11
|
+
|
|
12
|
+
export interface ScriptedTurn {
|
|
13
|
+
/** Assistant text for this turn. */
|
|
14
|
+
text?: string;
|
|
15
|
+
/** Tool to call — ignored when the tool is not in the turn's scoped tool list. */
|
|
16
|
+
tool?: string;
|
|
17
|
+
args?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function scriptedProvider(turns: ScriptedTurn[] = [], fallback = 'I need more information.'): LLMProvider {
|
|
21
|
+
let index = 0;
|
|
22
|
+
return {
|
|
23
|
+
name: 'scripted',
|
|
24
|
+
async runTurn(input) {
|
|
25
|
+
const planned = turns[index++] ?? { text: fallback };
|
|
26
|
+
const call: ToolCall | undefined = planned.tool && input.tools.some((t) => t.name === planned.tool)
|
|
27
|
+
? { id: `scripted-${index}`, name: planned.tool, arguments: planned.args ?? {} }
|
|
28
|
+
: undefined;
|
|
29
|
+
return {
|
|
30
|
+
text: planned.text ?? '',
|
|
31
|
+
rawContent: planned.text ?? '',
|
|
32
|
+
toolCalls: call ? [call] : [],
|
|
33
|
+
inference: { durationMs: 0, status: 'completed' },
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -48,6 +48,22 @@ describe('confirmReadback', () => {
|
|
|
48
48
|
expect(line).toBe('Pay Lightning invoice lnbc1p…abcd over Spark. Confirm?');
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
+
it('rln_send_asset: kaleido-mcp argument names', () => {
|
|
52
|
+
expect(confirmReadback({ name: 'rln_send_asset', arguments: { asset_id: 'rgb:abcdefgh-1234567890-xyz', amount: 5, recipient_id: 'utxob:ab12cd34ef56gh78ij90' } }))
|
|
53
|
+
.toBe('Send 5 rgb:ab…-xyz to utxob:…ij90 over RLN. Confirm?');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rln_send_btc / channel / atomic taker readbacks', () => {
|
|
57
|
+
expect(confirmReadback({ name: 'rln_send_btc', arguments: { address: 'tb1qexampleaddress0000000000xyz', amount_sat: 25000 } }))
|
|
58
|
+
.toBe('Send 25,000 sats on-chain to tb1qex…0xyz over RLN. Confirm?');
|
|
59
|
+
expect(confirmReadback({ name: 'rln_open_channel', arguments: { peer_pubkey_and_addr: '03abcdef0123456789abcdef0123456789@1.2.3.4:9735', capacity_sat: 100000 } }))
|
|
60
|
+
.toBe('Open a 100,000 sats channel to 03abcd…6789. Confirm?');
|
|
61
|
+
expect(confirmReadback({ name: 'rln_close_channel', arguments: { channel_id: 'chan0123456789abcdef0123', peer_pubkey: 'x', force: true } }))
|
|
62
|
+
.toBe('Force-close channel chan01…0123. Confirm?');
|
|
63
|
+
expect(confirmReadback({ name: 'rln_atomic_taker', arguments: { swapstring: '30/rgb:asset/10/btc/3600/abcdef0123456789' } }))
|
|
64
|
+
.toBe('Accept atomic swap 30/rgb…6789 on your node. Confirm?');
|
|
65
|
+
});
|
|
66
|
+
|
|
51
67
|
it('execute_swap: from → to with amount', () => {
|
|
52
68
|
expect(confirmReadback({ name: 'execute_swap', arguments: { from_asset: 'BTC', to_asset: 'USDT', amount: 0.01 } }))
|
|
53
69
|
.toBe('Swap 0.01 BTC for USDT. Confirm?');
|
package/src/wallet/confirm.ts
CHANGED
|
@@ -62,10 +62,30 @@ export function confirmReadback(call: { name: string; arguments: Record<string,
|
|
|
62
62
|
return ask(`Send ${sats(a.amount_sats)} to ${to()}${over(name, a)}`);
|
|
63
63
|
case 'rln_send_asset':
|
|
64
64
|
case 'liquid_send':
|
|
65
|
-
|
|
65
|
+
// kaleido-mcp names these asset_id / recipient_id.
|
|
66
|
+
return ask(`Send ${asset(a.amount, a.asset ?? shortRef(String(a.asset_id ?? '')))} to ${to(a.to != null ? 'to' : 'recipient_id')}${over(name, a)}`);
|
|
66
67
|
case 'rln_pay_invoice':
|
|
67
68
|
case 'spark_pay_invoice':
|
|
68
69
|
return ask(`Pay Lightning invoice ${shortRef(String(a.invoice ?? ''))}${over(name, a)}`);
|
|
70
|
+
case 'rln_issue_asset': {
|
|
71
|
+
const label = a.name != null && a.ticker != null && String(a.name) !== String(a.ticker) ? ` (${String(a.name)})` : '';
|
|
72
|
+
return a.schema === 'UDA'
|
|
73
|
+
? ask(`Issue unique asset ${String(a.ticker ?? a.name)}${label} on RGB`)
|
|
74
|
+
: ask(`Issue ${asset(a.amount, a.ticker ?? a.name)}${label}, a new RGB asset`);
|
|
75
|
+
}
|
|
76
|
+
case 'rln_create_utxos':
|
|
77
|
+
return ask(`Create ${fmtNum(Number(a.num ?? 5))} RGB UTXOs on-chain${over(name, a)}`);
|
|
78
|
+
case 'rln_send_btc':
|
|
79
|
+
return ask(`Send ${sats(a.amount_sat)} on-chain to ${to('address')}${over(name, a)}`);
|
|
80
|
+
case 'rln_open_channel': {
|
|
81
|
+
const peer = shortRef(String(a.peer_pubkey_and_addr ?? '').split('@')[0] ?? '');
|
|
82
|
+
const withAsset = a.asset_id != null && a.asset_amount != null ? ` with ${fmtNum(Number(a.asset_amount))} of ${shortRef(String(a.asset_id))}` : '';
|
|
83
|
+
return ask(`Open a ${sats(a.capacity_sat)} channel to ${peer}${withAsset}`);
|
|
84
|
+
}
|
|
85
|
+
case 'rln_close_channel':
|
|
86
|
+
return ask(`${a.force ? 'Force-close' : 'Close'} channel ${shortRef(String(a.channel_id ?? ''))}`);
|
|
87
|
+
case 'rln_atomic_taker':
|
|
88
|
+
return ask(`Accept atomic swap ${shortRef(String(a.swapstring ?? ''))} on your node`);
|
|
69
89
|
case 'execute_swap':
|
|
70
90
|
return ask(`Swap ${fmtNum(Number(a.amount))} ${String(a.from_asset)} for ${String(a.to_asset)}`);
|
|
71
91
|
default:
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** Wallet contract tests — integrity of the single source of truth. */
|
|
2
2
|
|
|
3
3
|
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
5
|
+
import { parseSkill } from '../skills/registry.js';
|
|
4
6
|
import {
|
|
5
7
|
WALLET_TOOLS,
|
|
6
8
|
SPEND_TOOLS,
|
|
@@ -40,9 +42,13 @@ describe('WALLET_TOOLS contract', () => {
|
|
|
40
42
|
expect(isSpendTool('execute_swap')).toBe(true);
|
|
41
43
|
expect(isSpendTool('spark_send')).toBe(true);
|
|
42
44
|
expect(isSpendTool('spark_pay_invoice')).toBe(true);
|
|
45
|
+
expect(isSpendTool('rln_issue_asset')).toBe(true);
|
|
46
|
+
expect(isSpendTool('rln_create_utxos')).toBe(true);
|
|
43
47
|
// reads are not
|
|
44
48
|
expect(isSpendTool('get_balances')).toBe(false);
|
|
45
49
|
expect(isSpendTool('get_price')).toBe(false);
|
|
50
|
+
expect(isSpendTool('rln_list_assets')).toBe(false);
|
|
51
|
+
expect(isSpendTool('rln_list_transfers')).toBe(false);
|
|
46
52
|
expect([...SPEND_TOOLS].length).toBeGreaterThanOrEqual(5);
|
|
47
53
|
});
|
|
48
54
|
|
|
@@ -97,3 +103,35 @@ describe('bindWalletTools', () => {
|
|
|
97
103
|
expect(src.listTools().map((t) => t.name)).toEqual(['spark_get_balance']);
|
|
98
104
|
});
|
|
99
105
|
});
|
|
106
|
+
|
|
107
|
+
describe('RLN tools vs kaleido-mcp and the rgb-lightning-node skill', () => {
|
|
108
|
+
// The rln_* tools kaleido-mcp registers (rln_mpp_pay belongs to paid-data).
|
|
109
|
+
const MCP_RLN = [
|
|
110
|
+
'rln_get_node_info', 'rln_get_balances', 'rln_get_asset_balance', 'rln_list_assets',
|
|
111
|
+
'rln_get_address', 'rln_create_rgb_invoice', 'rln_create_ln_invoice', 'rln_pay_invoice',
|
|
112
|
+
'rln_send_btc', 'rln_send_asset', 'rln_list_channels', 'rln_connect_peer', 'rln_open_channel',
|
|
113
|
+
'rln_close_channel', 'rln_get_channel_id', 'rln_list_payments', 'rln_refresh_transfers',
|
|
114
|
+
'rln_atomic_taker', 'rln_list_swaps', 'rln_get_swap',
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
it('covers every rln_* tool kaleido-mcp exposes', () => {
|
|
118
|
+
const missing = MCP_RLN.filter((n) => !getWalletTool(n));
|
|
119
|
+
expect(missing).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('gates every fund-moving or committing RLN tool', () => {
|
|
123
|
+
for (const n of ['rln_send_btc', 'rln_open_channel', 'rln_close_channel', 'rln_atomic_taker']) {
|
|
124
|
+
expect(isSpendTool(n)).toBe(true);
|
|
125
|
+
}
|
|
126
|
+
for (const n of ['rln_get_asset_balance', 'rln_refresh_transfers', 'rln_list_swaps', 'rln_get_swap']) {
|
|
127
|
+
expect(isSpendTool(n)).toBe(false);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('the skill only declares tools that exist in the contract', () => {
|
|
132
|
+
const md = readFileSync(new URL('../../skills/rgb-lightning-node/SKILL.md', import.meta.url), 'utf8');
|
|
133
|
+
const skill = parseSkill(md);
|
|
134
|
+
const unknown = (skill.tools ?? []).filter((n) => !getWalletTool(n));
|
|
135
|
+
expect(unknown).toEqual([]);
|
|
136
|
+
});
|
|
137
|
+
});
|
package/src/wallet/contract.ts
CHANGED
|
@@ -91,6 +91,42 @@ export const WALLET_TOOLS: WalletToolDef[] = [
|
|
|
91
91
|
t('rln', 'rln_create_rgb_invoice', 'Create an RGB asset invoice to receive an asset (e.g. USDT).', { asset, amount: { type: 'number', description: 'Asset amount' } }, ['asset', 'amount']),
|
|
92
92
|
t('rln', 'rln_pay_invoice', 'Pay a Lightning invoice from the RLN node.', { invoice: { type: 'string' } }, ['invoice'], true),
|
|
93
93
|
t('rln', 'rln_send_asset', 'Send an RGB asset (e.g. USDT) to a recipient.', { asset, amount: { type: 'number' }, to: { type: 'string' } }, ['asset', 'amount', 'to'], true),
|
|
94
|
+
t('rln', 'rln_list_assets', 'List RGB assets held by the RLN node (asset_id, ticker, name, precision, balances).'),
|
|
95
|
+
t('rln', 'rln_get_asset_balance', 'Get the balance of one RGB asset by asset_id (settled, future, spendable, off-chain).', { asset_id: { type: 'string', description: "RGB asset id, e.g. 'rgb:2JEUOrsc-…'" } }, ['asset_id']),
|
|
96
|
+
t('rln', 'rln_refresh_transfers', 'Refresh pending RGB transfers so balances and transfer status are up to date.'),
|
|
97
|
+
t('rln', 'rln_get_address', 'Get an on-chain BTC address of the RLN node for deposits.'),
|
|
98
|
+
t('rln', 'rln_send_btc', 'Send on-chain BTC from the RLN node.', { address: { type: 'string', description: 'Destination Bitcoin address' }, amount_sat: { type: 'number', description: 'Amount in satoshis' }, fee_rate: { type: 'number', description: 'sat/vbyte (default 3)' } }, ['address', 'amount_sat'], true),
|
|
99
|
+
t('rln', 'rln_list_payments', 'List recent Lightning payments (sent and received) on the RLN node.', { limit: { type: 'number', description: 'Max payments (default 20)' } }),
|
|
100
|
+
t('rln', 'rln_connect_peer', 'Connect the RLN node to a Lightning peer.', { peer_pubkey_and_addr: { type: 'string', description: 'pubkey@host:port' } }, ['peer_pubkey_and_addr']),
|
|
101
|
+
t('rln', 'rln_open_channel', 'Open a Lightning channel, optionally allocating an RGB asset to it.', {
|
|
102
|
+
peer_pubkey_and_addr: { type: 'string', description: 'pubkey@host:port' },
|
|
103
|
+
capacity_sat: { type: 'number', description: 'Channel capacity in satoshis' },
|
|
104
|
+
push_msat: { type: 'number', description: 'Millisatoshis pushed to the peer (default 0)' },
|
|
105
|
+
asset_id: { type: 'string', description: 'RGB asset id to allocate' },
|
|
106
|
+
asset_amount: { type: 'number', description: 'RGB asset amount to allocate' },
|
|
107
|
+
is_public: { type: 'boolean', description: 'Announce the channel (default false)' },
|
|
108
|
+
}, ['peer_pubkey_and_addr', 'capacity_sat'], true),
|
|
109
|
+
t('rln', 'rln_close_channel', 'Close a Lightning channel (force only for an unresponsive peer).', { channel_id: { type: 'string' }, peer_pubkey: { type: 'string' }, force: { type: 'boolean', description: 'Unilateral close (default false)' } }, ['channel_id', 'peer_pubkey'], true),
|
|
110
|
+
t('rln', 'rln_get_channel_id', 'Resolve a temporary_channel_id from rln_open_channel to the final channel_id.', { temporary_channel_id: { type: 'string' } }, ['temporary_channel_id']),
|
|
111
|
+
t('rln', 'rln_atomic_taker', 'Whitelist a maker swap on the node (taker side). Pass the swapstring from kaleidoswap_atomic_init, before kaleidoswap_atomic_execute.', { swapstring: { type: 'string' } }, ['swapstring'], true),
|
|
112
|
+
t('rln', 'rln_list_swaps', 'List atomic swaps on the RLN node (maker and taker sides).'),
|
|
113
|
+
t('rln', 'rln_get_swap', 'Get an atomic swap by payment_hash.', { payment_hash: { type: 'string' }, taker: { type: 'boolean', description: 'Taker-side swap' } }, ['payment_hash']),
|
|
114
|
+
t('rln', 'rln_list_transfers', 'List RGB transfers for one asset (issuance, sends, receives) with their status — use to check if an RGB invoice was paid.', { asset_id: { type: 'string', description: 'RGB asset id (in-app wallets also accept a ticker)' } }, ['asset_id']),
|
|
115
|
+
// Issuance + UTXO prep spend on-chain BTC and are irreversible → gated like any spend.
|
|
116
|
+
t('rln', 'rln_create_utxos', 'Create colorable UTXOs on the RLN node — needed before issuing or receiving RGB assets on a fresh node.', {
|
|
117
|
+
num: { type: 'number', description: 'How many UTXOs (default 5)' },
|
|
118
|
+
size: { type: 'number', description: 'Size of each UTXO in sats (default: node decides)' },
|
|
119
|
+
up_to: { type: 'boolean', description: 'Only top up to `num` free UTXOs (default false)' },
|
|
120
|
+
fee_rate: { type: 'number', description: 'sat/vbyte (default 1)' },
|
|
121
|
+
}, [], true),
|
|
122
|
+
t('rln', 'rln_issue_asset', 'Issue a NEW RGB asset owned by this node (e.g. an event ticket or loyalty token).', {
|
|
123
|
+
schema: { type: 'string', enum: ['NIA', 'CFA', 'UDA'], description: 'NIA = token (default), CFA = collectible, UDA = unique/NFT' },
|
|
124
|
+
name: { type: 'string', description: 'Asset name, e.g. "Hackathon Ticket"' },
|
|
125
|
+
ticker: { type: 'string', description: 'Uppercase ticker, e.g. "TICKET". Required for NIA and UDA' },
|
|
126
|
+
amount: { type: 'number', description: 'Total supply in display units. Required for NIA and CFA; UDA is always 1' },
|
|
127
|
+
precision: { type: 'number', description: 'Decimal places (default 0)' },
|
|
128
|
+
details: { type: 'string', description: 'Optional description (CFA and UDA)' },
|
|
129
|
+
}, ['name'], true),
|
|
94
130
|
|
|
95
131
|
// ── Arkade ─────────────────────────────────────────────────────────────
|
|
96
132
|
t('arkade', 'arkade_get_balance', 'Get the Arkade wallet balance.'),
|