@kaleidorg/mind 0.6.4 → 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.
Files changed (120) hide show
  1. package/README.md +257 -0
  2. package/dist/autonomy/risk.js.map +1 -1
  3. package/dist/autonomy/run-state.d.ts.map +1 -1
  4. package/dist/autonomy/run-state.js.map +1 -1
  5. package/dist/autonomy/task-store.d.ts.map +1 -1
  6. package/dist/autonomy/task-store.js.map +1 -1
  7. package/dist/bitrefill/contract.js.map +1 -1
  8. package/dist/context/budget.js.map +1 -1
  9. package/dist/context/builder.d.ts.map +1 -1
  10. package/dist/context/builder.js.map +1 -1
  11. package/dist/context/compress.js.map +1 -1
  12. package/dist/engine.d.ts.map +1 -1
  13. package/dist/engine.js.map +1 -1
  14. package/dist/evidence.d.ts +1 -1
  15. package/dist/evidence.d.ts.map +1 -1
  16. package/dist/evidence.js.map +1 -1
  17. package/dist/fastpath/fastpath.d.ts.map +1 -1
  18. package/dist/fastpath/fastpath.js.map +1 -1
  19. package/dist/flashnet/contract.js.map +1 -1
  20. package/dist/funnel.d.ts.map +1 -1
  21. package/dist/funnel.js.map +1 -1
  22. package/dist/index.d.ts +1 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +2 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/kaleidoswap/contract.js.map +1 -1
  27. package/dist/knowledge/btc-map.js.map +1 -1
  28. package/dist/logger.d.ts.map +1 -1
  29. package/dist/logger.js.map +1 -1
  30. package/dist/lsps1/contract.js.map +1 -1
  31. package/dist/memory/store.d.ts.map +1 -1
  32. package/dist/memory/store.js.map +1 -1
  33. package/dist/qvac/assistant.js.map +1 -1
  34. package/dist/qvac/config.d.ts +6 -6
  35. package/dist/qvac/config.d.ts.map +1 -1
  36. package/dist/qvac/delegate.d.ts +2 -0
  37. package/dist/qvac/delegate.d.ts.map +1 -1
  38. package/dist/qvac/delegate.js +2 -0
  39. package/dist/qvac/delegate.js.map +1 -1
  40. package/dist/qvac/index.d.ts +1 -0
  41. package/dist/qvac/index.d.ts.map +1 -1
  42. package/dist/qvac/index.js +1 -0
  43. package/dist/qvac/index.js.map +1 -1
  44. package/dist/qvac/parse.d.ts +5 -2
  45. package/dist/qvac/parse.d.ts.map +1 -1
  46. package/dist/qvac/parse.js.map +1 -1
  47. package/dist/qvac/provider.d.ts.map +1 -1
  48. package/dist/qvac/provider.js +8 -13
  49. package/dist/qvac/provider.js.map +1 -1
  50. package/dist/qvac/stream.d.ts.map +1 -1
  51. package/dist/qvac/stream.js +29 -1
  52. package/dist/qvac/stream.js.map +1 -1
  53. package/dist/qvac/tools.d.ts +32 -0
  54. package/dist/qvac/tools.d.ts.map +1 -0
  55. package/dist/qvac/tools.js +84 -0
  56. package/dist/qvac/tools.js.map +1 -0
  57. package/dist/qvac/voice.js.map +1 -1
  58. package/dist/rag/retriever.d.ts.map +1 -1
  59. package/dist/rag/tool.js.map +1 -1
  60. package/dist/rag/vector-store.d.ts.map +1 -1
  61. package/dist/rag/vector-store.js.map +1 -1
  62. package/dist/recipe/issue-asset.d.ts +16 -0
  63. package/dist/recipe/issue-asset.d.ts.map +1 -0
  64. package/dist/recipe/issue-asset.js +94 -0
  65. package/dist/recipe/issue-asset.js.map +1 -0
  66. package/dist/recipe/runner.d.ts.map +1 -1
  67. package/dist/recipe/runner.js.map +1 -1
  68. package/dist/skills/loader.js.map +1 -1
  69. package/dist/skills/registry.d.ts.map +1 -1
  70. package/dist/skills/registry.js.map +1 -1
  71. package/dist/testing/index.d.ts +14 -0
  72. package/dist/testing/index.d.ts.map +1 -0
  73. package/dist/testing/index.js +12 -0
  74. package/dist/testing/index.js.map +1 -0
  75. package/dist/testing/mock-wallet.d.ts +79 -0
  76. package/dist/testing/mock-wallet.d.ts.map +1 -0
  77. package/dist/testing/mock-wallet.js +180 -0
  78. package/dist/testing/mock-wallet.js.map +1 -0
  79. package/dist/testing/scripted-provider.d.ts +17 -0
  80. package/dist/testing/scripted-provider.d.ts.map +1 -0
  81. package/dist/testing/scripted-provider.js +26 -0
  82. package/dist/testing/scripted-provider.js.map +1 -0
  83. package/dist/tools/in-process.d.ts.map +1 -1
  84. package/dist/tools/mcp.d.ts.map +1 -1
  85. package/dist/tools/registry.d.ts.map +1 -1
  86. package/dist/tools/registry.js.map +1 -1
  87. package/dist/wallet/confirm.d.ts.map +1 -1
  88. package/dist/wallet/confirm.js +21 -1
  89. package/dist/wallet/confirm.js.map +1 -1
  90. package/dist/wallet/contract.d.ts.map +1 -1
  91. package/dist/wallet/contract.js +36 -0
  92. package/dist/wallet/contract.js.map +1 -1
  93. package/package.json +10 -5
  94. package/skills/README.md +1 -1
  95. package/skills/kaleido-node/SKILL.md +63 -0
  96. package/skills/kaleido-trading/SKILL.md +15 -2
  97. package/skills/kaleido-trading/references/api.md +61 -0
  98. package/skills/kaleido-trading/references/assets.md +57 -0
  99. package/skills/kaleido-trading/references/atomic.md +93 -0
  100. package/skills/paid-data/SKILL.md +59 -6
  101. package/skills/rgb-lightning-node/SKILL.md +116 -11
  102. package/src/index.ts +3 -0
  103. package/src/qvac/delegate.ts +2 -0
  104. package/src/qvac/index.ts +2 -0
  105. package/src/qvac/parse.ts +5 -2
  106. package/src/qvac/provider.test.ts +38 -0
  107. package/src/qvac/provider.ts +10 -14
  108. package/src/qvac/stream.test.ts +28 -0
  109. package/src/qvac/stream.ts +30 -1
  110. package/src/qvac/tools.test.ts +86 -0
  111. package/src/qvac/tools.ts +106 -0
  112. package/src/recipe/issue-asset.test.ts +92 -0
  113. package/src/recipe/issue-asset.ts +95 -0
  114. package/src/testing/index.ts +14 -0
  115. package/src/testing/mock-wallet.ts +211 -0
  116. package/src/testing/scripted-provider.ts +37 -0
  117. package/src/wallet/confirm.test.ts +16 -0
  118. package/src/wallet/confirm.ts +21 -1
  119. package/src/wallet/contract.test.ts +38 -0
  120. package/src/wallet/contract.ts +36 -0
@@ -55,6 +55,28 @@ export interface ConsumedTurn extends ParsedTurn {
55
55
  };
56
56
  }
57
57
 
58
+ /**
59
+ * Since QVAC 0.11 a cancelled run ends `events` normally but REJECTS `final`
60
+ * with `InferenceCancelledError { requestId, partial }`. Fold that back into a
61
+ * `final` frame with stopReason 'cancelled' so a stop or a blown thinking budget
62
+ * is a result, not an exception. Matched structurally to stay SDK-free.
63
+ */
64
+ function cancelledFinal(err: unknown): QvacFinalLike | null {
65
+ const e = err as {
66
+ requestId?: unknown;
67
+ partial?: { text?: string; toolCalls?: QvacFinalLike['toolCalls']; stats?: QvacFinalLike['stats'] };
68
+ } | null;
69
+ const named = (err as { constructor?: { name?: string } } | null)?.constructor?.name === 'InferenceCancelledError';
70
+ if (!e || !(named || (typeof e.requestId === 'string' && typeof e.partial === 'object'))) return null;
71
+ const partial = e.partial ?? {};
72
+ return {
73
+ contentText: partial.text ?? '',
74
+ toolCalls: partial.toolCalls ?? [],
75
+ ...(partial.stats ? { stats: partial.stats } : {}),
76
+ stopReason: 'cancelled',
77
+ };
78
+ }
79
+
58
80
  /** Rough token estimate (~4 chars/token) — same heuristic the context budget uses. */
59
81
  function approxTokens(chars: number): number {
60
82
  return Math.ceil(chars / 4);
@@ -95,7 +117,14 @@ export async function consumeRun(
95
117
  }
96
118
  }
97
119
  }
98
- const final = await run.final;
120
+ let final: QvacFinalLike;
121
+ try {
122
+ final = await run.final;
123
+ } catch (err) {
124
+ const recovered = cancelledFinal(err);
125
+ if (!recovered) throw err;
126
+ final = recovered;
127
+ }
99
128
  const finishedAt = now();
100
129
  return {
101
130
  ...finalToTurn(final, streamed),
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { toQvacTools } from './tools.js';
3
+ import { WALLET_TOOLS } from '../wallet/contract.js';
4
+
5
+ describe('toQvacTools', () => {
6
+ it('returns undefined for no tools', () => {
7
+ expect(toQvacTools([])).toBeUndefined();
8
+ });
9
+
10
+ it('keeps JSON-Schema parameters as a full function tool', () => {
11
+ const [tool] = toQvacTools([
12
+ {
13
+ name: 'rln_send_btc',
14
+ description: 'send',
15
+ parameters: {
16
+ type: 'object',
17
+ properties: {
18
+ address: { type: 'string', description: 'Destination' },
19
+ amount_sat: { type: 'integer' },
20
+ note: { type: ['string', 'null'] },
21
+ mode: { anyOf: [{ type: 'null' }, { type: 'boolean' }] },
22
+ tags: { type: 'array', items: { type: 'string' } },
23
+ schema: { type: 'string', enum: ['NIA', 'CFA'] },
24
+ },
25
+ required: ['address', 'amount_sat', 'ghost'],
26
+ additionalProperties: false,
27
+ },
28
+ },
29
+ ])!;
30
+ expect(tool).toEqual({
31
+ type: 'function',
32
+ name: 'rln_send_btc',
33
+ description: 'send',
34
+ parameters: {
35
+ type: 'object',
36
+ properties: {
37
+ address: { type: 'string', description: 'Destination' },
38
+ amount_sat: { type: 'integer' },
39
+ note: { type: 'string' },
40
+ mode: { type: 'boolean' },
41
+ tags: { type: 'array' },
42
+ schema: { type: 'string', enum: ['NIA', 'CFA'] },
43
+ },
44
+ required: ['address', 'amount_sat'],
45
+ },
46
+ });
47
+ });
48
+
49
+ it('passes an all-Zod tool list through for the SDK to convert', () => {
50
+ const zodish = { shape: {} };
51
+ expect(toQvacTools([{ name: 'a', description: 'd', parameters: zodish }])).toEqual([
52
+ { name: 'a', description: 'd', parameters: zodish },
53
+ ]);
54
+ });
55
+
56
+ it('converts Zod-like tools when mixed with JSON-Schema tools', () => {
57
+ const zodish = {
58
+ shape: {
59
+ city: { type: 'string', description: 'City' },
60
+ unit: { type: 'optional', def: { innerType: { type: 'enum', def: { entries: { c: 'c', f: 'f' } } } } },
61
+ },
62
+ };
63
+ const out = toQvacTools([
64
+ { name: 'weather', description: 'w', parameters: zodish },
65
+ { name: 'ping', description: 'p', parameters: { type: 'object', properties: {} } },
66
+ ])!;
67
+ expect(out[0]).toEqual({
68
+ type: 'function',
69
+ name: 'weather',
70
+ description: 'w',
71
+ parameters: {
72
+ type: 'object',
73
+ properties: { city: { type: 'string', description: 'City' }, unit: { type: 'string', enum: ['c', 'f'] } },
74
+ required: ['city'],
75
+ },
76
+ });
77
+ });
78
+
79
+ it('every wallet contract tool keeps its arguments', () => {
80
+ const out = toQvacTools(WALLET_TOOLS) as Array<{ name: string; parameters: { properties: object } }>;
81
+ for (const def of WALLET_TOOLS) {
82
+ const mapped = out.find((t) => t.name === def.name)!;
83
+ expect(Object.keys(mapped.parameters.properties)).toEqual(Object.keys((def.parameters as any).properties));
84
+ }
85
+ });
86
+ });
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Map engine ToolDefs to the tool shape `completion({ tools })` accepts.
3
+ *
4
+ * The SDK picks a mode from the FIRST tool: a full `Tool`
5
+ * (`{ type: 'function', name, description, parameters: <flat JSON Schema> }`)
6
+ * or a Zod `ToolInput`. A plain JSON-Schema def without `type: 'function'`
7
+ * falls into the Zod branch, which reads `parameters.shape` and silently drops
8
+ * every argument — the model then sees tool names with no parameters. So
9
+ * JSON-Schema tools (wallet contract, MCP) are normalised to full `Tool`s here.
10
+ * An all-Zod tool list is passed through for the SDK to convert, as before.
11
+ */
12
+ import type { ToolDef } from '../types.js';
13
+
14
+ type PropType = 'string' | 'number' | 'boolean' | 'object' | 'array' | 'integer';
15
+ const PROP_TYPES = new Set<PropType>(['string', 'number', 'boolean', 'object', 'array', 'integer']);
16
+
17
+ export interface QvacToolProp {
18
+ type: PropType;
19
+ description?: string;
20
+ enum?: Array<string | number | boolean | null>;
21
+ }
22
+
23
+ export interface QvacTool {
24
+ type: 'function';
25
+ name: string;
26
+ description: string;
27
+ parameters: { type: 'object'; properties: Record<string, QvacToolProp>; required?: string[] };
28
+ }
29
+
30
+ type Obj = Record<string, any>;
31
+
32
+ function isZodLike(p: unknown): boolean {
33
+ const o = p as Obj | null;
34
+ return !!o && typeof o === 'object' && ('shape' in o || '_def' in o || 'def' in o) && !('properties' in o);
35
+ }
36
+
37
+ function pickType(s: Obj): PropType {
38
+ const t = Array.isArray(s.type) ? s.type.find((x: unknown) => x !== 'null') : s.type;
39
+ if (PROP_TYPES.has(t)) return t;
40
+ for (const alt of [...(s.anyOf ?? []), ...(s.oneOf ?? [])]) {
41
+ if (alt && typeof alt === 'object' && alt.type !== 'null') return pickType(alt);
42
+ }
43
+ if (Array.isArray(s.enum) && s.enum.length) {
44
+ const first = typeof s.enum[0];
45
+ if (first === 'number' || first === 'boolean') return first;
46
+ }
47
+ return 'string';
48
+ }
49
+
50
+ function jsonProp(s: Obj): QvacToolProp {
51
+ const prop: QvacToolProp = { type: pickType(s ?? {}) };
52
+ if (typeof s?.description === 'string') prop.description = s.description;
53
+ if (Array.isArray(s?.enum)) {
54
+ const values = s.enum.filter((v: unknown) => v === null || ['string', 'number', 'boolean'].includes(typeof v));
55
+ if (values.length) prop.enum = values;
56
+ }
57
+ return prop;
58
+ }
59
+
60
+ function zodProp(field: Obj): { prop: QvacToolProp; optional: boolean } {
61
+ const kind = field?.type ?? field?.def?.type ?? 'string';
62
+ const optional = kind === 'optional';
63
+ const inner = optional ? field.def?.innerType ?? {} : field;
64
+ const innerKind = inner.type ?? inner.def?.type ?? 'string';
65
+ const prop: QvacToolProp = { type: PROP_TYPES.has(innerKind) ? innerKind : 'string' };
66
+ const description = field.description ?? inner.description;
67
+ if (description) prop.description = description;
68
+ const values = inner.def?.values ?? inner.def?.entries;
69
+ if (Array.isArray(values)) prop.enum = values;
70
+ else if (values && typeof values === 'object') prop.enum = Object.values(values);
71
+ return { prop, optional };
72
+ }
73
+
74
+ function toQvacTool(t: Pick<ToolDef, 'name' | 'description' | 'parameters'>): QvacTool {
75
+ const p = (t.parameters ?? {}) as Obj;
76
+ const properties: Record<string, QvacToolProp> = {};
77
+ const required: string[] = [];
78
+ if (isZodLike(p)) {
79
+ const shape = p.shape ?? p.def?.shape ?? {};
80
+ for (const [key, field] of Object.entries(shape as Obj)) {
81
+ const { prop, optional } = zodProp(field as Obj);
82
+ properties[key] = prop;
83
+ if (!optional) required.push(key);
84
+ }
85
+ } else {
86
+ for (const [key, s] of Object.entries((p.properties ?? {}) as Obj)) properties[key] = jsonProp(s as Obj);
87
+ for (const key of Array.isArray(p.required) ? p.required : []) if (key in properties) required.push(key);
88
+ }
89
+ return {
90
+ type: 'function',
91
+ name: t.name,
92
+ description: t.description ?? '',
93
+ parameters: { type: 'object', properties, ...(required.length ? { required } : {}) },
94
+ };
95
+ }
96
+
97
+ /** Tools for `completion()`; undefined when there are none. */
98
+ export function toQvacTools(
99
+ tools: ReadonlyArray<Pick<ToolDef, 'name' | 'description' | 'parameters'>>,
100
+ ): QvacTool[] | Array<Pick<ToolDef, 'name' | 'description' | 'parameters'>> | undefined {
101
+ if (!tools.length) return undefined;
102
+ if (tools.every((t) => isZodLike(t.parameters))) {
103
+ return tools.map((t) => ({ name: t.name, description: t.description, parameters: t.parameters }));
104
+ }
105
+ return tools.map(toQvacTool);
106
+ }
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { extractIssueAsset, issueAssetRecipe } from './issue-asset.js';
3
+ import { runRecipe } from './runner.js';
4
+ import { Funnel } from '../funnel.js';
5
+ import { confirmReadback } from '../wallet/confirm.js';
6
+ import { MockWallet, scriptedProvider } from '../testing/index.js';
7
+
8
+ describe('extractIssueAsset (deterministic Tier-0)', () => {
9
+ it('ticker + supply + quoted-less name', () => {
10
+ expect(extractIssueAsset('issue 1000 TICKET tokens called Hackathon Ticket')).toEqual({
11
+ name: 'Hackathon Ticket', ticker: 'TICKET', amount: 1000, schema: 'NIA',
12
+ });
13
+ });
14
+ it('explicit ticker keyword and k shorthand', () => {
15
+ expect(extractIssueAsset('mint 21k tokens named "Pizza Points" with ticker pizza')).toEqual({
16
+ name: 'Pizza Points', ticker: 'PIZZA', amount: 21_000, schema: 'NIA',
17
+ });
18
+ });
19
+ it('NFT → UDA with supply 1, ticker derived from the name', () => {
20
+ expect(extractIssueAsset('mint an NFT called Genesis Badge')).toEqual({
21
+ name: 'Genesis Badge', ticker: 'GENESISB', amount: 1, schema: 'UDA',
22
+ });
23
+ });
24
+ it('Italian phrasing', () => {
25
+ expect(extractIssueAsset('crea 500 token CAFE chiamati Caffè Club')).toEqual({
26
+ name: 'Caffè Club', ticker: 'CAFE', amount: 500, schema: 'NIA',
27
+ });
28
+ });
29
+ it('ignores invoices, channels, sends and swaps', () => {
30
+ expect(extractIssueAsset('create an invoice for 10 USDT tokens')).toBeNull();
31
+ expect(extractIssueAsset('create a channel with 100 USDT')).toBeNull();
32
+ expect(extractIssueAsset('send 10 tokens to bob')).toBeNull();
33
+ expect(extractIssueAsset('what is my balance')).toBeNull();
34
+ expect(issueAssetRecipe.match!('buy 100 USDT tokens')).toBe(false);
35
+ });
36
+ });
37
+
38
+ describe('issue-asset readback', () => {
39
+ it('fungible: supply + ticker + name', () => {
40
+ expect(confirmReadback({ name: 'rln_issue_asset', arguments: { name: 'Hackathon Ticket', ticker: 'TICKET', amount: 1000 } }))
41
+ .toBe('Issue 1,000 TICKET (Hackathon Ticket), a new RGB asset. Confirm?');
42
+ });
43
+ it('UDA: unique asset wording', () => {
44
+ expect(confirmReadback({ name: 'rln_issue_asset', arguments: { name: 'Genesis Badge', ticker: 'GENESISB', amount: 1, schema: 'UDA' } }))
45
+ .toBe('Issue unique asset GENESISB (Genesis Badge) on RGB. Confirm?');
46
+ });
47
+ });
48
+
49
+ describe('runRecipe — issue asset against MockWallet', () => {
50
+ it('one confirmation, asset lands in the wallet', async () => {
51
+ const wallet = new MockWallet();
52
+ const onConfirm = vi.fn(async () => ({ approved: true }));
53
+ const res = await runRecipe(issueAssetRecipe, 'issue 1000 TICKET tokens called Hackathon Ticket', {
54
+ provider: scriptedProvider(), tools: wallet.registry(), onConfirm,
55
+ });
56
+ expect(res.status).toBe('done');
57
+ expect(res.inferences).toBe(0);
58
+ expect(onConfirm).toHaveBeenCalledOnce();
59
+ expect(wallet.assets.TICKET).toBe(1000);
60
+ expect(res.text).toMatch(/^Issued 1000 TICKET\. Asset id: rgb:mock-ticket-/);
61
+ });
62
+
63
+ it('declined confirmation issues nothing', async () => {
64
+ const wallet = new MockWallet();
65
+ const res = await runRecipe(issueAssetRecipe, 'issue 1000 TICKET tokens', {
66
+ provider: scriptedProvider(), tools: wallet.registry(), onConfirm: async () => ({ approved: false }),
67
+ });
68
+ expect(res.status).toBe('cancelled');
69
+ expect(wallet.assets.TICKET).toBeUndefined();
70
+ });
71
+
72
+ it('surfaces the node-style "no UTXOs" failure', async () => {
73
+ const wallet = new MockWallet({ utxos: 0 });
74
+ const res = await runRecipe(issueAssetRecipe, 'issue 1000 TICKET tokens', {
75
+ provider: scriptedProvider(), tools: wallet.registry(), onConfirm: async () => ({ approved: true }),
76
+ });
77
+ expect(res.status).toBe('error');
78
+ expect(res.error ?? res.text).toMatch(/rln_create_utxos/);
79
+ });
80
+ });
81
+
82
+ describe('Funnel — issue-asset as an opt-in recipe', () => {
83
+ it('routes to the recipe tier and is listable afterwards', async () => {
84
+ const wallet = new MockWallet();
85
+ const funnel = new Funnel({ provider: scriptedProvider(), tools: wallet.registry(), recipes: [issueAssetRecipe] });
86
+ const out = await funnel.runTurn('mint an NFT called Genesis Badge', { onConfirm: async () => ({ approved: true }) });
87
+ expect(out.tier).toBe('recipe');
88
+ expect(out.route).toBe('issue-asset');
89
+ const listed = (await wallet.registry().execute('rln_list_assets', {})) as { assets: Array<{ ticker: string; schema: string }> };
90
+ expect(listed.assets.find((a) => a.ticker === 'GENESISB')?.schema).toBe('UDA');
91
+ });
92
+ });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * "Issue an RGB asset" recipe — the hackathon one-liner:
3
+ *
4
+ * "issue 1000 TICKET tokens called Hackathon Ticket" → rln_issue_asset 🔒
5
+ * "mint an NFT called Genesis Badge" → rln_issue_asset 🔒 (UDA)
6
+ * "crea 500 token CAFE chiamati Caffè Club" → rln_issue_asset 🔒
7
+ *
8
+ * No intermediate steps: the plan is a single gated call, so the recipe's job
9
+ * is the slot extraction (name / ticker / supply / schema) and a deterministic
10
+ * readback — exactly the part a 0.6B model gets wrong. Opt-in: register via
11
+ * `Funnel.recipes` on hosts that bind `rln_issue_asset`.
12
+ */
13
+
14
+ import type { Recipe, RecipeContext } from './types.js';
15
+
16
+ const VERB = /\b(issue|mint|create|launch|emetti|conia|crea)\b/i;
17
+ const NOUN = /\b(tokens?|assets?|coins?|nfts?|tickets?|gettoni|badges?)\b/i;
18
+ const NOT_ISSUE = /\b(invoice|fattura|channel|canale|swap|buy|sell|send|pay|compra|vendi|invia|paga)\b/i;
19
+ const NFT = /\b(nfts?|unique|unico|unica)\b/i;
20
+ // Words that look like tickers but are part of the request, not the asset.
21
+ const STOPWORDS = new Set(['RGB', 'NFT', 'NFTS', 'NIA', 'CFA', 'UDA', 'BTC', 'LN', 'AN', 'A']);
22
+
23
+ function parseAmount(t: string): number | undefined {
24
+ const m = t.match(/(?:^|\s)(\d[\d.,]*)\s*([km])?(?=\s|$)/i);
25
+ if (!m) return undefined;
26
+ let n = Number(m[1]!.replace(/,/g, ''));
27
+ if (m[2]) n *= m[2].toLowerCase() === 'k' ? 1_000 : 1_000_000;
28
+ return Number.isFinite(n) && n > 0 ? n : undefined;
29
+ }
30
+
31
+ function parseName(t: string): string | undefined {
32
+ const quoted = t.match(/["“'‘]([^"”'’]{1,40})["”'’]/)?.[1];
33
+ if (quoted) return quoted.trim();
34
+ const m = t.match(/\b(?:called|named|chiamat[oaie])\s+(.+?)(?=\s+(?:with|ticker|con)\b|[,.;]|$)/i);
35
+ return m?.[1]?.trim() || undefined;
36
+ }
37
+
38
+ function parseTicker(t: string, name?: string): string | undefined {
39
+ const explicit = t.match(/\bticker\s+([A-Za-z0-9]{1,8})\b/i)?.[1];
40
+ if (explicit) return explicit.toUpperCase();
41
+ // An all-caps word that isn't part of the name ("1000 TICKET tokens").
42
+ const nameWords = new Set((name ?? '').split(/\s+/));
43
+ for (const w of t.match(/\b[A-Z][A-Z0-9]{1,7}\b/g) ?? []) {
44
+ if (!STOPWORDS.has(w) && !nameWords.has(w)) return w;
45
+ }
46
+ return undefined;
47
+ }
48
+
49
+ /** Ticker fallback from the name: "Hackathon Ticket" → "HACKATHO". */
50
+ function tickerFromName(name: string): string {
51
+ return name.replace(/[^A-Za-z0-9]/g, '').toUpperCase().slice(0, 8);
52
+ }
53
+
54
+ export function extractIssueAsset(text: string): Record<string, unknown> | null {
55
+ const t = text.trim();
56
+ if (!VERB.test(t) || !NOUN.test(t) || NOT_ISSUE.test(t)) return null;
57
+ const schema = NFT.test(t) ? 'UDA' : 'NIA';
58
+ const name = parseName(t);
59
+ const ticker = parseTicker(t, name) ?? (name ? tickerFromName(name) : undefined);
60
+ const amount = schema === 'UDA' ? 1 : parseAmount(t);
61
+ if (!ticker && !name) return null;
62
+ return { name: name ?? ticker, ticker, amount, schema };
63
+ }
64
+
65
+ export const issueAssetRecipe: Recipe = {
66
+ name: 'issue-asset',
67
+ description: 'Issue a new RGB asset (token, ticket, NFT) on the RLN node — one confirmation-gated call.',
68
+ match: (t) => VERB.test(t) && NOUN.test(t) && !NOT_ISSUE.test(t),
69
+ triggers: ['issue', 'mint', 'create token', 'launch token', 'nft'],
70
+ slots: [
71
+ { name: 'name', type: 'string', description: 'Asset name, e.g. "Hackathon Ticket"', required: true },
72
+ { name: 'ticker', type: 'string', description: 'Uppercase ticker, up to 8 chars, e.g. "TICKET"' },
73
+ { name: 'amount', type: 'number', description: 'Total supply in whole units (1 for an NFT)', required: true },
74
+ { name: 'schema', type: 'string', description: 'NIA for a fungible token, UDA for a unique NFT' },
75
+ ],
76
+ extract: extractIssueAsset,
77
+ confident: (s) => !!(s.ticker || s.name) && Number(s.amount) > 0,
78
+ steps: [],
79
+ final: {
80
+ tool: 'rln_issue_asset',
81
+ args: (ctx: RecipeContext) => {
82
+ const name = String(ctx.slots.name ?? ctx.slots.ticker);
83
+ const ticker = String(ctx.slots.ticker ?? tickerFromName(name)).toUpperCase();
84
+ const schema = ctx.slots.schema === 'UDA' ? 'UDA' : 'NIA';
85
+ return { name, ticker, amount: schema === 'UDA' ? 1 : Number(ctx.slots.amount), schema };
86
+ },
87
+ },
88
+ summary: (ctx, final) => {
89
+ // wdk/kaleido MCP return `asset_id` flat; the raw node API nests it under `asset`.
90
+ const r = final as { asset_id?: string; asset?: { asset_id?: string } } | undefined;
91
+ const id = r?.asset_id ?? r?.asset?.asset_id;
92
+ const what = ctx.slots.schema === 'UDA' ? `unique asset ${ctx.slots.ticker ?? ctx.slots.name}` : `${ctx.slots.amount} ${ctx.slots.ticker ?? ctx.slots.name}`;
93
+ return id ? `Issued ${what}. Asset id: ${id}` : `Issued ${what}.`;
94
+ },
95
+ };
@@ -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
+ }