@kaleidorg/mind 0.5.1 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/dist/autonomy/index.d.ts +21 -0
  2. package/dist/autonomy/index.d.ts.map +1 -0
  3. package/dist/autonomy/index.js +16 -0
  4. package/dist/autonomy/index.js.map +1 -0
  5. package/dist/autonomy/prompt.d.ts +21 -0
  6. package/dist/autonomy/prompt.d.ts.map +1 -0
  7. package/dist/autonomy/prompt.js +37 -0
  8. package/dist/autonomy/prompt.js.map +1 -0
  9. package/dist/autonomy/risk.d.ts +53 -0
  10. package/dist/autonomy/risk.d.ts.map +1 -0
  11. package/dist/autonomy/risk.js +74 -0
  12. package/dist/autonomy/risk.js.map +1 -0
  13. package/dist/autonomy/run-state.d.ts +39 -0
  14. package/dist/autonomy/run-state.d.ts.map +1 -0
  15. package/dist/autonomy/run-state.js +118 -0
  16. package/dist/autonomy/run-state.js.map +1 -0
  17. package/dist/autonomy/scheduler.d.ts +18 -0
  18. package/dist/autonomy/scheduler.d.ts.map +1 -0
  19. package/dist/autonomy/scheduler.js +113 -0
  20. package/dist/autonomy/scheduler.js.map +1 -0
  21. package/dist/autonomy/task-store.d.ts +44 -0
  22. package/dist/autonomy/task-store.d.ts.map +1 -0
  23. package/dist/autonomy/task-store.js +139 -0
  24. package/dist/autonomy/task-store.js.map +1 -0
  25. package/dist/autonomy/types.d.ts +164 -0
  26. package/dist/autonomy/types.d.ts.map +1 -0
  27. package/dist/autonomy/types.js +20 -0
  28. package/dist/autonomy/types.js.map +1 -0
  29. package/dist/bitrefill/contract.d.ts +60 -0
  30. package/dist/bitrefill/contract.d.ts.map +1 -0
  31. package/dist/bitrefill/contract.js +119 -0
  32. package/dist/bitrefill/contract.js.map +1 -0
  33. package/dist/context/compress.d.ts +65 -0
  34. package/dist/context/compress.d.ts.map +1 -0
  35. package/dist/context/compress.js +181 -0
  36. package/dist/context/compress.js.map +1 -0
  37. package/dist/engine.d.ts +20 -0
  38. package/dist/engine.d.ts.map +1 -1
  39. package/dist/engine.js +23 -4
  40. package/dist/engine.js.map +1 -1
  41. package/dist/evidence.d.ts +62 -0
  42. package/dist/evidence.d.ts.map +1 -0
  43. package/dist/evidence.js +47 -0
  44. package/dist/evidence.js.map +1 -0
  45. package/dist/flashnet/contract.d.ts +56 -0
  46. package/dist/flashnet/contract.d.ts.map +1 -0
  47. package/dist/flashnet/contract.js +100 -0
  48. package/dist/flashnet/contract.js.map +1 -0
  49. package/dist/funnel.d.ts +11 -0
  50. package/dist/funnel.d.ts.map +1 -1
  51. package/dist/funnel.js +62 -7
  52. package/dist/funnel.js.map +1 -1
  53. package/dist/index.d.ts +12 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +11 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/kaleidoswap/contract.js +1 -1
  58. package/dist/kaleidoswap/contract.js.map +1 -1
  59. package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
  60. package/dist/knowledge/bitcoin-copilot.js +85 -2
  61. package/dist/knowledge/bitcoin-copilot.js.map +1 -1
  62. package/dist/providers/types.d.ts +17 -0
  63. package/dist/providers/types.d.ts.map +1 -1
  64. package/dist/qvac/index.d.ts +1 -1
  65. package/dist/qvac/index.d.ts.map +1 -1
  66. package/dist/qvac/index.js.map +1 -1
  67. package/dist/qvac/parse.d.ts +18 -0
  68. package/dist/qvac/parse.d.ts.map +1 -1
  69. package/dist/qvac/parse.js +1 -0
  70. package/dist/qvac/parse.js.map +1 -1
  71. package/dist/qvac/provider.d.ts +16 -0
  72. package/dist/qvac/provider.d.ts.map +1 -1
  73. package/dist/qvac/provider.js +40 -1
  74. package/dist/qvac/provider.js.map +1 -1
  75. package/dist/qvac/stream.d.ts +22 -0
  76. package/dist/qvac/stream.d.ts.map +1 -1
  77. package/dist/qvac/stream.js +33 -1
  78. package/dist/qvac/stream.js.map +1 -1
  79. package/dist/recipe/buy-asset-channel.d.ts +1 -1
  80. package/dist/recipe/buy-asset-channel.d.ts.map +1 -1
  81. package/dist/recipe/buy-asset-channel.js +4 -3
  82. package/dist/recipe/buy-asset-channel.js.map +1 -1
  83. package/dist/recipe/flashnet-swap.d.ts +35 -0
  84. package/dist/recipe/flashnet-swap.d.ts.map +1 -0
  85. package/dist/recipe/flashnet-swap.js +239 -0
  86. package/dist/recipe/flashnet-swap.js.map +1 -0
  87. package/dist/recipe/kaleidoswap-atomic.d.ts +1 -1
  88. package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
  89. package/dist/recipe/kaleidoswap-atomic.js +42 -20
  90. package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
  91. package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -1
  92. package/dist/recipe/kaleidoswap-channel-order.js +31 -10
  93. package/dist/recipe/kaleidoswap-channel-order.js.map +1 -1
  94. package/dist/recipe/kaleidoswap-price.d.ts.map +1 -1
  95. package/dist/recipe/kaleidoswap-price.js +7 -1
  96. package/dist/recipe/kaleidoswap-price.js.map +1 -1
  97. package/dist/recipe/runner.d.ts.map +1 -1
  98. package/dist/recipe/runner.js +43 -3
  99. package/dist/recipe/runner.js.map +1 -1
  100. package/dist/recipe/swap.d.ts.map +1 -1
  101. package/dist/recipe/swap.js +14 -1
  102. package/dist/recipe/swap.js.map +1 -1
  103. package/dist/tools/mcp.d.ts +19 -0
  104. package/dist/tools/mcp.d.ts.map +1 -1
  105. package/dist/tools/mcp.js +51 -9
  106. package/dist/tools/mcp.js.map +1 -1
  107. package/dist/wallet/confirm.d.ts.map +1 -1
  108. package/dist/wallet/confirm.js +1 -0
  109. package/dist/wallet/confirm.js.map +1 -1
  110. package/dist/wallet/contract.d.ts.map +1 -1
  111. package/dist/wallet/contract.js +20 -4
  112. package/dist/wallet/contract.js.map +1 -1
  113. package/package.json +5 -4
  114. package/skills/bitrefill/SKILL.md +152 -52
  115. package/skills/channel-manager/SKILL.md +59 -0
  116. package/skills/dca/SKILL.md +48 -0
  117. package/skills/flashnet-swaps/SKILL.md +158 -0
  118. package/skills/kaleido-lsps/SKILL.md +34 -17
  119. package/skills/kaleido-trading/SKILL.md +37 -13
  120. package/skills/liquidity-optimizer/SKILL.md +91 -0
  121. package/skills/merchant-finder/SKILL.md +2 -2
  122. package/skills/portfolio-manager/SKILL.md +67 -0
  123. package/skills/rgb-lightning-node/SKILL.md +38 -11
  124. package/skills/spark-wallet/SKILL.md +235 -0
  125. package/skills/wallet-assistant/SKILL.md +2 -2
  126. package/src/autonomy/autonomy.test.ts +348 -0
  127. package/src/autonomy/index.ts +50 -0
  128. package/src/autonomy/prompt.ts +48 -0
  129. package/src/autonomy/risk.ts +139 -0
  130. package/src/autonomy/run-state.ts +144 -0
  131. package/src/autonomy/scheduler.ts +120 -0
  132. package/src/autonomy/task-store.ts +167 -0
  133. package/src/autonomy/types.ts +186 -0
  134. package/src/bitrefill/contract.test.ts +89 -0
  135. package/src/bitrefill/contract.ts +190 -0
  136. package/src/context/compress.test.ts +120 -0
  137. package/src/context/compress.ts +230 -0
  138. package/src/engine.test.ts +34 -0
  139. package/src/engine.ts +35 -4
  140. package/src/evidence.test.ts +80 -0
  141. package/src/evidence.ts +114 -0
  142. package/src/flashnet/contract.test.ts +101 -0
  143. package/src/flashnet/contract.ts +164 -0
  144. package/src/funnel.mind.test.ts +390 -0
  145. package/src/funnel.ts +73 -8
  146. package/src/index.ts +92 -1
  147. package/src/kaleidoswap/contract.ts +1 -1
  148. package/src/knowledge/bitcoin-copilot.ts +96 -2
  149. package/src/providers/types.ts +18 -0
  150. package/src/qvac/index.ts +1 -0
  151. package/src/qvac/parse.ts +20 -0
  152. package/src/qvac/provider.test.ts +17 -0
  153. package/src/qvac/provider.ts +62 -2
  154. package/src/qvac/stream.test.ts +36 -0
  155. package/src/qvac/stream.ts +54 -1
  156. package/src/recipe/buy-asset-channel.test.ts +5 -0
  157. package/src/recipe/buy-asset-channel.ts +6 -3
  158. package/src/recipe/flashnet-swap.test.ts +114 -0
  159. package/src/recipe/flashnet-swap.ts +266 -0
  160. package/src/recipe/kaleidoswap-atomic.test.ts +24 -3
  161. package/src/recipe/kaleidoswap-atomic.ts +39 -20
  162. package/src/recipe/kaleidoswap-channel-order.test.ts +38 -0
  163. package/src/recipe/kaleidoswap-channel-order.ts +27 -9
  164. package/src/recipe/kaleidoswap-price.ts +7 -1
  165. package/src/recipe/recipe.test.ts +21 -0
  166. package/src/recipe/runner.ts +46 -3
  167. package/src/recipe/swap.ts +16 -1
  168. package/src/tools/mcp.live.test.ts +116 -0
  169. package/src/tools/mcp.parse.test.ts +37 -0
  170. package/src/tools/mcp.ts +55 -9
  171. package/src/wallet/confirm.test.ts +8 -0
  172. package/src/wallet/confirm.ts +1 -0
  173. package/src/wallet/contract.test.ts +10 -0
  174. package/src/wallet/contract.ts +26 -4
package/src/funnel.ts CHANGED
@@ -20,6 +20,7 @@
20
20
  */
21
21
 
22
22
  import { Engine } from './engine.js';
23
+ import type { ToolCrushOptions } from './context/compress.js';
23
24
  import type { ToolRegistry } from './tools/registry.js';
24
25
  import { FastPath, WALLET_FAST_INTENTS } from './fastpath/fastpath.js';
25
26
  import type { FastIntent } from './fastpath/fastpath.js';
@@ -31,6 +32,7 @@ import type { Recipe } from './recipe/types.js';
31
32
  import { SkillRegistry } from './skills/registry.js';
32
33
  import type { Skill } from './skills/types.js';
33
34
  import type { LLMProvider } from './providers/types.js';
35
+ import type { InferenceMetrics } from './providers/types.js';
34
36
  import type { Retriever } from './rag/retriever.js';
35
37
  import type { ConfirmDecision, Message, ToolResult } from './types.js';
36
38
 
@@ -123,6 +125,8 @@ export interface FunnelResult {
123
125
  /** Agentic tier only: executed tool calls + reasoning turns. */
124
126
  toolCalls?: ToolResult[];
125
127
  turns?: number;
128
+ /** Agentic tier only: one local-inference receipt per model call. */
129
+ inference?: InferenceMetrics[];
126
130
  }
127
131
 
128
132
  export interface FunnelOptions {
@@ -139,6 +143,13 @@ export interface FunnelOptions {
139
143
  system?: string;
140
144
  /** Max reasoning↔tool rounds in the agentic tier. Default 5. */
141
145
  maxTurns?: number;
146
+ /**
147
+ * Crush verbose tool results before they re-enter the agentic loop's history,
148
+ * so a tiny on-device model's window isn't drowned in repetitive JSON. `true`
149
+ * uses safe defaults (amounts/addresses/invoices preserved); pass options to
150
+ * tune. Off by default. See compressToolResult.
151
+ */
152
+ compressToolOutput?: boolean | ToolCrushOptions;
142
153
  /** User settings, read fresh each turn. */
143
154
  getSettings?: () => FunnelSettings;
144
155
  /** Render a fast-path tool result as user-facing text. Default: built-in. */
@@ -196,6 +207,7 @@ export class Funnel {
196
207
  provider: opts.provider,
197
208
  tools: opts.tools,
198
209
  defaultMaxTurns: opts.maxTurns ?? 5,
210
+ compressToolOutput: opts.compressToolOutput,
199
211
  });
200
212
  this.fastPath = new FastPath(opts.fastIntents ?? WALLET_FAST_INTENTS);
201
213
  this.recipes = new RecipeRegistry(opts.recipes ?? [assetSendRecipe, paymentsRecipe, receiveRecipe]);
@@ -226,6 +238,8 @@ export class Funnel {
226
238
 
227
239
  async runTurn(text: string, cbs: FunnelCallbacks = {}): Promise<FunnelResult> {
228
240
  const settings = this.getSettings();
241
+ const memoryOn = settings.memoryEnabled !== false;
242
+ const ragOn = settings.ragEnabled !== false;
229
243
 
230
244
  // ── T0: deterministic fast-path (no LLM) ──
231
245
  // Only fires when the host's registry actually implements the intent's
@@ -247,15 +261,27 @@ export class Funnel {
247
261
  // running steps with bad data.
248
262
  // Either way the registry must implement the recipe's final action.
249
263
  const recipe = this.recipes.select(text);
250
- const slots = recipe?.extract?.(text) ?? null;
251
- const deterministicallyConfident =
252
- !!slots && (recipe?.confident ? recipe.confident(slots) : Object.keys(slots).length > 0);
264
+ // For forceModelExtract recipes (channel-order, atomic) the det extractor is
265
+ // de-emphasized: only used inside runRecipe as a backfill safety net; firing
266
+ // decision + log do not depend on brittle regex for varied NL.
267
+ let slotsForLog: any = null;
268
+ let detConfident = false;
269
+ if (recipe) {
270
+ if (recipe.forceModelExtract === true) {
271
+ slotsForLog = { forceModelExtract: true };
272
+ detConfident = true; // force path handles via LLM inside; prefilter only needs tool presence
273
+ } else {
274
+ const d = recipe.extract?.(text) ?? null;
275
+ slotsForLog = d;
276
+ detConfident = !!d && (recipe.confident ? recipe.confident(d) : Object.keys(d).length > 0);
277
+ }
278
+ }
253
279
  const fires =
254
280
  !!recipe &&
255
- (recipe.forceModelExtract === true || deterministicallyConfident) &&
281
+ (recipe.forceModelExtract === true || detConfident) &&
256
282
  !!(await this.registry.getDef(recipe.final.tool));
257
283
  if (recipe && fires) {
258
- this.log(`tier=recipe:${recipe.name} slots=${JSON.stringify(slots)}`);
284
+ this.log(`tier=recipe:${recipe.name} slots=${JSON.stringify(slotsForLog)}`);
259
285
  const res = await runRecipe(recipe, text, {
260
286
  provider: this.provider,
261
287
  tools: this.registry,
@@ -265,6 +291,26 @@ export class Funnel {
265
291
  cbs.onStep?.(name);
266
292
  },
267
293
  });
294
+ // Auto-remember ids/tokens from recipe summaries (the "remember: ..." lines)
295
+ // via the tool so status follow-ups can reliably recall even cross-session.
296
+ if (res.status === 'done' && memoryOn) {
297
+ try {
298
+ const hasRemember = await this.registry.getDef('remember');
299
+ if (hasRemember) {
300
+ const text = res.text || '';
301
+ const lines = text.split(/\n+/).filter((l) => /^\s*remember:/i.test(l));
302
+ for (const line of lines) {
303
+ const clean = line.trim();
304
+ if (clean.length > 8) {
305
+ void this.registry
306
+ .execute('remember', { text: clean, kind: 'event', tags: ['recipe', 'order', 'status'] })
307
+ .catch(() => {});
308
+ this.log(`auto-remembered: ${clean.slice(0, 80)}`);
309
+ }
310
+ }
311
+ }
312
+ } catch {}
313
+ }
268
314
  return { text: res.text, tier: 'recipe', route: recipe.name };
269
315
  }
270
316
 
@@ -278,7 +324,6 @@ export class Funnel {
278
324
  // RAG block sits above history so the model treats it as authoritative).
279
325
  // Only fires for agentic turns and only when the host opts in via
280
326
  // `retriever` AND the user hasn't disabled RAG in settings.
281
- const ragOn = settings.ragEnabled !== false;
282
327
  if (this.retriever && ragOn && this.topKRag > 0) {
283
328
  try {
284
329
  const hits = await this.retriever.search(text, this.topKRag);
@@ -296,12 +341,25 @@ export class Funnel {
296
341
 
297
342
  // Ambient tools stay available even when a skill narrows the set — gated
298
343
  // by the user's memory/knowledge toggles (default on).
299
- const memoryOn = settings.memoryEnabled !== false;
300
344
  const ambient = [...(memoryOn ? AMBIENT_MEMORY : []), ...(ragOn ? AMBIENT_RAG : [])];
301
345
  const disabledAmbient = [...(memoryOn ? [] : AMBIENT_MEMORY), ...(ragOn ? [] : AMBIENT_RAG)];
302
346
  let scoped: string[] | undefined;
303
347
  if (allowedTools) {
304
348
  scoped = [...new Set([...allowedTools, ...ambient])];
349
+ // Resilience against host tool-name drift: a skill's allowlist may name
350
+ // tools that don't exist on this host (e.g. the skill says `get_balances`
351
+ // but the desktop MCP exposes `rln_get_balances`). engine.runAgentic
352
+ // filters the model's tools to this list, so a fully-mismatched skill
353
+ // leaves the model TOOL-LESS — it then narrates "the tool isn't available"
354
+ // instead of acting. If NONE of the scoped tools resolve against the live
355
+ // registry, widen to the full surface so the agent can still work.
356
+ const present = new Set((await this.registry.listTools()).map((t) => t.name));
357
+ if (!scoped.some((n) => present.has(n))) {
358
+ this.log(
359
+ `tier=agentic: skill '${skill?.name ?? '?'}' tools resolved to 0 live tools — using full tool surface`,
360
+ );
361
+ scoped = undefined;
362
+ }
305
363
  } else if (disabledAmbient.length) {
306
364
  // No skill matched but a toggle is off: expose everything except the
307
365
  // disabled ambient tools (the sources stay mounted — no rebuild).
@@ -335,6 +393,13 @@ export class Funnel {
335
393
  onToolResult: cbs.onToolResult,
336
394
  onConfirm: cbs.onConfirm,
337
395
  });
338
- return { text: res.text ?? '', tier: 'agentic', route: skill?.name, toolCalls: res.toolCalls, turns: res.turns };
396
+ return {
397
+ text: res.text ?? '',
398
+ tier: 'agentic',
399
+ route: skill?.name,
400
+ toolCalls: res.toolCalls,
401
+ turns: res.turns,
402
+ inference: res.inference,
403
+ };
339
404
  }
340
405
  }
package/src/index.ts CHANGED
@@ -17,7 +17,12 @@ export type {
17
17
  ConfirmDecision,
18
18
  } from './types.js';
19
19
 
20
- export type { LLMProvider, TurnInput, TurnOutput } from './providers/types.js';
20
+ export type {
21
+ InferenceMetrics,
22
+ LLMProvider,
23
+ TurnInput,
24
+ TurnOutput,
25
+ } from './providers/types.js';
21
26
 
22
27
  export type { ToolSource } from './tools/source.js';
23
28
  export { InProcessToolSource } from './tools/in-process.js';
@@ -81,12 +86,41 @@ export type {
81
86
  BindLsps1Options,
82
87
  } from './lsps1/contract.js';
83
88
 
89
+ // ── Bitrefill (gift cards / mobile top-ups / eSIMs) ─────────────────────────
90
+ export {
91
+ BITREFILL_TOOLS,
92
+ BITREFILL_SPEND_TOOLS,
93
+ isBitrefillSpendTool,
94
+ getBitrefillTool,
95
+ bindBitrefillTools,
96
+ } from './bitrefill/contract.js';
97
+ export type {
98
+ BitrefillToolDef,
99
+ BitrefillHandler,
100
+ BindBitrefillOptions,
101
+ } from './bitrefill/contract.js';
102
+
103
+ // ── Flashnet (Spark-native AMM — swaps over Spark) ──────────────────────────
104
+ export {
105
+ FLASHNET_TOOLS,
106
+ FLASHNET_SPEND_TOOLS,
107
+ isFlashnetSpendTool,
108
+ getFlashnetTool,
109
+ bindFlashnetTools,
110
+ } from './flashnet/contract.js';
111
+ export type {
112
+ FlashnetToolDef,
113
+ FlashnetHandler,
114
+ BindFlashnetOptions,
115
+ } from './flashnet/contract.js';
116
+
84
117
  // ── KaleidoSwap recipes (opt-in — register via Funnel.recipes) ──
85
118
  // price recipe is read-only (quote-only); atomic recipe runs the full swap.
86
119
  // Register the price recipe FIRST so phrasings like "BTC price" are answered
87
120
  // without firing any spend.
88
121
  export { kaleidoswapPriceRecipe } from './recipe/kaleidoswap-price.js';
89
122
  export { kaleidoswapAtomicRecipe } from './recipe/kaleidoswap-atomic.js';
123
+ export { flashnetSwapRecipe } from './recipe/flashnet-swap.js';
90
124
  export {
91
125
  kaleidoswapChannelOrderRecipe,
92
126
  extractChannelOrder,
@@ -147,6 +181,8 @@ export {
147
181
  contextBudgetTokens,
148
182
  } from './context/budget.js';
149
183
  export type { BudgetReserves } from './context/budget.js';
184
+ export { compressToolResult, DEFAULT_PRESERVE_KEYS } from './context/compress.js';
185
+ export type { ToolCrushOptions, CrushResult } from './context/compress.js';
150
186
  export { capabilityProfile } from './capabilities.js';
151
187
  export type { CapabilityInput, MindCapabilities } from './capabilities.js';
152
188
 
@@ -186,3 +222,58 @@ export type { Skill, SkillReference, SkillSelector } from './skills/types.js';
186
222
 
187
223
  export { TurnLogger, defaultMask } from './logger.js';
188
224
  export type { TurnLog, Device, LoggerIO, LoggerOptions } from './logger.js';
225
+
226
+ export {
227
+ EVIDENCE_SCHEMA,
228
+ EvidenceRecorder,
229
+ sanitizeEvidenceEvent,
230
+ } from './evidence.js';
231
+ export type {
232
+ EvidenceEvent,
233
+ EvidenceEventType,
234
+ EvidenceInput,
235
+ EvidenceIO,
236
+ EvidenceRecorderOptions,
237
+ EvidenceSurface,
238
+ } from './evidence.js';
239
+
240
+ // ── Autonomy (the task brain: scheduled tasks + run history + spend guardrails)
241
+ // The operational half of the agent's memory — the state nanobot kept in
242
+ // tasks.json + cron + run history, lifted into core (storage/timers injected).
243
+ export {
244
+ InMemoryTaskStore,
245
+ defaultTaskSeeds,
246
+ TaskRunLog,
247
+ createTaskScheduler,
248
+ evaluateSpend,
249
+ DEFAULT_RISK_LIMITS,
250
+ buildTaskPrompt,
251
+ ZERO_ALLOCATION,
252
+ } from './autonomy/index.js';
253
+ export type {
254
+ TaskAllocation,
255
+ AgentTask,
256
+ NewTask,
257
+ TaskSeed,
258
+ TaskStore,
259
+ TaskStoreIO,
260
+ TaskStoreOptions,
261
+ TaskRunCost,
262
+ TaskStats,
263
+ TaskRunRecord,
264
+ RunLogSnapshot,
265
+ RunLogIO,
266
+ RunLogOptions,
267
+ TaskRunOutcome,
268
+ RunTask,
269
+ TimerHandle,
270
+ SchedulerOptions,
271
+ TaskScheduler,
272
+ SpendKind,
273
+ RiskLimits,
274
+ SpendAction,
275
+ RiskContext,
276
+ RiskOutcome,
277
+ RiskVerdict,
278
+ TaskPromptOptions,
279
+ } from './autonomy/index.js';
@@ -100,7 +100,7 @@ export const KALEIDOSWAP_TOOLS: KaleidoswapToolDef[] = [
100
100
  order_id: { type: 'string', description: 'The order id returned by kaleidoswap_place_order.' },
101
101
  access_token: { type: 'string', description: 'The per-order access token returned by kaleidoswap_place_order. Required for status checks on the order.' },
102
102
  },
103
- ['order_id']),
103
+ ['order_id', 'access_token']),
104
104
 
105
105
  t('orders',
106
106
  'kaleidoswap_get_order_history',
@@ -218,8 +218,8 @@ export const BITCOIN_COPILOT_DOCS: RagDocument[] = [
218
218
  'channel size you can buy, fees, accepted payment options). It is NOT ' +
219
219
  'your current inbound capacity — it describes what the LSP is willing ' +
220
220
  'to sell you. To learn your CURRENT receive capacity, sum the remote ' +
221
- 'balance of your existing channels; to BUY MORE, use lsp_get_info and ' +
222
- 'lsp_create_order.',
221
+ 'balance of your existing channels; to BUY MORE, use kaleidoswap_lsp_get_info and ' +
222
+ 'kaleidoswap_lsp_create_order.',
223
223
  metadata: { topic: 'channels' },
224
224
  },
225
225
  {
@@ -285,4 +285,98 @@ export const BITCOIN_COPILOT_DOCS: RagDocument[] = [
285
285
  "(not just inbound capacity).",
286
286
  metadata: { topic: 'rgb-channels' },
287
287
  },
288
+
289
+ // ── Layer / protocol taxonomy ─────────────────────────────────────────
290
+ // The single biggest source of model confusion is mixing up which assets
291
+ // live on which layer. Small models pattern-match on "USDT" or "Bitcoin"
292
+ // and assume every L2 supports every asset — they don't. Each L2 has its
293
+ // OWN asset family, and assets do not move between them without an
294
+ // explicit cross-layer swap or bridge.
295
+
296
+ {
297
+ id: 'kaleidomind-layers-overview',
298
+ text:
299
+ 'This wallet supports THREE distinct Bitcoin L2s, each with its own ' +
300
+ 'asset family. They are NOT interchangeable: a balance on one layer ' +
301
+ 'cannot be spent on another without an explicit swap. ' +
302
+ '(1) SPARK — an off-chain BTC scaling layer (Lightspark / buildonspark, ' +
303
+ 'Statechains-based). Assets: BTC (sats) + Spark-native tokens like ' +
304
+ 'USDB. Tools: spark_* (balance/address/invoice/pay). Swap venue: ' +
305
+ 'Flashnet AMM (BTC ⇄ Spark tokens). ' +
306
+ '(2) RLN / RGB — a Lightning node that carries RGB assets over ' +
307
+ 'BOLT11 channels (colored channels). Assets: BTC + RGB assets like ' +
308
+ 'USDT, XAUT. Tools: rln_* (nodeinfo/invoice/pay/whitelist). Swap ' +
309
+ 'venue: KaleidoSwap maker (BTC ⇄ RGB assets via atomic HTLC swap). ' +
310
+ '(3) ARKADE — an Ark-based off-chain BTC layer. Assets: BTC. Tools: ' +
311
+ 'arkade_* (balance/address/send). No native non-BTC assets today.',
312
+ metadata: { topic: 'layers' },
313
+ },
314
+
315
+ {
316
+ id: 'spark-layer-assets',
317
+ text:
318
+ 'Spark is an off-chain BTC scaling layer (Lightspark / buildonspark). ' +
319
+ "It holds BTC (sats) and Spark-native tokens. USDB is a Spark token. " +
320
+ 'Spark addresses look like spark1… (or sparkrt1… on regtest). ' +
321
+ 'CRITICAL: Spark does NOT carry RGB assets. USDT and XAUT are RGB ' +
322
+ 'assets that live on the RLN (RGB Lightning Node) layer — not on ' +
323
+ "Spark. A user's USDT balance, if they have one, is on RLN, NOT " +
324
+ 'Spark. Conversely, USDB lives only on Spark (and trades on ' +
325
+ 'Flashnet); it has no presence on RLN/RGB. When asked "what assets ' +
326
+ 'are on Spark / what can I trade on Spark", answer with Spark-native ' +
327
+ 'tokens (BTC + USDB and any other Spark tokens the AMM lists via ' +
328
+ 'flashnet_list_pools), NOT USDT/XAUT/RGB.',
329
+ metadata: { topic: 'layers' },
330
+ },
331
+
332
+ {
333
+ id: 'rln-layer-assets',
334
+ text:
335
+ 'RLN (RGB Lightning Node) is a Lightning node that carries RGB ' +
336
+ 'assets over BOLT11 channels (a.k.a. colored channels). It holds ' +
337
+ 'BTC on standard Lightning channels and RGB assets — USDT, XAUT, ' +
338
+ 'and any other client-side-validated asset issued via RGB — on ' +
339
+ 'asset channels. Each asset needs its own channel. RGB assets do ' +
340
+ 'NOT live on Spark or Arkade; they are RLN-only. Swap venue for ' +
341
+ 'BTC ⇄ RGB asset is the KaleidoSwap maker (atomic HTLC: quote → ' +
342
+ 'init → whitelist → execute). To receive an RGB asset over ' +
343
+ 'Lightning, you first need an LSPS1-opened asset channel.',
344
+ metadata: { topic: 'layers' },
345
+ },
346
+
347
+ {
348
+ id: 'swap-venue-split',
349
+ text:
350
+ "Two swap venues, two asset families — DO NOT confuse them. " +
351
+ "FLASHNET is a Spark-native AMM. It trades between BTC and " +
352
+ "Spark-native tokens (e.g. USDB). It uses the same Spark wallet " +
353
+ "as the user's balance. Tools: flashnet_list_pools, " +
354
+ "flashnet_simulate_swap, flashnet_execute_swap. Skill: " +
355
+ "flashnet-swaps. ASSETS: BTC, USDB, and anything else " +
356
+ "flashnet_list_pools returns. NEVER offer USDT/XAUT on Flashnet. " +
357
+ "KALEIDOSWAP is an atomic HTLC maker. It trades between BTC and " +
358
+ "RGB assets (USDT, XAUT). It uses the RLN node. Tools: " +
359
+ "kaleidoswap_get_quote, kaleidoswap_atomic_init, " +
360
+ "kaleidoswap_atomic_execute. Skill: kaleido-trading. ASSETS: BTC, " +
361
+ "USDT, XAUT, and other RGB assets the maker prices. NEVER offer " +
362
+ "USDB on KaleidoSwap.",
363
+ metadata: { topic: 'venues' },
364
+ },
365
+
366
+ {
367
+ id: 'asset-to-layer-routing',
368
+ text:
369
+ "How to route by asset name. The asset names tell you which layer " +
370
+ "to use — don't guess: " +
371
+ "BTC / sats → all layers (Spark / RLN / Arkade / on-chain) carry " +
372
+ "BTC; pick by user context. " +
373
+ "USDB → Spark only, via Flashnet (flashnet-swaps). " +
374
+ "USDT → RLN/RGB only, via KaleidoSwap (kaleido-trading). " +
375
+ "XAUT (tether-gold) → RLN/RGB only, via KaleidoSwap. " +
376
+ "If a user names an asset you don't recognise, do NOT assume a " +
377
+ "layer — ask, or list pools/assets via the right tool first " +
378
+ "(flashnet_list_pools for Spark-side, kaleidoswap_get_pairs / " +
379
+ "kaleidoswap_get_assets for RGB-side).",
380
+ metadata: { topic: 'venues' },
381
+ },
288
382
  ];
@@ -22,6 +22,22 @@ export interface TurnInput {
22
22
  signal?: AbortSignal;
23
23
  }
24
24
 
25
+ /** Judge-auditable metrics for one provider inference request. */
26
+ export interface InferenceMetrics {
27
+ requestId?: string;
28
+ backendDevice?: 'cpu' | 'gpu';
29
+ promptTokens?: number;
30
+ completionTokens?: number;
31
+ totalTokens?: number;
32
+ /** Milliseconds from completion() start to the first generated delta. */
33
+ ttftMs?: number;
34
+ /** End-to-end completion duration measured by the host. */
35
+ durationMs: number;
36
+ tokensPerSecond?: number;
37
+ stopReason?: string;
38
+ status: 'completed' | 'cancelled' | 'truncated' | 'failed';
39
+ }
40
+
25
41
  export interface TurnOutput {
26
42
  /** Cleaned assistant content for display. */
27
43
  text: string;
@@ -36,6 +52,8 @@ export interface TurnOutput {
36
52
  toolCalls: ToolCall[];
37
53
  /** Provider request id, for cancellation. */
38
54
  requestId?: string;
55
+ /** Optional local-inference receipt. Hosts may persist this as JSONL evidence. */
56
+ inference?: InferenceMetrics;
39
57
  }
40
58
 
41
59
  export interface LLMProvider {
package/src/qvac/index.ts CHANGED
@@ -27,6 +27,7 @@ export {
27
27
  finalToTurn,
28
28
  type QvacFinalLike,
29
29
  type ParsedTurn,
30
+ type QvacTurnStats,
30
31
  } from './parse.js';
31
32
 
32
33
  export {
package/src/qvac/parse.ts CHANGED
@@ -6,6 +6,21 @@
6
6
  */
7
7
  import { cleanAssistantVisibleText } from './text.js';
8
8
 
9
+ /**
10
+ * Per-turn inference stats from a QVAC `completion().final.stats` frame. The
11
+ * authoritative source for which backend actually ran (`backendDevice`) and the
12
+ * real throughput — hosts surface these instead of guessing from load config.
13
+ */
14
+ export interface QvacTurnStats {
15
+ /** The backend that actually executed this turn — the real "is GPU active". */
16
+ backendDevice?: 'cpu' | 'gpu';
17
+ tokensPerSecond?: number;
18
+ totalTokens?: number;
19
+ promptTokens?: number;
20
+ contextSize?: number;
21
+ totalTime?: number;
22
+ }
23
+
9
24
  /** Structural subset of a QVAC `completion().final` we depend on. */
10
25
  export interface QvacFinalLike {
11
26
  /** Visible assistant text (excludes `<think>` reasoning). */
@@ -20,6 +35,8 @@ export interface QvacFinalLike {
20
35
  * it so the funnel can tell a truncated tool-call from a complete one.
21
36
  */
22
37
  stopReason?: 'length' | 'cancelled' | string;
38
+ /** Inference stats (backend device, throughput). Present on a natural finish. */
39
+ stats?: QvacTurnStats;
23
40
  }
24
41
 
25
42
  export interface ParsedTurn {
@@ -33,6 +50,8 @@ export interface ParsedTurn {
33
50
  truncated: boolean;
34
51
  /** Raw stop reason from the SDK, when provided. */
35
52
  stopReason?: string;
53
+ /** Inference stats for this turn (backend device, throughput), when provided. */
54
+ stats?: QvacTurnStats;
36
55
  }
37
56
 
38
57
  /** Parse the first balanced `{…}` from a string as a `{name, arguments}` call. */
@@ -119,5 +138,6 @@ export function finalToTurn(final: QvacFinalLike, streamed = ''): ParsedTurn {
119
138
  toolCalls,
120
139
  truncated: final.stopReason === 'length',
121
140
  stopReason: final.stopReason,
141
+ stats: final.stats,
122
142
  };
123
143
  }
@@ -84,6 +84,23 @@ describe('createQvacProvider.runTurn', () => {
84
84
  expect(calls[0].generationParams).toBeUndefined();
85
85
  });
86
86
 
87
+ it('caps thinking by tokens — cancels the run and returns a fallback', async () => {
88
+ const cancel = vi.fn(async () => {});
89
+ const { fn } = fakeCompletion(
90
+ { contentText: '', toolCalls: [], raw: { fullText: '' }, stopReason: 'cancelled' },
91
+ [{ type: 'thinkingDelta', text: 'z'.repeat(40) }], // ~10 tokens, budget 4
92
+ );
93
+ const p = createQvacProvider({
94
+ completion: fn as any,
95
+ cancel: cancel as any,
96
+ getModelId: () => 'm1',
97
+ maxThinkingTokens: 4,
98
+ });
99
+ const out = await p.runTurn({ messages: [{ role: 'user', content: 'think hard' }], tools: [] });
100
+ expect(cancel).toHaveBeenCalledWith({ requestId: 'req-1' });
101
+ expect(out.text).toMatch(/thinking budget/i);
102
+ });
103
+
87
104
  it('streams visible content tokens to onToken', async () => {
88
105
  const { fn } = fakeCompletion(
89
106
  { contentText: 'Hi there', toolCalls: [], raw: { fullText: 'Hi there' } },
@@ -17,7 +17,8 @@
17
17
  * delegated to a desktop peer.
18
18
  */
19
19
  import type * as QvacSdk from '@qvac/sdk';
20
- import type { LLMProvider, TurnInput, TurnOutput } from '../providers/types.js';
20
+ import type { InferenceMetrics, LLMProvider, TurnInput, TurnOutput } from '../providers/types.js';
21
+ import type { QvacTurnStats } from './parse.js';
21
22
  import { consumeRun } from './stream.js';
22
23
 
23
24
  type CompletionFn = typeof QvacSdk.completion;
@@ -38,17 +39,37 @@ export interface QvacProviderOptions {
38
39
  defaultTemperature?: number;
39
40
  /** Default max output tokens — caps a turn so it can't ramble. Omit for uncapped. */
40
41
  defaultMaxTokens?: number;
42
+ /**
43
+ * Cap `<think>` reasoning at this many TOKENS (not seconds — tok/s varies, and
44
+ * the SDK has no numeric reasoning budget). When a turn's thinking exceeds it,
45
+ * the run is cancelled and a short fallback is returned instead of hanging on
46
+ * "Thinking…". Omit for unlimited reasoning.
47
+ */
48
+ maxThinkingTokens?: number;
41
49
  /** Stream the model's `<think>` reasoning, when a host wants to surface it. */
42
50
  onThinking?: (token: string) => void;
51
+ /**
52
+ * Per-turn inference stats (real backend device + throughput), when a host
53
+ * wants to surface them. Fires once per turn after the `final` frame resolves.
54
+ */
55
+ onStats?: (stats: QvacTurnStats) => void;
43
56
  }
44
57
 
45
58
  /** TurnInput plus the per-call knobs the funnel/voice paths pass through. */
46
59
  export interface QvacTurnInput extends TurnInput {
47
60
  temperature?: number;
48
61
  maxTokens?: number;
62
+ /** Per-turn override of the thinking-token cap (see QvacProviderOptions). */
63
+ maxThinkingTokens?: number;
49
64
  onThinking?: (token: string) => void;
65
+ onStats?: (stats: QvacTurnStats) => void;
50
66
  }
51
67
 
68
+ /** Shown when a turn is cut off because it blew its thinking-token budget. */
69
+ const THINKING_BUDGET_FALLBACK =
70
+ 'I spent my whole thinking budget on that one without landing an answer. ' +
71
+ 'Try asking again, more specifically.';
72
+
52
73
  export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
53
74
  return {
54
75
  name: 'qvac',
@@ -98,16 +119,55 @@ export function createQvacProvider(options: QvacProviderOptions): LLMProvider {
98
119
  ...(tools ? { tools } : {}),
99
120
  } as unknown as Parameters<CompletionFn>[0]);
100
121
 
122
+ const maxThinkingTokens = input.maxThinkingTokens ?? options.maxThinkingTokens;
101
123
  const result = await consumeRun(run, {
102
124
  onToken: input.onToken,
103
125
  onThinking: input.onThinking ?? options.onThinking,
126
+ maxThinkingTokens,
127
+ // Cancel the in-flight run the moment the thinking budget is blown — the
128
+ // SDK keeps generating otherwise. Fire-and-forget; `final` then resolves.
129
+ onThinkingBudgetExceeded: () => {
130
+ void options.cancel({ requestId: run.requestId }).catch(() => {});
131
+ },
104
132
  });
105
133
 
134
+ // Surface the real per-turn inference stats (backend device + throughput).
135
+ if (result.stats) (input.onStats ?? options.onStats)?.(result.stats);
136
+
137
+ // A turn cut off mid-reasoning has no visible answer — return a short note
138
+ // instead of an empty bubble so the agentic loop ends cleanly.
139
+ const text =
140
+ result.text || (result.thinkingBudgetExceeded ? THINKING_BUDGET_FALLBACK : result.text);
141
+ const totalTokens = result.stats?.totalTokens;
142
+ const promptTokens = result.stats?.promptTokens;
143
+ const inference: InferenceMetrics = {
144
+ requestId: result.requestId,
145
+ durationMs: result.timing.durationMs,
146
+ status:
147
+ result.stopReason === 'cancelled'
148
+ ? 'cancelled'
149
+ : result.truncated
150
+ ? 'truncated'
151
+ : 'completed',
152
+ ...(result.stats?.backendDevice ? { backendDevice: result.stats.backendDevice } : {}),
153
+ ...(typeof promptTokens === 'number' ? { promptTokens } : {}),
154
+ ...(typeof totalTokens === 'number' ? { totalTokens } : {}),
155
+ ...(typeof totalTokens === 'number' && typeof promptTokens === 'number'
156
+ ? { completionTokens: Math.max(0, totalTokens - promptTokens) }
157
+ : {}),
158
+ ...(typeof result.timing.ttftMs === 'number' ? { ttftMs: result.timing.ttftMs } : {}),
159
+ ...(typeof result.stats?.tokensPerSecond === 'number'
160
+ ? { tokensPerSecond: result.stats.tokensPerSecond }
161
+ : {}),
162
+ ...(result.stopReason ? { stopReason: result.stopReason } : {}),
163
+ };
164
+
106
165
  return {
107
- text: result.text,
166
+ text,
108
167
  rawContent: result.rawContent,
109
168
  toolCalls: result.toolCalls,
110
169
  requestId: result.requestId,
170
+ inference,
111
171
  };
112
172
  },
113
173
 
@@ -67,6 +67,31 @@ describe('consumeRun', () => {
67
67
  expect(out.truncated).toBe(true);
68
68
  });
69
69
 
70
+ it('stops forwarding and flags when thinking exceeds maxThinkingTokens', async () => {
71
+ const thinking: string[] = [];
72
+ let exceeded = 0;
73
+ // 8-char deltas ≈ 2 tokens each; budget 4 tokens trips after the 2nd.
74
+ const run = fakeRun(
75
+ [
76
+ { type: 'thinkingDelta', text: 'aaaaaaaa' },
77
+ { type: 'thinkingDelta', text: 'bbbbbbbb' },
78
+ { type: 'thinkingDelta', text: 'cccccccc' },
79
+ { type: 'contentDelta', text: 'should-not-arrive' },
80
+ ],
81
+ { contentText: '', toolCalls: [], raw: { fullText: '' }, stopReason: 'cancelled' },
82
+ );
83
+ const out = await consumeRun(run, {
84
+ onThinking: (t) => thinking.push(t),
85
+ maxThinkingTokens: 4,
86
+ onThinkingBudgetExceeded: () => {
87
+ exceeded += 1;
88
+ },
89
+ });
90
+ expect(exceeded).toBe(1);
91
+ expect(out.thinkingBudgetExceeded).toBe(true);
92
+ expect(thinking).toEqual(['aaaaaaaa', 'bbbbbbbb']); // stopped at the trip
93
+ });
94
+
70
95
  it('ignores delta events with no text', async () => {
71
96
  const tokens: string[] = [];
72
97
  const run = fakeRun(
@@ -76,4 +101,15 @@ describe('consumeRun', () => {
76
101
  await consumeRun(run, { onToken: (t) => tokens.push(t) });
77
102
  expect(tokens).toEqual(['hi']);
78
103
  });
104
+
105
+ it('measures first-token and total completion timing', async () => {
106
+ const ticks = [100, 145, 190];
107
+ const out = await consumeRun(
108
+ fakeRun([{ type: 'thinkingDelta', text: 'plan' }, { type: 'contentDelta', text: 'answer' }], {
109
+ contentText: 'answer',
110
+ }),
111
+ { now: () => ticks.shift() ?? 190 },
112
+ );
113
+ expect(out.timing).toEqual({ ttftMs: 45, durationMs: 90 });
114
+ });
79
115
  });