@kaleidorg/mind 0.1.0 → 0.2.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/dist/capabilities.d.ts +34 -0
- package/dist/capabilities.d.ts.map +1 -0
- package/dist/capabilities.js +34 -0
- package/dist/capabilities.js.map +1 -0
- package/dist/context/budget.d.ts +29 -0
- package/dist/context/budget.d.ts.map +1 -0
- package/dist/context/budget.js +36 -0
- package/dist/context/budget.js.map +1 -0
- package/dist/context/builder.d.ts +39 -0
- package/dist/context/builder.d.ts.map +1 -0
- package/dist/context/builder.js +77 -0
- package/dist/context/builder.js.map +1 -0
- package/dist/fastpath/fastpath.d.ts +38 -0
- package/dist/fastpath/fastpath.d.ts.map +1 -0
- package/dist/fastpath/fastpath.js +52 -0
- package/dist/fastpath/fastpath.js.map +1 -0
- package/dist/funnel.d.ts +111 -0
- package/dist/funnel.d.ts.map +1 -0
- package/dist/funnel.js +175 -0
- package/dist/funnel.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -1
- package/dist/knowledge/bitcoin-copilot.d.ts +11 -0
- package/dist/knowledge/bitcoin-copilot.d.ts.map +1 -0
- package/dist/knowledge/bitcoin-copilot.js +155 -0
- package/dist/knowledge/bitcoin-copilot.js.map +1 -0
- package/dist/knowledge/merchants.d.ts +24 -0
- package/dist/knowledge/merchants.d.ts.map +1 -0
- package/dist/knowledge/merchants.js +34 -0
- package/dist/knowledge/merchants.js.map +1 -0
- package/dist/knowledge/wallet.d.ts +34 -0
- package/dist/knowledge/wallet.d.ts.map +1 -0
- package/dist/knowledge/wallet.js +63 -0
- package/dist/knowledge/wallet.js.map +1 -0
- package/dist/memory/store.d.ts +34 -0
- package/dist/memory/store.d.ts.map +1 -0
- package/dist/memory/store.js +103 -0
- package/dist/memory/store.js.map +1 -0
- package/dist/memory/tool.d.ts +9 -0
- package/dist/memory/tool.d.ts.map +1 -0
- package/dist/memory/tool.js +70 -0
- package/dist/memory/tool.js.map +1 -0
- package/dist/memory/types.d.ts +56 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +14 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/rag/retriever.d.ts +30 -0
- package/dist/rag/retriever.d.ts.map +1 -0
- package/dist/rag/retriever.js +72 -0
- package/dist/rag/retriever.js.map +1 -0
- package/dist/rag/tool.d.ts +15 -0
- package/dist/rag/tool.d.ts.map +1 -0
- package/dist/rag/tool.js +42 -0
- package/dist/rag/tool.js.map +1 -0
- package/dist/rag/types.d.ts +44 -0
- package/dist/rag/types.d.ts.map +1 -0
- package/dist/rag/types.js +11 -0
- package/dist/rag/types.js.map +1 -0
- package/dist/rag/vector-store.d.ts +23 -0
- package/dist/rag/vector-store.d.ts.map +1 -0
- package/dist/rag/vector-store.js +72 -0
- package/dist/rag/vector-store.js.map +1 -0
- package/dist/recipe/asset-send.d.ts +15 -0
- package/dist/recipe/asset-send.d.ts.map +1 -0
- package/dist/recipe/asset-send.js +83 -0
- package/dist/recipe/asset-send.js.map +1 -0
- package/dist/recipe/payments.d.ts +15 -0
- package/dist/recipe/payments.d.ts.map +1 -0
- package/dist/recipe/payments.js +119 -0
- package/dist/recipe/payments.js.map +1 -0
- package/dist/recipe/receive.d.ts +14 -0
- package/dist/recipe/receive.d.ts.map +1 -0
- package/dist/recipe/receive.js +109 -0
- package/dist/recipe/receive.js.map +1 -0
- package/dist/recipe/runner.d.ts +42 -0
- package/dist/recipe/runner.d.ts.map +1 -0
- package/dist/recipe/runner.js +94 -0
- package/dist/recipe/runner.js.map +1 -0
- package/dist/recipe/swap.d.ts +16 -0
- package/dist/recipe/swap.d.ts.map +1 -0
- package/dist/recipe/swap.js +73 -0
- package/dist/recipe/swap.js.map +1 -0
- package/dist/recipe/types.d.ts +71 -0
- package/dist/recipe/types.d.ts.map +1 -0
- package/dist/recipe/types.js +13 -0
- package/dist/recipe/types.js.map +1 -0
- package/dist/tools/cli.d.ts +43 -0
- package/dist/tools/cli.d.ts.map +1 -0
- package/dist/tools/cli.js +61 -0
- package/dist/tools/cli.js.map +1 -0
- package/dist/tools/mcp.d.ts +3 -2
- package/dist/tools/mcp.d.ts.map +1 -1
- package/dist/tools/mcp.js +3 -2
- package/dist/tools/mcp.js.map +1 -1
- package/dist/wallet/contract.d.ts +57 -0
- package/dist/wallet/contract.d.ts.map +1 -0
- package/dist/wallet/contract.js +113 -0
- package/dist/wallet/contract.js.map +1 -0
- package/package.json +9 -5
- package/src/capabilities.ts +67 -0
- package/src/context/budget.ts +46 -0
- package/src/context/builder.ts +100 -0
- package/src/context/context.test.ts +83 -0
- package/src/fastpath/fastpath.test.ts +34 -0
- package/src/fastpath/fastpath.ts +70 -0
- package/src/funnel.test.ts +207 -0
- package/src/funnel.ts +260 -0
- package/src/index.ts +85 -0
- package/src/knowledge/bitcoin-copilot.ts +177 -0
- package/src/knowledge/knowledge.test.ts +63 -0
- package/src/knowledge/merchants.ts +49 -0
- package/src/knowledge/wallet.ts +84 -0
- package/src/memory/memory.test.ts +85 -0
- package/src/memory/store.ts +129 -0
- package/src/memory/tool.ts +76 -0
- package/src/memory/types.ts +63 -0
- package/src/rag/rag.test.ts +85 -0
- package/src/rag/retriever.ts +94 -0
- package/src/rag/tool.ts +55 -0
- package/src/rag/types.ts +49 -0
- package/src/rag/vector-store.ts +78 -0
- package/src/recipe/asset-send.ts +79 -0
- package/src/recipe/payments.ts +116 -0
- package/src/recipe/receive.ts +98 -0
- package/src/recipe/recipe.test.ts +193 -0
- package/src/recipe/runner.ts +122 -0
- package/src/recipe/swap.ts +74 -0
- package/src/recipe/types.ts +76 -0
- package/src/tools/cli.test.ts +53 -0
- package/src/tools/cli.ts +98 -0
- package/src/tools/mcp.ts +3 -2
- package/src/wallet/contract.test.ts +89 -0
- package/src/wallet/contract.ts +157 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical multi-L2 wallet tool contract — the single source of truth for
|
|
3
|
+
* KaleidoMind's wallet tools (names + JSON schemas + spend flags).
|
|
4
|
+
*
|
|
5
|
+
* Every surface implements THESE EXACT tools, only the transport differs:
|
|
6
|
+
* - mobile → in-process handlers over the WDK adapters (`bindWalletTools`)
|
|
7
|
+
* - desktop → kaleido-mcp (tools namespaced per layer) + a `kaleido` CLI
|
|
8
|
+
* - eval → stub handlers
|
|
9
|
+
*
|
|
10
|
+
* Because the schemas are identical everywhere, skills are portable and the
|
|
11
|
+
* model comparison is honest. Tools are namespaced per layer (`spark_*`,
|
|
12
|
+
* `rln_*`, `arkade_*`, `liquid_*`); cross-cutting router/helpers are unprefixed.
|
|
13
|
+
*
|
|
14
|
+
* Spend tools (move funds) carry `spend: true` → `requiresConfirmation: true`,
|
|
15
|
+
* so the Engine always pauses for the host's confirm gate before executing.
|
|
16
|
+
*
|
|
17
|
+
* Pure data — no deps, RN-safe.
|
|
18
|
+
*/
|
|
19
|
+
import { InProcessToolSource } from '../tools/in-process.js';
|
|
20
|
+
function t(layer, name, description, properties = {}, required = [], spend = false) {
|
|
21
|
+
return {
|
|
22
|
+
layer,
|
|
23
|
+
name,
|
|
24
|
+
description,
|
|
25
|
+
spend,
|
|
26
|
+
requiresConfirmation: spend,
|
|
27
|
+
parameters: { type: 'object', properties, required },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const sats = { type: 'number', description: 'Amount in satoshis' };
|
|
31
|
+
const asset = { type: 'string', description: "Asset ticker, e.g. 'USDT', 'XAUT', 'BTC'" };
|
|
32
|
+
/** The full contract. Keep descriptions terse — small models read every word. */
|
|
33
|
+
export const WALLET_TOOLS = [
|
|
34
|
+
// ── Spark ──────────────────────────────────────────────────────────────
|
|
35
|
+
t('spark', 'spark_get_balance', 'Get the Spark wallet BTC balance.'),
|
|
36
|
+
t('spark', 'spark_get_address', 'Get a Spark deposit address to receive BTC.'),
|
|
37
|
+
t('spark', 'spark_create_invoice', 'Create a Spark Lightning invoice to receive BTC.', { amount_sats: sats }),
|
|
38
|
+
t('spark', 'spark_send', 'Send BTC from Spark to an address or invoice.', { amount_sats: sats, to: { type: 'string', description: 'Address or invoice' } }, ['amount_sats', 'to'], true),
|
|
39
|
+
// ── RLN / RGB ──────────────────────────────────────────────────────────
|
|
40
|
+
t('rln', 'rln_get_balances', 'Get RLN node balances (BTC + RGB assets).'),
|
|
41
|
+
t('rln', 'rln_get_node_info', 'Get RLN node status and sync state.'),
|
|
42
|
+
t('rln', 'rln_list_channels', 'List the RLN node Lightning channels.'),
|
|
43
|
+
t('rln', 'rln_create_ln_invoice', 'Create a Lightning (BTC) invoice on the RLN node.', { amount_sats: sats }),
|
|
44
|
+
t('rln', 'rln_create_rgb_invoice', 'Create an RGB asset invoice to receive an asset (e.g. USDT).', { asset, amount: { type: 'number', description: 'Asset amount' } }, ['asset', 'amount']),
|
|
45
|
+
t('rln', 'rln_pay_invoice', 'Pay a Lightning invoice from the RLN node.', { invoice: { type: 'string' } }, ['invoice'], true),
|
|
46
|
+
t('rln', 'rln_send_asset', 'Send an RGB asset (e.g. USDT) to a recipient.', { asset, amount: { type: 'number' }, to: { type: 'string' } }, ['asset', 'amount', 'to'], true),
|
|
47
|
+
// ── Arkade ─────────────────────────────────────────────────────────────
|
|
48
|
+
t('arkade', 'arkade_get_balance', 'Get the Arkade wallet balance.'),
|
|
49
|
+
t('arkade', 'arkade_get_address', 'Get an Arkade address to receive funds.'),
|
|
50
|
+
t('arkade', 'arkade_send', 'Send BTC from Arkade to a recipient.', { amount_sats: sats, to: { type: 'string' } }, ['amount_sats', 'to'], true),
|
|
51
|
+
// ── Liquid (later) ─────────────────────────────────────────────────────
|
|
52
|
+
t('liquid', 'liquid_get_balance', 'Get the Liquid wallet balance (L-BTC + assets).'),
|
|
53
|
+
t('liquid', 'liquid_create_invoice', 'Create a Liquid invoice/address to receive (L-BTC or L-USDt).', { asset, amount: { type: 'number' } }),
|
|
54
|
+
t('liquid', 'liquid_send', 'Send a Liquid asset (L-BTC or L-USDt) to a recipient.', { asset, amount: { type: 'number' }, to: { type: 'string' } }, ['asset', 'amount', 'to'], true),
|
|
55
|
+
// ── Core: router + helpers ─────────────────────────────────────────────
|
|
56
|
+
t('core', 'get_balances', 'Get balances across all layers (or one layer).', { layer: { type: 'string', enum: ['spark', 'rln', 'arkade', 'liquid'], description: 'Optional: a single layer' } }),
|
|
57
|
+
t('core', 'resolve_contact', 'Resolve a contact name to a Lightning address / Nostr / preferred rail.', { name: { type: 'string', description: 'Contact name, e.g. "bob"' } }, ['name']),
|
|
58
|
+
t('core', 'get_price', 'Get the current price of an asset, optionally in a fiat currency.', { asset, fiat: { type: 'string', description: "Fiat code, e.g. 'EUR', 'USD'" } }),
|
|
59
|
+
t('core', 'fiat_to_sats', 'Convert a fiat amount to satoshis at the current rate.', { amount: { type: 'number' }, currency: { type: 'string', description: "Fiat code, e.g. 'EUR'" } }, ['amount', 'currency']),
|
|
60
|
+
t('core', 'get_swap_quote', 'Quote a swap between two assets.', { from_asset: asset, to_asset: asset, amount: { type: 'number' } }, ['from_asset', 'to_asset', 'amount']),
|
|
61
|
+
t('core', 'execute_swap', 'Execute a previously quoted swap.', { quote_id: { type: 'string' }, from_asset: asset, to_asset: asset, amount: { type: 'number' } }, [], true),
|
|
62
|
+
// The high-level entry a skill prefers — picks the rail for the asset, or uses `layer`.
|
|
63
|
+
t('core', 'send_payment', 'Send a payment, automatically choosing the best layer for the asset (or use `layer`).', { asset, amount_sats: sats, to: { type: 'string', description: 'Contact, address, or invoice' }, layer: { type: 'string', enum: ['spark', 'rln', 'arkade', 'liquid'] } }, ['to'], true),
|
|
64
|
+
// High-level receive — picks the right invoice/address tool for the asset/layer.
|
|
65
|
+
t('core', 'create_invoice', 'Create an invoice or address to receive funds, choosing the rail for the asset (or use `layer`). Omit amount for an any-amount invoice.', { asset, amount: { type: 'number', description: 'Amount (sats for BTC, asset units otherwise) — optional' }, layer: { type: 'string', enum: ['spark', 'rln', 'arkade', 'liquid'] } }),
|
|
66
|
+
];
|
|
67
|
+
// ── Selectors ───────────────────────────────────────────────────────────────
|
|
68
|
+
export const WALLET_LAYERS = ['spark', 'rln', 'arkade', 'liquid', 'core'];
|
|
69
|
+
/** Names of all spend (fund-moving) tools — these are confirmation-gated. */
|
|
70
|
+
export const SPEND_TOOLS = new Set(WALLET_TOOLS.filter((x) => x.spend).map((x) => x.name));
|
|
71
|
+
export function isSpendTool(name) {
|
|
72
|
+
return SPEND_TOOLS.has(name);
|
|
73
|
+
}
|
|
74
|
+
export function getWalletTool(name) {
|
|
75
|
+
return WALLET_TOOLS.find((x) => x.name === name);
|
|
76
|
+
}
|
|
77
|
+
/** Pick the contract tools for the given layers (core helpers included by default). */
|
|
78
|
+
export function walletTools(opts = {}) {
|
|
79
|
+
const layers = new Set(opts.layers ?? ['spark', 'rln', 'arkade', 'liquid']);
|
|
80
|
+
if (opts.includeCore !== false)
|
|
81
|
+
layers.add('core');
|
|
82
|
+
return WALLET_TOOLS.filter((x) => layers.has(x.layer));
|
|
83
|
+
}
|
|
84
|
+
/** Strip to plain ToolDefs (drop the layer/spend metadata). */
|
|
85
|
+
export function toToolDefs(tools) {
|
|
86
|
+
return tools.map(({ name, description, parameters, requiresConfirmation }) => ({ name, description, parameters, requiresConfirmation }));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Bind contract tools to in-process handlers → an InProcessToolSource. The
|
|
90
|
+
* mobile (and eval) binding: pass a map of `{ toolName: handler }` and you get a
|
|
91
|
+
* ToolSource implementing the canonical schemas with spend flags preserved.
|
|
92
|
+
*/
|
|
93
|
+
export function bindWalletTools(handlers, opts = {}) {
|
|
94
|
+
const tools = walletTools(opts);
|
|
95
|
+
const bound = [];
|
|
96
|
+
for (const def of tools) {
|
|
97
|
+
const handler = handlers[def.name];
|
|
98
|
+
if (!handler) {
|
|
99
|
+
if (opts.allowMissing)
|
|
100
|
+
continue;
|
|
101
|
+
throw new Error(`bindWalletTools: no handler for "${def.name}"`);
|
|
102
|
+
}
|
|
103
|
+
bound.push({
|
|
104
|
+
name: def.name,
|
|
105
|
+
description: def.description,
|
|
106
|
+
parameters: def.parameters,
|
|
107
|
+
requiresConfirmation: def.requiresConfirmation,
|
|
108
|
+
handler,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return new InProcessToolSource(opts.id ?? 'wallet', bound);
|
|
112
|
+
}
|
|
113
|
+
//# sourceMappingURL=contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/wallet/contract.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kaleidorg/mind",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Local-first reasoning + function-calling engine for KaleidoSwap. QVAC-powered.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -16,20 +16,24 @@
|
|
|
16
16
|
"types": "./dist/index.d.ts",
|
|
17
17
|
"exports": {
|
|
18
18
|
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
19
20
|
"import": "./dist/index.js",
|
|
20
|
-
"
|
|
21
|
+
"default": "./dist/index.js"
|
|
21
22
|
},
|
|
22
23
|
"./mcp": {
|
|
24
|
+
"types": "./dist/tools/mcp.d.ts",
|
|
23
25
|
"import": "./dist/tools/mcp.js",
|
|
24
|
-
"
|
|
26
|
+
"default": "./dist/tools/mcp.js"
|
|
25
27
|
},
|
|
26
28
|
"./skills": {
|
|
29
|
+
"types": "./dist/skills/loader.d.ts",
|
|
27
30
|
"import": "./dist/skills/loader.js",
|
|
28
|
-
"
|
|
31
|
+
"default": "./dist/skills/loader.js"
|
|
29
32
|
},
|
|
30
33
|
"./logger": {
|
|
34
|
+
"types": "./dist/logger.d.ts",
|
|
31
35
|
"import": "./dist/logger.js",
|
|
32
|
-
"
|
|
36
|
+
"default": "./dist/logger.js"
|
|
33
37
|
}
|
|
34
38
|
},
|
|
35
39
|
"files": [
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability profiling — decide which features to turn on for a given device +
|
|
3
|
+
* model, so a 2 GB phone running a 0.6B model doesn't try to do everything.
|
|
4
|
+
*
|
|
5
|
+
* Pure heuristic. Hosts call this once (with device RAM + the model's context
|
|
6
|
+
* size + whether an embedder is available) and get back feature flags + sane
|
|
7
|
+
* retrieval defaults to feed the ContextBuilder.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { contextBudgetTokens } from './context/budget.js';
|
|
11
|
+
|
|
12
|
+
export interface CapabilityInput {
|
|
13
|
+
/** Total device RAM in bytes (e.g. react-native-device-info getTotalMemory). */
|
|
14
|
+
ramBytes?: number;
|
|
15
|
+
/** The loaded model's context window in tokens (modelConfig.ctx_size). */
|
|
16
|
+
modelCtxTokens: number;
|
|
17
|
+
/** Whether an EmbeddingProvider is wired (QVAC embed, etc.). */
|
|
18
|
+
hasEmbeddings?: boolean;
|
|
19
|
+
/** Running inference on a remote provider (desktop/server) — relaxes limits. */
|
|
20
|
+
delegated?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface MindCapabilities {
|
|
24
|
+
/** Long-term memory (cheap — on unless the window is tiny). */
|
|
25
|
+
memory: boolean;
|
|
26
|
+
/** Semantic recall for memory (needs embeddings). */
|
|
27
|
+
semanticMemory: boolean;
|
|
28
|
+
/** Retrieval-augmented generation (needs embeddings + enough RAM/context). */
|
|
29
|
+
rag: boolean;
|
|
30
|
+
/** Token budget for injected system context. */
|
|
31
|
+
contextBudgetTokens: number;
|
|
32
|
+
/** Memories to auto-recall into context. */
|
|
33
|
+
topKMemory: number;
|
|
34
|
+
/** Knowledge chunks the search_knowledge tool returns. */
|
|
35
|
+
topKRag: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const GiB = 1024 * 1024 * 1024;
|
|
39
|
+
|
|
40
|
+
export function capabilityProfile(input: CapabilityInput): MindCapabilities {
|
|
41
|
+
const ramGb = input.delegated ? Infinity : (input.ramBytes ?? 0) / GiB;
|
|
42
|
+
const ctx = input.modelCtxTokens;
|
|
43
|
+
const hasEmb = !!input.hasEmbeddings;
|
|
44
|
+
|
|
45
|
+
const budget = contextBudgetTokens(ctx);
|
|
46
|
+
|
|
47
|
+
// Memory: on whenever there's room for a few lines. Semantic needs embeddings.
|
|
48
|
+
const memory = budget >= 256;
|
|
49
|
+
const semanticMemory = memory && hasEmb;
|
|
50
|
+
|
|
51
|
+
// RAG is the expensive one: needs embeddings, a non-tiny context window, and
|
|
52
|
+
// (on-device) enough RAM to hold an embedding model + index.
|
|
53
|
+
const rag = hasEmb && ctx >= 4096 && (input.delegated || ramGb >= 3);
|
|
54
|
+
|
|
55
|
+
// Scale how much we pull in with the available window.
|
|
56
|
+
const topKMemory = budget >= 1500 ? 4 : budget >= 700 ? 3 : 2;
|
|
57
|
+
const topKRag = rag ? (budget >= 2500 ? 5 : 3) : 0;
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
memory,
|
|
61
|
+
semanticMemory,
|
|
62
|
+
rag,
|
|
63
|
+
contextBudgetTokens: budget,
|
|
64
|
+
topKMemory,
|
|
65
|
+
topKRag,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context budgeting — the hardware-aware part.
|
|
3
|
+
*
|
|
4
|
+
* Small on-device models have small context windows (a 0.6B may run at 2k, a
|
|
5
|
+
* desktop 8B at 8k+). Memory, skills, RAG and tool schemas all compete for that
|
|
6
|
+
* window. These helpers turn a model's `ctx_size` into a token budget for the
|
|
7
|
+
* injected context, so we never overflow a tiny model.
|
|
8
|
+
*
|
|
9
|
+
* Token estimation is deliberately rough (≈ 4 chars/token) — no tokenizer dep.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Rough token count for a string (no tokenizer — ~4 chars/token). */
|
|
13
|
+
export function estimateTokens(text: string): number {
|
|
14
|
+
return Math.ceil(text.length / 4);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Trim text to at most `maxTokens` (on a word boundary where possible). */
|
|
18
|
+
export function clampToTokens(text: string, maxTokens: number): string {
|
|
19
|
+
const maxChars = maxTokens * 4;
|
|
20
|
+
if (text.length <= maxChars) return text;
|
|
21
|
+
const cut = text.slice(0, maxChars);
|
|
22
|
+
const sp = cut.lastIndexOf(' ');
|
|
23
|
+
return (sp > maxChars * 0.6 ? cut.slice(0, sp) : cut).trimEnd() + '…';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BudgetReserves {
|
|
27
|
+
/** Tokens to leave for the model's reply. Default 512. */
|
|
28
|
+
output?: number;
|
|
29
|
+
/** Tokens to budget for tool schemas the engine sends. Default 600. */
|
|
30
|
+
tools?: number;
|
|
31
|
+
/** Tokens to budget for the running conversation (user + history). Default 768. */
|
|
32
|
+
conversation?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Tokens available for the *injected system context* (soul + instructions +
|
|
37
|
+
* skill + memory + RAG), given the model's context window and what else must
|
|
38
|
+
* fit. Never negative; clamped to a sane floor so something always gets in.
|
|
39
|
+
*/
|
|
40
|
+
export function contextBudgetTokens(ctxSize: number, reserves: BudgetReserves = {}): number {
|
|
41
|
+
const output = reserves.output ?? 512;
|
|
42
|
+
const tools = reserves.tools ?? 600;
|
|
43
|
+
const conversation = reserves.conversation ?? 768;
|
|
44
|
+
const available = ctxSize - output - tools - conversation;
|
|
45
|
+
return Math.max(256, available);
|
|
46
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ContextBuilder — assembles the system prompt for a turn from, in priority
|
|
3
|
+
* order: the agent's identity (soul) → operating instructions → the active
|
|
4
|
+
* skill playbook → auto-recalled memories → auto-retrieved knowledge, trimmed
|
|
5
|
+
* to a token budget so it never overflows a small model.
|
|
6
|
+
*
|
|
7
|
+
* Memory and RAG can ALSO be tools (the model pulls them in itself). Use this
|
|
8
|
+
* for the always-on slice (identity + the most relevant memory/snippet); use
|
|
9
|
+
* the tools (`recall`, `search_knowledge`) for deeper, on-demand lookups.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AgentProfile, MemoryStore } from '../memory/types.js';
|
|
13
|
+
import type { Retriever } from '../rag/retriever.js';
|
|
14
|
+
import { clampToTokens, estimateTokens } from './budget.js';
|
|
15
|
+
|
|
16
|
+
export interface ContextBuilderOptions {
|
|
17
|
+
profile: AgentProfile;
|
|
18
|
+
/** Auto-recall relevant memories into context. */
|
|
19
|
+
memory?: MemoryStore;
|
|
20
|
+
/** Auto-retrieve relevant knowledge into context. */
|
|
21
|
+
retriever?: Retriever;
|
|
22
|
+
/** Max tokens for the whole assembled system prompt (see contextBudgetTokens). */
|
|
23
|
+
budgetTokens?: number;
|
|
24
|
+
/** Memories to recall (default 3). */
|
|
25
|
+
topKMemory?: number;
|
|
26
|
+
/** Knowledge chunks to retrieve (default 0 — prefer the search_knowledge tool). */
|
|
27
|
+
topKRag?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface BuildInput {
|
|
31
|
+
/** The user's message — drives memory recall + retrieval. */
|
|
32
|
+
query: string;
|
|
33
|
+
/** A composed skill playbook to splice in (from SkillRegistry.compose). */
|
|
34
|
+
skillSystem?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class ContextBuilder {
|
|
38
|
+
constructor(private readonly opts: ContextBuilderOptions) {}
|
|
39
|
+
|
|
40
|
+
async build(input: BuildInput): Promise<{ system: string }> {
|
|
41
|
+
const { profile } = this.opts;
|
|
42
|
+
const budget = this.opts.budgetTokens ?? 1024;
|
|
43
|
+
const sections: string[] = [];
|
|
44
|
+
let used = 0;
|
|
45
|
+
|
|
46
|
+
const add = (text: string, { force = false } = {}): void => {
|
|
47
|
+
const t = text.trim();
|
|
48
|
+
if (!t) return;
|
|
49
|
+
const cost = estimateTokens(t) + 1;
|
|
50
|
+
if (!force && used + cost > budget) {
|
|
51
|
+
// Try to fit a trimmed version of optional sections.
|
|
52
|
+
const room = budget - used - 1;
|
|
53
|
+
if (room < 40) return;
|
|
54
|
+
const trimmed = clampToTokens(t, room);
|
|
55
|
+
sections.push(trimmed);
|
|
56
|
+
used += estimateTokens(trimmed) + 1;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
sections.push(t);
|
|
60
|
+
used += cost;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// 1. Identity (always) — name + soul.
|
|
64
|
+
add(`# ${profile.name}\n${profile.soul}`, { force: true });
|
|
65
|
+
|
|
66
|
+
// 2. Operating instructions (always, if any).
|
|
67
|
+
if (profile.instructions) add(`## Instructions\n${profile.instructions}`, { force: true });
|
|
68
|
+
|
|
69
|
+
// 3. Active skill playbook.
|
|
70
|
+
if (input.skillSystem) add(input.skillSystem);
|
|
71
|
+
|
|
72
|
+
// 4. Auto-recalled memory.
|
|
73
|
+
const kMem = this.opts.topKMemory ?? 3;
|
|
74
|
+
if (this.opts.memory && kMem > 0) {
|
|
75
|
+
try {
|
|
76
|
+
const mems = await this.opts.memory.search({ text: input.query, limit: kMem });
|
|
77
|
+
if (mems.length) {
|
|
78
|
+
add(`## What you remember\n${mems.map((m) => `- (${m.kind}) ${m.text}`).join('\n')}`);
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
/* memory is best-effort */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// 5. Auto-retrieved knowledge (opt-in; default prefers the tool).
|
|
86
|
+
const kRag = this.opts.topKRag ?? 0;
|
|
87
|
+
if (this.opts.retriever && kRag > 0) {
|
|
88
|
+
try {
|
|
89
|
+
const hits = await this.opts.retriever.search(input.query, kRag);
|
|
90
|
+
if (hits.length) {
|
|
91
|
+
add(`## Relevant context\n${hits.map((h) => h.text).join('\n\n')}`);
|
|
92
|
+
}
|
|
93
|
+
} catch {
|
|
94
|
+
/* retrieval is best-effort */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return { system: sections.join('\n\n') };
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/** Context budget + builder + capability tests. */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { estimateTokens, clampToTokens, contextBudgetTokens } from './budget.js';
|
|
5
|
+
import { ContextBuilder } from './builder.js';
|
|
6
|
+
import { capabilityProfile } from '../capabilities.js';
|
|
7
|
+
import { InMemoryMemoryStore } from '../memory/store.js';
|
|
8
|
+
import type { AgentProfile } from '../memory/types.js';
|
|
9
|
+
|
|
10
|
+
const profile: AgentProfile = {
|
|
11
|
+
name: 'KaleidoMind',
|
|
12
|
+
soul: 'You are a sovereign, local-first Bitcoin assistant. Calm, precise, private.',
|
|
13
|
+
instructions: 'Never reveal seeds. Confirm spends.',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('budget helpers', () => {
|
|
17
|
+
it('estimates + clamps by ~4 chars/token', () => {
|
|
18
|
+
expect(estimateTokens('a'.repeat(40))).toBe(10);
|
|
19
|
+
const clamped = clampToTokens('word '.repeat(100), 10);
|
|
20
|
+
expect(clamped.length).toBeLessThanOrEqual(10 * 4 + 1);
|
|
21
|
+
expect(clamped.endsWith('…')).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('reserves output/tools/conversation from the window', () => {
|
|
25
|
+
expect(contextBudgetTokens(8192)).toBe(8192 - 512 - 600 - 768);
|
|
26
|
+
expect(contextBudgetTokens(2048)).toBeGreaterThanOrEqual(256); // floor
|
|
27
|
+
expect(contextBudgetTokens(512)).toBe(256); // clamped to floor
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('ContextBuilder', () => {
|
|
32
|
+
it('always includes identity + instructions, then memory', async () => {
|
|
33
|
+
const memory = new InMemoryMemoryStore({ now: () => 1 });
|
|
34
|
+
await memory.add({ text: 'user prefers sats over USD', kind: 'preference' });
|
|
35
|
+
const builder = new ContextBuilder({ profile, memory, topKMemory: 3, budgetTokens: 1024 });
|
|
36
|
+
|
|
37
|
+
const { system } = await builder.build({ query: 'show my balance in sats' });
|
|
38
|
+
expect(system).toMatch(/# KaleidoMind/);
|
|
39
|
+
expect(system).toMatch(/Never reveal seeds/);
|
|
40
|
+
expect(system).toMatch(/What you remember/);
|
|
41
|
+
expect(system).toMatch(/prefers sats/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('keeps identity even under a tiny budget (drops optional sections)', async () => {
|
|
45
|
+
const memory = new InMemoryMemoryStore({ now: () => 1 });
|
|
46
|
+
await memory.add({ text: 'a'.repeat(400), kind: 'note' });
|
|
47
|
+
const builder = new ContextBuilder({ profile, memory, topKMemory: 3, budgetTokens: 40 });
|
|
48
|
+
const { system } = await builder.build({ query: 'x' });
|
|
49
|
+
expect(system).toMatch(/# KaleidoMind/); // identity survives
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('splices in a composed skill playbook', async () => {
|
|
53
|
+
const builder = new ContextBuilder({ profile, budgetTokens: 2048 });
|
|
54
|
+
const { system } = await builder.build({
|
|
55
|
+
query: 'buy a gift card',
|
|
56
|
+
skillSystem: '## Active skill: bitrefill\nRoute the purchase.',
|
|
57
|
+
});
|
|
58
|
+
expect(system).toMatch(/Active skill: bitrefill/);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('capabilityProfile', () => {
|
|
63
|
+
it('low-end phone: memory yes, RAG no', () => {
|
|
64
|
+
const c = capabilityProfile({ ramBytes: 2 * 1024 ** 3, modelCtxTokens: 2048, hasEmbeddings: true });
|
|
65
|
+
expect(c.memory).toBe(true);
|
|
66
|
+
expect(c.rag).toBe(false); // ctx too small + low RAM
|
|
67
|
+
expect(c.topKRag).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('desktop / delegated: RAG on', () => {
|
|
71
|
+
const c = capabilityProfile({ modelCtxTokens: 8192, hasEmbeddings: true, delegated: true });
|
|
72
|
+
expect(c.rag).toBe(true);
|
|
73
|
+
expect(c.topKRag).toBeGreaterThan(0);
|
|
74
|
+
expect(c.semanticMemory).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('no embeddings → no RAG, no semantic memory', () => {
|
|
78
|
+
const c = capabilityProfile({ ramBytes: 16 * 1024 ** 3, modelCtxTokens: 8192, hasEmbeddings: false });
|
|
79
|
+
expect(c.rag).toBe(false);
|
|
80
|
+
expect(c.semanticMemory).toBe(false);
|
|
81
|
+
expect(c.memory).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { FastPath, WALLET_FAST_INTENTS } from './fastpath.js';
|
|
3
|
+
|
|
4
|
+
const fp = new FastPath(WALLET_FAST_INTENTS);
|
|
5
|
+
|
|
6
|
+
describe('FastPath (Tier-0)', () => {
|
|
7
|
+
it('routes balance asks → get_balances', () => {
|
|
8
|
+
expect(fp.select("what's my balance")?.tool).toBe('get_balances');
|
|
9
|
+
expect(fp.select('how much do i have')?.tool).toBe('get_balances');
|
|
10
|
+
expect(fp.select('show my funds')?.tool).toBe('get_balances');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('routes receive-address asks → spark_get_address', () => {
|
|
14
|
+
expect(fp.select('give me a receive address')?.tool).toBe('spark_get_address');
|
|
15
|
+
expect(fp.select('what is my address')?.tool).toBe('spark_get_address');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('routes price asks → get_price', () => {
|
|
19
|
+
expect(fp.select('btc price')?.tool).toBe('get_price');
|
|
20
|
+
expect(fp.select('how much is bitcoin')?.tool).toBe('get_price');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('does NOT fire on spend / compound requests', () => {
|
|
24
|
+
expect(fp.select('pay bob 3 eur')).toBeNull();
|
|
25
|
+
expect(fp.select('send 5000 sats to alice')).toBeNull();
|
|
26
|
+
expect(fp.select('check my balance and then send 1000 to bob')).toBeNull(); // compound → LLM
|
|
27
|
+
expect(fp.select('swap 10 usdt for btc')).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('does NOT fire on unrelated chatter', () => {
|
|
31
|
+
expect(fp.select('hello there')).toBeNull();
|
|
32
|
+
expect(fp.select('what can you do')).toBeNull();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-0 deterministic fast-path — answer the common, unambiguous wallet asks
|
|
3
|
+
* with NO LLM at all. "balance", "receive address", "btc price" map straight to
|
|
4
|
+
* a single tool call. The model is reserved for genuine ambiguity.
|
|
5
|
+
*
|
|
6
|
+
* This is the biggest mobile UX lever: ~60-80% of wallet requests are simple,
|
|
7
|
+
* and our eval showed tiny models are slow + weak at args — so skip them here.
|
|
8
|
+
*
|
|
9
|
+
* Matchers are intentionally CONSERVATIVE: when in doubt, return null and let
|
|
10
|
+
* the recipe / agentic loop handle it. Under-firing is fine; mis-firing is not.
|
|
11
|
+
*
|
|
12
|
+
* Pure data — no deps. The host executes the returned tool + renders the result.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface FastIntent {
|
|
16
|
+
name: string;
|
|
17
|
+
/** Contract tool to call when this intent matches. */
|
|
18
|
+
tool: string;
|
|
19
|
+
/** True only when this intent UNAMBIGUOUSLY matches the text. */
|
|
20
|
+
match: (text: string) => boolean;
|
|
21
|
+
/** Optional args derived from the text (default: none). */
|
|
22
|
+
args?: (text: string) => Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface FastHit {
|
|
26
|
+
intent: FastIntent;
|
|
27
|
+
tool: string;
|
|
28
|
+
args: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class FastPath {
|
|
32
|
+
private intents: FastIntent[];
|
|
33
|
+
constructor(intents: FastIntent[] = []) {
|
|
34
|
+
this.intents = [...intents];
|
|
35
|
+
}
|
|
36
|
+
add(intent: FastIntent): void {
|
|
37
|
+
this.intents.push(intent);
|
|
38
|
+
}
|
|
39
|
+
list(): FastIntent[] {
|
|
40
|
+
return [...this.intents];
|
|
41
|
+
}
|
|
42
|
+
/** The first unambiguously-matching intent, or null. */
|
|
43
|
+
select(text: string): FastHit | null {
|
|
44
|
+
const intent = this.intents.find((i) => i.match(text));
|
|
45
|
+
return intent ? { intent, tool: intent.tool, args: intent.args?.(text) ?? {} } : null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// A "spend or compound" guard — never fast-path anything that moves money or
|
|
50
|
+
// chains another action ("send", "pay", "and then", "swap").
|
|
51
|
+
const ACTIONY = /\b(send|pay|transfer|swap|buy|sell|then|after that)\b/i;
|
|
52
|
+
|
|
53
|
+
/** Default wallet read intents (balance / receive address / price). */
|
|
54
|
+
export const WALLET_FAST_INTENTS: FastIntent[] = [
|
|
55
|
+
{
|
|
56
|
+
name: 'balance',
|
|
57
|
+
tool: 'get_balances',
|
|
58
|
+
match: (t) => !ACTIONY.test(t) && /\b(balance|funds|how much (do i|have i|i have)|how much.* (do i have|in my wallet))\b/i.test(t),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'address',
|
|
62
|
+
tool: 'spark_get_address',
|
|
63
|
+
match: (t) => !ACTIONY.test(t) && /\b(receive address|deposit address|my address|an address|get .*address|where.* receive)\b/i.test(t),
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: 'price',
|
|
67
|
+
tool: 'get_price',
|
|
68
|
+
match: (t) => !ACTIONY.test(t) && /\b(btc price|bitcoin price|price of (btc|bitcoin)|how much is (a |one )?(btc|bitcoin))\b/i.test(t),
|
|
69
|
+
},
|
|
70
|
+
];
|