@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,235 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: spark-wallet
|
|
3
|
+
description: "Operate the user's Spark BTC wallet on this device — check Spark balance, get a Spark deposit address, create a Spark Lightning invoice to receive, pay any BOLT11 Lightning invoice with Spark, or send BTC on-chain from Spark. Use this when the user names Spark explicitly OR when paying a Lightning invoice on a phone where Spark is the connected layer. Pairs with the bitrefill skill: a Bitrefill purchase that returns a Lightning invoice is paid with `spark_pay_invoice`."
|
|
4
|
+
tools: spark_get_balance, spark_get_address, spark_get_onchain_address, spark_create_invoice, spark_pay_invoice, spark_send, get_price, fiat_to_sats, bitrefill_search, bitrefill_get_product, bitrefill_get_balance, bitrefill_create_invoice, bitrefill_get_invoice, bitrefill_get_order
|
|
5
|
+
triggers: spark, sprak, spakr, spark wallet, pay with spark, send with spark, spark balance, spark address, spark invoice, lightning invoice, pay invoice, bolt11, ln invoice, pay this invoice, on-chain address, onchain address, deposit address, deposit btc, fund spark
|
|
6
|
+
metadata:
|
|
7
|
+
author: kaleidoswap
|
|
8
|
+
version: "1.0.0"
|
|
9
|
+
layer: spark
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# Spark wallet
|
|
13
|
+
|
|
14
|
+
Spark is one of the user's connected BTC layers (alongside RLN/RGB and Arkade
|
|
15
|
+
on some hosts). It speaks Lightning natively — receive via `spark_create_invoice`,
|
|
16
|
+
send via `spark_pay_invoice` (BOLT11) or `spark_send` (on-chain). All numbers
|
|
17
|
+
are in **satoshis** unless stated otherwise.
|
|
18
|
+
|
|
19
|
+
## Three "addresses", three different tools (read this carefully)
|
|
20
|
+
|
|
21
|
+
The word "address" can mean three completely different things on Spark.
|
|
22
|
+
Each has a DIFFERENT tool, a DIFFERENT shape, and a DIFFERENT use. If you
|
|
23
|
+
return the wrong one, the user loses money on a bad deposit. Pick by what
|
|
24
|
+
the user is trying to DO, not just by the word "address":
|
|
25
|
+
|
|
26
|
+
| User intent | Tool | What you get | Looks like |
|
|
27
|
+
|---|---|---|---|
|
|
28
|
+
| Receive a **Spark-to-Spark** transfer (off-chain, within Spark) | `spark_get_address` | Spark identity / pubkey | `sparkrt1…` / `spark1…` |
|
|
29
|
+
| Deposit **L1 Bitcoin** into Spark from the on-chain world | `spark_get_onchain_address` | Real Bitcoin on-chain address | `bc1…` / `tb1…` / `bcrt1…` |
|
|
30
|
+
| Receive over **Lightning** (BOLT11) | `spark_create_invoice` | A Lightning invoice string | `lnbc…` / `lntb…` / `lnbcrt…` |
|
|
31
|
+
|
|
32
|
+
**Disambiguation by phrasing — examples:**
|
|
33
|
+
|
|
34
|
+
- "give me my **on-chain address**" / "**bitcoin address** to fund Spark"
|
|
35
|
+
/ "**deposit address**" / "where do I send BTC from my hardware wallet
|
|
36
|
+
/ mainnet" → `spark_get_onchain_address`. Result starts with bc1/tb1/
|
|
37
|
+
bcrt1 — verify before replying.
|
|
38
|
+
- "my **Spark address**" / "give me a **Spark address**" / "where do I
|
|
39
|
+
receive a **Spark transfer**" → `spark_get_address`. Result starts with
|
|
40
|
+
spark1/sparkrt1. **DO NOT label this as an on-chain address — it is
|
|
41
|
+
off-chain.**
|
|
42
|
+
- "give me an **invoice for N sats**" / "an **LN invoice**" / "**pay me**
|
|
43
|
+
N sats" → `spark_create_invoice({amount_sats: N})`. Result is a `lnbc…`
|
|
44
|
+
string.
|
|
45
|
+
|
|
46
|
+
**Critical: when the user says "on-chain", they mean L1 Bitcoin.** Never
|
|
47
|
+
return a `sparkrt1…` and call it "on-chain". If you return a
|
|
48
|
+
`spark_get_address` result, you MUST describe it as a Spark (off-chain)
|
|
49
|
+
address — never as an on-chain or Bitcoin address. If you return a
|
|
50
|
+
`spark_get_onchain_address` result, you can call it an on-chain Bitcoin
|
|
51
|
+
deposit address.
|
|
52
|
+
|
|
53
|
+
A useful sanity check before you send the reply: glance at the
|
|
54
|
+
`address` string's prefix. If it begins with `spark`, it's the off-chain
|
|
55
|
+
Spark identity. If it begins with `bc1`/`tb1`/`bcrt1`, it's an on-chain
|
|
56
|
+
BTC address. If it begins with `lnbc`/`lntb`/`lnbcrt`, it's a Lightning
|
|
57
|
+
invoice. Whatever you call it in your reply MUST match its actual prefix.
|
|
58
|
+
|
|
59
|
+
## What Spark holds (and what it does NOT)
|
|
60
|
+
|
|
61
|
+
This is the single most important thing to get right when the user asks
|
|
62
|
+
about "assets on Spark" or "what can I trade on Spark".
|
|
63
|
+
|
|
64
|
+
**Spark holds:**
|
|
65
|
+
- **BTC** (sats) — Spark's native on-chain-pegged BTC.
|
|
66
|
+
- **Spark-native tokens** — e.g. **USDB**. These are tokens issued on the
|
|
67
|
+
Spark protocol itself, traded on **Flashnet** (Spark-native AMM).
|
|
68
|
+
|
|
69
|
+
**Spark does NOT hold:**
|
|
70
|
+
- **RGB assets** (USDT, XAUT, …). RGB assets are a DIFFERENT protocol on
|
|
71
|
+
Bitcoin/Lightning — they live on the user's **RLN** (RGB Lightning Node),
|
|
72
|
+
NOT Spark. A USDT balance, if the user has one, is on RLN — never on
|
|
73
|
+
Spark.
|
|
74
|
+
- Tokens from any other chain (Ethereum USDT, Tron USDT, Solana, …). The
|
|
75
|
+
wallet does not custody those at all.
|
|
76
|
+
|
|
77
|
+
**Asset → which skill / venue:**
|
|
78
|
+
|
|
79
|
+
| Asset | Layer | Swap venue | Skill |
|
|
80
|
+
|---|---|---|---|
|
|
81
|
+
| BTC / sats | Spark / RLN / on-chain | Either (depends on direction) | spark-wallet (Spark side), wallet-assistant |
|
|
82
|
+
| USDB (and other Spark tokens) | Spark | **Flashnet** (AMM) | **flashnet-swaps** |
|
|
83
|
+
| USDT, XAUT (RGB assets) | RLN/RGB | **KaleidoSwap maker** | **kaleido-trading** |
|
|
84
|
+
|
|
85
|
+
If the user asks "what can I trade on Spark?", the correct answer lists
|
|
86
|
+
Spark-native tokens (BTC + USDB and anything else
|
|
87
|
+
`flashnet_list_pools` shows). **Never** answer USDT/XAUT for Spark.
|
|
88
|
+
Conversely, if asked "what can I trade on KaleidoSwap?", that's RGB
|
|
89
|
+
assets (USDT, XAUT) — **not** USDB.
|
|
90
|
+
|
|
91
|
+
When in doubt about what's actually tradeable, the source of truth is the
|
|
92
|
+
TOOL, not your training data — call `flashnet_list_pools` (Spark side) or
|
|
93
|
+
`kaleidoswap_get_assets` (RGB side) and report what comes back.
|
|
94
|
+
|
|
95
|
+
## Critical rules (read first)
|
|
96
|
+
|
|
97
|
+
1. **Always re-fetch volatile state — every turn, every time.** Balance,
|
|
98
|
+
address, invoice status, and any number that can change MUST come from a
|
|
99
|
+
tool call THIS turn. Do NOT reuse a value from a previous turn, even if
|
|
100
|
+
the user asked the exact same question 30 seconds ago. The user wouldn't
|
|
101
|
+
ask twice if they didn't want a fresh check.
|
|
102
|
+
|
|
103
|
+
- "what's my balance?" → ALWAYS call `spark_get_balance`. Yes, even if
|
|
104
|
+
you just called it. The whole point of asking again is to get a new
|
|
105
|
+
reading.
|
|
106
|
+
- "give me my address" → ALWAYS call `spark_get_address`. Spark may
|
|
107
|
+
rotate or surface a fresh address.
|
|
108
|
+
- "did my invoice settle?" → ALWAYS re-fetch the invoice/order status.
|
|
109
|
+
|
|
110
|
+
The ONLY thing you can reuse from history is the user's own input
|
|
111
|
+
(e.g. "the invoice I just made" → look up its id in history and call
|
|
112
|
+
the status tool on it).
|
|
113
|
+
|
|
114
|
+
2. **Never invent a balance, invoice or address.** Every BTC number, BOLT11
|
|
115
|
+
string, and address in your reply MUST come from a Spark tool result
|
|
116
|
+
returned in the CURRENT turn — never guessed, never quoted from memory.
|
|
117
|
+
|
|
118
|
+
3. **Read the tool result exactly.** `spark_get_balance` returns
|
|
119
|
+
`{ total, layer, network, connected }`. `connected: true` means the
|
|
120
|
+
Spark wallet IS active and reachable; `total: 0` with `connected: true`
|
|
121
|
+
simply means the user has no sats yet (perfectly normal for a fresh
|
|
122
|
+
wallet on regtest). Do NOT say "your wallet isn't connected" unless
|
|
123
|
+
`connected: false` or the tool threw an error. Say "your Spark wallet
|
|
124
|
+
is connected but empty — fund it with `spark_get_address`" when
|
|
125
|
+
`total: 0, connected: true`.
|
|
126
|
+
|
|
127
|
+
4. **Choose the right send tool by destination shape.**
|
|
128
|
+
- Starts with `lnbc…` / `lntb…` / `lnbcrt…` → BOLT11 Lightning invoice → use
|
|
129
|
+
**`spark_pay_invoice`**.
|
|
130
|
+
- Starts with `bc1…` / `tb1…` / `bcrt1…` → on-chain Bitcoin address → use
|
|
131
|
+
**`spark_send`**.
|
|
132
|
+
- Looks like `name@domain` (a Lightning address) → not a Spark target;
|
|
133
|
+
either ask the host to resolve it first or use the cross-cutting
|
|
134
|
+
`send_payment` router. Spark itself doesn't dereference LNURL.
|
|
135
|
+
|
|
136
|
+
5. **BOLT11 invoices encode their amount.** Don't pass `amount_sats` to
|
|
137
|
+
`spark_pay_invoice` unless the invoice is amount-less. Re-stating the
|
|
138
|
+
amount can produce silently-wrong sends on amount-less invoices.
|
|
139
|
+
|
|
140
|
+
6. **Confirm before spending.** `spark_pay_invoice` and `spark_send` are
|
|
141
|
+
confirmation-gated by the contract — the host fires the gate
|
|
142
|
+
automatically. Before the call, summarize in one line:
|
|
143
|
+
`Paying 12,540 sats to lnbc12540n… from Spark. Confirm?`
|
|
144
|
+
|
|
145
|
+
7. **Spark genuinely unavailable = stop, don't guess.** If the tool
|
|
146
|
+
THROWS with "Your SPARK wallet isn't connected yet" (an actual error,
|
|
147
|
+
not a 0 balance), say so plainly and stop — don't substitute RLN or
|
|
148
|
+
Arkade silently. The user may genuinely want Spark.
|
|
149
|
+
|
|
150
|
+
8. **Never refuse an action a listed tool performs.** If a tool in your
|
|
151
|
+
set does what the user asked, CALL IT — do not reason about whether
|
|
152
|
+
"get" means "create", whether the wording is an exact match, or
|
|
153
|
+
whether some other tool would be "more correct". The user asking to
|
|
154
|
+
"create an address" when you have `spark_get_address` means: call
|
|
155
|
+
`spark_get_address`. Refusing to use an available tool is always wrong.
|
|
156
|
+
|
|
157
|
+
## How to call the tools
|
|
158
|
+
|
|
159
|
+
### Reads
|
|
160
|
+
|
|
161
|
+
- **`spark_get_balance({})`** — current Spark balances. Returns the BTC
|
|
162
|
+
balance (`total` in sats) AND every Spark-native **token** the wallet
|
|
163
|
+
holds (`tokens[]` — each with `address`, `balance`, optional `symbol`
|
|
164
|
+
and `decimals`). When the user asks "what do I have on Spark" or
|
|
165
|
+
"what's my Spark balance", surface BOTH the sats AND any non-zero
|
|
166
|
+
token balances; an answer that only mentions BTC when tokens are
|
|
167
|
+
present is incomplete.
|
|
168
|
+
- `flashnet_get_balance` returns the same numbers (it's the AMM-client
|
|
169
|
+
view of the same wallet) — there's no need to call it just to learn
|
|
170
|
+
the token balance.
|
|
171
|
+
- **`spark_get_address({})`** — the user's **Spark identity**
|
|
172
|
+
(`sparkrt1…`/`spark1…`), an OFF-CHAIN Spark-to-Spark receive target.
|
|
173
|
+
Reusable — getting and creating are the same operation, so the right
|
|
174
|
+
response to "create a Spark address" is ALSO this tool. NEVER call its
|
|
175
|
+
result an on-chain address or a Bitcoin address; it is neither. NEVER
|
|
176
|
+
reply "I cannot create an address" — this tool IS how you create one.
|
|
177
|
+
- **`spark_get_onchain_address({})`** — a real Bitcoin **on-chain
|
|
178
|
+
deposit** address (`bc1…`/`tb1…`/`bcrt1…`) for funding Spark from L1.
|
|
179
|
+
Use whenever the user says "on-chain", "bitcoin address", "deposit
|
|
180
|
+
address", "fund Spark", or otherwise indicates they want to send L1
|
|
181
|
+
BTC. NEVER substitute `spark_get_address` here.
|
|
182
|
+
|
|
183
|
+
### Receive
|
|
184
|
+
|
|
185
|
+
- **`spark_create_invoice({ amount_sats? })`** — Spark Lightning invoice.
|
|
186
|
+
- Omit `amount_sats` for an "any amount" invoice.
|
|
187
|
+
- Returns `{ invoice: "lnbc…", … }`.
|
|
188
|
+
- This is the tool for "give me an invoice for N sats", NOT for any
|
|
189
|
+
"address" ask.
|
|
190
|
+
|
|
191
|
+
### Send — pick by destination
|
|
192
|
+
|
|
193
|
+
- **`spark_pay_invoice({ invoice, amount_sats? })`** — pay a BOLT11 invoice.
|
|
194
|
+
- The model's default Lightning spend tool. Use this for invoices from
|
|
195
|
+
Bitrefill, a contact's invoice paste, or any `ln…` string.
|
|
196
|
+
- Pass `amount_sats` ONLY when the invoice is amount-less (a 0-amount
|
|
197
|
+
invoice). For ordinary amount-bound invoices, OMIT it.
|
|
198
|
+
- **`spark_send({ amount_sats, to })`** — on-chain Bitcoin send.
|
|
199
|
+
- Use only when `to` is an on-chain address (`bc1…`). Never pass a BOLT11
|
|
200
|
+
invoice here.
|
|
201
|
+
|
|
202
|
+
### Helpers
|
|
203
|
+
|
|
204
|
+
- **`get_price({ fiat? })`** / **`fiat_to_sats({ amount, currency })`** —
|
|
205
|
+
for "how many sats is €10" style sub-questions before a Spark spend.
|
|
206
|
+
|
|
207
|
+
## Cross-skill flow with Bitrefill
|
|
208
|
+
|
|
209
|
+
When the user wants to buy something from Bitrefill and pay with Spark, the
|
|
210
|
+
typical chain is:
|
|
211
|
+
|
|
212
|
+
1. Call `bitrefill_search` / `bitrefill_get_product` to confirm the product +
|
|
213
|
+
the right `package_id` (see the `bitrefill` skill).
|
|
214
|
+
2. Call `bitrefill_create_invoice({ products, payment_method: "lightning",
|
|
215
|
+
refund_address: <a Spark or on-chain address> })` — Bitrefill returns a
|
|
216
|
+
BOLT11 invoice on the response under `payment.lightning_invoice` (or
|
|
217
|
+
similar — relay whatever the host surfaces).
|
|
218
|
+
3. **Pay with Spark**: `spark_pay_invoice({ invoice: <that BOLT11> })`. One
|
|
219
|
+
confirmation gate; Spark settles the invoice in seconds.
|
|
220
|
+
4. Poll `bitrefill_get_invoice` until `status:"complete"`, then
|
|
221
|
+
`bitrefill_get_order` for the redemption code.
|
|
222
|
+
|
|
223
|
+
If the user pre-funded a Bitrefill account, prefer
|
|
224
|
+
`payment_method:"balance"` instead — no Spark spend, instant settlement. Use
|
|
225
|
+
the Lightning path when the user explicitly says "pay with Spark/Lightning"
|
|
226
|
+
or has no Bitrefill balance.
|
|
227
|
+
|
|
228
|
+
## Reply style
|
|
229
|
+
|
|
230
|
+
- One short sentence per fact ("Spark holds 124,500 sats.").
|
|
231
|
+
- For invoices: show the invoice on its own line so the user can copy it.
|
|
232
|
+
- For sends: one-line pre-spend summary (amount + destination + "from
|
|
233
|
+
Spark"), then the result.
|
|
234
|
+
- When `spark_get_balance` says zero and the user asked to spend, stop and
|
|
235
|
+
say so — don't try to source funds from another layer silently.
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: wallet-assistant
|
|
3
3
|
description: Everyday wallet tasks on this phone — check the BTC/asset balance, create an invoice to receive, send a payment, look up a contact, or quote a swap (e.g. "how many sats is 10 USDT?"). Triggers when the user asks about their balance, wants to receive or send money, pay an invoice, pay a contact, or convert between BTC and supported assets.
|
|
4
|
-
tools: get_balances, resolve_contact, send_payment, rln_pay_invoice, rln_create_ln_invoice, spark_create_invoice, kaleidoswap_get_quote
|
|
5
|
-
triggers: balance, pay, send, receive, address, invoice, transactions, contact, funds, money
|
|
4
|
+
tools: get_balances, rln_get_balances, wdk_get_balances, spark_get_balance, rln_get_asset_balance, wdk_get_asset_balance, rln_list_assets, wdk_list_assets, rln_get_address, wdk_get_address, spark_get_address, spark_get_onchain_address, resolve_contact, send_payment, rln_send_btc, wdk_send_btc, rln_send_asset, wdk_send_asset, spark_send, spark_send_sats, rln_pay_invoice, wdk_pay_invoice, spark_pay_invoice, spark_pay_lightning_invoice, rln_create_ln_invoice, wdk_create_ln_invoice, spark_create_invoice, spark_create_lightning_invoice, rln_create_rgb_invoice, wdk_create_rgb_invoice, rln_list_payments, wdk_list_payments, kaleidoswap_get_quote
|
|
5
|
+
triggers: balance, pay, send, receive, address, invoice, transactions, contact, funds, money
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
# Wallet assistant
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
/** Autonomy tests — deterministic: injected clock + timer, no real wallet/LLM. */
|
|
2
|
+
|
|
3
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
4
|
+
import { InMemoryTaskStore, defaultTaskSeeds } from './task-store.js';
|
|
5
|
+
import { createTaskScheduler } from './scheduler.js';
|
|
6
|
+
import { TaskRunLog } from './run-state.js';
|
|
7
|
+
import { evaluateSpend, DEFAULT_RISK_LIMITS } from './risk.js';
|
|
8
|
+
import { buildTaskPrompt } from './prompt.js';
|
|
9
|
+
import type { AgentTask, RunLogSnapshot, RunLogIO, TaskStoreIO } from './types.js';
|
|
10
|
+
|
|
11
|
+
const SEC = 1000;
|
|
12
|
+
|
|
13
|
+
/** Drain pending microtasks (real timer — these tests don't fake setTimeout). */
|
|
14
|
+
const flush = (): Promise<void> => new Promise((resolve) => setTimeout(resolve, 0));
|
|
15
|
+
|
|
16
|
+
describe('InMemoryTaskStore', () => {
|
|
17
|
+
it('creates with defaults and lists', async () => {
|
|
18
|
+
const store = new InMemoryTaskStore({ now: () => 1 });
|
|
19
|
+
const t = await store.create({
|
|
20
|
+
name: 'Rebalance',
|
|
21
|
+
description: 'd',
|
|
22
|
+
skill: 'portfolio-manager',
|
|
23
|
+
scheduleSec: 3600,
|
|
24
|
+
enabled: true,
|
|
25
|
+
});
|
|
26
|
+
expect(t.id).toBe('task_1_1');
|
|
27
|
+
expect(t.runOnStartup).toBe(false);
|
|
28
|
+
expect(t.allocation).toEqual({ btcSat: 0, usdt: 0, xaut: 0 });
|
|
29
|
+
expect(t.lastRunAt).toBeNull();
|
|
30
|
+
expect(await store.list()).toHaveLength(1);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('update cannot mutate id/createdAt; remove works', async () => {
|
|
34
|
+
const store = new InMemoryTaskStore({ now: () => 5 });
|
|
35
|
+
const t = await store.create({ name: 'x', description: '', skill: 's', scheduleSec: 0, enabled: false });
|
|
36
|
+
const patched = await store.update(t.id, {
|
|
37
|
+
enabled: true,
|
|
38
|
+
lastRunAt: 99,
|
|
39
|
+
// @ts-expect-error — id is not patchable, proving the type guard
|
|
40
|
+
id: 'evil',
|
|
41
|
+
} as Partial<AgentTask>);
|
|
42
|
+
expect(patched?.id).toBe(t.id);
|
|
43
|
+
expect(patched?.createdAt).toBe(5);
|
|
44
|
+
expect(patched?.enabled).toBe(true);
|
|
45
|
+
expect(patched?.lastRunAt).toBe(99);
|
|
46
|
+
expect(await store.remove(t.id)).toBe(true);
|
|
47
|
+
expect(await store.remove(t.id)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('seedDefaults is idempotent by id', async () => {
|
|
51
|
+
const store = new InMemoryTaskStore({ now: () => 1 });
|
|
52
|
+
const first = await store.seedDefaults(defaultTaskSeeds());
|
|
53
|
+
expect(first.map((t) => t.id)).toEqual(['heartbeat', 'rebalance', 'daily_summary']);
|
|
54
|
+
const second = await store.seedDefaults(defaultTaskSeeds());
|
|
55
|
+
expect(second).toHaveLength(0); // already present
|
|
56
|
+
expect(await store.list()).toHaveLength(3);
|
|
57
|
+
// seeds disabled by default — an agent never auto-arms itself
|
|
58
|
+
expect((await store.list()).every((t) => !t.enabled)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('persists through injected IO and a fresh store hydrates', async () => {
|
|
62
|
+
let saved: AgentTask[] = [];
|
|
63
|
+
const io: TaskStoreIO = {
|
|
64
|
+
load: vi.fn(async () => [...saved]),
|
|
65
|
+
save: vi.fn(async (tasks) => {
|
|
66
|
+
saved = [...tasks];
|
|
67
|
+
}),
|
|
68
|
+
};
|
|
69
|
+
const store = new InMemoryTaskStore({ io, now: () => 1 });
|
|
70
|
+
await store.create({ name: 'a', description: '', skill: 's', scheduleSec: 60, enabled: true });
|
|
71
|
+
expect(io.save).toHaveBeenCalled();
|
|
72
|
+
|
|
73
|
+
const store2 = new InMemoryTaskStore({ io, now: () => 2 });
|
|
74
|
+
expect(await store2.list()).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('createTaskScheduler', () => {
|
|
79
|
+
function fixtureTask(over: Partial<AgentTask> = {}): AgentTask {
|
|
80
|
+
return {
|
|
81
|
+
id: 't1',
|
|
82
|
+
name: 'Heartbeat',
|
|
83
|
+
description: '',
|
|
84
|
+
skill: 'channel-manager',
|
|
85
|
+
scheduleSec: 300,
|
|
86
|
+
runOnStartup: false,
|
|
87
|
+
allocation: { btcSat: 0, usdt: 0, xaut: 0 },
|
|
88
|
+
enabled: true,
|
|
89
|
+
createdAt: 0,
|
|
90
|
+
lastRunAt: null,
|
|
91
|
+
...over,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
it('runs a task only once its interval has elapsed since creation', async () => {
|
|
96
|
+
let t = 0;
|
|
97
|
+
const store = new InMemoryTaskStore({ now: () => t });
|
|
98
|
+
await store.create(fixtureTask());
|
|
99
|
+
const run = vi.fn(async () => ({ ok: true }));
|
|
100
|
+
const sched = createTaskScheduler({ store, run, now: () => t });
|
|
101
|
+
|
|
102
|
+
t = 200 * SEC; // < 300s since createdAt=0 → not due
|
|
103
|
+
await sched.tick();
|
|
104
|
+
expect(run).not.toHaveBeenCalled();
|
|
105
|
+
|
|
106
|
+
t = 300 * SEC; // exactly due
|
|
107
|
+
await sched.tick();
|
|
108
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
109
|
+
// lastRunAt stamped at run start
|
|
110
|
+
expect((await store.get('t1'))?.lastRunAt).toBe(300 * SEC);
|
|
111
|
+
|
|
112
|
+
t = 400 * SEC; // < 300s since last run → not due again
|
|
113
|
+
await sched.tick();
|
|
114
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
115
|
+
|
|
116
|
+
t = 600 * SEC; // 300s elapsed → due
|
|
117
|
+
await sched.tick();
|
|
118
|
+
expect(run).toHaveBeenCalledTimes(2);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('skips disabled tasks and scheduleSec=0 (manual-only)', async () => {
|
|
122
|
+
const store = new InMemoryTaskStore({ now: () => 0 });
|
|
123
|
+
await store.create(fixtureTask({ id: 'off', enabled: false, createdAt: 0 }));
|
|
124
|
+
await store.create(fixtureTask({ id: 'manual', scheduleSec: 0, createdAt: 0 }));
|
|
125
|
+
const run = vi.fn(async () => ({ ok: true }));
|
|
126
|
+
const sched = createTaskScheduler({ store, run, now: () => 10 ** 9 });
|
|
127
|
+
await sched.tick();
|
|
128
|
+
expect(run).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('start() runs runOnStartup tasks immediately via injected timer', async () => {
|
|
132
|
+
const store = new InMemoryTaskStore({ now: () => 0 });
|
|
133
|
+
await store.create(fixtureTask({ id: 'boot', runOnStartup: true, createdAt: 0 }));
|
|
134
|
+
const run = vi.fn(async () => ({ ok: true }));
|
|
135
|
+
const setTimer = vi.fn(() => 'h');
|
|
136
|
+
const clearTimer = vi.fn();
|
|
137
|
+
const sched = createTaskScheduler({ store, run, now: () => 0, setTimer, clearTimer });
|
|
138
|
+
|
|
139
|
+
sched.start();
|
|
140
|
+
expect(setTimer).toHaveBeenCalledTimes(1);
|
|
141
|
+
await flush(); // let the async startup pass settle
|
|
142
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
143
|
+
expect(sched.isRunning()).toBe(true);
|
|
144
|
+
|
|
145
|
+
sched.stop();
|
|
146
|
+
expect(clearTimer).toHaveBeenCalledWith('h');
|
|
147
|
+
expect(sched.isRunning()).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('runNow forces a run regardless of schedule/enabled and reports outcome', async () => {
|
|
151
|
+
const store = new InMemoryTaskStore({ now: () => 0 });
|
|
152
|
+
await store.create(fixtureTask({ id: 'x', enabled: false, scheduleSec: 0 }));
|
|
153
|
+
const run = vi.fn(async () => ({ ok: true, text: 'done', toolCalls: 2 }));
|
|
154
|
+
const sched = createTaskScheduler({ store, run, now: () => 42 });
|
|
155
|
+
const outcome = await sched.runNow('x');
|
|
156
|
+
expect(outcome).toEqual({ ok: true, text: 'done', toolCalls: 2 });
|
|
157
|
+
expect((await store.get('x'))?.lastRunAt).toBe(42);
|
|
158
|
+
expect(await sched.runNow('nope')).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('advances lastRunAt even when the run throws (no hot-loop)', async () => {
|
|
162
|
+
let t = 10 ** 6;
|
|
163
|
+
const store = new InMemoryTaskStore({ now: () => t });
|
|
164
|
+
await store.create(fixtureTask({ id: 'boom', createdAt: 0 }));
|
|
165
|
+
const onOutcome = vi.fn();
|
|
166
|
+
const run = vi.fn(async () => {
|
|
167
|
+
throw new Error('rpc down');
|
|
168
|
+
});
|
|
169
|
+
const sched = createTaskScheduler({ store, run, now: () => t, onOutcome });
|
|
170
|
+
await sched.tick();
|
|
171
|
+
expect((await store.get('boom'))?.lastRunAt).toBe(t);
|
|
172
|
+
expect(onOutcome).toHaveBeenCalledWith(
|
|
173
|
+
expect.objectContaining({ id: 'boom' }),
|
|
174
|
+
expect.objectContaining({ ok: false, error: 'rpc down' }),
|
|
175
|
+
expect.any(Number),
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('honors concurrency=1 — a slow task does not overlap itself', async () => {
|
|
180
|
+
let t = 10 ** 6;
|
|
181
|
+
const store = new InMemoryTaskStore({ now: () => t });
|
|
182
|
+
await store.create(fixtureTask({ id: 'a', createdAt: 0 }));
|
|
183
|
+
await store.create(fixtureTask({ id: 'b', createdAt: 0 }));
|
|
184
|
+
let resolveA: (() => void) | null = null;
|
|
185
|
+
const run = vi.fn((task: AgentTask) =>
|
|
186
|
+
task.id === 'a'
|
|
187
|
+
? new Promise<{ ok: boolean }>((res) => {
|
|
188
|
+
resolveA = () => res({ ok: true });
|
|
189
|
+
})
|
|
190
|
+
: Promise.resolve({ ok: true }),
|
|
191
|
+
);
|
|
192
|
+
const sched = createTaskScheduler({ store, run, now: () => t, concurrency: 1 });
|
|
193
|
+
void sched.tick();
|
|
194
|
+
await flush();
|
|
195
|
+
// a is in-flight (its run never resolves); concurrency 1 holds b off this tick
|
|
196
|
+
expect(sched.active()).toEqual(['a']);
|
|
197
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
198
|
+
resolveA?.();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
describe('TaskRunLog', () => {
|
|
203
|
+
it('aggregates stats, recent runs, and cumulative cost', async () => {
|
|
204
|
+
const log = new TaskRunLog({ now: () => 1, maxRecent: 2 });
|
|
205
|
+
await log.record({
|
|
206
|
+
taskId: 'r', taskName: 'Rebalance', startedAt: 100, durationMs: 5, toolCalls: 3,
|
|
207
|
+
ok: true, error: null, text: 'ok', cost: { usd: 0.01, inputTokens: 10, outputTokens: 5 },
|
|
208
|
+
});
|
|
209
|
+
await log.record({
|
|
210
|
+
taskId: 'r', taskName: 'Rebalance', startedAt: 200, durationMs: 7, toolCalls: 1,
|
|
211
|
+
ok: false, error: 'boom', text: '', cost: { usd: 0.02, inputTokens: 4, outputTokens: 2 },
|
|
212
|
+
});
|
|
213
|
+
const s = await log.statsFor('r');
|
|
214
|
+
expect(s?.runs).toBe(2);
|
|
215
|
+
expect(s?.errors).toBe(1);
|
|
216
|
+
expect(s?.lastError).toBe('boom');
|
|
217
|
+
expect(await log.totalCost()).toEqual({ usd: 0.03, inputTokens: 14, outputTokens: 7 });
|
|
218
|
+
expect((await log.recent()).map((r) => r.startedAt)).toEqual([200, 100]); // newest first
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('caps the recent ring buffer at maxRecent', async () => {
|
|
222
|
+
const log = new TaskRunLog({ now: () => 1, maxRecent: 2 });
|
|
223
|
+
for (let i = 0; i < 5; i++) {
|
|
224
|
+
await log.record({
|
|
225
|
+
taskId: 't', taskName: 'T', startedAt: i, durationMs: 1, toolCalls: 0,
|
|
226
|
+
ok: true, error: null, text: '', cost: { usd: 0, inputTokens: 0, outputTokens: 0 },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
expect(await log.recent()).toHaveLength(2);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('persists + hydrates through injected IO', async () => {
|
|
233
|
+
let snap: RunLogSnapshot | null = null;
|
|
234
|
+
const io: RunLogIO = {
|
|
235
|
+
load: vi.fn(async () => snap),
|
|
236
|
+
save: vi.fn(async (s) => {
|
|
237
|
+
snap = s;
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
const log = new TaskRunLog({ io, now: () => 1 });
|
|
241
|
+
await log.record({
|
|
242
|
+
taskId: 't', taskName: 'T', startedAt: 1, durationMs: 1, toolCalls: 0,
|
|
243
|
+
ok: true, error: null, text: 'hello', cost: { usd: 1, inputTokens: 0, outputTokens: 0 },
|
|
244
|
+
});
|
|
245
|
+
expect(io.save).toHaveBeenCalled();
|
|
246
|
+
const log2 = new TaskRunLog({ io, now: () => 2 });
|
|
247
|
+
expect((await log2.totalCost()).usd).toBe(1);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('evaluateSpend (risk guardrails)', () => {
|
|
252
|
+
const live = { ...DEFAULT_RISK_LIMITS, dryRun: false };
|
|
253
|
+
|
|
254
|
+
it('blocks every spend when dry-run is on', () => {
|
|
255
|
+
const v = evaluateSpend({ kind: 'swap', amountUsd: 1 }, DEFAULT_RISK_LIMITS);
|
|
256
|
+
expect(v.outcome).toBe('block');
|
|
257
|
+
expect(v.reason).toMatch(/dry-run/);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it('blocks at/below the stop-loss floor', () => {
|
|
261
|
+
const v = evaluateSpend(
|
|
262
|
+
{ kind: 'pay', amountSat: 1 },
|
|
263
|
+
{ ...live, stopLossBtcSat: 50_000 },
|
|
264
|
+
{ btcBalanceSat: 50_000 },
|
|
265
|
+
);
|
|
266
|
+
expect(v.outcome).toBe('block');
|
|
267
|
+
expect(v.reason).toMatch(/stop-loss/);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('blocks a spend that would dip below the reserve', () => {
|
|
271
|
+
const v = evaluateSpend(
|
|
272
|
+
{ kind: 'send', amountSat: 60_000 },
|
|
273
|
+
{ ...live, minBtcReserveSat: 50_000, stopLossBtcSat: 0 },
|
|
274
|
+
{ btcBalanceSat: 100_000 },
|
|
275
|
+
);
|
|
276
|
+
expect(v.outcome).toBe('block');
|
|
277
|
+
expect(v.reason).toMatch(/reserve/);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('blocks above the max single spend', () => {
|
|
281
|
+
const v = evaluateSpend({ kind: 'swap', amountUsd: 100 }, { ...live, maxSpendUsd: 50 });
|
|
282
|
+
expect(v.outcome).toBe('block');
|
|
283
|
+
expect(v.reason).toMatch(/exceeds/);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('blocks a new swap when the open-order cap is reached', () => {
|
|
287
|
+
const v = evaluateSpend(
|
|
288
|
+
{ kind: 'swap', amountUsd: 1 },
|
|
289
|
+
{ ...live, maxOpenOrders: 3, autoApproveUnderUsd: 100 },
|
|
290
|
+
{ openOrders: 3 },
|
|
291
|
+
);
|
|
292
|
+
expect(v.outcome).toBe('block');
|
|
293
|
+
expect(v.reason).toMatch(/open orders/);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('auto-approves a small spend under the threshold', () => {
|
|
297
|
+
const v = evaluateSpend(
|
|
298
|
+
{ kind: 'pay', amountUsd: 2, amountSat: 3000 },
|
|
299
|
+
{ ...live, autoApproveUnderUsd: 5, maxSpendUsd: 50, minBtcReserveSat: 0, stopLossBtcSat: 0 },
|
|
300
|
+
{ btcBalanceSat: 1_000_000 },
|
|
301
|
+
);
|
|
302
|
+
expect(v.outcome).toBe('allow');
|
|
303
|
+
expect(v.requiresConfirmation).toBe(false);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('requires confirmation above the auto-approve threshold', () => {
|
|
307
|
+
const v = evaluateSpend(
|
|
308
|
+
{ kind: 'swap', amountUsd: 20 },
|
|
309
|
+
{ ...live, autoApproveUnderUsd: 5, maxSpendUsd: 50 },
|
|
310
|
+
);
|
|
311
|
+
expect(v.outcome).toBe('confirm');
|
|
312
|
+
expect(v.requiresConfirmation).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('requires confirmation when the USD value is unknown (never spends blind)', () => {
|
|
316
|
+
const v = evaluateSpend({ kind: 'channel' }, { ...live, autoApproveUnderUsd: 100 });
|
|
317
|
+
expect(v.outcome).toBe('confirm');
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('buildTaskPrompt', () => {
|
|
322
|
+
const task: AgentTask = {
|
|
323
|
+
id: 'rebalance',
|
|
324
|
+
name: 'Portfolio Rebalance',
|
|
325
|
+
description: 'detect drift',
|
|
326
|
+
skill: 'portfolio-manager',
|
|
327
|
+
scheduleSec: 3600,
|
|
328
|
+
runOnStartup: false,
|
|
329
|
+
allocation: { btcSat: 100, usdt: 5, xaut: 0 },
|
|
330
|
+
enabled: true,
|
|
331
|
+
createdAt: 0,
|
|
332
|
+
lastRunAt: null,
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
it('embeds skill, dry-run flag, allocation, and the strict-JSON contract', () => {
|
|
336
|
+
const p = buildTaskPrompt(task, { dryRun: true, nowIso: '2026-06-19T00:00:00.000Z' });
|
|
337
|
+
expect(p).toMatch(/portfolio-manager/);
|
|
338
|
+
expect(p).toMatch(/dry_run: true/);
|
|
339
|
+
expect(p).toMatch(/Do NOT pay, send, swap/);
|
|
340
|
+
expect(p).toMatch(/"task":"rebalance"/);
|
|
341
|
+
expect(p).toMatch(/"btcSat":100/);
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('switches guidance to fund-safety language when live', () => {
|
|
345
|
+
const p = buildTaskPrompt(task, { dryRun: false, nowIso: '2026-06-19T00:00:00.000Z' });
|
|
346
|
+
expect(p).toMatch(/reserve or stop-loss/);
|
|
347
|
+
});
|
|
348
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Autonomy — the agent's task brain: a registry of scheduled tasks (TaskStore),
|
|
3
|
+
* the record of what they did (TaskRunLog), an interval engine that fires them
|
|
4
|
+
* (createTaskScheduler), and enforced spend guardrails (evaluateSpend).
|
|
5
|
+
*
|
|
6
|
+
* This is the half of the agent's memory the MemoryStore (soul + facts) doesn't
|
|
7
|
+
* cover — the operational state nanobot kept in tasks.json + cron + run history.
|
|
8
|
+
* Storage and timers are injected; the logic is pure TS.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type {
|
|
12
|
+
TaskAllocation,
|
|
13
|
+
AgentTask,
|
|
14
|
+
NewTask,
|
|
15
|
+
TaskSeed,
|
|
16
|
+
TaskStore,
|
|
17
|
+
TaskStoreIO,
|
|
18
|
+
TaskRunCost,
|
|
19
|
+
TaskStats,
|
|
20
|
+
TaskRunRecord,
|
|
21
|
+
RunLogSnapshot,
|
|
22
|
+
RunLogIO,
|
|
23
|
+
TaskRunOutcome,
|
|
24
|
+
RunTask,
|
|
25
|
+
TimerHandle,
|
|
26
|
+
SchedulerOptions,
|
|
27
|
+
TaskScheduler,
|
|
28
|
+
} from './types.js';
|
|
29
|
+
export { ZERO_ALLOCATION } from './types.js';
|
|
30
|
+
|
|
31
|
+
export { InMemoryTaskStore, defaultTaskSeeds } from './task-store.js';
|
|
32
|
+
export type { TaskStoreOptions } from './task-store.js';
|
|
33
|
+
|
|
34
|
+
export { TaskRunLog } from './run-state.js';
|
|
35
|
+
export type { RunLogOptions } from './run-state.js';
|
|
36
|
+
|
|
37
|
+
export { createTaskScheduler } from './scheduler.js';
|
|
38
|
+
|
|
39
|
+
export { evaluateSpend, DEFAULT_RISK_LIMITS } from './risk.js';
|
|
40
|
+
export type {
|
|
41
|
+
SpendKind,
|
|
42
|
+
RiskLimits,
|
|
43
|
+
SpendAction,
|
|
44
|
+
RiskContext,
|
|
45
|
+
RiskOutcome,
|
|
46
|
+
RiskVerdict,
|
|
47
|
+
} from './risk.js';
|
|
48
|
+
|
|
49
|
+
export { buildTaskPrompt } from './prompt.js';
|
|
50
|
+
export type { TaskPromptOptions } from './prompt.js';
|