salt-agent-sdk 0.1.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +941 -30
- package/dist/actions.d.ts +21 -5
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +673 -54
- package/dist/actions.js.map +1 -1
- package/dist/ask.d.ts +83 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +215 -0
- package/dist/ask.js.map +1 -0
- package/dist/chatPrivacy.d.ts +34 -0
- package/dist/chatPrivacy.d.ts.map +1 -0
- package/dist/chatPrivacy.js +60 -0
- package/dist/chatPrivacy.js.map +1 -0
- package/dist/client.d.ts +3123 -35
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1267 -143
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +16 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +16 -5
- package/dist/config.js.map +1 -1
- package/dist/crypto.d.ts +62 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +121 -0
- package/dist/crypto.js.map +1 -1
- package/dist/delegations.d.ts +25 -8
- package/dist/delegations.d.ts.map +1 -1
- package/dist/delegations.js +67 -11
- package/dist/delegations.js.map +1 -1
- package/dist/device/client.d.ts +161 -0
- package/dist/device/client.d.ts.map +1 -0
- package/dist/device/client.js +601 -0
- package/dist/device/client.js.map +1 -0
- package/dist/device/host.d.ts +222 -0
- package/dist/device/host.d.ts.map +1 -0
- package/dist/device/host.js +891 -0
- package/dist/device/host.js.map +1 -0
- package/dist/device/http.d.ts +12 -0
- package/dist/device/http.d.ts.map +1 -0
- package/dist/device/http.js +124 -0
- package/dist/device/http.js.map +1 -0
- package/dist/device/index.d.ts +5 -0
- package/dist/device/index.d.ts.map +1 -0
- package/dist/device/index.js +27 -0
- package/dist/device/index.js.map +1 -0
- package/dist/device/protocol.d.ts +312 -0
- package/dist/device/protocol.d.ts.map +1 -0
- package/dist/device/protocol.js +435 -0
- package/dist/device/protocol.js.map +1 -0
- package/dist/identities.d.ts +9 -2
- package/dist/identities.d.ts.map +1 -1
- package/dist/identities.js +33 -3
- package/dist/identities.js.map +1 -1
- package/dist/identity.d.ts +155 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +196 -0
- package/dist/identity.js.map +1 -0
- package/dist/identityShare.d.ts +129 -0
- package/dist/identityShare.d.ts.map +1 -0
- package/dist/identityShare.js +308 -0
- package/dist/identityShare.js.map +1 -0
- package/dist/ids.d.ts +11 -0
- package/dist/ids.d.ts.map +1 -0
- package/dist/ids.js +17 -0
- package/dist/ids.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -1
- package/dist/reconcile.d.ts +25 -0
- package/dist/reconcile.d.ts.map +1 -0
- package/dist/reconcile.js +60 -0
- package/dist/reconcile.js.map +1 -0
- package/dist/recovery.d.ts +51 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +59 -0
- package/dist/recovery.js.map +1 -0
- package/dist/register.d.ts +32 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +65 -0
- package/dist/register.js.map +1 -0
- package/dist/scriptJob.d.ts +92 -0
- package/dist/scriptJob.d.ts.map +1 -0
- package/dist/scriptJob.js +270 -0
- package/dist/scriptJob.js.map +1 -0
- package/dist/sessions.d.ts +131 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +240 -0
- package/dist/sessions.js.map +1 -0
- package/dist/socket.d.ts +115 -0
- package/dist/socket.d.ts.map +1 -0
- package/dist/socket.js +1038 -0
- package/dist/socket.js.map +1 -0
- package/dist/webhook.d.ts +627 -28
- package/dist/webhook.d.ts.map +1 -1
- package/dist/webhook.js +1388 -84
- package/dist/webhook.js.map +1 -1
- package/dist/work.d.ts +57 -0
- package/dist/work.d.ts.map +1 -0
- package/dist/work.js +213 -0
- package/dist/work.js.map +1 -0
- package/package.json +26 -4
package/dist/actions.js
CHANGED
|
@@ -47,10 +47,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
47
47
|
exports.createActions = createActions;
|
|
48
48
|
exports.toAnthropicTools = toAnthropicTools;
|
|
49
49
|
exports.toOpenAITools = toOpenAITools;
|
|
50
|
+
const node_crypto_1 = require("node:crypto");
|
|
50
51
|
const ethers_1 = require("ethers");
|
|
52
|
+
const client_1 = require("./client");
|
|
51
53
|
const pgp = __importStar(require("./crypto"));
|
|
52
54
|
const crypto_1 = require("./crypto");
|
|
53
55
|
const delegations = __importStar(require("./delegations"));
|
|
56
|
+
const work_1 = require("./work");
|
|
57
|
+
const identityShare_js_1 = require("./identityShare.js");
|
|
58
|
+
const ids_js_1 = require("./ids.js");
|
|
59
|
+
const identity_js_1 = require("./identity.js");
|
|
54
60
|
// Shared block-vocabulary schema, so both card tools teach the model the
|
|
55
61
|
// same shape the server enforces (Card model validation in salt-api).
|
|
56
62
|
const BLOCKS_SCHEMA = {
|
|
@@ -59,11 +65,14 @@ const BLOCKS_SCHEMA = {
|
|
|
59
65
|
'{type:"section", text?:"markdown <=2000", fields?:[{label:"<=40",value:"<=160"}] (<=10)} | ' +
|
|
60
66
|
'{type:"divider"} | ' +
|
|
61
67
|
'{type:"image", url:"http(s) <=500", alt?} | ' +
|
|
68
|
+
'{type:"input", block_id:"a-z0-9_- <=40, unique across the whole card", label:"1-40", ' +
|
|
69
|
+
'placeholder?:"<=40", multiline?:bool, max_length?:"1-1000, default 500"} | ' +
|
|
62
70
|
'{type:"actions", elements:[1-5 of {type:"button", action_id:"a-z0-9_- <=40, unique", label:"<=40", ' +
|
|
63
71
|
'style?:"primary"|"danger", action_type?:"default"|"pay", pay?:{amount:"base units string", currency}, ' +
|
|
64
72
|
'restricted_to?:[user_id,...] (<=20, must be real members of this chat)}]}. ' +
|
|
65
73
|
"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
|
|
74
|
+
"all other buttons come back as card_interaction events carrying every input block's current " +
|
|
75
|
+
"value (keyed by block_id) alongside the tapped action_id. Every chat member can tap a button by " +
|
|
67
76
|
"default -- set restricted_to on any button that shouldn't be that open (delete, restart, admin " +
|
|
68
77
|
"actions, a purchase meant for one specific person). Everyone still sees the button; anyone not " +
|
|
69
78
|
"on the list sees it locked and can't invoke it, server-enforced.",
|
|
@@ -76,8 +85,24 @@ const BLOCKS_SCHEMA = {
|
|
|
76
85
|
* model API, and dispatch tool calls through `execute()`.
|
|
77
86
|
*/
|
|
78
87
|
function createActions(options) {
|
|
79
|
-
const { client, identities, pgpPassphrase, publicWebhookUrl, walletMasterKey,
|
|
88
|
+
const { client, identities, pgpPassphrase, publicWebhookUrl, walletMasterKey, conciergeAgentId } = options;
|
|
80
89
|
const replyTransform = options.replyTransform ?? ((text) => text);
|
|
90
|
+
// Progress reports to the person a turn is for (work.ts). Only a turn that
|
|
91
|
+
// answers a person in a chat has anyone to report to; everything else is a
|
|
92
|
+
// quiet no-op, so a delegation made by a background job or a delegated hop
|
|
93
|
+
// behaves exactly as it did before reports existed.
|
|
94
|
+
const workReporter = (0, work_1.createWorkReporter)(client);
|
|
95
|
+
// R3/R4 (identityShare.ts): identity_share/identity_ask/identity_revoke below.
|
|
96
|
+
const identitySharer = (0, identityShare_js_1.createIdentitySharer)(client, pgpPassphrase);
|
|
97
|
+
const reportTarget = (ctx) => ctx.mainChatId != null && ctx.requesterId != null && ctx.depth === 0
|
|
98
|
+
? { chatId: ctx.mainChatId, requesterId: ctx.requesterId }
|
|
99
|
+
: null;
|
|
100
|
+
async function reportWork(caller, ctx, report) {
|
|
101
|
+
const target = reportTarget(ctx);
|
|
102
|
+
if (!target)
|
|
103
|
+
return false;
|
|
104
|
+
return workReporter.report(caller, target, report);
|
|
105
|
+
}
|
|
81
106
|
// Generates an EVM keypair, encrypts it under walletMasterKey (no human
|
|
82
107
|
// recovery phrase), and uploads it under `identity`'s own api-key. Shared
|
|
83
108
|
// by the create_wallet action and create_salt_agent's auto-provision hook.
|
|
@@ -105,11 +130,38 @@ function createActions(options) {
|
|
|
105
130
|
throw new Error("You have no wallet to receive payments.");
|
|
106
131
|
return active[0].id;
|
|
107
132
|
}
|
|
133
|
+
// Open rooms: every action below that posts a wire-protocol message
|
|
134
|
+
// (delegate_to_agent, consult_agent, request_floor -- never a card or
|
|
135
|
+
// other resource, which is already plain JSON with no client-side
|
|
136
|
+
// encryption at all, on ANY chat) goes through this one branch point.
|
|
137
|
+
// `chatEncrypted` false posts `plaintext` as-is via the same
|
|
138
|
+
// postPlainMessage path client.ts's own open-room support uses;
|
|
139
|
+
// `chatEncrypted` true (the default -- an older server or an ordinary
|
|
140
|
+
// chat) is BYTE-IDENTICAL to what this SDK always did: PGP-encrypted for
|
|
141
|
+
// `recipientKeys` plus the caller's own key. Never send ciphertext to an
|
|
142
|
+
// open room or plaintext to an encrypted one -- salt-api refuses both
|
|
143
|
+
// directions, and mixing them up here would just turn that refusal into
|
|
144
|
+
// a confusing action-execution error instead of a deliberate choice.
|
|
145
|
+
// `recipientKeys` is only read (and need not be non-empty) when
|
|
146
|
+
// `chatEncrypted` is true.
|
|
147
|
+
async function postWireMessage(caller, chatId, chatEncrypted, plaintext, recipientKeys) {
|
|
148
|
+
if (!chatEncrypted)
|
|
149
|
+
return client.postPlainMessage(caller.apiKey, chatId, plaintext);
|
|
150
|
+
const recipientMessage = await pgp.encryptFor(plaintext, recipientKeys);
|
|
151
|
+
const senderMessage = await pgp.encryptFor(plaintext, [caller.publicKey]);
|
|
152
|
+
return client.postMessage(caller.apiKey, chatId, recipientMessage, senderMessage);
|
|
153
|
+
}
|
|
108
154
|
// --- create_salt_agent ---------------------------------------------
|
|
109
155
|
async function createSaltAgent(creator, input) {
|
|
110
156
|
const keys = await pgp.generateKeypair(pgpPassphrase);
|
|
111
157
|
// No email or password: salt-api synthesizes a placeholder email and
|
|
112
158
|
// leaves the account non-loginable -- agents are pure API entities.
|
|
159
|
+
//
|
|
160
|
+
// Key custody (salt-api docs/KEY_CUSTODY.md Phase 5): keys.privateKey is
|
|
161
|
+
// deliberately never sent. Salt now rejects a plaintext private_key on
|
|
162
|
+
// create outright, and there is no vault to wrap one under here -- this
|
|
163
|
+
// process, not Salt, is the key's only custodian. It is registered below
|
|
164
|
+
// via identities.register, which is where it actually lives.
|
|
113
165
|
const created = await client.createAgent(creator.apiKey, {
|
|
114
166
|
display_name: input.display_name,
|
|
115
167
|
username: input.username,
|
|
@@ -117,18 +169,18 @@ function createActions(options) {
|
|
|
117
169
|
webhook: publicWebhookUrl,
|
|
118
170
|
category: input.category,
|
|
119
171
|
public_key: keys.publicKey,
|
|
120
|
-
private_key: keys.privateKey,
|
|
121
172
|
public_fingerprint: keys.fingerprint,
|
|
122
173
|
});
|
|
123
|
-
// The
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
const
|
|
174
|
+
// The raw api key rides on the create response exactly once -- the server
|
|
175
|
+
// stores a digest, so the admin endpoint can no longer show it. The
|
|
176
|
+
// rotate fallback covers an older server that predates that change.
|
|
177
|
+
const childApiKey = created.api_key ??
|
|
178
|
+
(await client.rotateAgentApiKey(creator.apiKey, created.id)).api_key;
|
|
127
179
|
const childIdentity = {
|
|
128
180
|
saltAppId: created.id,
|
|
129
181
|
username: created.username,
|
|
130
182
|
displayName: created.display_name,
|
|
131
|
-
apiKey:
|
|
183
|
+
apiKey: childApiKey,
|
|
132
184
|
publicKey: keys.publicKey,
|
|
133
185
|
privateKey: keys.privateKey,
|
|
134
186
|
systemPrompt: input.persona,
|
|
@@ -157,7 +209,7 @@ function createActions(options) {
|
|
|
157
209
|
// --- list_salt_agents ------------------------------------------------
|
|
158
210
|
async function listSaltAgents(caller, input = {}) {
|
|
159
211
|
const agents = (await client.listAgents(caller.apiKey));
|
|
160
|
-
let filtered = agents.filter((a) => a.id
|
|
212
|
+
let filtered = agents.filter((a) => !(0, ids_js_1.sameId)(a.id, caller.saltAppId));
|
|
161
213
|
if (input.category) {
|
|
162
214
|
const needle = input.category.toLowerCase();
|
|
163
215
|
filtered = filtered.filter((a) => String(a.category || "").toLowerCase().includes(needle));
|
|
@@ -183,11 +235,15 @@ function createActions(options) {
|
|
|
183
235
|
}
|
|
184
236
|
// --- delegate_to_agent ------------------------------------------------
|
|
185
237
|
async function delegateToAgent(caller, input, ctx) {
|
|
186
|
-
|
|
238
|
+
// Ids arrive here from a model's tool call, so they are taken as written
|
|
239
|
+
// and never parsed. `Number(...)` made every real id NaN, which is falsy,
|
|
240
|
+
// so the required-argument check below rejected every valid delegation --
|
|
241
|
+
// and the self-delegation guard, comparing NaN, could never fire.
|
|
242
|
+
const targetId = String(input.target_agent_id || "").trim();
|
|
187
243
|
const task = (input.task || "").trim();
|
|
188
244
|
if (!targetId || !task)
|
|
189
245
|
throw new Error("target_agent_id and task are both required.");
|
|
190
|
-
if (targetId
|
|
246
|
+
if ((0, ids_js_1.sameId)(targetId, caller.saltAppId))
|
|
191
247
|
throw new Error("You can't delegate to yourself.");
|
|
192
248
|
if (ctx.depth >= delegations.MAX_DELEGATION_DEPTH) {
|
|
193
249
|
throw new Error("Delegation depth limit reached -- handle this yourself (or answer with what you already know) instead of delegating further.");
|
|
@@ -203,33 +259,58 @@ function createActions(options) {
|
|
|
203
259
|
throw new Error(`${targetId} isn't an agent -- delegate_to_agent only targets other agents.`);
|
|
204
260
|
}
|
|
205
261
|
const chat = (await client.createOrGetChat(caller.apiKey, targetId));
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
262
|
+
// Open room (rare for a delegation chat today, but general): no keys
|
|
263
|
+
// needed at all, so the "usable public key" guard below doesn't apply.
|
|
264
|
+
const chatEncrypted = chat.encrypted !== false;
|
|
265
|
+
if (chatEncrypted) {
|
|
266
|
+
const targetMember = (chat.users || []).find((u) => (0, ids_js_1.sameId)(u.id, targetId));
|
|
267
|
+
if (!targetMember || !targetMember.public_key) {
|
|
268
|
+
throw new Error(`Agent ${targetId} has no usable public key; can't message it securely.`);
|
|
269
|
+
}
|
|
209
270
|
}
|
|
210
271
|
// Encrypt for every non-self member with a key, not just the target --
|
|
211
272
|
// delegation chats carry a silent human observer (the caller's root
|
|
212
273
|
// owner, added server-side) whose key must be a recipient for the
|
|
213
274
|
// conversation to be auditable. Falls out naturally from the member list.
|
|
214
|
-
const recipientKeys =
|
|
215
|
-
.filter((u) =>
|
|
216
|
-
|
|
275
|
+
const recipientKeys = chatEncrypted
|
|
276
|
+
? (chat.users || []).filter((u) => !(0, ids_js_1.sameId)(u.id, caller.saltAppId) && u.public_key).map((u) => u.public_key)
|
|
277
|
+
: [];
|
|
217
278
|
const marked = delegations.wrap(ctx.depth + 1, task);
|
|
218
|
-
|
|
219
|
-
|
|
279
|
+
// The person this turn is for sees the delegation as it happens, in their
|
|
280
|
+
// Tasks panel: who was asked, what, and how it ended. Awaited so the
|
|
281
|
+
// reports land in order; a report that fails never fails the delegation.
|
|
282
|
+
// Each step carries its own detail (0.6.1): the ask on the way out, the
|
|
283
|
+
// reply's first line on the way back, the reason on a failure. One detail
|
|
284
|
+
// reused for every step made a finished delegation repeat the question.
|
|
285
|
+
const firstLine = (s) => (s || "").split("\n").find((l) => l.trim()) || "";
|
|
286
|
+
const work = {
|
|
287
|
+
id: (0, work_1.newWorkId)(),
|
|
288
|
+
kind: "delegation",
|
|
289
|
+
with: target.username,
|
|
290
|
+
};
|
|
291
|
+
const handle = target.username ? `@${target.username}` : target.display_name || "another agent";
|
|
220
292
|
// register() throws synchronously (before any network call) if this
|
|
221
293
|
// chat already has a pending wait -- surfaces as a normal action error
|
|
222
294
|
// rather than an ambiguous race over which reply belongs to which caller.
|
|
223
295
|
const waitForReply = delegations.register(chat.id, targetId, delegations.DELEGATION_TIMEOUT_MS);
|
|
224
296
|
try {
|
|
225
|
-
await
|
|
297
|
+
await postWireMessage(caller, chat.id, chatEncrypted, marked, recipientKeys);
|
|
226
298
|
}
|
|
227
299
|
catch (err) {
|
|
228
300
|
delegations.cancel(chat.id);
|
|
229
301
|
throw err;
|
|
230
302
|
}
|
|
231
|
-
|
|
303
|
+
await reportWork(caller, ctx, { ...work, status: "running", title: `Asking ${handle}`, detail: firstLine(task) });
|
|
304
|
+
let rawReplyText;
|
|
305
|
+
try {
|
|
306
|
+
rawReplyText = await waitForReply; // rejects with a timeout Error if nothing arrives in time
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
await reportWork(caller, ctx, { ...work, status: "failed", title: `No answer from ${handle}`, detail: `No reply within ${Math.round(delegations.DELEGATION_TIMEOUT_MS / 1000)} s.` });
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
232
312
|
const replyText = replyTransform(rawReplyText);
|
|
313
|
+
await reportWork(caller, ctx, { ...work, status: "done", title: `${handle} answered`, detail: firstLine(replyText) });
|
|
233
314
|
if (ctx.mainChatId != null) {
|
|
234
315
|
delegations.recordTrail(caller.saltAppId, ctx.mainChatId, {
|
|
235
316
|
agent_id: target.id,
|
|
@@ -245,8 +326,155 @@ function createActions(options) {
|
|
|
245
326
|
delegated: true,
|
|
246
327
|
target: { id: target.id, username: target.username, display_name: target.display_name },
|
|
247
328
|
reply: replyText,
|
|
329
|
+
// Seen live (2026-09-15): after consulting Faucet, the Concierge told the
|
|
330
|
+
// person "They're in this chat now -- you can message them directly."
|
|
331
|
+
// They were not. Say so in the result the model reads.
|
|
332
|
+
note: `${target.display_name || target.username} answered YOU in a separate chat and is NOT a member of the chat you are replying in. The person cannot message them here. Give them the answer yourself; if they should talk to that agent directly, use hand_off_to_agent, which actually brings it in.`,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
// --- consult_agent -------------------------------------------------------
|
|
336
|
+
//
|
|
337
|
+
// Unlike delegate_to_agent (a separate 1:1, the target never in this
|
|
338
|
+
// chat), consult_agent stays inline: it opens a lane off the CURRENT chat
|
|
339
|
+
// with a fellow member who's already here, and later messages from that
|
|
340
|
+
// agent ride the ordinary onMessage path in that lane (ctx.roomId points
|
|
341
|
+
// back here) rather than needing another tool call. Reply-matching reuses
|
|
342
|
+
// the exact same delegations registry delegate_to_agent does.
|
|
343
|
+
async function consultAgent(caller, input, ctx) {
|
|
344
|
+
if (ctx.mainChatId == null)
|
|
345
|
+
throw new Error("consult_agent is only available while replying in a chat.");
|
|
346
|
+
const handle = (input.handle || "").replace(/^@/, "").trim();
|
|
347
|
+
const briefing = (input.briefing || "").trim();
|
|
348
|
+
const question = (input.question || "").trim();
|
|
349
|
+
if (!handle || !question)
|
|
350
|
+
throw new Error("handle and question are both required.");
|
|
351
|
+
const members = (await client.getChatMembers(caller.apiKey, ctx.mainChatId));
|
|
352
|
+
const target = members.find((m) => m.username === handle);
|
|
353
|
+
if (!target) {
|
|
354
|
+
throw new Error(`No chat member named @${handle} -- consult_agent only reaches someone already in this chat (use delegate_to_agent for anyone else).`);
|
|
355
|
+
}
|
|
356
|
+
if ((0, ids_js_1.sameId)(target.id, caller.saltAppId))
|
|
357
|
+
throw new Error("You can't consult yourself.");
|
|
358
|
+
if (target.account_type !== "Agent")
|
|
359
|
+
throw new Error(`@${handle} isn't an agent -- consult_agent only targets other agents.`);
|
|
360
|
+
// No public-key check here: whether one is actually NEEDED depends on
|
|
361
|
+
// whether the LANE (opened below) turns out to be encrypted, which
|
|
362
|
+
// this parent chat's member list can't answer -- the lane's own
|
|
363
|
+
// member list is the authoritative, encryption-aware check just below.
|
|
364
|
+
const lane = (await client.openConsultLane(caller.apiKey, ctx.mainChatId, target.id));
|
|
365
|
+
// Open room (general capability -- a consult lane is normally its own
|
|
366
|
+
// private aside regardless of the parent chat, but the branch costs
|
|
367
|
+
// nothing to support): no keys needed at all when the lane itself isn't
|
|
368
|
+
// encrypted.
|
|
369
|
+
const laneEncrypted = lane.session.encrypted !== false;
|
|
370
|
+
const recipientKeys = laneEncrypted
|
|
371
|
+
? (lane.session.users || []).filter((u) => !(0, ids_js_1.sameId)(u.id, caller.saltAppId) && u.public_key).map((u) => u.public_key)
|
|
372
|
+
: [];
|
|
373
|
+
if (laneEncrypted && recipientKeys.length === 0)
|
|
374
|
+
throw new Error(`@${handle} has no usable public key in the lane; can't message them securely.`);
|
|
375
|
+
const body = briefing ? `${briefing}\n\n${question}` : question;
|
|
376
|
+
const marked = delegations.wrapConsult(ctx.mainChatId, body);
|
|
377
|
+
// So webhook.ts's own floor-request handler knows, later, that WE are
|
|
378
|
+
// the one who should hand off if @handle asks for the floor in this lane.
|
|
379
|
+
delegations.registerConsultAsker(lane.session.id, caller.saltAppId, ctx.mainChatId);
|
|
380
|
+
const firstLine = (s) => (s || "").split("\n").find((l) => l.trim()) || "";
|
|
381
|
+
const work = { id: (0, work_1.newWorkId)(), kind: "delegation", with: target.username };
|
|
382
|
+
const atHandle = `@${target.username}`;
|
|
383
|
+
const waitForReply = delegations.register(lane.session.id, target.id, delegations.DELEGATION_TIMEOUT_MS);
|
|
384
|
+
try {
|
|
385
|
+
await postWireMessage(caller, lane.session.id, laneEncrypted, marked, recipientKeys);
|
|
386
|
+
}
|
|
387
|
+
catch (err) {
|
|
388
|
+
delegations.cancel(lane.session.id);
|
|
389
|
+
throw err;
|
|
390
|
+
}
|
|
391
|
+
await reportWork(caller, ctx, { ...work, status: "running", title: `Asking ${atHandle}`, detail: firstLine(question) });
|
|
392
|
+
let rawReplyText;
|
|
393
|
+
try {
|
|
394
|
+
rawReplyText = await waitForReply; // rejects with a timeout Error if nothing arrives in time
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
await reportWork(caller, ctx, {
|
|
398
|
+
...work,
|
|
399
|
+
status: "failed",
|
|
400
|
+
title: `No answer from ${atHandle}`,
|
|
401
|
+
detail: `No reply within ${Math.round(delegations.DELEGATION_TIMEOUT_MS / 1000)} s.`,
|
|
402
|
+
});
|
|
403
|
+
throw err;
|
|
404
|
+
}
|
|
405
|
+
const replyText = replyTransform(rawReplyText);
|
|
406
|
+
await reportWork(caller, ctx, { ...work, status: "done", title: `${atHandle} answered`, detail: firstLine(replyText) });
|
|
407
|
+
client.trackEvent(caller.apiKey, "consult_made", { to_agent_username: target.username });
|
|
408
|
+
return {
|
|
409
|
+
consulted: true,
|
|
410
|
+
target: { id: target.id, username: target.username, display_name: target.display_name },
|
|
411
|
+
lane_chat_id: lane.session.id,
|
|
412
|
+
reply: replyText,
|
|
413
|
+
note: `${target.display_name || target.username} is answering you in a separate private lane off this chat -- ` +
|
|
414
|
+
"the person here cannot see it. Later messages from them arrive as ordinary turns in that lane (you " +
|
|
415
|
+
"don't need to call consult_agent again to keep talking); if they ask for the floor, you'll be handed off automatically.",
|
|
248
416
|
};
|
|
249
417
|
}
|
|
418
|
+
// --- request_floor ---------------------------------------------------------
|
|
419
|
+
async function requestFloor(caller, input, ctx) {
|
|
420
|
+
if (ctx.mainChatId == null)
|
|
421
|
+
throw new Error("request_floor is only available while replying in a chat.");
|
|
422
|
+
if (ctx.laneKind !== "consult") {
|
|
423
|
+
throw new Error("request_floor is only available inside a consult lane (the chat a consult_agent call opened with you) -- not in an ordinary chat.");
|
|
424
|
+
}
|
|
425
|
+
const reason = (input.reason || "").trim();
|
|
426
|
+
const marker = reason ? `${delegations.FLOOR_REQUEST_MARKER}\n${reason}` : delegations.FLOOR_REQUEST_MARKER;
|
|
427
|
+
// Unlike delegate_to_agent/consult_agent, request_floor had no other
|
|
428
|
+
// reason to fetch the chat itself (only its members) -- getChat is the
|
|
429
|
+
// one call that answers both "who's here" and "is this lane encrypted".
|
|
430
|
+
const chat = await client.getChat(caller.apiKey, ctx.mainChatId);
|
|
431
|
+
const recipientKeys = chat.encrypted
|
|
432
|
+
? chat.users.filter((u) => !(0, ids_js_1.sameId)(u.id, caller.saltAppId) && u.public_key).map((u) => u.public_key)
|
|
433
|
+
: [];
|
|
434
|
+
if (chat.encrypted && recipientKeys.length === 0)
|
|
435
|
+
throw new Error("No one in this lane to ask for the floor.");
|
|
436
|
+
await postWireMessage(caller, ctx.mainChatId, chat.encrypted, marker, recipientKeys);
|
|
437
|
+
return {
|
|
438
|
+
requested: true,
|
|
439
|
+
note: "Sent. If they hand off, you'll be brought into the room directly and prompted separately to introduce yourself.",
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
// --- report_progress --------------------------------------------------
|
|
443
|
+
async function reportProgress(caller, input, ctx) {
|
|
444
|
+
if (!reportTarget(ctx)) {
|
|
445
|
+
throw new Error("report_progress is only available while answering a person in a chat.");
|
|
446
|
+
}
|
|
447
|
+
if (!work_1.WORK_STATUSES.includes(input.status)) {
|
|
448
|
+
throw new Error(`status must be one of ${work_1.WORK_STATUSES.join(", ")}.`);
|
|
449
|
+
}
|
|
450
|
+
const title = (input.title || "").trim();
|
|
451
|
+
if (!title)
|
|
452
|
+
throw new Error("title is required -- one short line saying what is happening.");
|
|
453
|
+
// Money evidence: claiming money work "done" is only trusted with a
|
|
454
|
+
// real, CONFIRMED transfer behind it -- otherwise a report is just the
|
|
455
|
+
// model's own say-so. Only checked when evidence is actually supplied
|
|
456
|
+
// (most 'done' reports aren't about money at all); the tool description
|
|
457
|
+
// is what tells the model money work needs this.
|
|
458
|
+
const transferId = input.evidence?.transfer_id ? String(input.evidence.transfer_id).trim() : undefined;
|
|
459
|
+
if (input.status === "done" && transferId) {
|
|
460
|
+
let transfer;
|
|
461
|
+
try {
|
|
462
|
+
transfer = await client.getTransfer(caller.apiKey, transferId);
|
|
463
|
+
}
|
|
464
|
+
catch (err) {
|
|
465
|
+
throw new Error(`Could not verify transfer ${transferId}: ${err.message}`);
|
|
466
|
+
}
|
|
467
|
+
if (transfer.status !== "Confirmed") {
|
|
468
|
+
throw new Error(`Transfer ${transferId} has not confirmed on-chain yet (status: ${transfer.status || "unknown"}) -- ` +
|
|
469
|
+
"report 'done' only once it has; use 'running' (or 'failed', if it failed) until then.");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const id = input.id && /^[A-Za-z0-9_-]{1,64}$/.test(input.id) ? input.id : (0, work_1.newWorkId)();
|
|
473
|
+
const reported = await reportWork(caller, ctx, { id, status: input.status, kind: "task", title, detail: input.detail });
|
|
474
|
+
return reported
|
|
475
|
+
? { reported: true, id, note: "Only they can see this, in their Tasks panel for this chat." }
|
|
476
|
+
: { reported: false, id, note: "The report could not be delivered; carry on with the work." };
|
|
477
|
+
}
|
|
250
478
|
// --- post_card / update_card ------------------------------------------
|
|
251
479
|
async function postCard(caller, input, ctx) {
|
|
252
480
|
if (ctx.mainChatId == null)
|
|
@@ -255,7 +483,7 @@ function createActions(options) {
|
|
|
255
483
|
return { posted: true, card_id: result.resource_id, message_id: result.message_id };
|
|
256
484
|
}
|
|
257
485
|
async function updateCard(caller, input) {
|
|
258
|
-
const cardId =
|
|
486
|
+
const cardId = String(input.card_id || "").trim();
|
|
259
487
|
if (!cardId)
|
|
260
488
|
throw new Error("card_id is required.");
|
|
261
489
|
await client.updateCard(caller.apiKey, cardId, input.blocks);
|
|
@@ -274,7 +502,7 @@ function createActions(options) {
|
|
|
274
502
|
async function offerProduct(caller, input, ctx) {
|
|
275
503
|
if (ctx.mainChatId == null)
|
|
276
504
|
throw new Error("offer_product is only available while replying in a chat.");
|
|
277
|
-
const productId =
|
|
505
|
+
const productId = String(input.product_id || "").trim();
|
|
278
506
|
if (!productId)
|
|
279
507
|
throw new Error("product_id is required.");
|
|
280
508
|
await client.shareProduct(caller.apiKey, productId, ctx.mainChatId);
|
|
@@ -294,7 +522,7 @@ function createActions(options) {
|
|
|
294
522
|
throw new Error(`No chat member named @${input.receiver_username}.`);
|
|
295
523
|
}
|
|
296
524
|
else {
|
|
297
|
-
const others = members.filter((m) => m.id
|
|
525
|
+
const others = members.filter((m) => !(0, ids_js_1.sameId)(m.id, caller.saltAppId) && !m.observer);
|
|
298
526
|
if (others.length !== 1)
|
|
299
527
|
throw new Error("Say who to bill (receiver_username) in a group chat.");
|
|
300
528
|
receiver = others[0];
|
|
@@ -319,14 +547,23 @@ function createActions(options) {
|
|
|
319
547
|
async function addUsage(caller, input, ctx) {
|
|
320
548
|
if (ctx.mainChatId == null)
|
|
321
549
|
throw new Error("add_usage is only available while replying in a chat.");
|
|
322
|
-
const productId =
|
|
550
|
+
const productId = String(input.product_id || "").trim();
|
|
323
551
|
if (!productId)
|
|
324
552
|
throw new Error("product_id is required.");
|
|
553
|
+
// Generated once per call to this function -- i.e. once per model tool
|
|
554
|
+
// call -- NOT re-generated if the underlying HTTP request is retried.
|
|
555
|
+
// That's the point: usage_events_controller includes salt-api's
|
|
556
|
+
// Idempotent concern, so replaying the SAME key on a retried request is
|
|
557
|
+
// what makes a network-level retry safe against double-billing prepaid
|
|
558
|
+
// credits. Minting a fresh key per attempt would defeat that -- it would
|
|
559
|
+
// look like a brand-new usage event each time.
|
|
560
|
+
const idempotencyKey = (0, node_crypto_1.randomUUID)();
|
|
325
561
|
const result = (await client.addUsage(caller.apiKey, {
|
|
326
562
|
productId,
|
|
327
563
|
chatId: ctx.mainChatId,
|
|
328
564
|
qty: parseFloat(input.qty || "1"),
|
|
329
565
|
description: input.description,
|
|
566
|
+
idempotencyKey,
|
|
330
567
|
}));
|
|
331
568
|
return { recorded: true, balance_remaining: result.balance };
|
|
332
569
|
}
|
|
@@ -334,33 +571,76 @@ function createActions(options) {
|
|
|
334
571
|
async function handOffToAgent(caller, input, ctx) {
|
|
335
572
|
if (ctx.mainChatId == null)
|
|
336
573
|
throw new Error("hand_off_to_agent is only available while replying in a chat.");
|
|
337
|
-
const agentId =
|
|
574
|
+
const agentId = String(input.agent_id || "").trim();
|
|
338
575
|
if (!agentId)
|
|
339
576
|
throw new Error("agent_id is required.");
|
|
340
|
-
if (agentId
|
|
577
|
+
if ((0, ids_js_1.sameId)(agentId, caller.saltAppId))
|
|
341
578
|
throw new Error("You can't hand off to yourself.");
|
|
342
579
|
const target = await client.getAgent(caller.apiKey, agentId);
|
|
343
580
|
if (!target || target.account_type !== "Agent")
|
|
344
581
|
throw new Error(`${agentId} isn't an agent.`);
|
|
345
|
-
|
|
582
|
+
const handle = target.username ? `@${target.username}` : target.display_name || "another agent";
|
|
583
|
+
try {
|
|
584
|
+
await client.handOff(caller.apiKey, ctx.mainChatId, agentId, input.reason);
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
// A refused hand-off (Salt says no, for whatever reason it has) is not
|
|
588
|
+
// a bug in this tool call -- it's an answer. Hand it back as a normal
|
|
589
|
+
// RESULT, not a throw, so the model gets one consistent instruction
|
|
590
|
+
// instead of three different guesses at what a raw error meant.
|
|
591
|
+
if (!(err instanceof client_1.SaltApiError) || (err.status !== 403 && err.status !== 422))
|
|
592
|
+
throw err;
|
|
593
|
+
const reason = err.body && typeof err.body === "object" && typeof err.body.error === "string"
|
|
594
|
+
? err.body.error
|
|
595
|
+
: err.message;
|
|
596
|
+
return {
|
|
597
|
+
ok: false,
|
|
598
|
+
refused: true,
|
|
599
|
+
reason,
|
|
600
|
+
next_step: `Tell the person they can open ${handle} from Salt's agent directory and message them directly.`,
|
|
601
|
+
};
|
|
602
|
+
}
|
|
346
603
|
return {
|
|
347
604
|
handed_off: true,
|
|
605
|
+
handedOff: true,
|
|
348
606
|
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.
|
|
607
|
+
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. Write nothing else this turn: the hand-off itself and the briefing you're about to write are the whole goodbye, and anything else you write now is sent to nobody.",
|
|
350
608
|
};
|
|
351
609
|
}
|
|
352
610
|
async function handBackToConcierge(caller, input, ctx) {
|
|
353
611
|
if (ctx.mainChatId == null)
|
|
354
612
|
throw new Error("hand_back_to_concierge is only available while replying in a chat.");
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
613
|
+
// A real one step back: whoever handed THIS chat to you, which on a
|
|
614
|
+
// Concierge -> A -> B chain is A, not the fixed concierge id -- a fixed
|
|
615
|
+
// destination would skip A entirely. Only when there is no previous hop
|
|
616
|
+
// (this identity is the FIRST agent in the chat, or the trail runs out)
|
|
617
|
+
// does salt-api answer 422, and only then do we fall back to a fixed
|
|
618
|
+
// destination at all.
|
|
619
|
+
try {
|
|
620
|
+
await client.handBack(caller.apiKey, ctx.mainChatId);
|
|
621
|
+
return {
|
|
622
|
+
handed_off: true,
|
|
623
|
+
handedOff: true,
|
|
624
|
+
to: "previous",
|
|
625
|
+
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. Write nothing else this turn: the hand-off itself and the briefing you're about to write are the whole goodbye, and anything else you write now is sent to nobody.",
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
catch (err) {
|
|
629
|
+
if (!(err instanceof client_1.SaltApiError) || err.status !== 422)
|
|
630
|
+
throw err;
|
|
631
|
+
// "Already at the start of this conversation." -- nowhere further
|
|
632
|
+
// back to go, so fall through to the configured front door below.
|
|
633
|
+
}
|
|
634
|
+
if (!conciergeAgentId)
|
|
635
|
+
throw new Error("No concierge is configured on this server.");
|
|
636
|
+
if ((0, ids_js_1.sameId)(conciergeAgentId, caller.saltAppId))
|
|
358
637
|
throw new Error("You already are the concierge.");
|
|
359
|
-
await client.handOff(caller.apiKey, ctx.mainChatId,
|
|
638
|
+
await client.handOff(caller.apiKey, ctx.mainChatId, conciergeAgentId, input.reason);
|
|
360
639
|
return {
|
|
361
640
|
handed_off: true,
|
|
641
|
+
handedOff: true,
|
|
362
642
|
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.
|
|
643
|
+
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. Write nothing else this turn: the hand-off itself and the briefing you're about to write are the whole goodbye, and anything else you write now is sent to nobody.",
|
|
364
644
|
};
|
|
365
645
|
}
|
|
366
646
|
async function offerHandoffChoices(caller, input, ctx) {
|
|
@@ -373,13 +653,16 @@ function createActions(options) {
|
|
|
373
653
|
client.listAgents(caller.apiKey),
|
|
374
654
|
client.getChatMembers(caller.apiKey, ctx.mainChatId),
|
|
375
655
|
]);
|
|
376
|
-
|
|
656
|
+
// These become the button's `restricted_to` allowlist. parseInt turned
|
|
657
|
+
// every one into NaN, which serialises to null and matches nobody -- so
|
|
658
|
+
// "only these humans may press this" silently meant "nobody may".
|
|
659
|
+
const humanIds = members.filter((m) => m.account_type !== "Agent" && !m.observer).map((m) => String(m.id));
|
|
377
660
|
if (humanIds.length === 0)
|
|
378
661
|
throw new Error("No human member to offer choices to.");
|
|
379
662
|
const blocks = [{ type: "section", text: "Who should take this over? Pick one:" }];
|
|
380
663
|
for (const c of candidates) {
|
|
381
|
-
const agent = directory.find((a) => a.id
|
|
382
|
-
if (!agent || agent.id
|
|
664
|
+
const agent = directory.find((a) => (0, ids_js_1.sameId)(a.id, c.agent_id));
|
|
665
|
+
if (!agent || (0, ids_js_1.sameId)(agent.id, caller.saltAppId))
|
|
383
666
|
continue;
|
|
384
667
|
const fields = [];
|
|
385
668
|
if (agent.category)
|
|
@@ -429,6 +712,137 @@ function createActions(options) {
|
|
|
429
712
|
note: "No recovery phrase exists for this wallet -- it's only as safe as this server's wallet master key.",
|
|
430
713
|
};
|
|
431
714
|
}
|
|
715
|
+
// --- identity_set / identity_get --------------------------------------
|
|
716
|
+
async function identitySet(caller, input) {
|
|
717
|
+
const claims = {};
|
|
718
|
+
for (const key of identity_js_1.AGENT_CLAIM_SECTION_KEYS) {
|
|
719
|
+
const raw = input[key];
|
|
720
|
+
if (raw === undefined || raw === null)
|
|
721
|
+
continue;
|
|
722
|
+
claims[key] = String(raw);
|
|
723
|
+
}
|
|
724
|
+
if (input.capabilities !== undefined && input.capabilities !== null) {
|
|
725
|
+
if (!Array.isArray(input.capabilities))
|
|
726
|
+
throw new Error("capabilities must be a list of {title, detail}.");
|
|
727
|
+
if (input.capabilities.length > identity_js_1.MAX_CAPABILITIES)
|
|
728
|
+
throw new Error(`List at most ${identity_js_1.MAX_CAPABILITIES} capabilities.`);
|
|
729
|
+
claims.capabilities = input.capabilities.map((c) => {
|
|
730
|
+
const item = (c ?? {});
|
|
731
|
+
const title = String(item.title ?? "").trim();
|
|
732
|
+
if (!title)
|
|
733
|
+
throw new Error("Every capability needs a title.");
|
|
734
|
+
const detail = item.detail === undefined || item.detail === null ? "" : String(item.detail).trim();
|
|
735
|
+
return detail ? { title, detail } : { title };
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
if (Object.keys(claims).length === 0) {
|
|
739
|
+
throw new Error(`Provide at least one section to set: ${[...identity_js_1.AGENT_CLAIM_SECTION_KEYS, "capabilities"].join(", ")}.`);
|
|
740
|
+
}
|
|
741
|
+
const result = await client.setIdentity(caller.apiKey, claims);
|
|
742
|
+
return {
|
|
743
|
+
updated: true,
|
|
744
|
+
sections: Object.keys(claims),
|
|
745
|
+
card_url: result.card_url,
|
|
746
|
+
note: "These are CLAIMS you made about yourself -- signed as coming from you, not verified as true by " +
|
|
747
|
+
"anyone. A PROOF section (like your PGP fingerprint, Salt DID, or trust score) is different: Salt " +
|
|
748
|
+
"itself checks and signs those. Never present a claim as if it were checked, and never claim a " +
|
|
749
|
+
"proof section's value yourself -- this action can't set one anyway.",
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
async function identityGet(caller, input) {
|
|
753
|
+
const handle = String(input.handle || "").trim();
|
|
754
|
+
if (!handle)
|
|
755
|
+
throw new Error("handle is required.");
|
|
756
|
+
let result;
|
|
757
|
+
try {
|
|
758
|
+
result = await client.card(handle, input.kind ? { kind: input.kind } : undefined);
|
|
759
|
+
}
|
|
760
|
+
catch (err) {
|
|
761
|
+
if (err instanceof identity_js_1.IdentityCardInvalidError) {
|
|
762
|
+
return { found: false, reason: `Found a card for ${handle}, but could not trust it: ${err.message}` };
|
|
763
|
+
}
|
|
764
|
+
if (err instanceof client_1.SaltApiError && err.status === 404) {
|
|
765
|
+
return { found: false, reason: `No card is served for ${handle} -- the handle may not exist, or that account's card isn't discoverable.` };
|
|
766
|
+
}
|
|
767
|
+
throw err;
|
|
768
|
+
}
|
|
769
|
+
// `proof` is deliberately opaque (identity.ts never assumes its shape --
|
|
770
|
+
// see CardSection's comment): today it's just null-or-not, so
|
|
771
|
+
// checked_by comes back null and the note tells the model to say "a
|
|
772
|
+
// proof" rather than inventing a checker. Once the API sends
|
|
773
|
+
// `{by: "Salt" | "Grains" | "contacts"}` on a proof section, that name
|
|
774
|
+
// rides straight through with no change here.
|
|
775
|
+
const sections = (result.card.sections ?? []).map((s) => {
|
|
776
|
+
const isProof = s.proof != null;
|
|
777
|
+
const by = isProof && typeof s.proof === "object" && s.proof !== null ? s.proof.by : undefined;
|
|
778
|
+
return {
|
|
779
|
+
key: s.key,
|
|
780
|
+
value: s.value,
|
|
781
|
+
is_proof: isProof,
|
|
782
|
+
checked_by: typeof by === "string" ? by : null,
|
|
783
|
+
};
|
|
784
|
+
});
|
|
785
|
+
return {
|
|
786
|
+
found: true,
|
|
787
|
+
verified: true,
|
|
788
|
+
name: result.card.name,
|
|
789
|
+
description: result.card.description,
|
|
790
|
+
sections,
|
|
791
|
+
note: "verified: true means Salt's signature on this card checked out -- it really is what that account " +
|
|
792
|
+
"published. Sections with is_proof=true are PROOFS, checked by someone other than the subject " +
|
|
793
|
+
"itself -- checked_by names who, when known; if checked_by is null, call it 'a proof' and don't " +
|
|
794
|
+
"guess who checked it. Every other section is only that account's own claim about itself, " +
|
|
795
|
+
"unverified by anyone. Never repeat a claim section as if it had been checked.",
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
// --- identity_share / identity_ask / identity_revoke (R3/R4) -----------
|
|
799
|
+
async function identityShareAction(caller, input, ctx) {
|
|
800
|
+
const chatId = input.chat_id || ctx.mainChatId;
|
|
801
|
+
if (chatId == null) {
|
|
802
|
+
throw new Error("identity_share needs a chat_id, or must be called while replying in a chat.");
|
|
803
|
+
}
|
|
804
|
+
const keys = Array.isArray(input.keys) ? input.keys.map(String) : [];
|
|
805
|
+
if (keys.length === 0)
|
|
806
|
+
throw new Error("Provide at least one section key to share, e.g. [\"bio\", \"link\"].");
|
|
807
|
+
const result = await identitySharer.share(caller, chatId, keys);
|
|
808
|
+
return {
|
|
809
|
+
shared: true,
|
|
810
|
+
id: result.id,
|
|
811
|
+
message_id: result.messageId,
|
|
812
|
+
recipients: result.recipients,
|
|
813
|
+
note: "Sent as one signed message, readable only by the chat's members -- Salt's server only ever learned " +
|
|
814
|
+
"which section keys you shared and with whom, never the values themselves.",
|
|
815
|
+
};
|
|
816
|
+
}
|
|
817
|
+
async function identityAskAction(caller, input, ctx) {
|
|
818
|
+
if (ctx.mainChatId == null) {
|
|
819
|
+
throw new Error("identity_ask is only available while replying in a chat.");
|
|
820
|
+
}
|
|
821
|
+
const keys = Array.isArray(input.keys) ? input.keys.map(String) : [];
|
|
822
|
+
if (keys.length === 0)
|
|
823
|
+
throw new Error("Provide at least one section key to ask for, e.g. [\"legal_name\"].");
|
|
824
|
+
const text = typeof input.text === "string" && input.text.trim() ? input.text.trim() : undefined;
|
|
825
|
+
const id = await identitySharer.ask(caller, ctx.mainChatId, keys, text);
|
|
826
|
+
return {
|
|
827
|
+
asked: true,
|
|
828
|
+
id,
|
|
829
|
+
note: "Only works in a 1:1 -- there is no group equivalent yet. Their answer (a share or a decline) arrives " +
|
|
830
|
+
"as its own event, not as a return value from this call.",
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
async function identityRevokeAction(caller, input) {
|
|
834
|
+
const id = typeof input.id === "string" ? input.id.trim() : "";
|
|
835
|
+
if (!id)
|
|
836
|
+
throw new Error("id is required -- the id identity_share returned.");
|
|
837
|
+
const rows = await identitySharer.revoke(caller, id);
|
|
838
|
+
return {
|
|
839
|
+
revoked: true,
|
|
840
|
+
id,
|
|
841
|
+
chat_id: rows[0]?.chat_id ?? null,
|
|
842
|
+
recipients: rows.length,
|
|
843
|
+
note: "Stops it being served again and sends a notice into the chat -- cannot reach a copy already read on someone's device.",
|
|
844
|
+
};
|
|
845
|
+
}
|
|
432
846
|
const definitions = [
|
|
433
847
|
{
|
|
434
848
|
name: "create_salt_agent",
|
|
@@ -480,6 +894,9 @@ function createActions(options) {
|
|
|
480
894
|
"exactly like a human messaging that agent. Creates (or reuses) a 1:1 chat with the target, " +
|
|
481
895
|
"sends it your request, and waits for its reply, which is returned to you to use. This is a " +
|
|
482
896
|
"real, visible conversation, not a shortcut: anyone who opens that chat can see the " +
|
|
897
|
+
"delegation happen. The target joins THAT chat, never the one you are replying in -- the person " +
|
|
898
|
+
"you are helping cannot message it there, so never tell them it is 'in this chat' (use " +
|
|
899
|
+
"hand_off_to_agent when they should talk to it directly). Anyone who opens the delegation chat can see the " +
|
|
483
900
|
"delegation happen. Use list_salt_agents first to pick a suitable, well-rated target rather " +
|
|
484
901
|
"than guessing an id. Only delegate work the other agent is plausibly better positioned for " +
|
|
485
902
|
"-- don't delegate something you can already just answer yourself. You can't delegate to " +
|
|
@@ -488,13 +905,79 @@ function createActions(options) {
|
|
|
488
905
|
schema: {
|
|
489
906
|
type: "object",
|
|
490
907
|
properties: {
|
|
491
|
-
target_agent_id: { type: "
|
|
908
|
+
target_agent_id: { type: "string", description: "The Salt user id of the agent to delegate to (from list_salt_agents)." },
|
|
492
909
|
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
910
|
},
|
|
494
911
|
required: ["target_agent_id", "task"],
|
|
495
912
|
},
|
|
496
913
|
execute: delegateToAgent,
|
|
497
914
|
},
|
|
915
|
+
{
|
|
916
|
+
name: "report_progress",
|
|
917
|
+
description: "Keep the person you are answering up to date on work that takes a while, privately. Salt " +
|
|
918
|
+
"shows the report in that person's Tasks panel for this chat -- never in the conversation, " +
|
|
919
|
+
"and nobody else in the chat can see it. Use status 'running' when you start something that " +
|
|
920
|
+
"will take more than a few seconds, and again with the same id when the step changes; " +
|
|
921
|
+
"'waiting' when you cannot continue without something from them (they are notified and can " +
|
|
922
|
+
"answer you privately); 'done' or 'failed' when it ends. Keep one id for one piece of work: " +
|
|
923
|
+
"omit id to start a new one and reuse the id you get back. Delegations through " +
|
|
924
|
+
"delegate_to_agent/consult_agent are reported for you -- do not report those again. Do not " +
|
|
925
|
+
"report quick answers; just answer. MONEY WORK: report 'done' on a payment only with " +
|
|
926
|
+
"evidence: {transfer_id} -- it is verified as actually confirmed on-chain before the report " +
|
|
927
|
+
"is delivered, and refused otherwise.",
|
|
928
|
+
schema: {
|
|
929
|
+
type: "object",
|
|
930
|
+
properties: {
|
|
931
|
+
id: { type: "string", description: "The id returned by an earlier report of the same work. Omit to start a new piece of work." },
|
|
932
|
+
status: { type: "string", enum: ["running", "waiting", "done", "failed"], description: "Where the work stands now." },
|
|
933
|
+
title: { type: "string", description: 'One short line, e.g. "Comparing three flights" or "I need your departure date".' },
|
|
934
|
+
detail: { type: "string", description: "Optional: a sentence or two more, e.g. what you found so far or exactly what you need." },
|
|
935
|
+
evidence: {
|
|
936
|
+
type: "object",
|
|
937
|
+
description: "Required to report 'done' on money work: {transfer_id}. Verified against the real " +
|
|
938
|
+
"transfer (must have actually confirmed) before the report is delivered -- claiming a " +
|
|
939
|
+
"payment is done without this, or before it confirms, is refused.",
|
|
940
|
+
properties: { transfer_id: { type: "string", description: "The Transfer id (e.g. from send_invoice's payment, once paid)." } },
|
|
941
|
+
},
|
|
942
|
+
},
|
|
943
|
+
required: ["status", "title"],
|
|
944
|
+
},
|
|
945
|
+
execute: reportProgress,
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
name: "consult_agent",
|
|
949
|
+
description: "Ask a fellow member of THIS chat -- someone already in the room, human-visible as a " +
|
|
950
|
+
"participant -- a question in a private lane off it, and wait for their reply, which is " +
|
|
951
|
+
"returned to you to use. Unlike delegate_to_agent (a separate 1:1 the target never joins), " +
|
|
952
|
+
"the consulted agent stays inline: their later messages arrive in your ordinary onMessage " +
|
|
953
|
+
"flow, in that lane, without you calling this again. Use this instead of delegate_to_agent " +
|
|
954
|
+
"specifically when the agent you want is already a member of the current chat. They can ask " +
|
|
955
|
+
"to be brought into the room directly instead (request_floor); you'll be handed off to them " +
|
|
956
|
+
"automatically if so.",
|
|
957
|
+
schema: {
|
|
958
|
+
type: "object",
|
|
959
|
+
properties: {
|
|
960
|
+
handle: { type: "string", description: "Username (without @) of the agent to consult -- must already be a member of this chat." },
|
|
961
|
+
briefing: { type: "string", description: "Optional short context the target has no other way to get (they can't see this chat)." },
|
|
962
|
+
question: { type: "string", description: "The question to ask." },
|
|
963
|
+
},
|
|
964
|
+
required: ["handle", "question"],
|
|
965
|
+
},
|
|
966
|
+
execute: consultAgent,
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
name: "request_floor",
|
|
970
|
+
description: "From INSIDE a consult lane only (one someone opened with you via consult_agent): ask the " +
|
|
971
|
+
"asking agent to bring you into the room directly instead of continuing to relay your " +
|
|
972
|
+
"answers through this lane. If they do, you'll be handed off into the room and prompted " +
|
|
973
|
+
"separately to introduce yourself. Use this when the conversation would go faster with you " +
|
|
974
|
+
"talking to the person directly rather than through an intermediary.",
|
|
975
|
+
schema: {
|
|
976
|
+
type: "object",
|
|
977
|
+
properties: { reason: { type: "string", description: "One line on why you'd rather join directly." } },
|
|
978
|
+
},
|
|
979
|
+
execute: requestFloor,
|
|
980
|
+
},
|
|
498
981
|
{
|
|
499
982
|
name: "post_card",
|
|
500
983
|
description: "Post an interactive card (a mini app) into the CURRENT chat: sections of markdown text " +
|
|
@@ -520,7 +1003,7 @@ function createActions(options) {
|
|
|
520
1003
|
schema: {
|
|
521
1004
|
type: "object",
|
|
522
1005
|
properties: {
|
|
523
|
-
card_id: { type: "
|
|
1006
|
+
card_id: { type: "string", description: "The card's id (from post_card's result or the card_interaction event)." },
|
|
524
1007
|
blocks: BLOCKS_SCHEMA,
|
|
525
1008
|
},
|
|
526
1009
|
required: ["card_id", "blocks"],
|
|
@@ -553,7 +1036,7 @@ function createActions(options) {
|
|
|
553
1036
|
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
1037
|
schema: {
|
|
555
1038
|
type: "object",
|
|
556
|
-
properties: { seller_id: { type: "
|
|
1039
|
+
properties: { seller_id: { type: "string", description: "Omit for your own products." } },
|
|
557
1040
|
},
|
|
558
1041
|
execute: listProducts,
|
|
559
1042
|
},
|
|
@@ -562,7 +1045,7 @@ function createActions(options) {
|
|
|
562
1045
|
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
1046
|
schema: {
|
|
564
1047
|
type: "object",
|
|
565
|
-
properties: { product_id: { type: "
|
|
1048
|
+
properties: { product_id: { type: "string" } },
|
|
566
1049
|
required: ["product_id"],
|
|
567
1050
|
},
|
|
568
1051
|
execute: offerProduct,
|
|
@@ -598,7 +1081,7 @@ function createActions(options) {
|
|
|
598
1081
|
schema: {
|
|
599
1082
|
type: "object",
|
|
600
1083
|
properties: {
|
|
601
|
-
product_id: { type: "
|
|
1084
|
+
product_id: { type: "string" },
|
|
602
1085
|
qty: { type: "string", description: 'Units delivered, e.g. "1" or "3". Default "1".' },
|
|
603
1086
|
description: { type: "string", description: "Short human-readable line for the ledger." },
|
|
604
1087
|
},
|
|
@@ -629,12 +1112,18 @@ function createActions(options) {
|
|
|
629
1112
|
"this same chat (no new chat is created), you stay in the room silently, and you'll be " +
|
|
630
1113
|
"asked to write a briefing for them right after. Use in AUTO-mode hand-off chats when " +
|
|
631
1114
|
"another agent clearly serves the person better (check list_salt_agents first for a " +
|
|
632
|
-
"well-rated match).
|
|
633
|
-
"
|
|
1115
|
+
"well-rated match). Never use this in MANUAL mode -- offer_handoff_choices is for that. On " +
|
|
1116
|
+
"success, Salt itself posts a visible line naming the hand-off -- that seam plus the " +
|
|
1117
|
+
"briefing you're asked for next ARE the goodbye, so write nothing else this turn: say " +
|
|
1118
|
+
"whatever the person needs to know BEFORE calling this, in an earlier reply, never after -- " +
|
|
1119
|
+
"any text you write in the same turn as a successful call is never sent. If Salt refuses " +
|
|
1120
|
+
"the hand-off, this returns a plain result ({ok: false, refused: true, reason, next_step}) " +
|
|
1121
|
+
"instead of an error -- say the reason in one sentence and follow next_step; don't call it a " +
|
|
1122
|
+
"technical issue or guess at a cause.",
|
|
634
1123
|
schema: {
|
|
635
1124
|
type: "object",
|
|
636
1125
|
properties: {
|
|
637
|
-
agent_id: { type: "
|
|
1126
|
+
agent_id: { type: "string", description: "The agent taking over (from list_salt_agents)." },
|
|
638
1127
|
reason: { type: "string", description: "One line on why -- shown in the hand-off trail." },
|
|
639
1128
|
},
|
|
640
1129
|
required: ["agent_id", "reason"],
|
|
@@ -643,14 +1132,18 @@ function createActions(options) {
|
|
|
643
1132
|
},
|
|
644
1133
|
{
|
|
645
1134
|
name: "hand_back_to_concierge",
|
|
646
|
-
description: "Hand this conversation back to
|
|
647
|
-
"
|
|
648
|
-
"
|
|
649
|
-
"
|
|
650
|
-
"
|
|
651
|
-
"
|
|
652
|
-
"
|
|
653
|
-
"
|
|
1135
|
+
description: "Hand this conversation back to whoever handed it over to you -- usually the Global " +
|
|
1136
|
+
"concierge agent, the router this person originally started with, but exactly whoever " +
|
|
1137
|
+
"handed off to you if that was someone else. Use this as soon as you've wrapped up what " +
|
|
1138
|
+
"you were brought in for: you answered their question, finished the task, hit something " +
|
|
1139
|
+
"outside your scope, or they signal they're done / want something else. Don't wait to be " +
|
|
1140
|
+
"asked -- a person who has to explicitly request 'take me back' is a failure of this tool's " +
|
|
1141
|
+
"whole point. Same mechanics as hand_off_to_agent: you stay in the room silently, you'll be " +
|
|
1142
|
+
"asked to write a briefing right after, and that briefing plus Salt's own visible hand-off " +
|
|
1143
|
+
"line are the whole goodbye -- write nothing else this turn; any text you write in the same " +
|
|
1144
|
+
"turn as a successful call is never sent, so say anything the person needs to hear BEFORE " +
|
|
1145
|
+
"calling this, in an earlier reply. AUTO-mode hand-off chats only -- in MANUAL mode, tell the " +
|
|
1146
|
+
"person they can jump back to the concierge from chat info themselves.",
|
|
654
1147
|
schema: {
|
|
655
1148
|
type: "object",
|
|
656
1149
|
properties: { reason: { type: "string", description: "One line on why you're handing back -- shown in the hand-off trail." } },
|
|
@@ -672,7 +1165,7 @@ function createActions(options) {
|
|
|
672
1165
|
description: "2-4 entries: {agent_id, why} -- `why` is your one-line pitch for this candidate.",
|
|
673
1166
|
items: {
|
|
674
1167
|
type: "object",
|
|
675
|
-
properties: { agent_id: { type: "
|
|
1168
|
+
properties: { agent_id: { type: "string" }, why: { type: "string" } },
|
|
676
1169
|
required: ["agent_id", "why"],
|
|
677
1170
|
},
|
|
678
1171
|
},
|
|
@@ -681,6 +1174,132 @@ function createActions(options) {
|
|
|
681
1174
|
},
|
|
682
1175
|
execute: offerHandoffChoices,
|
|
683
1176
|
},
|
|
1177
|
+
{
|
|
1178
|
+
name: "identity_set",
|
|
1179
|
+
description: "State one or more CLAIM sections about YOURSELF on your Salt identity card -- your public, " +
|
|
1180
|
+
"signed profile (display_name, bio, link, avatar, category, message_price, funding_disclosure, and " +
|
|
1181
|
+
"capabilities: your \"What I can do\" list, up to 5 lines, shown to anyone who opens your page). " +
|
|
1182
|
+
"This is a CLAIM, not a proof: it's signed as coming from you, never verified as true by anyone. " +
|
|
1183
|
+
"Don't set message_price to advertise a price you don't actually enforce, and don't claim a " +
|
|
1184
|
+
"PROOF section (like your PGP fingerprint or trust score) here -- those are computed and signed " +
|
|
1185
|
+
"by Salt itself and can't be set through this action. You cannot set who else can see a section " +
|
|
1186
|
+
"(scope) -- that's controlled by your owner, not you.",
|
|
1187
|
+
schema: {
|
|
1188
|
+
type: "object",
|
|
1189
|
+
properties: {
|
|
1190
|
+
display_name: { type: "string", description: "Shown on your card and message bubbles." },
|
|
1191
|
+
bio: { type: "string", description: "One-line description of who/what you are." },
|
|
1192
|
+
link: { type: "string", description: "A URL you want associated with you (e.g. your homepage)." },
|
|
1193
|
+
avatar: { type: "string", description: "A URL to an image to use as your avatar." },
|
|
1194
|
+
category: { type: "string", description: "Directory category, e.g. Assistant, Trading, Fun, Utilities." },
|
|
1195
|
+
message_price: { type: "string", description: 'Decimal string price to message you 1:1, e.g. "0.01". Omit for free.' },
|
|
1196
|
+
funding_disclosure: { type: "string", description: 'How you\'re funded, e.g. "self-funded" -- a real, honest answer, not left blank.' },
|
|
1197
|
+
capabilities: {
|
|
1198
|
+
type: "array",
|
|
1199
|
+
maxItems: 5,
|
|
1200
|
+
description: "What you can do, in plain words, most useful first. Replaces the whole list. Each line a short title and an optional one-line detail.",
|
|
1201
|
+
items: {
|
|
1202
|
+
type: "object",
|
|
1203
|
+
properties: {
|
|
1204
|
+
title: { type: "string", maxLength: 60, description: 'e.g. "Builds plugins for your chats"' },
|
|
1205
|
+
detail: { type: "string", maxLength: 120, description: 'e.g. "Lists, trackers and polls, right in the conversation."' },
|
|
1206
|
+
},
|
|
1207
|
+
required: ["title"],
|
|
1208
|
+
},
|
|
1209
|
+
},
|
|
1210
|
+
},
|
|
1211
|
+
required: [],
|
|
1212
|
+
},
|
|
1213
|
+
execute: identitySet,
|
|
1214
|
+
},
|
|
1215
|
+
{
|
|
1216
|
+
name: "identity_get",
|
|
1217
|
+
description: "Fetch and cryptographically verify another person's or agent's Salt identity card -- their " +
|
|
1218
|
+
"public, signed profile. The whole card is signed by Salt, but that only proves it really is " +
|
|
1219
|
+
"what that account published -- individual sections are marked is_proof (checked by someone " +
|
|
1220
|
+
"other than the subject, named in checked_by when known) or not (that account's own unverified " +
|
|
1221
|
+
"claim about itself), so you can tell the two apart before repeating anything from it. " +
|
|
1222
|
+
"Verification failing at all raises an error rather than a false result.",
|
|
1223
|
+
schema: {
|
|
1224
|
+
type: "object",
|
|
1225
|
+
properties: {
|
|
1226
|
+
handle: { type: "string", description: "Username (without @) of the person or agent whose card to fetch." },
|
|
1227
|
+
kind: { type: "string", enum: ["agent", "user"], description: "Optional: fetch only the agent card or only the person card. Omit to try both." },
|
|
1228
|
+
},
|
|
1229
|
+
required: ["handle"],
|
|
1230
|
+
},
|
|
1231
|
+
execute: identityGet,
|
|
1232
|
+
},
|
|
1233
|
+
{
|
|
1234
|
+
name: "identity_share",
|
|
1235
|
+
description: "Send one or more of YOUR OWN identity card sections into a chat as a signed, end-to-end encrypted " +
|
|
1236
|
+
"message -- Salt's server only ever learns which section keys you shared and with whom, never the " +
|
|
1237
|
+
"values. Defaults to the chat you're currently replying in; pass chat_id to share into a different " +
|
|
1238
|
+
"one. Every non-observer member of that chat receives it. Use identity_revoke with the returned id " +
|
|
1239
|
+
"to take a share back.",
|
|
1240
|
+
schema: {
|
|
1241
|
+
type: "object",
|
|
1242
|
+
properties: {
|
|
1243
|
+
keys: {
|
|
1244
|
+
type: "array",
|
|
1245
|
+
items: { type: "string" },
|
|
1246
|
+
description: 'Section keys to share, e.g. ["bio", "link"] -- must be sections you have actually stated (identity_set) or a proof Salt computed for you.',
|
|
1247
|
+
},
|
|
1248
|
+
chat_id: { type: "string", description: "Optional -- defaults to the current chat." },
|
|
1249
|
+
},
|
|
1250
|
+
required: ["keys"],
|
|
1251
|
+
},
|
|
1252
|
+
execute: identityShareAction,
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
name: "identity_ask",
|
|
1256
|
+
description: "Ask the other person in the current 1:1 chat to share one or more sections of THEIR OWN identity " +
|
|
1257
|
+
"card with you. Only works in a 1:1 -- there is no group equivalent yet. They may share what you " +
|
|
1258
|
+
"asked for, share something else, decline, or ignore it; their answer arrives as its own event, not " +
|
|
1259
|
+
"as a result of this call.",
|
|
1260
|
+
schema: {
|
|
1261
|
+
type: "object",
|
|
1262
|
+
properties: {
|
|
1263
|
+
keys: { type: "array", items: { type: "string" }, description: 'Section keys you are asking for, e.g. ["legal_name"].' },
|
|
1264
|
+
text: { type: "string", description: "Optional one-line reason, shown alongside the ask." },
|
|
1265
|
+
},
|
|
1266
|
+
required: ["keys"],
|
|
1267
|
+
},
|
|
1268
|
+
execute: identityAskAction,
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
name: "react_to_message",
|
|
1272
|
+
description: "React to a message with ONE emoji. Use sparingly: not all the time, just when you choose, and only if it " +
|
|
1273
|
+
"relevantly complements the chat in a friendly way (thank someone back, mark a request done with a check, " +
|
|
1274
|
+
"celebrate good news, show you are on it). Never react instead of answering a question, never to every " +
|
|
1275
|
+
"message, never to your own, at most one per message. Calling it again with the same emoji removes yours.",
|
|
1276
|
+
schema: {
|
|
1277
|
+
type: "object",
|
|
1278
|
+
properties: {
|
|
1279
|
+
message_id: { type: "string", description: "The id of the message to react to (the incoming message's message_id)." },
|
|
1280
|
+
emoji: { type: "string", description: "Exactly one emoji, e.g. a thumbs up or a check mark." },
|
|
1281
|
+
},
|
|
1282
|
+
required: ["message_id", "emoji"],
|
|
1283
|
+
},
|
|
1284
|
+
execute: async (caller, input) => {
|
|
1285
|
+
const id = String(input?.message_id ?? "").trim();
|
|
1286
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id))
|
|
1287
|
+
throw new Error("message_id must be a plain Salt message id.");
|
|
1288
|
+
const result = await client.react(caller.apiKey, id, String(input?.emoji ?? ""));
|
|
1289
|
+
return { ok: true, ...result };
|
|
1290
|
+
},
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
name: "identity_revoke",
|
|
1294
|
+
description: "Revoke a section disclosure you previously sent with identity_share -- stops it being served again " +
|
|
1295
|
+
"and sends a notice into the chat it was shared in. Cannot reach a copy already read on someone's device.",
|
|
1296
|
+
schema: {
|
|
1297
|
+
type: "object",
|
|
1298
|
+
properties: { id: { type: "string", description: "The id identity_share returned." } },
|
|
1299
|
+
required: ["id"],
|
|
1300
|
+
},
|
|
1301
|
+
execute: identityRevokeAction,
|
|
1302
|
+
},
|
|
684
1303
|
];
|
|
685
1304
|
const byName = new Map(definitions.map((d) => [d.name, d]));
|
|
686
1305
|
async function execute(name, input, caller, ctx) {
|