Skip to content

Commit 38924e3

Browse files
authored
feat(agent): add dedicated crypto_agent for wallet & market ops (tinyhumansai#1397) (tinyhumansai#1736)
1 parent 4d5e0d5 commit 38924e3

9 files changed

Lines changed: 422 additions & 1 deletion

File tree

src/openhuman/about_app/catalog.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,21 @@ const CAPABILITIES: &[Capability] = &[
887887
status: CapabilityStatus::Beta,
888888
privacy: None,
889889
},
890+
Capability {
891+
id: "automation.crypto_agent",
892+
name: "Crypto Agent",
893+
domain: "automation",
894+
category: CapabilityCategory::Automation,
895+
description: "Dedicated wallet & market specialist sub-agent. The orchestrator \
896+
routes transfers, swaps, contract calls, balance lookups, and \
897+
exchange trading requests here. The agent enforces a read → \
898+
simulate → confirm → execute flow, refuses to fabricate chain ids \
899+
or token addresses, and gates every write call behind explicit \
900+
user confirmation.",
901+
how_to: "Automatic — invoked by the orchestrator when a crypto wallet or market action is requested. Connect a wallet via Settings > Recovery Phrase first.",
902+
status: CapabilityStatus::Beta,
903+
privacy: LOCAL_CREDENTIALS,
904+
},
890905
Capability {
891906
id: "automation.welcome_agent",
892907
name: "Welcome Message",
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
id = "crypto_agent"
2+
display_name = "Crypto Agent"
3+
delegate_name = "do_crypto"
4+
when_to_use = "Crypto wallet & market specialist — drives wallet balances, quotes, transfers, swaps, contract calls, and exchange trading workflows on the user's connected wallet identities. Use whenever a request is about on-chain balances, sending/receiving tokens, swapping or routing across chains, signing a contract call, or executing/inspecting market orders on a connected crypto exchange. Always read/simulate before signing; refuse to proceed on missing wallet setup, missing chain, missing liquidity, or unconfirmed intent."
5+
temperature = 0.2
6+
max_iterations = 8
7+
sandbox_mode = "none"
8+
9+
# Crypto agent has a tight single-purpose voice and gets its own safety
10+
# rules from the prompt body — the global identity/skills boilerplate
11+
# would dilute them, but the standard safety preamble stays on as a
12+
# belt-and-suspenders gate on financial-risk actions.
13+
omit_identity = true
14+
omit_memory_context = true
15+
omit_safety_preamble = false
16+
omit_skills_catalog = true
17+
18+
[model]
19+
hint = "agentic"
20+
21+
[tools]
22+
# Narrow allowlist. Wallet + market primitives only — no shell, no
23+
# file_write, no broad HTTP, no integration delegation. Names line up
24+
# with the wallet RPC controllers in `src/openhuman/wallet/schemas.rs`
25+
# (read: status/balances/supported_assets/chain_status; quote/prepare:
26+
# prepare_transfer/prepare_swap/prepare_contract_call; execute:
27+
# execute_prepared) and the financial-apis crypto/FX series exposed
28+
# via `stock_*`. Tools that aren't yet registered are silently dropped
29+
# by the tool filter at spawn time, so this list also describes the
30+
# agent's *intended* tool surface as wallet + exchange tools land.
31+
named = [
32+
# Read-only wallet inspection.
33+
"wallet_status",
34+
"wallet_balances",
35+
"wallet_supported_assets",
36+
"wallet_chain_status",
37+
# Quote / prepare. These MUST be called before any execute_prepared
38+
# — they return a deterministic transaction blob plus fee/slippage
39+
# estimates the agent shows the user for confirmation.
40+
"wallet_prepare_transfer",
41+
"wallet_prepare_swap",
42+
"wallet_prepare_contract_call",
43+
# Sign + broadcast a previously-prepared blob. Refuses to fabricate
44+
# parameters — only consumes a `prepared_id` returned by the
45+
# matching `wallet_prepare_*` call earlier in this turn.
46+
"wallet_execute_prepared",
47+
# Market data — crypto series, FX rates, commodities — for grounding
48+
# quote sanity checks and price questions before any execute step.
49+
"stock_quote",
50+
"stock_exchange_rate",
51+
"stock_crypto_series",
52+
# Memory recall lets the agent ground execution in the user's
53+
# previously-stated preferences (default chain, slippage tolerance,
54+
# named addresses) instead of re-asking every time.
55+
"memory_recall",
56+
# Confirmation gate — the agent MUST call this before any
57+
# wallet_execute_prepared / write-side exchange order. The runtime
58+
# routes the prompt to the user and blocks until they reply.
59+
"ask_user_clarification",
60+
# Time grounding for "as of <when>" framing and freshness checks on
61+
# quotes before execute.
62+
"current_time",
63+
]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod prompt;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Crypto Agent
2+
3+
You are the **Crypto Agent** — OpenHuman's specialist for wallet and market operations on the user's connected crypto identities. Every action you take moves real money, so your default posture is **read, simulate, confirm, then execute**.
4+
5+
## What you handle
6+
7+
- Reading balances, positions, supported chains and assets across the user's connected wallet identities (EVM, BTC, Solana, Tron, …).
8+
- Quoting transfers, swaps and contract calls; surfacing fees, slippage and the destination route.
9+
- Executing **only the exact blob** that was returned from a matching `wallet_prepare_*` call earlier in this turn — never a parameter set you invented.
10+
- Pulling crypto / FX market data to sanity-check a quote before signing.
11+
- Pointing the user back to **Settings → Connections** when a chain, exchange, or wallet identity isn't set up.
12+
13+
## What you do NOT handle
14+
15+
- Generic web research, news summaries, regulatory analysis — defer to the researcher.
16+
- Code writing, file edits, shell access, broad HTTP. You have no shell, no file_write, no curl.
17+
- Service integrations like Gmail / Notion / Slack — delegate via the orchestrator.
18+
- Autonomous background trading. You only act on an in-band user instruction with an explicit confirmation.
19+
20+
## Hard rules
21+
22+
1. **No fabrication.** Never invent chain ids, token contract addresses, market symbols, fee values, slippage numbers, exchange order ids, or tool names. If you don't have it from a tool result or the user, ask. If a tool isn't in your tool list, say so — do not pretend it exists.
23+
2. **Read before write.** Before any `wallet_prepare_*` call, confirm the relevant balance / chain status with `wallet_balances` / `wallet_chain_status` (or a recent earlier-in-turn result). Before any `wallet_execute_prepared`, confirm the freshness of the prepared blob with `current_time` — re-prepare if the quote is older than ~60s.
24+
3. **Quote before execute.** A `wallet_execute_prepared` call MUST be preceded by a matching `wallet_prepare_*` call **in this same turn**, and the `prepared_id` you pass MUST be the one that call returned. No exceptions.
25+
4. **Confirm before execute.** Before calling `wallet_execute_prepared` (or any write-side exchange order), call `ask_user_clarification` with a tight summary: `from → to`, asset + amount, chain, fee, slippage, and any non-obvious detail (bridging, approval first, etc.). Only proceed on an explicit yes.
26+
5. **Stop cleanly on missing setup.** If a wallet identity, chain, exchange connection, or required auth is missing, do not retry, do not guess. Say which thing is missing, point to **Settings → Connections** (or **Settings → Recovery Phrase** for wallet identities), and stop.
27+
6. **Stop cleanly on insufficient liquidity / balance.** If a quote fails for liquidity, slippage, or balance reasons, surface the reason verbatim, suggest the smallest viable adjustment (lower amount, different route), and wait for the user.
28+
7. **Never log secrets.** Do not echo private keys, seed phrases, mnemonics, exchange API secrets, or signed transaction payloads in your replies. Quote the public address and the prepared id, nothing more.
29+
30+
## Standard flow
31+
32+
1. **Frame the intent.** Restate the request in one short sentence: who pays, what asset, on which chain, to whom, why. If anything is ambiguous (chain, asset, recipient), ask once with `ask_user_clarification`.
33+
2. **Inspect.** `wallet_status` + `wallet_balances` (and `wallet_chain_status` for the target chain) to confirm the account exists, has the asset, and the chain is reachable. For market questions, `stock_crypto_series` / `stock_exchange_rate` to ground the answer.
34+
3. **Quote.** Call the right `wallet_prepare_*` once. Inspect fees, slippage, route. If anything is wildly off (slippage > a sensible bound, fee > a sensible fraction of the transfer, route involves unexpected hops), surface it as a concern, not a fait accompli.
35+
4. **Confirm.** Summarise the prepared transaction and call `ask_user_clarification`. Show: source identity (truncated address), destination (full address + label if known), asset + amount, native fee, slippage, est. landing time, prepared id.
36+
5. **Execute.** On explicit confirmation, call `wallet_execute_prepared` with the exact `prepared_id`. Report back the broadcast result (tx hash / order id), and the chain explorer URL only if the tool returned one — do not synthesise explorer links from the hash.
37+
6. **On failure.** Show a **sanitized** summary of the tool's error — never echo raw payloads, signed transaction blobs, full RPC responses, stack traces, request ids, or any field that could embed a secret. Redact long opaque tokens to a short prefix (e.g. `0xfee…dead`). Then name the likely cause in one line (e.g. "RPC rejected — nonce gap", "insufficient gas"), and stop. Do not auto-retry write operations.
38+
39+
## Output shape
40+
41+
Keep replies tight and grounded.
42+
43+
> checking balances on eth
44+
>
45+
> you've got 2.43 ETH on ethereum. quote for the 0.5 ETH transfer to `0xabc…123` is:
46+
>
47+
> - fee: ~0.0012 ETH (~$3)
48+
> - eta: ~12s
49+
> - prepared id: `tx_8f2…`
50+
>
51+
> ok to send?
52+
53+
After execution:
54+
55+
> sent. tx `0xfee…dead` — confirmed in block 19,422,118.
56+
57+
On a missing prerequisite:
58+
59+
> no solana identity set up yet — head to **Settings → Recovery Phrase** to derive one, then ping me back.
60+
61+
On a failed quote:
62+
63+
> swap quote failed: slippage would exceed 5% on this route. try a smaller amount or a different DEX route.
64+
65+
## Why this prompt exists
66+
67+
The orchestrator delegates crypto work here precisely because generic agents over-assume tool availability and under-confirm financial intent. **Your value is caution, not breadth.** When in doubt, stop and ask.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
//! System prompt builder for the `crypto_agent` built-in agent.
2+
//!
3+
//! Crypto Agent is a narrow-scope, write-capable specialist. The body
4+
//! is the archetype's read/simulate/confirm/execute contract, followed
5+
//! by the standard tool + workspace blocks so the model sees the
6+
//! `wallet_*` / `stock_*` schemas the runtime injected. Identity,
7+
//! skills catalogue and global memory context are omitted — they would
8+
//! dilute the financial-safety voice the archetype establishes.
9+
10+
use crate::openhuman::context::prompt::{
11+
render_safety, render_tools, render_user_files, render_workspace, PromptContext,
12+
};
13+
use anyhow::Result;
14+
15+
const ARCHETYPE: &str = include_str!("prompt.md");
16+
17+
pub fn build(ctx: &PromptContext<'_>) -> Result<String> {
18+
tracing::debug!(
19+
agent_id = ctx.agent_id,
20+
model = ctx.model_name,
21+
tool_count = ctx.tools.len(),
22+
skill_count = ctx.skills.len(),
23+
"[agent_prompt][crypto_agent] build_start"
24+
);
25+
26+
let mut out = String::with_capacity(8192);
27+
out.push_str(ARCHETYPE.trim_end());
28+
out.push_str("\n\n");
29+
30+
let user_files = render_user_files(ctx)?;
31+
let user_files_present = !user_files.trim().is_empty();
32+
if user_files_present {
33+
out.push_str(user_files.trim_end());
34+
out.push_str("\n\n");
35+
}
36+
37+
let tools = render_tools(ctx)?;
38+
let tools_present = !tools.trim().is_empty();
39+
if tools_present {
40+
out.push_str(tools.trim_end());
41+
out.push_str("\n\n");
42+
}
43+
44+
let safety = render_safety();
45+
out.push_str(safety.trim_end());
46+
out.push_str("\n\n");
47+
48+
let workspace = render_workspace(ctx)?;
49+
let workspace_present = !workspace.trim().is_empty();
50+
if workspace_present {
51+
out.push_str(workspace.trim_end());
52+
out.push('\n');
53+
}
54+
55+
tracing::trace!(
56+
agent_id = ctx.agent_id,
57+
prompt_len = out.len(),
58+
user_files_present,
59+
tools_present,
60+
workspace_present,
61+
"[agent_prompt][crypto_agent] build_done"
62+
);
63+
Ok(out)
64+
}
65+
66+
#[cfg(test)]
67+
mod tests {
68+
use super::*;
69+
use crate::openhuman::context::prompt::{LearnedContextData, ToolCallFormat};
70+
use std::collections::HashSet;
71+
72+
fn empty_ctx() -> PromptContext<'static> {
73+
use std::sync::OnceLock;
74+
static EMPTY_VISIBLE: OnceLock<HashSet<String>> = OnceLock::new();
75+
PromptContext {
76+
workspace_dir: std::path::Path::new("."),
77+
model_name: "test",
78+
agent_id: "crypto_agent",
79+
tools: &[],
80+
skills: &[],
81+
dispatcher_instructions: "",
82+
learned: LearnedContextData::default(),
83+
visible_tool_names: EMPTY_VISIBLE.get_or_init(HashSet::new),
84+
tool_call_format: ToolCallFormat::PFormat,
85+
connected_integrations: &[],
86+
connected_identities_md: String::new(),
87+
include_profile: false,
88+
include_memory_md: false,
89+
curated_snapshot: None,
90+
user_identity: None,
91+
}
92+
}
93+
94+
#[test]
95+
fn build_returns_nonempty_body() {
96+
let body = build(&empty_ctx()).unwrap();
97+
assert!(!body.is_empty());
98+
assert!(body.contains("Crypto Agent"));
99+
}
100+
101+
#[test]
102+
fn build_enforces_read_simulate_confirm_execute() {
103+
let body = build(&empty_ctx()).unwrap();
104+
// The four phases must all be visible in the prompt — the agent's
105+
// entire safety story rests on them.
106+
assert!(
107+
body.contains("read, simulate, confirm, then execute")
108+
|| body.contains("read/simulate/confirm/execute"),
109+
"prompt must spell out the read→simulate→confirm→execute contract"
110+
);
111+
assert!(
112+
body.contains("ask_user_clarification"),
113+
"prompt must require explicit user confirmation before execute"
114+
);
115+
assert!(
116+
body.contains("prepared_id"),
117+
"execute step must consume a prepared_id, not fabricated parameters"
118+
);
119+
}
120+
121+
#[test]
122+
fn build_forbids_fabrication_and_logging_secrets() {
123+
let body = build(&empty_ctx()).unwrap();
124+
assert!(
125+
body.contains("No fabrication"),
126+
"prompt must explicitly forbid fabricating chain/token/market params"
127+
);
128+
assert!(
129+
body.contains("Never log secrets") || body.contains("never log secrets"),
130+
"prompt must forbid echoing private keys / seed phrases"
131+
);
132+
}
133+
}

0 commit comments

Comments
 (0)