salt-agent-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,706 @@
1
+ "use strict";
2
+ // Generic executor functions for every Salt-platform action a hosted agent
3
+ // might take (spawning agents, delegation, cards, commerce, wallets,
4
+ // hand-offs), packaged as {name, description, schema, execute} tuples with
5
+ // PLAIN JSON Schema -- not any one provider's tool-calling wrapper shape.
6
+ // Use toAnthropicTools()/toOpenAITools() to adapt for a specific model API.
7
+ //
8
+ // Ported from salt-claude-agent/src/tools.js. What's NOT here: the
9
+ // THINKING_MARKER_RE stripping (Claude-specific wire convention -- exposed
10
+ // instead as an optional `replyTransform` hook) and WEB_SEARCH_TOOL
11
+ // (Anthropic's native server tool, not something this code executes at
12
+ // all -- stays in the reference implementation's own tool list).
13
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
14
+ if (k2 === undefined) k2 = k;
15
+ var desc = Object.getOwnPropertyDescriptor(m, k);
16
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
17
+ desc = { enumerable: true, get: function() { return m[k]; } };
18
+ }
19
+ Object.defineProperty(o, k2, desc);
20
+ }) : (function(o, m, k, k2) {
21
+ if (k2 === undefined) k2 = k;
22
+ o[k2] = m[k];
23
+ }));
24
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
25
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
26
+ }) : function(o, v) {
27
+ o["default"] = v;
28
+ });
29
+ var __importStar = (this && this.__importStar) || (function () {
30
+ var ownKeys = function(o) {
31
+ ownKeys = Object.getOwnPropertyNames || function (o) {
32
+ var ar = [];
33
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
34
+ return ar;
35
+ };
36
+ return ownKeys(o);
37
+ };
38
+ return function (mod) {
39
+ if (mod && mod.__esModule) return mod;
40
+ var result = {};
41
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
42
+ __setModuleDefault(result, mod);
43
+ return result;
44
+ };
45
+ })();
46
+ Object.defineProperty(exports, "__esModule", { value: true });
47
+ exports.createActions = createActions;
48
+ exports.toAnthropicTools = toAnthropicTools;
49
+ exports.toOpenAITools = toOpenAITools;
50
+ const ethers_1 = require("ethers");
51
+ const pgp = __importStar(require("./crypto"));
52
+ const crypto_1 = require("./crypto");
53
+ const delegations = __importStar(require("./delegations"));
54
+ // Shared block-vocabulary schema, so both card tools teach the model the
55
+ // same shape the server enforces (Card model validation in salt-api).
56
+ const BLOCKS_SCHEMA = {
57
+ type: "array",
58
+ description: "1-20 blocks. Types: " +
59
+ '{type:"section", text?:"markdown <=2000", fields?:[{label:"<=40",value:"<=160"}] (<=10)} | ' +
60
+ '{type:"divider"} | ' +
61
+ '{type:"image", url:"http(s) <=500", alt?} | ' +
62
+ '{type:"actions", elements:[1-5 of {type:"button", action_id:"a-z0-9_- <=40, unique", label:"<=40", ' +
63
+ 'style?:"primary"|"danger", action_type?:"default"|"pay", pay?:{amount:"base units string", currency}, ' +
64
+ 'restricted_to?:[user_id,...] (<=20, must be real members of this chat)}]}. ' +
65
+ "Buttons with action_type 'pay' become real Salt payment requests handled by the app itself; " +
66
+ "all other buttons come back as card_interaction events. Every chat member can tap a button by " +
67
+ "default -- set restricted_to on any button that shouldn't be that open (delete, restart, admin " +
68
+ "actions, a purchase meant for one specific person). Everyone still sees the button; anyone not " +
69
+ "on the list sees it locked and can't invoke it, server-enforced.",
70
+ items: { type: "object" },
71
+ };
72
+ /**
73
+ * Builds the full set of Salt-platform actions bound to one client +
74
+ * identity store. Each entry's `schema` is plain JSON Schema; feed the
75
+ * whole set through toAnthropicTools/toOpenAITools before handing it to a
76
+ * model API, and dispatch tool calls through `execute()`.
77
+ */
78
+ function createActions(options) {
79
+ const { client, identities, pgpPassphrase, publicWebhookUrl, walletMasterKey, globalAgentId } = options;
80
+ const replyTransform = options.replyTransform ?? ((text) => text);
81
+ // Generates an EVM keypair, encrypts it under walletMasterKey (no human
82
+ // recovery phrase), and uploads it under `identity`'s own api-key. Shared
83
+ // by the create_wallet action and create_salt_agent's auto-provision hook.
84
+ async function provisionWallet(identity, opts = {}) {
85
+ if (!walletMasterKey) {
86
+ throw new Error("No wallet master key is configured on this server, so I can't generate a wallet.");
87
+ }
88
+ const wallet = ethers_1.ethers.Wallet.createRandom();
89
+ const encrypted_payload = (0, crypto_1.encryptWalletPayload)(walletMasterKey, { private_key: wallet.privateKey, mnemonic: wallet.mnemonic ? wallet.mnemonic.phrase : null }, wallet.address);
90
+ return client.createWallet(identity.apiKey, {
91
+ chain: opts.chain ?? "ethereum",
92
+ testnet: opts.testnet ?? true,
93
+ wallet_type: "ETH",
94
+ public_address: wallet.address,
95
+ public_key: wallet.publicKey,
96
+ encrypted_payload,
97
+ public: false,
98
+ default: false,
99
+ });
100
+ }
101
+ async function myWalletId(caller) {
102
+ const wallets = (await client.listWallets(caller.apiKey));
103
+ const active = (Array.isArray(wallets) ? wallets : []).filter((w) => !w.deleted_at);
104
+ if (active.length === 0)
105
+ throw new Error("You have no wallet to receive payments.");
106
+ return active[0].id;
107
+ }
108
+ // --- create_salt_agent ---------------------------------------------
109
+ async function createSaltAgent(creator, input) {
110
+ const keys = await pgp.generateKeypair(pgpPassphrase);
111
+ // No email or password: salt-api synthesizes a placeholder email and
112
+ // leaves the account non-loginable -- agents are pure API entities.
113
+ const created = await client.createAgent(creator.apiKey, {
114
+ display_name: input.display_name,
115
+ username: input.username,
116
+ description: input.description,
117
+ webhook: publicWebhookUrl,
118
+ category: input.category,
119
+ public_key: keys.publicKey,
120
+ private_key: keys.privateKey,
121
+ public_fingerprint: keys.fingerprint,
122
+ });
123
+ // The create response doesn't include an API key -- fetch it via the
124
+ // owner-only admin endpoint, callable immediately since `creator` is
125
+ // the new agent's owner.
126
+ const admin = await client.getAgentAdmin(creator.apiKey, created.id);
127
+ const childIdentity = {
128
+ saltAppId: created.id,
129
+ username: created.username,
130
+ displayName: created.display_name,
131
+ apiKey: admin.apikey.api_key,
132
+ publicKey: keys.publicKey,
133
+ privateKey: keys.privateKey,
134
+ systemPrompt: input.persona,
135
+ };
136
+ identities.register(childIdentity);
137
+ // Best-effort: a child that can't get a wallet right now can still
138
+ // provision one itself later. Never let a wallet hiccup fail agent creation.
139
+ let walletNote = "It has no receiving wallet yet -- it can provision one itself with create_wallet.";
140
+ try {
141
+ await provisionWallet(childIdentity, { chain: "ethereum", testnet: true });
142
+ walletNote =
143
+ "It was also given a receiving wallet (Ethereum testnet) so it can be paid right away -- " +
144
+ "note that agent-provisioned wallets have no human recovery phrase.";
145
+ }
146
+ catch {
147
+ // Missing wallet master key or a transient API error -- not fatal.
148
+ }
149
+ return {
150
+ created: true,
151
+ id: created.id,
152
+ username: created.username,
153
+ display_name: created.display_name,
154
+ note: `Live now on the same server as you. People can find and message it in the Agents directory at /agents/${created.id}. ${walletNote}`,
155
+ };
156
+ }
157
+ // --- list_salt_agents ------------------------------------------------
158
+ async function listSaltAgents(caller, input = {}) {
159
+ const agents = (await client.listAgents(caller.apiKey));
160
+ let filtered = agents.filter((a) => a.id !== caller.saltAppId);
161
+ if (input.category) {
162
+ const needle = input.category.toLowerCase();
163
+ filtered = filtered.filter((a) => String(a.category || "").toLowerCase().includes(needle));
164
+ }
165
+ filtered.sort((a, b) => (b.average_rating || 0) - (a.average_rating || 0) ||
166
+ (b.ratings_count || 0) - (a.ratings_count || 0) ||
167
+ (b.chats_count || 0) - (a.chats_count || 0));
168
+ const cap = Math.min(Math.max(input.limit || 10, 1), 20);
169
+ return {
170
+ agents: filtered.slice(0, cap).map((a) => ({
171
+ id: a.id,
172
+ username: a.username,
173
+ display_name: a.display_name,
174
+ category: a.category,
175
+ bio: a.bio,
176
+ average_rating: a.average_rating,
177
+ ratings_count: a.ratings_count,
178
+ chats_count: a.chats_count,
179
+ message_price: a.message_price,
180
+ owner: a.owner && a.owner.username,
181
+ })),
182
+ };
183
+ }
184
+ // --- delegate_to_agent ------------------------------------------------
185
+ async function delegateToAgent(caller, input, ctx) {
186
+ const targetId = Number(input.target_agent_id);
187
+ const task = (input.task || "").trim();
188
+ if (!targetId || !task)
189
+ throw new Error("target_agent_id and task are both required.");
190
+ if (targetId === caller.saltAppId)
191
+ throw new Error("You can't delegate to yourself.");
192
+ if (ctx.depth >= delegations.MAX_DELEGATION_DEPTH) {
193
+ throw new Error("Delegation depth limit reached -- handle this yourself (or answer with what you already know) instead of delegating further.");
194
+ }
195
+ let target;
196
+ try {
197
+ target = await client.getAgent(caller.apiKey, targetId);
198
+ }
199
+ catch {
200
+ throw new Error(`No agent with id ${targetId} could be found.`);
201
+ }
202
+ if (target.account_type !== "Agent") {
203
+ throw new Error(`${targetId} isn't an agent -- delegate_to_agent only targets other agents.`);
204
+ }
205
+ const chat = (await client.createOrGetChat(caller.apiKey, targetId));
206
+ const targetMember = (chat.users || []).find((u) => parseInt(u.id, 10) === targetId);
207
+ if (!targetMember || !targetMember.public_key) {
208
+ throw new Error(`Agent ${targetId} has no usable public key; can't message it securely.`);
209
+ }
210
+ // Encrypt for every non-self member with a key, not just the target --
211
+ // delegation chats carry a silent human observer (the caller's root
212
+ // owner, added server-side) whose key must be a recipient for the
213
+ // conversation to be auditable. Falls out naturally from the member list.
214
+ const recipientKeys = (chat.users || [])
215
+ .filter((u) => parseInt(u.id, 10) !== caller.saltAppId && u.public_key)
216
+ .map((u) => u.public_key);
217
+ const marked = delegations.wrap(ctx.depth + 1, task);
218
+ const recipientMessage = await pgp.encryptFor(marked, recipientKeys);
219
+ const senderMessage = await pgp.encryptFor(marked, [caller.publicKey]);
220
+ // register() throws synchronously (before any network call) if this
221
+ // chat already has a pending wait -- surfaces as a normal action error
222
+ // rather than an ambiguous race over which reply belongs to which caller.
223
+ const waitForReply = delegations.register(chat.id, targetId, delegations.DELEGATION_TIMEOUT_MS);
224
+ try {
225
+ await client.postMessage(caller.apiKey, chat.id, recipientMessage, senderMessage);
226
+ }
227
+ catch (err) {
228
+ delegations.cancel(chat.id);
229
+ throw err;
230
+ }
231
+ const rawReplyText = await waitForReply; // rejects with a timeout Error if nothing arrives in time
232
+ const replyText = replyTransform(rawReplyText);
233
+ if (ctx.mainChatId != null) {
234
+ delegations.recordTrail(caller.saltAppId, ctx.mainChatId, {
235
+ agent_id: target.id,
236
+ chat_id: chat.id,
237
+ username: target.username,
238
+ // fee is advisory here; salt-api overwrites it with the agent's
239
+ // actual message_price so the displayed cost can't be fabricated.
240
+ fee: target.message_price || null,
241
+ });
242
+ }
243
+ client.trackEvent(caller.apiKey, "delegation_made", { to_agent_username: target.username });
244
+ return {
245
+ delegated: true,
246
+ target: { id: target.id, username: target.username, display_name: target.display_name },
247
+ reply: replyText,
248
+ };
249
+ }
250
+ // --- post_card / update_card ------------------------------------------
251
+ async function postCard(caller, input, ctx) {
252
+ if (ctx.mainChatId == null)
253
+ throw new Error("post_card is only available while replying in a chat.");
254
+ const result = (await client.postCard(caller.apiKey, ctx.mainChatId, input.blocks, input.text || ""));
255
+ return { posted: true, card_id: result.resource_id, message_id: result.message_id };
256
+ }
257
+ async function updateCard(caller, input) {
258
+ const cardId = Number(input.card_id);
259
+ if (!cardId)
260
+ throw new Error("card_id is required.");
261
+ await client.updateCard(caller.apiKey, cardId, input.blocks);
262
+ return { updated: true, card_id: cardId };
263
+ }
264
+ // --- commerce: create_product / list_products / offer_product / send_invoice / add_usage ---
265
+ async function createProduct(caller, input) {
266
+ const walletId = await myWalletId(caller);
267
+ const product = await client.createProduct(caller.apiKey, { ...input, wallet_id: walletId });
268
+ return { created: true, product };
269
+ }
270
+ async function listProducts(caller, input = {}) {
271
+ const products = await client.listProducts(caller.apiKey, input.seller_id);
272
+ return { products };
273
+ }
274
+ async function offerProduct(caller, input, ctx) {
275
+ if (ctx.mainChatId == null)
276
+ throw new Error("offer_product is only available while replying in a chat.");
277
+ const productId = Number(input.product_id);
278
+ if (!productId)
279
+ throw new Error("product_id is required.");
280
+ await client.shareProduct(caller.apiKey, productId, ctx.mainChatId);
281
+ return { offered: true, product_id: productId };
282
+ }
283
+ async function sendInvoice(caller, input, ctx) {
284
+ if (ctx.mainChatId == null)
285
+ throw new Error("send_invoice is only available while replying in a chat.");
286
+ const items = Array.isArray(input.line_items) ? input.line_items : [];
287
+ if (items.length === 0)
288
+ throw new Error("line_items is required.");
289
+ const members = (await client.getChatMembers(caller.apiKey, ctx.mainChatId));
290
+ let receiver;
291
+ if (input.receiver_username) {
292
+ receiver = members.find((m) => m.username === input.receiver_username);
293
+ if (!receiver)
294
+ throw new Error(`No chat member named @${input.receiver_username}.`);
295
+ }
296
+ else {
297
+ const others = members.filter((m) => m.id !== caller.saltAppId && !m.observer);
298
+ if (others.length !== 1)
299
+ throw new Error("Say who to bill (receiver_username) in a group chat.");
300
+ receiver = others[0];
301
+ }
302
+ // Total must equal the item sum exactly (server checks with BigDecimal)
303
+ // -- compute it here rather than trusting the model's arithmetic, and
304
+ // round to 12 decimals so float noise can't fail the server's equality
305
+ // check. Real prices never need more.
306
+ const total = items.reduce((sum, item) => sum + parseFloat(String(item.subtotal || 0)), 0);
307
+ const amount = total.toFixed(12).replace(/0+$/, "").replace(/\.$/, "");
308
+ const invoice = (await client.createInvoice(caller.apiKey, {
309
+ chatId: ctx.mainChatId,
310
+ receiverId: receiver.id,
311
+ walletId: await myWalletId(caller),
312
+ amount,
313
+ lineItems: items,
314
+ message: items.map((item) => item.name).join(", ").slice(0, 100),
315
+ dueAt: input.due_date,
316
+ }));
317
+ return { sent: true, invoice_id: invoice.id, amount };
318
+ }
319
+ async function addUsage(caller, input, ctx) {
320
+ if (ctx.mainChatId == null)
321
+ throw new Error("add_usage is only available while replying in a chat.");
322
+ const productId = Number(input.product_id);
323
+ if (!productId)
324
+ throw new Error("product_id is required.");
325
+ const result = (await client.addUsage(caller.apiKey, {
326
+ productId,
327
+ chatId: ctx.mainChatId,
328
+ qty: parseFloat(input.qty || "1"),
329
+ description: input.description,
330
+ }));
331
+ return { recorded: true, balance_remaining: result.balance };
332
+ }
333
+ // --- hand-offs: hand_off_to_agent / hand_back_to_concierge / offer_handoff_choices ---
334
+ async function handOffToAgent(caller, input, ctx) {
335
+ if (ctx.mainChatId == null)
336
+ throw new Error("hand_off_to_agent is only available while replying in a chat.");
337
+ const agentId = Number(input.agent_id);
338
+ if (!agentId)
339
+ throw new Error("agent_id is required.");
340
+ if (agentId === caller.saltAppId)
341
+ throw new Error("You can't hand off to yourself.");
342
+ const target = await client.getAgent(caller.apiKey, agentId);
343
+ if (!target || target.account_type !== "Agent")
344
+ throw new Error(`${agentId} isn't an agent.`);
345
+ await client.handOff(caller.apiKey, ctx.mainChatId, agentId, input.reason);
346
+ return {
347
+ handed_off: true,
348
+ to: { id: target.id, username: target.username, display_name: target.display_name },
349
+ note: "They're live in this chat now; you'll be prompted SEPARATELY to write them a briefing -- do NOT include any briefing or summary in your current reply. Your current reply is just the goodbye: one short sentence telling the person who's taking over.",
350
+ };
351
+ }
352
+ async function handBackToConcierge(caller, input, ctx) {
353
+ if (ctx.mainChatId == null)
354
+ throw new Error("hand_back_to_concierge is only available while replying in a chat.");
355
+ if (!globalAgentId)
356
+ throw new Error("No Global agent is configured on this server.");
357
+ if (globalAgentId === caller.saltAppId)
358
+ throw new Error("You already are the concierge.");
359
+ await client.handOff(caller.apiKey, ctx.mainChatId, globalAgentId, input.reason);
360
+ return {
361
+ handed_off: true,
362
+ to: "concierge",
363
+ note: "The concierge is live in this chat now; you'll be prompted SEPARATELY to write them a briefing -- do NOT include any briefing or summary in your current reply. Your current reply is just the goodbye: one short sentence letting the person know you're wrapping up and handing back.",
364
+ };
365
+ }
366
+ async function offerHandoffChoices(caller, input, ctx) {
367
+ if (ctx.mainChatId == null)
368
+ throw new Error("offer_handoff_choices is only available while replying in a chat.");
369
+ const candidates = (Array.isArray(input.candidates) ? input.candidates : []).slice(0, 4);
370
+ if (candidates.length === 0)
371
+ throw new Error("candidates is required.");
372
+ const [directory, members] = await Promise.all([
373
+ client.listAgents(caller.apiKey),
374
+ client.getChatMembers(caller.apiKey, ctx.mainChatId),
375
+ ]);
376
+ const humanIds = members.filter((m) => m.account_type !== "Agent" && !m.observer).map((m) => parseInt(m.id, 10));
377
+ if (humanIds.length === 0)
378
+ throw new Error("No human member to offer choices to.");
379
+ const blocks = [{ type: "section", text: "Who should take this over? Pick one:" }];
380
+ for (const c of candidates) {
381
+ const agent = directory.find((a) => a.id === Number(c.agent_id));
382
+ if (!agent || agent.id === caller.saltAppId)
383
+ continue;
384
+ const fields = [];
385
+ if (agent.category)
386
+ fields.push({ label: "Category", value: String(agent.category).slice(0, 160) });
387
+ if (agent.average_rating)
388
+ fields.push({ label: "Rating", value: `${agent.average_rating}/5 (${agent.ratings_count})` });
389
+ if (agent.message_price)
390
+ fields.push({ label: "Price", value: `${agent.message_price}/message` });
391
+ blocks.push({
392
+ type: "section",
393
+ text: `**${agent.display_name}** -- ${String(c.why || agent.bio || "").slice(0, 300)}`,
394
+ fields,
395
+ });
396
+ blocks.push({
397
+ type: "actions",
398
+ elements: [
399
+ {
400
+ type: "button",
401
+ action_id: `handoff_${agent.id}`,
402
+ label: `Hand off to @${agent.username}`.slice(0, 40),
403
+ style: "primary",
404
+ action_type: "handoff",
405
+ handoff_target_agent_id: agent.id,
406
+ restricted_to: humanIds,
407
+ },
408
+ ],
409
+ });
410
+ }
411
+ if (blocks.length === 1)
412
+ throw new Error("None of those candidates exist in the directory.");
413
+ const card = (await client.postCard(caller.apiKey, ctx.mainChatId, blocks, "Choose who takes over"));
414
+ return {
415
+ offered: true,
416
+ card_id: card.card_id || card.id,
417
+ note: "Card posted; the person picks and the server performs the hand-off. Don't hand off yourself now -- just tell them to choose.",
418
+ };
419
+ }
420
+ // --- create_wallet ------------------------------------------------
421
+ async function createWalletAction(caller, input = {}) {
422
+ const wallet = await provisionWallet(caller, { chain: input.chain || "ethereum", testnet: input.testnet ?? true });
423
+ return {
424
+ created: true,
425
+ wallet_id: wallet.id,
426
+ chain: wallet.chain,
427
+ testnet: wallet.testnet,
428
+ public_address: wallet.public_address,
429
+ note: "No recovery phrase exists for this wallet -- it's only as safe as this server's wallet master key.",
430
+ };
431
+ }
432
+ const definitions = [
433
+ {
434
+ name: "create_salt_agent",
435
+ description: "Register a brand-new Salt agent that YOU own, and bring it fully online. This is a real " +
436
+ "action with a real effect: it generates the new agent a fresh PGP keypair, registers it " +
437
+ "with Salt under your ownership (exactly the same call a human uses to create an agent, " +
438
+ "just with your API key instead of theirs), and starts it running on this same server " +
439
+ "immediately -- it will actually answer messages people send it. Use this when someone " +
440
+ "genuinely wants a new agent to exist, not as a joke or hypothetical. Requires a distinct " +
441
+ "username (lowercase, no spaces).",
442
+ schema: {
443
+ type: "object",
444
+ properties: {
445
+ display_name: { type: "string", description: "Shown on the new agent's profile and message bubbles." },
446
+ username: { type: "string", description: 'Unique handle, lowercase, no spaces, e.g. "weatherbot".' },
447
+ description: { type: "string", description: "One-line bio shown in the Agents directory." },
448
+ category: { type: "string", description: "Optional directory category, e.g. Assistant, Trading, Fun, Utilities." },
449
+ persona: {
450
+ type: "string",
451
+ description: "The new agent's personality and instructions -- this becomes its own system prompt, " +
452
+ "separate from yours. Be specific: tone, what it's for, how it should behave.",
453
+ },
454
+ },
455
+ required: ["display_name", "username", "description", "persona"],
456
+ },
457
+ execute: createSaltAgent,
458
+ },
459
+ {
460
+ name: "list_salt_agents",
461
+ description: "Browse Salt's public Agents directory to find another agent that might help accomplish " +
462
+ "part of your task -- e.g. a specialist for research, trading, or a domain you're not " +
463
+ "suited for. Returns each agent's id, username, category, bio, and reputation (average " +
464
+ "rating out of 5, number of ratings, how many chats have adopted it) so you can judge who's " +
465
+ "actually good, not just who exists -- results are sorted best-rated first. Use this before " +
466
+ "delegate_to_agent so you pick a real, well-regarded agent rather than guessing an id.",
467
+ schema: {
468
+ type: "object",
469
+ properties: {
470
+ category: { type: "string", description: 'Optional loose filter against each agent\'s category, e.g. "Trading", "Assistant". Omit for all categories.' },
471
+ limit: { type: "integer", description: "Max agents to return, highest-rated first. Default 10, max 20." },
472
+ },
473
+ required: [],
474
+ },
475
+ execute: listSaltAgents,
476
+ },
477
+ {
478
+ name: "delegate_to_agent",
479
+ description: "Ask another Salt agent to help with part of your task, over a real Salt chat message -- " +
480
+ "exactly like a human messaging that agent. Creates (or reuses) a 1:1 chat with the target, " +
481
+ "sends it your request, and waits for its reply, which is returned to you to use. This is a " +
482
+ "real, visible conversation, not a shortcut: anyone who opens that chat can see the " +
483
+ "delegation happen. Use list_salt_agents first to pick a suitable, well-rated target rather " +
484
+ "than guessing an id. Only delegate work the other agent is plausibly better positioned for " +
485
+ "-- don't delegate something you can already just answer yourself. You can't delegate to " +
486
+ "yourself, and delegation depth is limited, so don't chain delegations further than the " +
487
+ "task actually needs.",
488
+ schema: {
489
+ type: "object",
490
+ properties: {
491
+ target_agent_id: { type: "integer", description: "The Salt user id of the agent to delegate to (from list_salt_agents)." },
492
+ task: { type: "string", description: "The sub-task or question to send, written as a clear, self-contained request -- the target agent has no other context about your conversation." },
493
+ },
494
+ required: ["target_agent_id", "task"],
495
+ },
496
+ execute: delegateToAgent,
497
+ },
498
+ {
499
+ name: "post_card",
500
+ description: "Post an interactive card (a mini app) into the CURRENT chat: sections of markdown text " +
501
+ "and label/value fields, dividers, images, and button rows. Buttons are live -- when a " +
502
+ "member taps one you receive a card_interaction event and should answer it with " +
503
+ "update_card. Use for polls, order status, dashboards, menus, RSVPs -- anywhere tappable " +
504
+ "structure beats prose. Returns the created card's id; remember it to update the card later.",
505
+ schema: {
506
+ type: "object",
507
+ properties: {
508
+ text: { type: "string", description: 'Short plaintext preview shown in chat lists / notifications, e.g. "Poll: lunch spot".' },
509
+ blocks: BLOCKS_SCHEMA,
510
+ },
511
+ required: ["blocks"],
512
+ },
513
+ execute: postCard,
514
+ },
515
+ {
516
+ name: "update_card",
517
+ description: "Replace the blocks of a card YOU posted -- your response to a card_interaction event " +
518
+ "(e.g. increment the tapped option's vote count and re-render the results). The new state " +
519
+ "updates live in everyone's chat. Send the card's COMPLETE new blocks array, not a diff.",
520
+ schema: {
521
+ type: "object",
522
+ properties: {
523
+ card_id: { type: "integer", description: "The card's id (from post_card's result or the card_interaction event)." },
524
+ blocks: BLOCKS_SCHEMA,
525
+ },
526
+ required: ["card_id", "blocks"],
527
+ },
528
+ execute: updateCard,
529
+ },
530
+ {
531
+ name: "create_product",
532
+ description: "Add a product to YOUR shop (shown on your profile, shareable into chats). Three kinds: " +
533
+ "'one_time' (a good/service bought outright), 'metered' (each use draws down the buyer's " +
534
+ "prepaid credits -- requires `unit`, e.g. 'haiku'), 'subscription' (re-invoiced " +
535
+ "automatically -- requires `interval`: daily|weekly|monthly). Price is a human-decimal " +
536
+ 'string in your wallet\'s native token (e.g. "0.01"). Payments land in your first active wallet.',
537
+ schema: {
538
+ type: "object",
539
+ properties: {
540
+ name: { type: "string", description: "<=80 chars" },
541
+ description: { type: "string" },
542
+ kind: { type: "string", enum: ["one_time", "metered", "subscription"] },
543
+ price: { type: "string", description: 'Positive decimal string, e.g. "0.01"' },
544
+ unit: { type: "string", description: "metered only: what one qty means" },
545
+ interval: { type: "string", enum: ["daily", "weekly", "monthly"], description: "subscription only" },
546
+ },
547
+ required: ["name", "kind", "price"],
548
+ },
549
+ execute: createProduct,
550
+ },
551
+ {
552
+ name: "list_products",
553
+ description: "List shop products -- your own (default) or another seller's (pass seller_id). Use before offering/metering so you reference real product ids and current prices.",
554
+ schema: {
555
+ type: "object",
556
+ properties: { seller_id: { type: "integer", description: "Omit for your own products." } },
557
+ },
558
+ execute: listProducts,
559
+ },
560
+ {
561
+ name: "offer_product",
562
+ description: "Share one of YOUR products into the current chat as a bubble with a real Buy button. Buying drops an invoice the buyer pays on Salt's normal rail -- you never handle the money.",
563
+ schema: {
564
+ type: "object",
565
+ properties: { product_id: { type: "integer" } },
566
+ required: ["product_id"],
567
+ },
568
+ execute: offerProduct,
569
+ },
570
+ {
571
+ name: "send_invoice",
572
+ description: "Send an itemized invoice into the current chat. Each line item needs name, qty, " +
573
+ "unit_price, and subtotal (qty x unit_price, exact); the invoice total must equal the sum " +
574
+ "of subtotals -- the server rejects any mismatch. The payer gets a Pay button with the " +
575
+ "full breakdown. NEVER state amounts in prose that differ from the invoice.",
576
+ schema: {
577
+ type: "object",
578
+ properties: {
579
+ receiver_username: { type: "string", description: "Who pays. Omit in a 1:1 (inferred)." },
580
+ line_items: {
581
+ type: "array",
582
+ description: '[{name:"<=120", qty:"1", unit_price:"0.01", subtotal:"0.01", product_id?}]',
583
+ items: { type: "object" },
584
+ },
585
+ due_date: { type: "string", description: "Optional, YYYY-MM-DD." },
586
+ },
587
+ required: ["line_items"],
588
+ },
589
+ execute: sendInvoice,
590
+ },
591
+ {
592
+ name: "add_usage",
593
+ description: "Record metered usage of one of YOUR metered products against the chat partner's prepaid " +
594
+ "credits -- call it right after doing the billable work (qty = units delivered). The " +
595
+ "amount is computed server-side from the product's price. If it fails with " +
596
+ "insufficient_credits, tell the user their balance and ask them to top up from the Credits " +
597
+ "strip to continue.",
598
+ schema: {
599
+ type: "object",
600
+ properties: {
601
+ product_id: { type: "integer" },
602
+ qty: { type: "string", description: 'Units delivered, e.g. "1" or "3". Default "1".' },
603
+ description: { type: "string", description: "Short human-readable line for the ledger." },
604
+ },
605
+ required: ["product_id"],
606
+ },
607
+ execute: addUsage,
608
+ },
609
+ {
610
+ name: "create_wallet",
611
+ description: "Provision a real receiving wallet for YOURSELF, so you can be paid (sell products, " +
612
+ "invoice, meter usage). Generates a fresh Ethereum keypair server-side and uploads it " +
613
+ "encrypted -- there is NO human-memorable recovery phrase for this wallet (unlike a " +
614
+ "person's Salt wallet); if this server's wallet master key is ever lost, its funds are " +
615
+ "unrecoverable. Only call this if you don't already have a wallet (check via list_products " +
616
+ "or ask; calling again just adds another).",
617
+ schema: {
618
+ type: "object",
619
+ properties: {
620
+ chain: { type: "string", description: 'Chain key, e.g. "ethereum", "polygon", "base". Default "ethereum".' },
621
+ testnet: { type: "boolean", description: "Default true." },
622
+ },
623
+ },
624
+ execute: createWalletAction,
625
+ },
626
+ {
627
+ name: "hand_off_to_agent",
628
+ description: "Hand THIS conversation off to another Salt agent -- they become the live participant in " +
629
+ "this same chat (no new chat is created), you stay in the room silently, and you'll be " +
630
+ "asked to write a briefing for them right after. Use in AUTO-mode hand-off chats when " +
631
+ "another agent clearly serves the person better (check list_salt_agents first for a " +
632
+ "well-rated match). ALWAYS tell the person you're handing off and why, in the same reply or " +
633
+ "just before. Never use this in MANUAL mode -- offer_handoff_choices is for that.",
634
+ schema: {
635
+ type: "object",
636
+ properties: {
637
+ agent_id: { type: "integer", description: "The agent taking over (from list_salt_agents)." },
638
+ reason: { type: "string", description: "One line on why -- shown in the hand-off trail." },
639
+ },
640
+ required: ["agent_id", "reason"],
641
+ },
642
+ execute: handOffToAgent,
643
+ },
644
+ {
645
+ name: "hand_back_to_concierge",
646
+ description: "Hand this conversation back to the Global concierge agent -- the router this person " +
647
+ "originally started with. Use this as soon as you've wrapped up what you were brought in " +
648
+ "for: you answered their question, finished the task, hit something outside your scope, or " +
649
+ "they signal they're done / want something else. Don't wait to be asked -- a person who has " +
650
+ "to explicitly request 'take me back to the concierge' is a failure of this tool's whole " +
651
+ "point. Same mechanics as hand_off_to_agent: you stay in the room silently, and you'll be " +
652
+ "asked to write a briefing right after. AUTO-mode hand-off chats only -- in MANUAL mode, " +
653
+ "tell the person they can jump back to the concierge from chat info themselves.",
654
+ schema: {
655
+ type: "object",
656
+ properties: { reason: { type: "string", description: "One line on why you're handing back -- shown in the hand-off trail." } },
657
+ required: ["reason"],
658
+ },
659
+ execute: handBackToConcierge,
660
+ },
661
+ {
662
+ name: "offer_handoff_choices",
663
+ description: "MANUAL-mode hand-off: post an interactive picker into this chat with 2-4 candidate " +
664
+ "agents, and let the PERSON choose who takes over (tapping performs the hand-off " +
665
+ "server-side; you don't). Pick candidates via list_salt_agents first and give each a " +
666
+ "one-line reason tailored to what the person actually needs. Only the human can tap the buttons.",
667
+ schema: {
668
+ type: "object",
669
+ properties: {
670
+ candidates: {
671
+ type: "array",
672
+ description: "2-4 entries: {agent_id, why} -- `why` is your one-line pitch for this candidate.",
673
+ items: {
674
+ type: "object",
675
+ properties: { agent_id: { type: "integer" }, why: { type: "string" } },
676
+ required: ["agent_id", "why"],
677
+ },
678
+ },
679
+ },
680
+ required: ["candidates"],
681
+ },
682
+ execute: offerHandoffChoices,
683
+ },
684
+ ];
685
+ const byName = new Map(definitions.map((d) => [d.name, d]));
686
+ async function execute(name, input, caller, ctx) {
687
+ const definition = byName.get(name);
688
+ if (!definition)
689
+ throw new Error(`Unknown action: ${name}`);
690
+ // One capture per action a model invokes -- just the name, no
691
+ // arguments (those can contain content). Fire-and-forget.
692
+ client.trackEvent(caller.apiKey, "tool_used", { tool: name });
693
+ return definition.execute(caller, input, ctx);
694
+ }
695
+ return { definitions, execute, myWalletId, provisionWallet };
696
+ }
697
+ // --- provider adapters -------------------------------------------------
698
+ /** Anthropic Messages API tool format: {name, description, input_schema}. */
699
+ function toAnthropicTools(definitions) {
700
+ return definitions.map((d) => ({ name: d.name, description: d.description, input_schema: d.schema }));
701
+ }
702
+ /** OpenAI-style function-calling tool format: {type:"function", function:{name, description, parameters}}. */
703
+ function toOpenAITools(definitions) {
704
+ return definitions.map((d) => ({ type: "function", function: { name: d.name, description: d.description, parameters: d.schema } }));
705
+ }
706
+ //# sourceMappingURL=actions.js.map