@kaleidorg/mind 0.5.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/autonomy/index.d.ts +21 -0
- package/dist/autonomy/index.d.ts.map +1 -0
- package/dist/autonomy/index.js +16 -0
- package/dist/autonomy/index.js.map +1 -0
- package/dist/autonomy/prompt.d.ts +21 -0
- package/dist/autonomy/prompt.d.ts.map +1 -0
- package/dist/autonomy/prompt.js +37 -0
- package/dist/autonomy/prompt.js.map +1 -0
- package/dist/autonomy/risk.d.ts +53 -0
- package/dist/autonomy/risk.d.ts.map +1 -0
- package/dist/autonomy/risk.js +74 -0
- package/dist/autonomy/risk.js.map +1 -0
- package/dist/autonomy/run-state.d.ts +39 -0
- package/dist/autonomy/run-state.d.ts.map +1 -0
- package/dist/autonomy/run-state.js +118 -0
- package/dist/autonomy/run-state.js.map +1 -0
- package/dist/autonomy/scheduler.d.ts +18 -0
- package/dist/autonomy/scheduler.d.ts.map +1 -0
- package/dist/autonomy/scheduler.js +113 -0
- package/dist/autonomy/scheduler.js.map +1 -0
- package/dist/autonomy/task-store.d.ts +44 -0
- package/dist/autonomy/task-store.d.ts.map +1 -0
- package/dist/autonomy/task-store.js +139 -0
- package/dist/autonomy/task-store.js.map +1 -0
- package/dist/autonomy/types.d.ts +164 -0
- package/dist/autonomy/types.d.ts.map +1 -0
- package/dist/autonomy/types.js +20 -0
- package/dist/autonomy/types.js.map +1 -0
- package/dist/bitrefill/contract.d.ts +60 -0
- package/dist/bitrefill/contract.d.ts.map +1 -0
- package/dist/bitrefill/contract.js +119 -0
- package/dist/bitrefill/contract.js.map +1 -0
- package/dist/context/compress.d.ts +65 -0
- package/dist/context/compress.d.ts.map +1 -0
- package/dist/context/compress.js +181 -0
- package/dist/context/compress.js.map +1 -0
- package/dist/engine.d.ts +20 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +23 -4
- package/dist/engine.js.map +1 -1
- package/dist/evidence.d.ts +62 -0
- package/dist/evidence.d.ts.map +1 -0
- package/dist/evidence.js +47 -0
- package/dist/evidence.js.map +1 -0
- package/dist/flashnet/contract.d.ts +56 -0
- package/dist/flashnet/contract.d.ts.map +1 -0
- package/dist/flashnet/contract.js +100 -0
- package/dist/flashnet/contract.js.map +1 -0
- package/dist/funnel.d.ts +11 -0
- package/dist/funnel.d.ts.map +1 -1
- package/dist/funnel.js +62 -7
- package/dist/funnel.js.map +1 -1
- package/dist/index.d.ts +12 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/dist/kaleidoswap/contract.js +1 -1
- package/dist/kaleidoswap/contract.js.map +1 -1
- package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -1
- package/dist/knowledge/bitcoin-copilot.js +85 -2
- package/dist/knowledge/bitcoin-copilot.js.map +1 -1
- package/dist/providers/types.d.ts +17 -0
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/qvac/index.d.ts +1 -1
- package/dist/qvac/index.d.ts.map +1 -1
- package/dist/qvac/index.js.map +1 -1
- package/dist/qvac/parse.d.ts +18 -0
- package/dist/qvac/parse.d.ts.map +1 -1
- package/dist/qvac/parse.js +1 -0
- package/dist/qvac/parse.js.map +1 -1
- package/dist/qvac/provider.d.ts +16 -0
- package/dist/qvac/provider.d.ts.map +1 -1
- package/dist/qvac/provider.js +40 -1
- package/dist/qvac/provider.js.map +1 -1
- package/dist/qvac/stream.d.ts +22 -0
- package/dist/qvac/stream.d.ts.map +1 -1
- package/dist/qvac/stream.js +33 -1
- package/dist/qvac/stream.js.map +1 -1
- package/dist/recipe/buy-asset-channel.d.ts +1 -1
- package/dist/recipe/buy-asset-channel.d.ts.map +1 -1
- package/dist/recipe/buy-asset-channel.js +4 -3
- package/dist/recipe/buy-asset-channel.js.map +1 -1
- package/dist/recipe/flashnet-swap.d.ts +35 -0
- package/dist/recipe/flashnet-swap.d.ts.map +1 -0
- package/dist/recipe/flashnet-swap.js +239 -0
- package/dist/recipe/flashnet-swap.js.map +1 -0
- package/dist/recipe/kaleidoswap-atomic.d.ts +1 -1
- package/dist/recipe/kaleidoswap-atomic.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-atomic.js +42 -20
- package/dist/recipe/kaleidoswap-atomic.js.map +1 -1
- package/dist/recipe/kaleidoswap-channel-order.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-channel-order.js +31 -10
- package/dist/recipe/kaleidoswap-channel-order.js.map +1 -1
- package/dist/recipe/kaleidoswap-price.d.ts.map +1 -1
- package/dist/recipe/kaleidoswap-price.js +7 -1
- package/dist/recipe/kaleidoswap-price.js.map +1 -1
- package/dist/recipe/runner.d.ts.map +1 -1
- package/dist/recipe/runner.js +43 -3
- package/dist/recipe/runner.js.map +1 -1
- package/dist/recipe/swap.d.ts.map +1 -1
- package/dist/recipe/swap.js +14 -1
- package/dist/recipe/swap.js.map +1 -1
- package/dist/tools/mcp.d.ts +19 -0
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mcp.js +51 -9
- package/dist/tools/mcp.js.map +1 -1
- package/dist/wallet/confirm.d.ts.map +1 -1
- package/dist/wallet/confirm.js +1 -0
- package/dist/wallet/confirm.js.map +1 -1
- package/dist/wallet/contract.d.ts.map +1 -1
- package/dist/wallet/contract.js +20 -4
- package/dist/wallet/contract.js.map +1 -1
- package/package.json +5 -4
- package/skills/bitrefill/SKILL.md +152 -52
- package/skills/channel-manager/SKILL.md +59 -0
- package/skills/dca/SKILL.md +48 -0
- package/skills/flashnet-swaps/SKILL.md +158 -0
- package/skills/kaleido-lsps/SKILL.md +34 -17
- package/skills/kaleido-trading/SKILL.md +37 -13
- package/skills/liquidity-optimizer/SKILL.md +91 -0
- package/skills/merchant-finder/SKILL.md +2 -2
- package/skills/portfolio-manager/SKILL.md +67 -0
- package/skills/rgb-lightning-node/SKILL.md +38 -11
- package/skills/spark-wallet/SKILL.md +235 -0
- package/skills/wallet-assistant/SKILL.md +2 -2
- package/src/autonomy/autonomy.test.ts +348 -0
- package/src/autonomy/index.ts +50 -0
- package/src/autonomy/prompt.ts +48 -0
- package/src/autonomy/risk.ts +139 -0
- package/src/autonomy/run-state.ts +144 -0
- package/src/autonomy/scheduler.ts +120 -0
- package/src/autonomy/task-store.ts +167 -0
- package/src/autonomy/types.ts +186 -0
- package/src/bitrefill/contract.test.ts +89 -0
- package/src/bitrefill/contract.ts +190 -0
- package/src/context/compress.test.ts +120 -0
- package/src/context/compress.ts +230 -0
- package/src/engine.test.ts +34 -0
- package/src/engine.ts +35 -4
- package/src/evidence.test.ts +80 -0
- package/src/evidence.ts +114 -0
- package/src/flashnet/contract.test.ts +101 -0
- package/src/flashnet/contract.ts +164 -0
- package/src/funnel.mind.test.ts +390 -0
- package/src/funnel.ts +73 -8
- package/src/index.ts +92 -1
- package/src/kaleidoswap/contract.ts +1 -1
- package/src/knowledge/bitcoin-copilot.ts +96 -2
- package/src/providers/types.ts +18 -0
- package/src/qvac/index.ts +1 -0
- package/src/qvac/parse.ts +20 -0
- package/src/qvac/provider.test.ts +17 -0
- package/src/qvac/provider.ts +62 -2
- package/src/qvac/stream.test.ts +36 -0
- package/src/qvac/stream.ts +54 -1
- package/src/recipe/buy-asset-channel.test.ts +5 -0
- package/src/recipe/buy-asset-channel.ts +6 -3
- package/src/recipe/flashnet-swap.test.ts +114 -0
- package/src/recipe/flashnet-swap.ts +266 -0
- package/src/recipe/kaleidoswap-atomic.test.ts +24 -3
- package/src/recipe/kaleidoswap-atomic.ts +39 -20
- package/src/recipe/kaleidoswap-channel-order.test.ts +38 -0
- package/src/recipe/kaleidoswap-channel-order.ts +27 -9
- package/src/recipe/kaleidoswap-price.ts +7 -1
- package/src/recipe/recipe.test.ts +21 -0
- package/src/recipe/runner.ts +46 -3
- package/src/recipe/swap.ts +16 -1
- package/src/tools/mcp.live.test.ts +116 -0
- package/src/tools/mcp.parse.test.ts +37 -0
- package/src/tools/mcp.ts +55 -9
- package/src/wallet/confirm.test.ts +8 -0
- package/src/wallet/confirm.ts +1 -0
- package/src/wallet/contract.test.ts +10 -0
- package/src/wallet/contract.ts +26 -4
package/src/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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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 ||
|
|
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(
|
|
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 {
|
|
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 {
|
|
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
|
|
222
|
-
'
|
|
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
|
];
|
package/src/providers/types.ts
CHANGED
|
@@ -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
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' } },
|
package/src/qvac/provider.ts
CHANGED
|
@@ -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
|
|
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
|
|
package/src/qvac/stream.test.ts
CHANGED
|
@@ -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
|
});
|