@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
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomy — the agent's task brain.
|
|
3
|
+
*
|
|
4
|
+
* This is the half of "the agent's memory" the {@link ../memory/types MemoryStore}
|
|
5
|
+
* (soul + facts) does NOT cover: the *operational* state nanobot kept across
|
|
6
|
+
* `tasks.json` + `cron/jobs.json` + its run history. Lifted into core so every
|
|
7
|
+
* host (desktop sidecar, agent, cli) runs the same autonomous loop — storage
|
|
8
|
+
* and timers are injected (fs/SQLite on Node, AsyncStorage on RN), the logic is
|
|
9
|
+
* pure TS with zero runtime deps.
|
|
10
|
+
*
|
|
11
|
+
* Three pieces:
|
|
12
|
+
* - TaskStore — the registry of scheduled/manual tasks (was tasks-store.ts)
|
|
13
|
+
* - TaskRunLog — what each task did, when, and what it cost (was agent-state.ts)
|
|
14
|
+
* - Scheduler — fires due tasks on their interval (was nanobot cron)
|
|
15
|
+
*
|
|
16
|
+
* Spend safety lives alongside in {@link ./risk}.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Capital earmarked for a task — isolates how much a single task may touch. */
|
|
20
|
+
export interface TaskAllocation {
|
|
21
|
+
/** Satoshis of BTC the task may spend. */
|
|
22
|
+
btcSat: number;
|
|
23
|
+
/** USDT (display units) the task may spend. */
|
|
24
|
+
usdt: number;
|
|
25
|
+
/** XAUT (display units) the task may spend. */
|
|
26
|
+
xaut: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A zero allocation — the default for read-only / monitoring tasks. */
|
|
30
|
+
export const ZERO_ALLOCATION: TaskAllocation = { btcSat: 0, usdt: 0, xaut: 0 };
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A scheduled (or manual) autonomous task — the unit nanobot stored in
|
|
34
|
+
* `tasks.json`. A task names a skill to run on an interval, with an optional
|
|
35
|
+
* capital budget and an enable switch.
|
|
36
|
+
*/
|
|
37
|
+
export interface AgentTask {
|
|
38
|
+
id: string;
|
|
39
|
+
name: string;
|
|
40
|
+
description: string;
|
|
41
|
+
/** Skill that scopes the run, e.g. 'portfolio-manager'. */
|
|
42
|
+
skill: string;
|
|
43
|
+
/** Seconds between runs. 0 = manual-only (never auto-fires). */
|
|
44
|
+
scheduleSec: number;
|
|
45
|
+
/** Run once immediately when the scheduler starts. */
|
|
46
|
+
runOnStartup: boolean;
|
|
47
|
+
/** Capital this task is allowed to move. */
|
|
48
|
+
allocation: TaskAllocation;
|
|
49
|
+
enabled: boolean;
|
|
50
|
+
/** Epoch ms. */
|
|
51
|
+
createdAt: number;
|
|
52
|
+
/** Epoch ms of the last run, or null if never run. */
|
|
53
|
+
lastRunAt: number | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* What `create` accepts — id/createdAt/lastRunAt are filled by the store, and
|
|
58
|
+
* allocation/runOnStartup default when omitted.
|
|
59
|
+
*/
|
|
60
|
+
export type NewTask = Omit<
|
|
61
|
+
AgentTask,
|
|
62
|
+
'id' | 'createdAt' | 'lastRunAt' | 'allocation' | 'runOnStartup'
|
|
63
|
+
> &
|
|
64
|
+
Partial<Pick<AgentTask, 'id' | 'createdAt' | 'lastRunAt' | 'allocation' | 'runOnStartup'>>;
|
|
65
|
+
|
|
66
|
+
/** A default/seed task — carries a stable id so seeding is idempotent. */
|
|
67
|
+
export type TaskSeed = NewTask & { id: string };
|
|
68
|
+
|
|
69
|
+
/** The task registry. Mirrors {@link ../memory/types.MemoryStore}'s shape. */
|
|
70
|
+
export interface TaskStore {
|
|
71
|
+
list(): Promise<AgentTask[]>;
|
|
72
|
+
get(id: string): Promise<AgentTask | null>;
|
|
73
|
+
create(input: NewTask): Promise<AgentTask>;
|
|
74
|
+
/** Patch a task. id/createdAt are immutable. Returns null if not found. */
|
|
75
|
+
update(id: string, patch: Partial<Omit<AgentTask, 'id' | 'createdAt'>>): Promise<AgentTask | null>;
|
|
76
|
+
remove(id: string): Promise<boolean>;
|
|
77
|
+
/** Insert any seed whose id isn't already present. Returns the ones added. */
|
|
78
|
+
seedDefaults(seeds: TaskSeed[]): Promise<AgentTask[]>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Injected persistence — load once, save on every mutation. */
|
|
82
|
+
export interface TaskStoreIO {
|
|
83
|
+
load(): Promise<AgentTask[]>;
|
|
84
|
+
save(tasks: AgentTask[]): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Run history ────────────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
/** Token + dollar cost of a single run. */
|
|
90
|
+
export interface TaskRunCost {
|
|
91
|
+
usd: number;
|
|
92
|
+
inputTokens: number;
|
|
93
|
+
outputTokens: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Aggregated stats for one task across all its runs. */
|
|
97
|
+
export interface TaskStats {
|
|
98
|
+
runs: number;
|
|
99
|
+
errors: number;
|
|
100
|
+
lastRunAt: number | null;
|
|
101
|
+
lastDurationMs: number | null;
|
|
102
|
+
lastToolCalls: number | null;
|
|
103
|
+
lastError: string | null;
|
|
104
|
+
/** First ~800 chars of the last final response. */
|
|
105
|
+
lastText: string | null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** One completed run, newest-first in the recent ring buffer. */
|
|
109
|
+
export interface TaskRunRecord {
|
|
110
|
+
taskId: string;
|
|
111
|
+
taskName: string;
|
|
112
|
+
/** Epoch ms the run started. */
|
|
113
|
+
startedAt: number;
|
|
114
|
+
durationMs: number;
|
|
115
|
+
toolCalls: number;
|
|
116
|
+
ok: boolean;
|
|
117
|
+
error: string | null;
|
|
118
|
+
/** Final response text (truncated by the log). */
|
|
119
|
+
text: string;
|
|
120
|
+
cost: TaskRunCost;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A serializable point-in-time view of the run log, for persistence. */
|
|
124
|
+
export interface RunLogSnapshot {
|
|
125
|
+
stats: Record<string, TaskStats>;
|
|
126
|
+
recent: TaskRunRecord[];
|
|
127
|
+
cumulative: TaskRunCost;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Injected persistence for the run log. */
|
|
131
|
+
export interface RunLogIO {
|
|
132
|
+
load(): Promise<RunLogSnapshot | null>;
|
|
133
|
+
save(snapshot: RunLogSnapshot): Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Scheduler ──────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
/** The result of running a task once. The host's `run` callback returns this. */
|
|
139
|
+
export interface TaskRunOutcome {
|
|
140
|
+
ok: boolean;
|
|
141
|
+
text?: string;
|
|
142
|
+
toolCalls?: number;
|
|
143
|
+
error?: string;
|
|
144
|
+
cost?: Partial<TaskRunCost>;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Host-provided runner — typically wraps `Funnel.runTurn(buildTaskPrompt(task))`. */
|
|
148
|
+
export type RunTask = (task: AgentTask) => Promise<TaskRunOutcome>;
|
|
149
|
+
|
|
150
|
+
/** Opaque timer handle so the scheduler stays platform-agnostic. */
|
|
151
|
+
export type TimerHandle = unknown;
|
|
152
|
+
|
|
153
|
+
export interface SchedulerOptions {
|
|
154
|
+
store: TaskStore;
|
|
155
|
+
/** Runs a task and resolves with its outcome. Errors are caught by the scheduler. */
|
|
156
|
+
run: RunTask;
|
|
157
|
+
/** Notified after every run (success or error) — wire to a {@link TaskRunLog}. */
|
|
158
|
+
onOutcome?: (task: AgentTask, outcome: TaskRunOutcome, durationMs: number) => void;
|
|
159
|
+
/** Diagnostics sink. Default: silent. */
|
|
160
|
+
log?: (msg: string) => void;
|
|
161
|
+
/** Injectable clock. Default: Date.now. */
|
|
162
|
+
now?: () => number;
|
|
163
|
+
/** How often `start()` evaluates due tasks, ms. Default 30_000. */
|
|
164
|
+
tickMs?: number;
|
|
165
|
+
/** Max tasks running at once. Default 1 (serial — safest for a wallet). */
|
|
166
|
+
concurrency?: number;
|
|
167
|
+
/** Injectable timer. Default: setInterval. */
|
|
168
|
+
setTimer?: (fn: () => void, ms: number) => TimerHandle;
|
|
169
|
+
/** Injectable timer-clear. Default: clearInterval. */
|
|
170
|
+
clearTimer?: (handle: TimerHandle) => void;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface TaskScheduler {
|
|
174
|
+
/** Begin firing due tasks; runs `runOnStartup` tasks immediately. Idempotent. */
|
|
175
|
+
start(): void;
|
|
176
|
+
/** Stop firing. In-flight runs finish; no new ones start. */
|
|
177
|
+
stop(): void;
|
|
178
|
+
/** Evaluate all tasks once and run those that are due. Safe to call directly (tests). */
|
|
179
|
+
tick(): Promise<void>;
|
|
180
|
+
/** Force-run a task now regardless of schedule/enabled. Null if unknown/already running. */
|
|
181
|
+
runNow(id: string): Promise<TaskRunOutcome | null>;
|
|
182
|
+
/** Ids of tasks currently running. */
|
|
183
|
+
active(): string[];
|
|
184
|
+
/** Whether `start()` is in effect. */
|
|
185
|
+
isRunning(): boolean;
|
|
186
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
BITREFILL_TOOLS,
|
|
4
|
+
BITREFILL_SPEND_TOOLS,
|
|
5
|
+
isBitrefillSpendTool,
|
|
6
|
+
getBitrefillTool,
|
|
7
|
+
bindBitrefillTools,
|
|
8
|
+
type BitrefillHandler,
|
|
9
|
+
} from './contract.js';
|
|
10
|
+
|
|
11
|
+
describe('BITREFILL_TOOLS — shape invariants', () => {
|
|
12
|
+
it('exposes the expected tool names in order', () => {
|
|
13
|
+
expect(BITREFILL_TOOLS.map((t) => t.name)).toEqual([
|
|
14
|
+
'bitrefill_search',
|
|
15
|
+
'bitrefill_get_product',
|
|
16
|
+
'bitrefill_get_balance',
|
|
17
|
+
'bitrefill_create_invoice',
|
|
18
|
+
'bitrefill_get_invoice',
|
|
19
|
+
'bitrefill_get_order',
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('every tool has an object parameters schema', () => {
|
|
24
|
+
for (const t of BITREFILL_TOOLS) {
|
|
25
|
+
expect((t.parameters as any)?.type).toBe('object');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('aligns spend ↔ requiresConfirmation', () => {
|
|
30
|
+
for (const t of BITREFILL_TOOLS) {
|
|
31
|
+
expect(!!t.spend).toBe(!!t.requiresConfirmation);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('marks only bitrefill_create_invoice as spend', () => {
|
|
36
|
+
expect([...BITREFILL_SPEND_TOOLS]).toEqual(['bitrefill_create_invoice']);
|
|
37
|
+
expect(isBitrefillSpendTool('bitrefill_create_invoice')).toBe(true);
|
|
38
|
+
expect(isBitrefillSpendTool('bitrefill_search')).toBe(false);
|
|
39
|
+
expect(isBitrefillSpendTool('bitrefill_get_balance')).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('getBitrefillTool returns by name', () => {
|
|
43
|
+
expect(getBitrefillTool('bitrefill_get_product')?.name).toBe('bitrefill_get_product');
|
|
44
|
+
expect(getBitrefillTool('nope')).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('create_invoice requires products + payment_method', () => {
|
|
48
|
+
const def = getBitrefillTool('bitrefill_create_invoice')!;
|
|
49
|
+
expect((def.parameters as any).required).toEqual(['products', 'payment_method']);
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('bindBitrefillTools', () => {
|
|
54
|
+
const echoHandlers = (): Record<string, BitrefillHandler> => ({
|
|
55
|
+
bitrefill_search: async (a) => ({ ok: true, t: 'search', args: a }),
|
|
56
|
+
bitrefill_get_product: async (a) => ({ ok: true, t: 'get_product', args: a }),
|
|
57
|
+
bitrefill_get_balance: async () => ({ balance: 100, currency: 'USD' }),
|
|
58
|
+
bitrefill_create_invoice: async (a) => ({ ok: true, t: 'create_invoice', args: a }),
|
|
59
|
+
bitrefill_get_invoice: async (a) => ({ ok: true, t: 'get_invoice', args: a }),
|
|
60
|
+
bitrefill_get_order: async (a) => ({ ok: true, t: 'get_order', args: a }),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('binds every tool and preserves the spend gate', () => {
|
|
64
|
+
const src = bindBitrefillTools(echoHandlers());
|
|
65
|
+
expect(src.listTools().length).toBe(6);
|
|
66
|
+
const create = src.listTools().find((t) => t.name === 'bitrefill_create_invoice');
|
|
67
|
+
expect(create?.requiresConfirmation).toBe(true);
|
|
68
|
+
const search = src.listTools().find((t) => t.name === 'bitrefill_search');
|
|
69
|
+
expect(search?.requiresConfirmation).toBeFalsy();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('dispatches with args', async () => {
|
|
73
|
+
const src = bindBitrefillTools(echoHandlers());
|
|
74
|
+
const r = await src.execute('bitrefill_search', { query: 'amazon', country: 'US' });
|
|
75
|
+
expect(r).toMatchObject({ ok: true, t: 'search', args: { query: 'amazon', country: 'US' } });
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('throws on a missing handler unless allowMissing', () => {
|
|
79
|
+
const partial = { bitrefill_search: echoHandlers().bitrefill_search };
|
|
80
|
+
expect(() => bindBitrefillTools(partial)).toThrow(/no handler/);
|
|
81
|
+
const src = bindBitrefillTools(partial, { allowMissing: true });
|
|
82
|
+
expect(src.listTools().map((t) => t.name)).toEqual(['bitrefill_search']);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('uses opts.id for the ToolSource id', () => {
|
|
86
|
+
const src = bindBitrefillTools(echoHandlers(), { id: 'bitrefill-personal' });
|
|
87
|
+
expect(src.id).toBe('bitrefill-personal');
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical Bitrefill tool contract — gift cards, mobile top-ups, eSIMs.
|
|
3
|
+
*
|
|
4
|
+
* Same pattern as the LSPS1 contract: the tool *names + schemas* live here so
|
|
5
|
+
* every host (CLI REST adapter, desktop MCP, mobile WDK adapter) exposes the
|
|
6
|
+
* exact same surface to the agent. Only the transport differs.
|
|
7
|
+
*
|
|
8
|
+
* - CLI / desktop server → REST against `https://api.bitrefill.com/v2`
|
|
9
|
+
* (see `apps/cli/src/bitrefillTools.ts`).
|
|
10
|
+
* - Desktop sidecar → the remote MCP at `api.bitrefill.com/mcp` already
|
|
11
|
+
* exposes equivalent tools under different names; a binder there can
|
|
12
|
+
* rename them to this contract for parity.
|
|
13
|
+
*
|
|
14
|
+
* `bitrefill_create_invoice` is the spend — confirmation-gated by the contract.
|
|
15
|
+
* Everything else is read-only (search, product details, balance, invoice/order
|
|
16
|
+
* status). Invoice creation supports `payment_method:"balance"` (instant, pulls
|
|
17
|
+
* from pre-funded account) or `lightning|bitcoin|usdc_base|...` (the response
|
|
18
|
+
* carries a payment URI/invoice; the user pays out-of-band, then the order is
|
|
19
|
+
* fulfilled).
|
|
20
|
+
*
|
|
21
|
+
* Pure data — no deps, no fetch, RN-safe.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ToolDef } from '../types.js';
|
|
25
|
+
import { InProcessToolSource } from '../tools/in-process.js';
|
|
26
|
+
import type { InProcessTool } from '../tools/in-process.js';
|
|
27
|
+
|
|
28
|
+
export interface BitrefillToolDef extends ToolDef {
|
|
29
|
+
/** Moves real money → confirmation-gated. */
|
|
30
|
+
spend?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Props = Record<
|
|
34
|
+
string,
|
|
35
|
+
{ type: string; description?: string; enum?: string[]; items?: unknown }
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
function t(
|
|
39
|
+
name: string,
|
|
40
|
+
description: string,
|
|
41
|
+
properties: Props = {},
|
|
42
|
+
required: string[] = [],
|
|
43
|
+
spend = false,
|
|
44
|
+
): BitrefillToolDef {
|
|
45
|
+
return {
|
|
46
|
+
name,
|
|
47
|
+
description,
|
|
48
|
+
spend,
|
|
49
|
+
requiresConfirmation: spend,
|
|
50
|
+
parameters: { type: 'object', properties, required },
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The canonical Bitrefill tool list. Each host's binder translates these
|
|
56
|
+
* args into the Bitrefill REST body (CLI) or MCP/CLI call (other hosts).
|
|
57
|
+
*/
|
|
58
|
+
export const BITREFILL_TOOLS: BitrefillToolDef[] = [
|
|
59
|
+
t(
|
|
60
|
+
'bitrefill_search',
|
|
61
|
+
"Search Bitrefill's product catalog by keyword (brand, country, type). Returns up to ~20 matches with `id`, `name`, `country`, `category` and `denominations`. The model picks the right product id and then calls `bitrefill_get_product` for the package list.",
|
|
62
|
+
{
|
|
63
|
+
query: { type: 'string', description: 'Search keyword. e.g. "amazon", "steam", "vodafone uk", "esim europe".' },
|
|
64
|
+
country: { type: 'string', description: 'OPTIONAL — ISO country code to scope results (e.g. "US", "GB", "DE"). Many brands are country-specific.' },
|
|
65
|
+
limit: { type: 'number', description: 'OPTIONAL — max results (1–25, default 10).' },
|
|
66
|
+
},
|
|
67
|
+
['query'],
|
|
68
|
+
),
|
|
69
|
+
|
|
70
|
+
t(
|
|
71
|
+
'bitrefill_get_product',
|
|
72
|
+
"Get full details for one product, including its `packages` array (each package = a denomination with `id`, `value`, `price`, `currency`). Use the package `id` (NOT the bare value) when creating an invoice.",
|
|
73
|
+
{
|
|
74
|
+
product_id: { type: 'string', description: 'Product slug from bitrefill_search, e.g. "amazon-us", "steam-us".' },
|
|
75
|
+
},
|
|
76
|
+
['product_id'],
|
|
77
|
+
),
|
|
78
|
+
|
|
79
|
+
t(
|
|
80
|
+
'bitrefill_get_balance',
|
|
81
|
+
"Get the user's Bitrefill account balance (the pre-funded pool used by `payment_method:\"balance\"`). Returns `{ balance, currency }`. No args.",
|
|
82
|
+
),
|
|
83
|
+
|
|
84
|
+
t(
|
|
85
|
+
'bitrefill_create_invoice',
|
|
86
|
+
"SPEND: confirmation-gated. Create an invoice for one or more products. Pass `payment_method:\"balance\"` + `auto_pay:true` for instant fulfillment from the account balance (lowest blast radius). For Lightning/on-chain, omit `auto_pay`, set `payment_method:\"lightning\"` (etc.) and `refund_address` — the response carries the payment URI; poll `bitrefill_get_invoice` until status=\"complete\" and then read the order. Up to 20 line items per invoice.",
|
|
87
|
+
{
|
|
88
|
+
products: {
|
|
89
|
+
type: 'array',
|
|
90
|
+
description: 'Line items. Each: { product_id, package_id, quantity }. Get `package_id` from bitrefill_get_product (NOT the bare denomination value).',
|
|
91
|
+
items: {
|
|
92
|
+
type: 'object',
|
|
93
|
+
properties: {
|
|
94
|
+
product_id: { type: 'string' },
|
|
95
|
+
package_id: { type: 'string' },
|
|
96
|
+
quantity: { type: 'number' },
|
|
97
|
+
},
|
|
98
|
+
required: ['product_id', 'package_id', 'quantity'],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
payment_method: {
|
|
102
|
+
type: 'string',
|
|
103
|
+
description: 'How to pay: "balance" (account balance, instant), "lightning", "bitcoin", "usdc_base" (x402), "usdc_polygon", "usdt_tron", etc.',
|
|
104
|
+
enum: ['balance', 'lightning', 'bitcoin', 'usdc_base', 'usdc_polygon', 'usdc_ethereum', 'usdt_tron', 'usdt_ethereum'],
|
|
105
|
+
},
|
|
106
|
+
auto_pay: { type: 'boolean', description: 'Required true with `payment_method:"balance"` for instant settlement. Omit for crypto methods.' },
|
|
107
|
+
refund_address: { type: 'string', description: 'REQUIRED for non-balance crypto methods — refund destination if the invoice expires or partially pays.' },
|
|
108
|
+
email: { type: 'string', description: 'OPTIONAL — delivery / receipt email. Defaults to the account email when authenticated.' },
|
|
109
|
+
webhook_url: { type: 'string', description: 'OPTIONAL — URL Bitrefill calls when the order is delivered.' },
|
|
110
|
+
},
|
|
111
|
+
['products', 'payment_method'],
|
|
112
|
+
/* spend */ true,
|
|
113
|
+
),
|
|
114
|
+
|
|
115
|
+
t(
|
|
116
|
+
'bitrefill_get_invoice',
|
|
117
|
+
"Get the invoice's current status: `unpaid`, `pending`, `paid`, `complete`, `expired`, `failed`. For crypto payment methods, poll this until `complete`; then call `bitrefill_get_order` for redemption details.",
|
|
118
|
+
{
|
|
119
|
+
invoice_id: { type: 'string', description: 'Invoice id returned by bitrefill_create_invoice.' },
|
|
120
|
+
},
|
|
121
|
+
['invoice_id'],
|
|
122
|
+
),
|
|
123
|
+
|
|
124
|
+
t(
|
|
125
|
+
'bitrefill_get_order',
|
|
126
|
+
"Get an order's redemption details once delivered. Returns `redemption_info` containing the code, PIN (for prepaid cards), redemption link, instructions. ONLY call after the corresponding invoice status is `complete`. Treat the returned code as cash — never paste it in shared chats.",
|
|
127
|
+
{
|
|
128
|
+
order_id: { type: 'string', description: 'Order id from a completed invoice (`order_id` on the invoice or in its `orders[]`).' },
|
|
129
|
+
},
|
|
130
|
+
['order_id'],
|
|
131
|
+
),
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
/** All Bitrefill tool names that move money (confirmation-gated). */
|
|
135
|
+
export const BITREFILL_SPEND_TOOLS: Set<string> = new Set(
|
|
136
|
+
BITREFILL_TOOLS.filter((t) => t.spend).map((t) => t.name),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
export function isBitrefillSpendTool(name: string): boolean {
|
|
140
|
+
return BITREFILL_SPEND_TOOLS.has(name);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getBitrefillTool(name: string): BitrefillToolDef | undefined {
|
|
144
|
+
return BITREFILL_TOOLS.find((t) => t.name === name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A handler bound to one Bitrefill tool. */
|
|
148
|
+
export type BitrefillHandler = (args: Record<string, unknown>) => Promise<unknown>;
|
|
149
|
+
|
|
150
|
+
export interface BindBitrefillOptions {
|
|
151
|
+
/** Skip tools without a handler instead of throwing (default false). */
|
|
152
|
+
allowMissing?: boolean;
|
|
153
|
+
/** ToolSource id for the registry (default 'bitrefill'). */
|
|
154
|
+
id?: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Bind Bitrefill contract tools to in-process handlers → an InProcessToolSource.
|
|
159
|
+
*
|
|
160
|
+
* const source = bindBitrefillTools({
|
|
161
|
+
* bitrefill_search: async (args) => api.search(args),
|
|
162
|
+
* bitrefill_get_product: async ({ product_id }) => api.product(product_id),
|
|
163
|
+
* bitrefill_get_balance: async () => api.balance(),
|
|
164
|
+
* bitrefill_create_invoice: async (args) => api.createInvoice(args),
|
|
165
|
+
* bitrefill_get_invoice: async ({ invoice_id }) => api.invoice(invoice_id),
|
|
166
|
+
* bitrefill_get_order: async ({ order_id }) => api.order(order_id),
|
|
167
|
+
* });
|
|
168
|
+
* tools.register(source);
|
|
169
|
+
*/
|
|
170
|
+
export function bindBitrefillTools(
|
|
171
|
+
handlers: Record<string, BitrefillHandler>,
|
|
172
|
+
opts: BindBitrefillOptions = {},
|
|
173
|
+
): InProcessToolSource {
|
|
174
|
+
const bound: InProcessTool[] = [];
|
|
175
|
+
for (const def of BITREFILL_TOOLS) {
|
|
176
|
+
const handler = handlers[def.name];
|
|
177
|
+
if (!handler) {
|
|
178
|
+
if (opts.allowMissing) continue;
|
|
179
|
+
throw new Error(`bindBitrefillTools: no handler for "${def.name}"`);
|
|
180
|
+
}
|
|
181
|
+
bound.push({
|
|
182
|
+
name: def.name,
|
|
183
|
+
description: def.description,
|
|
184
|
+
parameters: def.parameters,
|
|
185
|
+
requiresConfirmation: def.requiresConfirmation,
|
|
186
|
+
handler,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
return new InProcessToolSource(opts.id ?? 'bitrefill', bound);
|
|
190
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/** Tool-output compression tests — savings + the safety guarantees. */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect } from 'vitest';
|
|
4
|
+
import { compressToolResult } from './compress.js';
|
|
5
|
+
import { estimateTokens } from './budget.js';
|
|
6
|
+
|
|
7
|
+
/** Build a verbose merchant-list-like result the agentic loop would crush. */
|
|
8
|
+
function merchants(n: number): { results: Array<Record<string, unknown>> } {
|
|
9
|
+
return {
|
|
10
|
+
results: Array.from({ length: n }, (_, i) => ({
|
|
11
|
+
name: `Coffee Shop ${i}`,
|
|
12
|
+
category: 'cafe',
|
|
13
|
+
description:
|
|
14
|
+
'Accepts Bitcoin on-chain and Lightning. Open daily. ' +
|
|
15
|
+
'A cozy spot with reliable wifi and great espresso for digital nomads.',
|
|
16
|
+
lat: 41.0 + i / 1000,
|
|
17
|
+
lng: 12.0 + i / 1000,
|
|
18
|
+
tags: ['bitcoin', 'lightning', 'cafe'],
|
|
19
|
+
})),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe('compressToolResult', () => {
|
|
24
|
+
it('passes small results through untouched', () => {
|
|
25
|
+
const small = { total_sats: 123_456, layers: 2 };
|
|
26
|
+
const r = compressToolResult(small);
|
|
27
|
+
expect(r.changed).toBe(false);
|
|
28
|
+
expect(r.content).toBe(JSON.stringify(small));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('elides the middle of a long array and reports the omitted count', () => {
|
|
32
|
+
const r = compressToolResult(merchants(40), { maxArrayItems: 6 });
|
|
33
|
+
expect(r.changed).toBe(true);
|
|
34
|
+
expect(r.compressedTokens).toBeLessThan(r.originalTokens);
|
|
35
|
+
expect(r.elided).toBeGreaterThan(0);
|
|
36
|
+
|
|
37
|
+
const parsed = JSON.parse(r.content) as { results: Array<Record<string, unknown>> };
|
|
38
|
+
const marker = parsed.results.find((x) => '__elided__' in x);
|
|
39
|
+
expect(marker).toBeDefined();
|
|
40
|
+
expect(marker!.__elided__).toBe(r.elided);
|
|
41
|
+
// Kept first/last anchors → fewer items than the original 40.
|
|
42
|
+
expect(parsed.results.length).toBeLessThan(40);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('dedupes identical array items before eliding', () => {
|
|
46
|
+
const dup = { rows: Array.from({ length: 30 }, () => ({ status: 'ok', code: 200 })) };
|
|
47
|
+
const r = compressToolResult(dup, { maxArrayItems: 4 });
|
|
48
|
+
const parsed = JSON.parse(r.content) as { rows: Array<Record<string, unknown>> };
|
|
49
|
+
const real = parsed.rows.filter((x) => !('__elided__' in x));
|
|
50
|
+
// 30 identical rows collapse to a single unique row (≤ maxArrayItems).
|
|
51
|
+
expect(real.length).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('never regresses: returns the original when crushing would not save tokens', () => {
|
|
55
|
+
// A flat array of unique short numbers compresses to roughly itself; the
|
|
56
|
+
// elision marker can cost more than it saves — must fall back to original.
|
|
57
|
+
const flat = { xs: Array.from({ length: 60 }, (_, i) => i) };
|
|
58
|
+
const r = compressToolResult(flat, { maxArrayItems: 50, dedupe: false });
|
|
59
|
+
expect(r.compressedTokens).toBeLessThanOrEqual(r.originalTokens);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('SAFETY: never truncates whitespace-free identifiers (invoices/addresses)', () => {
|
|
63
|
+
const invoice = 'lnbc' + '1'.repeat(1500); // long BOLT11-like, no spaces
|
|
64
|
+
const addr = 'bc1q' + 'a'.repeat(800);
|
|
65
|
+
const payload = {
|
|
66
|
+
filler: Array.from({ length: 20 }, (_, i) => ({ note: 'x'.repeat(50), i })),
|
|
67
|
+
invoice,
|
|
68
|
+
address: addr,
|
|
69
|
+
};
|
|
70
|
+
const r = compressToolResult(payload, { maxArrayItems: 4, maxStringLength: 80 });
|
|
71
|
+
expect(r.content).toContain(invoice); // intact, not truncated
|
|
72
|
+
expect(r.content).toContain(addr);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('SAFETY: never elides/truncates values under preserved money keys', () => {
|
|
76
|
+
const payload = {
|
|
77
|
+
// A long prose string under a preserve key stays intact.
|
|
78
|
+
balance: 'x '.repeat(1000),
|
|
79
|
+
// Numbers are never touched regardless.
|
|
80
|
+
total_sats: 4_800_123,
|
|
81
|
+
history: Array.from({ length: 40 }, (_, i) => ({ memo: 'spent on coffee number ' + i, i })),
|
|
82
|
+
};
|
|
83
|
+
const r = compressToolResult(payload, { maxArrayItems: 4, maxStringLength: 40 });
|
|
84
|
+
const parsed = JSON.parse(r.content) as Record<string, unknown>;
|
|
85
|
+
expect(parsed.balance).toBe(payload.balance); // preserved verbatim
|
|
86
|
+
expect(parsed.total_sats).toBe(4_800_123);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('SAFETY: numbers are never altered', () => {
|
|
90
|
+
const payload = {
|
|
91
|
+
quotes: Array.from({ length: 30 }, (_, i) => ({
|
|
92
|
+
amount_sats: 1000 + i,
|
|
93
|
+
rate: 0.00012345,
|
|
94
|
+
fee: 7,
|
|
95
|
+
})),
|
|
96
|
+
};
|
|
97
|
+
const r = compressToolResult(payload, { maxArrayItems: 5 });
|
|
98
|
+
const parsed = JSON.parse(r.content) as { quotes: Array<Record<string, number>> };
|
|
99
|
+
for (const q of parsed.quotes) {
|
|
100
|
+
if ('__elided__' in q) continue;
|
|
101
|
+
expect(Number.isInteger(q.amount_sats)).toBe(true);
|
|
102
|
+
expect(q.rate).toBe(0.00012345);
|
|
103
|
+
expect(q.fee).toBe(7);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('truncates long prose strings (with whitespace) when over the limit', () => {
|
|
108
|
+
const payload = { log: ('error happened at step ').repeat(200) };
|
|
109
|
+
const r = compressToolResult(payload, { maxStringLength: 100, minTokens: 1 });
|
|
110
|
+
expect(r.changed).toBe(true);
|
|
111
|
+
expect(r.content).toContain('… (+');
|
|
112
|
+
expect(estimateTokens(r.content)).toBeLessThan(r.originalTokens);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('collapses nesting beyond maxDepth to a shape summary', () => {
|
|
116
|
+
const deep = { a: { b: { c: { d: { e: { f: { g: 'too deep' } } } } } }, pad: 'p'.repeat(900) };
|
|
117
|
+
const r = compressToolResult(deep, { maxDepth: 3, minTokens: 1 });
|
|
118
|
+
expect(r.content).toMatch(/\[object: \d+ keys\]/);
|
|
119
|
+
});
|
|
120
|
+
});
|