@kaleidorg/mind 0.6.4 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +345 -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.d.ts.map +1 -1
- package/dist/context/compress.js +1 -0
- package/dist/context/compress.js.map +1 -1
- package/dist/context/rgb-units.d.ts +18 -0
- package/dist/context/rgb-units.d.ts.map +1 -0
- package/dist/context/rgb-units.js +86 -0
- package/dist/context/rgb-units.js.map +1 -0
- package/dist/engine.d.ts +53 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +179 -17
- package/dist/engine.js.map +1 -1
- package/dist/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.map +1 -1
- package/dist/funnel.js +23 -4
- package/dist/funnel.js.map +1 -1
- package/dist/guards.d.ts +63 -0
- package/dist/guards.d.ts.map +1 -0
- package/dist/guards.js +284 -0
- package/dist/guards.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.d.ts +3 -4
- package/dist/kaleidoswap/contract.d.ts.map +1 -1
- package/dist/kaleidoswap/contract.js +3 -17
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/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/providers/openai.d.ts +64 -0
- package/dist/providers/openai.d.ts.map +1 -0
- package/dist/providers/openai.js +233 -0
- package/dist/providers/openai.js.map +1 -0
- package/dist/providers/types.d.ts +20 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/qvac/assistant.js.map +1 -1
- package/dist/qvac/config.d.ts +7 -7
- package/dist/qvac/config.d.ts.map +1 -1
- package/dist/qvac/config.js +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 +2 -0
- package/dist/qvac/index.d.ts.map +1 -1
- package/dist/qvac/index.js +2 -0
- package/dist/qvac/index.js.map +1 -1
- package/dist/qvac/models.d.ts +31 -0
- package/dist/qvac/models.d.ts.map +1 -0
- package/dist/qvac/models.js +80 -0
- package/dist/qvac/models.js.map +1 -0
- package/dist/qvac/parse.d.ts +16 -2
- package/dist/qvac/parse.d.ts.map +1 -1
- package/dist/qvac/parse.js +29 -7
- package/dist/qvac/parse.js.map +1 -1
- package/dist/qvac/provider.d.ts +5 -4
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +30 -21
- package/dist/qvac/provider.js.map +1 -1
- package/dist/qvac/stream.d.ts +3 -5
- package/dist/qvac/stream.d.ts.map +1 -1
- package/dist/qvac/stream.js +29 -1
- package/dist/qvac/stream.js.map +1 -1
- package/dist/qvac/tools.d.ts +37 -0
- package/dist/qvac/tools.d.ts.map +1 -0
- package/dist/qvac/tools.js +94 -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 +120 -0
- package/dist/recipe/issue-asset.js.map +1 -0
- package/dist/recipe/runner.d.ts.map +1 -1
- package/dist/recipe/runner.js +1 -0
- package/dist/recipe/runner.js.map +1 -1
- package/dist/recipe/submarine-pay.d.ts +17 -0
- package/dist/recipe/submarine-pay.d.ts.map +1 -0
- package/dist/recipe/submarine-pay.js +79 -0
- package/dist/recipe/submarine-pay.js.map +1 -0
- package/dist/skills/loader.js.map +1 -1
- package/dist/skills/registry.d.ts.map +1 -1
- package/dist/skills/registry.js +1 -1
- package/dist/skills/registry.js.map +1 -1
- package/dist/skills/select.d.ts +20 -0
- package/dist/skills/select.d.ts.map +1 -0
- package/dist/skills/select.js +32 -0
- package/dist/skills/select.js.map +1 -0
- package/dist/submarine/contract.d.ts +42 -0
- package/dist/submarine/contract.d.ts.map +1 -0
- package/dist/submarine/contract.js +73 -0
- package/dist/submarine/contract.js.map +1 -0
- package/dist/testing/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 +103 -0
- package/dist/testing/mock-wallet.d.ts.map +1 -0
- package/dist/testing/mock-wallet.js +249 -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 +43 -5
- 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 +15 -5
- package/skills/README.md +2 -1
- package/skills/kaleido-node/SKILL.md +63 -0
- package/skills/kaleido-trading/SKILL.md +34 -27
- 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 +142 -17
- package/skills/spark-wallet/SKILL.md +1 -0
- package/skills/submarine-swaps/SKILL.md +48 -0
- package/src/context/compress.ts +1 -0
- package/src/context/rgb-units.test.ts +42 -0
- package/src/context/rgb-units.ts +89 -0
- package/src/engine.test.ts +94 -3
- package/src/engine.ts +234 -19
- package/src/funnel.mind.test.ts +4 -2
- package/src/funnel.ts +23 -4
- package/src/guards.test.ts +399 -0
- package/src/guards.ts +299 -0
- package/src/index.ts +40 -1
- package/src/kaleidoswap/contract.test.ts +8 -16
- package/src/kaleidoswap/contract.ts +4 -32
- package/src/providers/openai.test.ts +127 -0
- package/src/providers/openai.ts +282 -0
- package/src/providers/types.ts +22 -0
- package/src/qvac/config.ts +1 -1
- package/src/qvac/delegate.ts +2 -0
- package/src/qvac/index.ts +12 -0
- package/src/qvac/models.ts +98 -0
- package/src/qvac/parse.test.ts +7 -0
- package/src/qvac/parse.ts +38 -3
- package/src/qvac/provider.test.ts +72 -1
- package/src/qvac/provider.ts +40 -27
- package/src/qvac/stream.test.ts +28 -0
- package/src/qvac/stream.ts +33 -6
- package/src/qvac/tools.test.ts +86 -0
- package/src/qvac/tools.ts +116 -0
- package/src/recipe/issue-asset.test.ts +131 -0
- package/src/recipe/issue-asset.ts +123 -0
- package/src/recipe/runner.ts +1 -0
- package/src/recipe/submarine-pay.test.ts +87 -0
- package/src/recipe/submarine-pay.ts +78 -0
- package/src/skills/registry.ts +1 -1
- package/src/skills/select.ts +41 -0
- package/src/submarine/contract.ts +112 -0
- package/src/testing/index.ts +14 -0
- package/src/testing/mock-wallet.ts +278 -0
- package/src/testing/scripted-provider.ts +37 -0
- package/src/wallet/confirm.test.ts +16 -0
- package/src/wallet/confirm.ts +43 -5
- package/src/wallet/contract.test.ts +38 -0
- package/src/wallet/contract.ts +36 -0
package/src/qvac/provider.ts
CHANGED
|
@@ -20,6 +20,7 @@ import type * as QvacSdk from '@qvac/sdk';
|
|
|
20
20
|
import type { InferenceMetrics, LLMProvider, TurnInput, TurnOutput } from '../providers/types.js';
|
|
21
21
|
import type { QvacTurnStats } from './parse.js';
|
|
22
22
|
import { consumeRun } from './stream.js';
|
|
23
|
+
import { toQvacTools } from './tools.js';
|
|
23
24
|
|
|
24
25
|
type CompletionFn = typeof QvacSdk.completion;
|
|
25
26
|
type CancelFn = typeof QvacSdk.cancel;
|
|
@@ -40,10 +41,11 @@ export interface QvacProviderOptions {
|
|
|
40
41
|
/** Default max output tokens — caps a turn so it can't ramble. Omit for uncapped. */
|
|
41
42
|
defaultMaxTokens?: number;
|
|
42
43
|
/**
|
|
43
|
-
* Cap `<think>` reasoning at this many TOKENS (not seconds — tok/s varies
|
|
44
|
-
* the SDK
|
|
45
|
-
* the
|
|
46
|
-
* "Thinking…". Omit for
|
|
44
|
+
* Cap `<think>` reasoning at this many TOKENS (not seconds — tok/s varies).
|
|
45
|
+
* Sent as the SDK's `reasoning_budget`, so the model closes its reasoning and
|
|
46
|
+
* answers. If the stream still runs well past it, the run is cancelled and a
|
|
47
|
+
* short fallback is returned instead of hanging on "Thinking…". Omit for
|
|
48
|
+
* unlimited reasoning.
|
|
47
49
|
*/
|
|
48
50
|
maxThinkingTokens?: number;
|
|
49
51
|
/** Stream the model's `<think>` reasoning, when a host wants to surface it. */
|
|
@@ -82,31 +84,33 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
|
82
84
|
? [{ role: 'system', content: input.system }, ...input.messages]
|
|
83
85
|
: input.messages;
|
|
84
86
|
|
|
85
|
-
// Tools are forwarded by schema only
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
? input.tools.map((t) => ({
|
|
91
|
-
name: t.name,
|
|
92
|
-
description: t.description,
|
|
93
|
-
parameters: t.parameters,
|
|
94
|
-
}))
|
|
95
|
-
: undefined;
|
|
96
|
-
|
|
97
|
-
// QVAC 0.13 nests sampling under `generationParams`; top-level
|
|
87
|
+
// Tools are forwarded by schema only — the Engine validates + executes.
|
|
88
|
+
// JSON-Schema tools are normalised to the SDK's Tool shape (see tools.ts).
|
|
89
|
+
const tools = toQvacTools(input.tools);
|
|
90
|
+
|
|
91
|
+
// QVAC (0.13+) nests sampling under `generationParams`; top-level
|
|
98
92
|
// `temperature`/`max_tokens` (as older rate code passed) are dropped by
|
|
99
93
|
// validation, so the cap silently no-op'd. Build it here, and only send it
|
|
100
94
|
// when a value is set so a host that passes neither keeps SDK defaults.
|
|
101
95
|
const temp = input.temperature ?? options.defaultTemperature;
|
|
102
96
|
const predict = input.maxTokens ?? options.defaultMaxTokens;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
:
|
|
97
|
+
// A thinking budget at or above the output cap never binds: the model can
|
|
98
|
+
// spend the whole turn reasoning and return no answer. Keep half for it.
|
|
99
|
+
const thinkingCap = input.maxThinkingTokens ?? options.maxThinkingTokens;
|
|
100
|
+
const maxThinkingTokens =
|
|
101
|
+
thinkingCap !== undefined && predict !== undefined && thinkingCap >= predict
|
|
102
|
+
? Math.floor(predict / 2)
|
|
103
|
+
: thinkingCap;
|
|
104
|
+
// `tool_choice` is only meaningful with tools; the SDK rejects a named
|
|
105
|
+
// choice that isn't among them.
|
|
106
|
+
const toolChoice = tools && input.toolChoice ? input.toolChoice : undefined;
|
|
107
|
+
const generationParamsRaw = {
|
|
108
|
+
...(temp !== undefined ? { temp } : {}),
|
|
109
|
+
...(predict !== undefined ? { predict } : {}),
|
|
110
|
+
...(maxThinkingTokens !== undefined ? { reasoning_budget: maxThinkingTokens } : {}),
|
|
111
|
+
...(toolChoice ? { tool_choice: toolChoice } : {}),
|
|
112
|
+
};
|
|
113
|
+
const generationParams = Object.keys(generationParamsRaw).length ? generationParamsRaw : undefined;
|
|
110
114
|
|
|
111
115
|
const run = options.completion({
|
|
112
116
|
modelId,
|
|
@@ -136,11 +140,13 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
const maxThinkingTokens = input.maxThinkingTokens ?? options.maxThinkingTokens;
|
|
140
143
|
const result = await consumeRun(run, {
|
|
141
144
|
onToken: input.onToken,
|
|
142
145
|
onThinking: input.onThinking ?? options.onThinking,
|
|
143
|
-
|
|
146
|
+
// Backstop only: the SDK enforces the budget itself, and our count is a
|
|
147
|
+
// char-based estimate, so leave headroom before cancelling.
|
|
148
|
+
maxThinkingTokens:
|
|
149
|
+
maxThinkingTokens === undefined ? undefined : Math.ceil(maxThinkingTokens * 1.25) + 32,
|
|
144
150
|
// Cancel the in-flight run the moment the thinking budget is blown — the
|
|
145
151
|
// SDK keeps generating otherwise. Fire-and-forget; `final` then resolves.
|
|
146
152
|
onThinkingBudgetExceeded: () => {
|
|
@@ -155,8 +161,11 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
|
155
161
|
// instead of an empty bubble so the agentic loop ends cleanly.
|
|
156
162
|
const text =
|
|
157
163
|
result.text || (result.thinkingBudgetExceeded ? THINKING_BUDGET_FALLBACK : result.text);
|
|
158
|
-
const totalTokens = result.stats?.totalTokens;
|
|
159
164
|
const promptTokens = result.stats?.promptTokens;
|
|
165
|
+
const generated = result.stats?.generatedTokens;
|
|
166
|
+
const totalTokens =
|
|
167
|
+
result.stats?.totalTokens ??
|
|
168
|
+
(typeof generated === 'number' && typeof promptTokens === 'number' ? promptTokens + generated : undefined);
|
|
160
169
|
const inference: InferenceMetrics = {
|
|
161
170
|
requestId: result.requestId,
|
|
162
171
|
durationMs: result.timing.durationMs,
|
|
@@ -179,12 +188,16 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
|
|
|
179
188
|
...(result.stopReason ? { stopReason: result.stopReason } : {}),
|
|
180
189
|
};
|
|
181
190
|
|
|
191
|
+
const incomplete =
|
|
192
|
+
!result.text && result.toolCalls.length === 0 && (result.thinkingBudgetExceeded || !!result.truncated);
|
|
182
193
|
return {
|
|
183
194
|
text,
|
|
184
195
|
rawContent: result.rawContent,
|
|
185
196
|
toolCalls: result.toolCalls,
|
|
197
|
+
...(result.toolErrors ? { toolErrors: result.toolErrors } : {}),
|
|
186
198
|
requestId: result.requestId,
|
|
187
199
|
inference,
|
|
200
|
+
...(incomplete ? { incomplete: true } : {}),
|
|
188
201
|
};
|
|
189
202
|
},
|
|
190
203
|
|
package/src/qvac/stream.test.ts
CHANGED
|
@@ -112,4 +112,32 @@ describe('consumeRun', () => {
|
|
|
112
112
|
);
|
|
113
113
|
expect(out.timing).toEqual({ ttftMs: 45, durationMs: 90 });
|
|
114
114
|
});
|
|
115
|
+
|
|
116
|
+
it('folds a rejected final (InferenceCancelledError) into a cancelled turn', async () => {
|
|
117
|
+
class InferenceCancelledError extends Error {
|
|
118
|
+
constructor(readonly requestId: string, readonly partial: { text?: string }) {
|
|
119
|
+
super('cancelled');
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const run: CompletionRunLike = {
|
|
123
|
+
requestId: 'req-c',
|
|
124
|
+
events: (async function* () {
|
|
125
|
+
yield { type: 'contentDelta', text: 'Partial' };
|
|
126
|
+
})(),
|
|
127
|
+
final: Promise.reject(new InferenceCancelledError('req-c', { text: 'Partial' })),
|
|
128
|
+
};
|
|
129
|
+
const out = await consumeRun(run);
|
|
130
|
+
expect(out.stopReason).toBe('cancelled');
|
|
131
|
+
expect(out.text).toBe('Partial');
|
|
132
|
+
expect(out.toolCalls).toEqual([]);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('re-throws a rejected final that is not a cancellation', async () => {
|
|
136
|
+
const run: CompletionRunLike = {
|
|
137
|
+
requestId: 'req-e',
|
|
138
|
+
events: (async function* () {})(),
|
|
139
|
+
final: Promise.reject(new Error('boom')),
|
|
140
|
+
};
|
|
141
|
+
await expect(consumeRun(run)).rejects.toThrow('boom');
|
|
142
|
+
});
|
|
115
143
|
});
|
package/src/qvac/stream.ts
CHANGED
|
@@ -28,11 +28,9 @@ export interface StreamHandlers {
|
|
|
28
28
|
/** The model's `<think>` reasoning, streamed separately. */
|
|
29
29
|
onThinking?: (token: string) => void;
|
|
30
30
|
/**
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* only on/off), so we count thinking tokens and stop the run once they exceed
|
|
35
|
-
* this. Omit for unlimited reasoning.
|
|
31
|
+
* Stop the run once `<think>` reasoning exceeds this many tokens (estimated
|
|
32
|
+
* from characters). A backstop for the SDK's own `reasoning_budget`. Omit for
|
|
33
|
+
* unlimited reasoning.
|
|
36
34
|
*/
|
|
37
35
|
maxThinkingTokens?: number;
|
|
38
36
|
/**
|
|
@@ -55,6 +53,28 @@ export interface ConsumedTurn extends ParsedTurn {
|
|
|
55
53
|
};
|
|
56
54
|
}
|
|
57
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Since QVAC 0.11 a cancelled run ends `events` normally but REJECTS `final`
|
|
58
|
+
* with `InferenceCancelledError { requestId, partial }`. Fold that back into a
|
|
59
|
+
* `final` frame with stopReason 'cancelled' so a stop or a blown thinking budget
|
|
60
|
+
* is a result, not an exception. Matched structurally to stay SDK-free.
|
|
61
|
+
*/
|
|
62
|
+
function cancelledFinal(err: unknown): QvacFinalLike | null {
|
|
63
|
+
const e = err as {
|
|
64
|
+
requestId?: unknown;
|
|
65
|
+
partial?: { text?: string; toolCalls?: QvacFinalLike['toolCalls']; stats?: QvacFinalLike['stats'] };
|
|
66
|
+
} | null;
|
|
67
|
+
const named = (err as { constructor?: { name?: string } } | null)?.constructor?.name === 'InferenceCancelledError';
|
|
68
|
+
if (!e || !(named || (typeof e.requestId === 'string' && typeof e.partial === 'object'))) return null;
|
|
69
|
+
const partial = e.partial ?? {};
|
|
70
|
+
return {
|
|
71
|
+
contentText: partial.text ?? '',
|
|
72
|
+
toolCalls: partial.toolCalls ?? [],
|
|
73
|
+
...(partial.stats ? { stats: partial.stats } : {}),
|
|
74
|
+
stopReason: 'cancelled',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
58
78
|
/** Rough token estimate (~4 chars/token) — same heuristic the context budget uses. */
|
|
59
79
|
function approxTokens(chars: number): number {
|
|
60
80
|
return Math.ceil(chars / 4);
|
|
@@ -95,7 +115,14 @@ export async function consumeRun(
|
|
|
95
115
|
}
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
|
-
|
|
118
|
+
let final: QvacFinalLike;
|
|
119
|
+
try {
|
|
120
|
+
final = await run.final;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
const recovered = cancelledFinal(err);
|
|
123
|
+
if (!recovered) throw err;
|
|
124
|
+
final = recovered;
|
|
125
|
+
}
|
|
99
126
|
const finishedAt = now();
|
|
100
127
|
return {
|
|
101
128
|
...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,116 @@
|
|
|
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
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* JSON Schema for a tool's parameters: a JSON-Schema def is returned as is
|
|
110
|
+
* (nested objects intact), a Zod schema is converted to a flat object schema.
|
|
111
|
+
*/
|
|
112
|
+
export function toolParametersSchema(t: Pick<ToolDef, 'name' | 'description' | 'parameters'>): Record<string, unknown> {
|
|
113
|
+
const p = t.parameters as Obj | null | undefined;
|
|
114
|
+
if (p && typeof p === 'object' && !isZodLike(p) && ('properties' in p || p.type === 'object')) return p;
|
|
115
|
+
return toQvacTool(t).parameters;
|
|
116
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
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('extractIssueAsset — review regressions', () => {
|
|
39
|
+
it('reads thousands separators in both styles, and decimals with k/m', () => {
|
|
40
|
+
expect(extractIssueAsset('crea 1.000 token CAFE chiamati Caffè Club')).toMatchObject({ amount: 1000, ticker: 'CAFE', name: 'Caffè Club' });
|
|
41
|
+
expect(extractIssueAsset('issue 1,000 TICKET tokens')).toMatchObject({ amount: 1000 });
|
|
42
|
+
expect(extractIssueAsset('mint 1,5k PIZZA tokens')).toMatchObject({ amount: 1500 });
|
|
43
|
+
expect(extractIssueAsset('mint 1.5k PIZZA tokens')).toMatchObject({ amount: 1500 });
|
|
44
|
+
});
|
|
45
|
+
it('never takes the supply from a number inside the name', () => {
|
|
46
|
+
expect(extractIssueAsset('mint tokens named Web3 Summit 2026, supply 300')).toMatchObject({ name: 'Web3 Summit 2026', amount: 300 });
|
|
47
|
+
expect(extractIssueAsset('mint tokens named Web3 Summit 2026')).toMatchObject({ name: 'Web3 Summit 2026', amount: undefined });
|
|
48
|
+
expect(issueAssetRecipe.confident!(extractIssueAsset('mint tokens named Web3 Summit 2026')!)).toBe(false);
|
|
49
|
+
expect(extractIssueAsset('launch a token called Moon with ticker MOON and 1m supply')).toMatchObject({ name: 'Moon', ticker: 'MOON', amount: 1_000_000 });
|
|
50
|
+
});
|
|
51
|
+
it('only fires when the request opens with the verb', () => {
|
|
52
|
+
expect(issueAssetRecipe.match!('I have an issue with my USDT tokens')).toBe(false);
|
|
53
|
+
expect(issueAssetRecipe.match!('how do I create a token?')).toBe(false);
|
|
54
|
+
expect(extractIssueAsset('I have an issue with my USDT tokens')).toBeNull();
|
|
55
|
+
expect(issueAssetRecipe.match!('please mint 100 PIZZA tokens')).toBe(true);
|
|
56
|
+
expect(issueAssetRecipe.match!('puoi creare 100 token CAFE?')).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
it('apostrophes inside a name are not quotes', () => {
|
|
59
|
+
expect(extractIssueAsset("mint 100 tokens called Joe's Pizza with ticker 'JOE'")).toEqual({ name: "Joe's Pizza", ticker: 'JOE', amount: 100, schema: 'NIA' });
|
|
60
|
+
expect(extractIssueAsset("crea 50 token chiamati Bottega dell'Arte con ticker ARTE")).toMatchObject({ name: "Bottega dell'Arte", ticker: 'ARTE', amount: 50 });
|
|
61
|
+
expect(extractIssueAsset('mint 10 tokens "Pizza Points" ticker PP')).toMatchObject({ name: 'Pizza Points', ticker: 'PP', amount: 10 });
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe('MockWallet.reset', () => {
|
|
66
|
+
it('restores the initial state, including issued assets and UTXOs', async () => {
|
|
67
|
+
const wallet = new MockWallet({ utxos: 1 });
|
|
68
|
+
await wallet.registry().execute('rln_issue_asset', { name: 'Ticket', ticker: 'TICKET', amount: 10 });
|
|
69
|
+
expect(wallet.utxos).toBe(0);
|
|
70
|
+
wallet.reset();
|
|
71
|
+
expect(wallet.utxos).toBe(1);
|
|
72
|
+
expect(wallet.assets.TICKET).toBeUndefined();
|
|
73
|
+
await expect(wallet.registry().execute('rln_issue_asset', { name: 'Ticket', ticker: 'TICKET', amount: 10 })).resolves.toMatchObject({ issued: true });
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe('issue-asset readback', () => {
|
|
78
|
+
it('fungible: supply + ticker + name', () => {
|
|
79
|
+
expect(confirmReadback({ name: 'rln_issue_asset', arguments: { name: 'Hackathon Ticket', ticker: 'TICKET', amount: 1000 } }))
|
|
80
|
+
.toBe('Issue 1,000 TICKET (Hackathon Ticket), a new RGB asset. Confirm?');
|
|
81
|
+
});
|
|
82
|
+
it('UDA: unique asset wording', () => {
|
|
83
|
+
expect(confirmReadback({ name: 'rln_issue_asset', arguments: { name: 'Genesis Badge', ticker: 'GENESISB', amount: 1, schema: 'UDA' } }))
|
|
84
|
+
.toBe('Issue unique asset GENESISB (Genesis Badge) on RGB. Confirm?');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('runRecipe — issue asset against MockWallet', () => {
|
|
89
|
+
it('one confirmation, asset lands in the wallet', async () => {
|
|
90
|
+
const wallet = new MockWallet();
|
|
91
|
+
const onConfirm = vi.fn(async () => ({ approved: true }));
|
|
92
|
+
const res = await runRecipe(issueAssetRecipe, 'issue 1000 TICKET tokens called Hackathon Ticket', {
|
|
93
|
+
provider: scriptedProvider(), tools: wallet.registry(), onConfirm,
|
|
94
|
+
});
|
|
95
|
+
expect(res.status).toBe('done');
|
|
96
|
+
expect(res.inferences).toBe(0);
|
|
97
|
+
expect(onConfirm).toHaveBeenCalledOnce();
|
|
98
|
+
expect(wallet.assets.TICKET).toBe(1000);
|
|
99
|
+
expect(res.text).toMatch(/^Issued 1000 TICKET\. Asset id: rgb:mock-ticket-/);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('declined confirmation issues nothing', async () => {
|
|
103
|
+
const wallet = new MockWallet();
|
|
104
|
+
const res = await runRecipe(issueAssetRecipe, 'issue 1000 TICKET tokens', {
|
|
105
|
+
provider: scriptedProvider(), tools: wallet.registry(), onConfirm: async () => ({ approved: false }),
|
|
106
|
+
});
|
|
107
|
+
expect(res.status).toBe('cancelled');
|
|
108
|
+
expect(wallet.assets.TICKET).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('surfaces the node-style "no UTXOs" failure', async () => {
|
|
112
|
+
const wallet = new MockWallet({ utxos: 0 });
|
|
113
|
+
const res = await runRecipe(issueAssetRecipe, 'issue 1000 TICKET tokens', {
|
|
114
|
+
provider: scriptedProvider(), tools: wallet.registry(), onConfirm: async () => ({ approved: true }),
|
|
115
|
+
});
|
|
116
|
+
expect(res.status).toBe('error');
|
|
117
|
+
expect(res.error ?? res.text).toMatch(/rln_create_utxos/);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('Funnel — issue-asset as an opt-in recipe', () => {
|
|
122
|
+
it('routes to the recipe tier and is listable afterwards', async () => {
|
|
123
|
+
const wallet = new MockWallet();
|
|
124
|
+
const funnel = new Funnel({ provider: scriptedProvider(), tools: wallet.registry(), recipes: [issueAssetRecipe] });
|
|
125
|
+
const out = await funnel.runTurn('mint an NFT called Genesis Badge', { onConfirm: async () => ({ approved: true }) });
|
|
126
|
+
expect(out.tier).toBe('recipe');
|
|
127
|
+
expect(out.route).toBe('issue-asset');
|
|
128
|
+
const listed = (await wallet.registry().execute('rln_list_assets', {})) as { assets: Array<{ ticker: string; schema: string }> };
|
|
129
|
+
expect(listed.assets.find((a) => a.ticker === 'GENESISB')?.schema).toBe('UDA');
|
|
130
|
+
});
|
|
131
|
+
});
|