@cerebro-labs/agentnava-sdk 0.26.0 → 0.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -23
- package/account.d.ts +154 -0
- package/account.js +219 -0
- package/account.js.map +1 -0
- package/agent.d.ts +229 -87
- package/agent.js +310 -125
- package/agent.js.map +1 -1
- package/apps.generated.d.ts +154 -0
- package/apps.generated.js +152 -0
- package/apps.generated.js.map +1 -0
- package/attach.d.ts +25 -26
- package/attach.js +53 -66
- package/attach.js.map +1 -1
- package/connection.d.ts +79 -82
- package/connection.js +67 -134
- package/connection.js.map +1 -1
- package/conversation.d.ts +55 -35
- package/conversation.js +70 -50
- package/conversation.js.map +1 -1
- package/database.d.ts +53 -0
- package/database.js +69 -0
- package/database.js.map +1 -0
- package/end-user.d.ts +58 -47
- package/end-user.js +62 -72
- package/end-user.js.map +1 -1
- package/errors.d.ts +2 -1
- package/errors.js +2 -1
- package/errors.js.map +1 -1
- package/http.d.ts +5 -0
- package/http.js +109 -8
- package/http.js.map +1 -1
- package/import-tools.d.ts +54 -33
- package/import-tools.js +526 -124
- package/import-tools.js.map +1 -1
- package/index.d.ts +5 -2
- package/index.js +9 -2
- package/index.js.map +1 -1
- package/load-agent-dir.js +28 -5
- package/load-agent-dir.js.map +1 -1
- package/package.json +5 -1
- package/secret.d.ts +1 -1
- package/secret.js +1 -1
- package/secret.js.map +1 -1
- package/templates-inline.js +1 -1
- package/templates-inline.js.map +1 -1
- package/turn.d.ts +1 -1
- package/turn.js +77 -29
- package/turn.js.map +1 -1
- package/types.d.ts +462 -83
- package/types.js.map +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
- package/workflows.d.ts +10 -0
- package/workflows.js +16 -0
- package/workflows.js.map +1 -1
- package/workspace.d.ts +80 -62
- package/workspace.js +92 -75
- package/workspace.js.map +1 -1
- package/connection-status.d.ts +0 -28
- package/connection-status.js +0 -61
- package/connection-status.js.map +0 -1
package/agent.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
// An agent: a configuration you start conversations from.
|
|
2
2
|
var _a;
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { accountIdOf } from "./account.js";
|
|
4
|
+
import { isBindings, refuseConnections, secretIdsOf, secretsIn } from "./attach.js";
|
|
5
5
|
import { Conversation } from "./conversation.js";
|
|
6
|
+
import { AgentNavaError } from "./errors.js";
|
|
7
|
+
import { connectionForWire } from "./connection.js";
|
|
6
8
|
/**
|
|
7
9
|
* An agent you can configure and start conversations from.
|
|
8
10
|
*
|
|
@@ -46,7 +48,9 @@ export class Agent {
|
|
|
46
48
|
const res = await this.#http.get(`${this.base}/versions`);
|
|
47
49
|
return res.versions ?? [];
|
|
48
50
|
}
|
|
49
|
-
/** Publish a new version with these fields changed and everything else kept.
|
|
51
|
+
/** Publish a new version with these fields changed and everything else kept.
|
|
52
|
+
* `connections`, when sent, replaces the whole list; an `Account` in it goes
|
|
53
|
+
* over the wire as its id (`Account.toJSON`). */
|
|
50
54
|
async update(fields) {
|
|
51
55
|
const data = await this.#http.post(`${this.base}/versions`, fields);
|
|
52
56
|
this.snapshot = data;
|
|
@@ -78,7 +82,102 @@ export class Agent {
|
|
|
78
82
|
async setTriggers(triggers) {
|
|
79
83
|
return this.update({ triggers });
|
|
80
84
|
}
|
|
85
|
+
/**
|
|
86
|
+
* Replace the webhook's URL and return the new one.
|
|
87
|
+
*
|
|
88
|
+
* The old URL stops working the moment this returns, so update the system
|
|
89
|
+
* that calls it. Use it when a URL leaked. The webhook's `message` and who it
|
|
90
|
+
* runs for are kept. Publishes a version. Throws `no_webhook` (409) when the
|
|
91
|
+
* agent has no webhook trigger.
|
|
92
|
+
*/
|
|
93
|
+
async rotateWebhookUrl() {
|
|
94
|
+
const data = await this.#http.post(`${this.base}/webhook/rotate`, {});
|
|
95
|
+
this.snapshot = data;
|
|
96
|
+
const webhook = data.triggers.find((t) => t.kind === 'webhook');
|
|
97
|
+
if (webhook?.kind !== 'webhook' || !webhook.url) {
|
|
98
|
+
throw new AgentNavaError({
|
|
99
|
+
message: 'The webhook was rotated but no URL came back. Read it with agent.get().',
|
|
100
|
+
code: 'webhook_url_missing',
|
|
101
|
+
status: 502,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return webhook.url;
|
|
105
|
+
}
|
|
106
|
+
/** What the webhook received, newest first: accepted, refused (and why), a
|
|
107
|
+
* sender's retry recognised as a repeat, or a test. Up to `limit` (200). */
|
|
108
|
+
async webhookDeliveries(options = {}) {
|
|
109
|
+
const res = await this.#http.get(`${this.base}/webhook/deliveries`, options.limit !== undefined ? { limit: options.limit } : undefined);
|
|
110
|
+
return res.deliveries ?? [];
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Send the webhook a sample event and run the agent on it, the way a real
|
|
114
|
+
* delivery would (the signature check aside). A real turn, charged like
|
|
115
|
+
* one. Returns the conversation it ran in.
|
|
116
|
+
*/
|
|
117
|
+
async sendTestWebhook() {
|
|
118
|
+
const res = await this.#http.post(`${this.base}/webhook/test`, {});
|
|
119
|
+
return res.conversationId ? { conversationId: res.conversationId } : {};
|
|
120
|
+
}
|
|
121
|
+
// ------------------------------------------------------------ website chat
|
|
122
|
+
/**
|
|
123
|
+
* The agent's website chat: a public chat page, and the embed script that
|
|
124
|
+
* puts it on your website. ON from the moment the agent is created, for
|
|
125
|
+
* every agent, however it was made (here, through the API, or in the portal),
|
|
126
|
+
* until you turn it off. The one exception is an agent that needs each
|
|
127
|
+
* user's own account or secret, which a visitor cannot bring: it has none.
|
|
128
|
+
*
|
|
129
|
+
* You never need it to use the agent from code: the agent answers your API
|
|
130
|
+
* key, and the portal's Test, as soon as it exists.
|
|
131
|
+
*
|
|
132
|
+
* Anyone with the page link can talk to the agent, and each of their turns is
|
|
133
|
+
* charged to your workspace, so turn it off for an agent only your own code
|
|
134
|
+
* should reach. `turnOn()` on a new agent changes nothing and hands back what
|
|
135
|
+
* to put on a website. The portal has no switch for it; its Website chat panel
|
|
136
|
+
* holds the share link and the allowed websites (`allowedWebsites` below).
|
|
137
|
+
* Publishing a newer version does not move the page: call `turnOn()` while it
|
|
138
|
+
* is on to serve the newer one. Turned off and on again, it serves the
|
|
139
|
+
* version it served before.
|
|
140
|
+
*/
|
|
141
|
+
websiteChat = {
|
|
142
|
+
/** Turn it on and get what to put on a website. The first time, and when
|
|
143
|
+
* it is already on, the page serves the current version; after
|
|
144
|
+
* `turnOff()`, the version it served before. Refused (400) for an agent
|
|
145
|
+
* that needs each user's own account or secret, which a visitor cannot
|
|
146
|
+
* bring. */
|
|
147
|
+
turnOn: async () => {
|
|
148
|
+
const res = await this.#http.post(`${this.base}/activate`, {});
|
|
149
|
+
return {
|
|
150
|
+
chatUrl: res.chatUrl,
|
|
151
|
+
publicToken: res.publicToken,
|
|
152
|
+
embedAgentId: res.agentId,
|
|
153
|
+
versionNumber: res.versionNumber,
|
|
154
|
+
};
|
|
155
|
+
},
|
|
156
|
+
/** Turn it off. Visitors are refused from the next message, follow-ups a
|
|
157
|
+
* visitor's conversation booked are cancelled, and nothing more is
|
|
158
|
+
* charged for them. The agent keeps answering your API key, its schedules
|
|
159
|
+
* and its webhooks. The publishable token is kept, so turning it on again
|
|
160
|
+
* revives a snippet already on a website. Turning off a chat that is
|
|
161
|
+
* already off is not an error. */
|
|
162
|
+
turnOff: async () => {
|
|
163
|
+
await this.#http.post(`${this.base}/deactivate`, {});
|
|
164
|
+
},
|
|
165
|
+
/** The websites the chat may be shown on through the embed script. A
|
|
166
|
+
* domain covers its subdomains; an empty list means no other website.
|
|
167
|
+
* The share link opened directly always works. */
|
|
168
|
+
allowedWebsites: {
|
|
169
|
+
/** The list as it stands. */
|
|
170
|
+
get: async () => (await this.#http.get(`${this.base}/allowed-websites`)).websites,
|
|
171
|
+
/** Replace the list. Each entry is normalised ("https://acme.com/page"
|
|
172
|
+
* becomes "acme.com"; "localhost" is allowed for testing), and one that
|
|
173
|
+
* is not a website is refused (400). Returns the list as stored. */
|
|
174
|
+
set: async (websites) => (await this.#http.put(`${this.base}/allowed-websites`, { websites })).websites,
|
|
175
|
+
},
|
|
176
|
+
};
|
|
81
177
|
// --------------------------------------------------------------- knowledge
|
|
178
|
+
#readKnowledgeResponse(path) {
|
|
179
|
+
return this.#http.send(`${this.base}/knowledge/files/content`, { query: { path } });
|
|
180
|
+
}
|
|
82
181
|
/**
|
|
83
182
|
* The agent's knowledge: files every conversation can read.
|
|
84
183
|
*
|
|
@@ -95,6 +194,16 @@ export class Agent {
|
|
|
95
194
|
query: { path },
|
|
96
195
|
...(contentType && { contentType }),
|
|
97
196
|
}),
|
|
197
|
+
/** Read an agent-owned knowledge file as bytes, without starting a turn. */
|
|
198
|
+
get: async (path) => {
|
|
199
|
+
const res = await this.#readKnowledgeResponse(path);
|
|
200
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
201
|
+
},
|
|
202
|
+
/** Read UTF-8 text. JSON files remain text rather than being parsed. */
|
|
203
|
+
text: async (path) => {
|
|
204
|
+
const res = await this.#readKnowledgeResponse(path);
|
|
205
|
+
return res.text();
|
|
206
|
+
},
|
|
98
207
|
list: async (path = '/') => {
|
|
99
208
|
// The route returns EVERY knowledge file for the agent; it takes no
|
|
100
209
|
// directory parameter. Filtering here rather than sending one the server
|
|
@@ -157,24 +266,155 @@ export class Agent {
|
|
|
157
266
|
return current.knowledgeBases ?? [];
|
|
158
267
|
},
|
|
159
268
|
};
|
|
160
|
-
//
|
|
269
|
+
// -------------------------------------------------------------- databases
|
|
161
270
|
/**
|
|
162
|
-
*
|
|
163
|
-
* conversation. This does NOT satisfy a declaration scoped 'conversation'.
|
|
271
|
+
* The WORKSPACE databases this agent may query, by key.
|
|
164
272
|
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
273
|
+
* Register a database first with `ws.databases.create()`, which gives it its
|
|
274
|
+
* key; attaching names that key in this agent's configuration. Naming it is
|
|
275
|
+
* the grant: the database answers this agent's `sql_*` calls and no agent
|
|
276
|
+
* that does not name it, and the agent's prompt lists it by key.
|
|
169
277
|
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
278
|
+
* ATTACHING PUBLISHES A VERSION, like a knowledge base: which databases an
|
|
279
|
+
* agent reaches is part of its configuration, so the change is diffable and
|
|
280
|
+
* reversible. Detaching takes effect on the agent's next query, including in
|
|
281
|
+
* a conversation already under way.
|
|
282
|
+
*/
|
|
283
|
+
databases = {
|
|
284
|
+
/** Attach one database by key. The server appends, so two attaches cannot
|
|
285
|
+
* lose each other. A key that names no database is refused; one already
|
|
286
|
+
* attached is a no-op. */
|
|
287
|
+
attach: async (key) => {
|
|
288
|
+
const res = await this.#http.post(`${this.base}/databases`, { key });
|
|
289
|
+
this.snapshot = res;
|
|
290
|
+
return this;
|
|
291
|
+
},
|
|
292
|
+
/** Stop this agent querying one database. The database stays registered
|
|
293
|
+
* for every other agent that names it. */
|
|
294
|
+
detach: async (key) => {
|
|
295
|
+
const res = await this.#http.delete(`${this.base}/databases/${encodeURIComponent(key)}`);
|
|
296
|
+
if (res)
|
|
297
|
+
this.snapshot = res;
|
|
298
|
+
return this;
|
|
299
|
+
},
|
|
300
|
+
/** The keys this agent may query, in configuration order. */
|
|
301
|
+
list: async () => {
|
|
302
|
+
const current = this.snapshot ?? (await this.get());
|
|
303
|
+
return current.databases ?? [];
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
// ------------------------------------------------------------ connections
|
|
307
|
+
/**
|
|
308
|
+
* The apps this agent uses: each a NAMED connection that says WHOSE ACCOUNT
|
|
309
|
+
* it uses, built the way the portal's Add a connection screen asks.
|
|
173
310
|
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
311
|
+
* | built with | whose account | the person signs in |
|
|
312
|
+
* |---|---|---|
|
|
313
|
+
* | `Connection.creatorAccount(app, account)` | the agent creator's, from `ws.accounts.connect()` | once, and every conversation uses it |
|
|
314
|
+
* | `Connection.endUserAccount(app)` | the end user's: each person talking to the agent | once each, remembered for them |
|
|
315
|
+
*
|
|
316
|
+
* ```ts
|
|
317
|
+
* const { account: work } = await ws.accounts.connect({ app: App.GMAIL, nickname: 'Work' });
|
|
318
|
+
* await agent.connections.add(Connection.creatorAccount(App.GMAIL, work, { name: 'work-gmail' }));
|
|
319
|
+
* await agent.connections.add(Connection.endUserAccount(App.GOOGLE_CALENDAR));
|
|
320
|
+
* ```
|
|
321
|
+
*
|
|
322
|
+
* A turn uses EXACTLY the account the setup names, and never another one.
|
|
323
|
+
* There is no fallback to whichever account of that app happens to exist:
|
|
324
|
+
* that fallback is what this replaced, and it meant reading an agent's code
|
|
325
|
+
* told you nothing about whose mailbox it read.
|
|
326
|
+
*
|
|
327
|
+
* EVERY CHANGE PUBLISHES A VERSION, like `databases`: which accounts an
|
|
328
|
+
* agent reaches is part of its configuration, so the change is diffable and
|
|
329
|
+
* reversible. A conversation uses the connections of the version it runs.
|
|
330
|
+
* The ACCOUNTS are not versioned: signing one in again reaches every agent
|
|
331
|
+
* that names it, from the next turn.
|
|
332
|
+
*
|
|
333
|
+
* Removing a connection never touches the account. Disconnecting an account
|
|
334
|
+
* is the other operation, on the account: `account.disconnect()`.
|
|
335
|
+
*/
|
|
336
|
+
connections = {
|
|
337
|
+
/**
|
|
338
|
+
* Add one connection. One call that publishes, not read-modify-write, so
|
|
339
|
+
* two adds racing cannot lose each other.
|
|
340
|
+
*
|
|
341
|
+
* Takes what `Connection.creatorAccount()` or `Connection.endUserAccount()`
|
|
342
|
+
* builds, or the same plain object. Refused, naming the problem, when the
|
|
343
|
+
* name is taken on this agent (`connection_name_taken`), when the account
|
|
344
|
+
* is of a different app (`account_app_mismatch`), or when no account has
|
|
345
|
+
* that id (`account_not_found`).
|
|
346
|
+
*/
|
|
347
|
+
add: async (connection) => {
|
|
348
|
+
const res = await this.#http.post(`${this.base}/connections`, connectionForWire(connection));
|
|
349
|
+
this.snapshot = res;
|
|
350
|
+
return this;
|
|
351
|
+
},
|
|
352
|
+
/**
|
|
353
|
+
* Point one connection at another account: `{ account }` for the agent
|
|
354
|
+
* creator's (the `Account` you hold, or its id), or `{ endUser: true }`
|
|
355
|
+
* for each end user's own. The name and the app stay; an account of a
|
|
356
|
+
* different app is refused (`account_app_mismatch`).
|
|
357
|
+
*
|
|
358
|
+
* READ-MODIFY-WRITE, unlike `add` and `remove`, because there is no
|
|
359
|
+
* route that changes one connection in place: this reads the current
|
|
360
|
+
* version and publishes it again with the one connection changed. Two
|
|
361
|
+
* changes racing can therefore lose one, so make them one after the
|
|
362
|
+
* other. A name the agent does not have throws
|
|
363
|
+
* (`connection_not_found`) before anything is sent.
|
|
364
|
+
*/
|
|
365
|
+
change: async (name, change) => {
|
|
366
|
+
// Read FRESH, never from the snapshot. A stale list sent back whole
|
|
367
|
+
// would silently undo whatever changed since it was read.
|
|
368
|
+
const current = (await this.get()).connections ?? [];
|
|
369
|
+
if (!current.some((c) => c.name === name)) {
|
|
370
|
+
throw new AgentNavaError({
|
|
371
|
+
message: `This agent has no connection named "${name}". It has: ${current.map((c) => c.name).join(', ') || 'none'}.`,
|
|
372
|
+
code: 'connection_not_found',
|
|
373
|
+
status: 404,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
const whose = change.endUser === true ? { endUser: true } : { account: accountIdOf(change.account) };
|
|
377
|
+
// Every other connection goes back exactly as it was read: this changes
|
|
378
|
+
// one, and must not reinterpret the rest.
|
|
379
|
+
const connections = current.map((c) => (c.name === name ? { name: c.name, app: c.app, ...whose } : c));
|
|
380
|
+
const data = await this.#http.post(`${this.base}/versions`, { connections });
|
|
381
|
+
this.snapshot = data;
|
|
382
|
+
return this;
|
|
383
|
+
},
|
|
384
|
+
/** Remove one connection by name. Publishes a version. NEVER touches the
|
|
385
|
+
* account: it stays connected for every other agent that names it, and
|
|
386
|
+
* for this one if you add it back. */
|
|
387
|
+
remove: async (name) => {
|
|
388
|
+
const res = await this.#http.delete(`${this.base}/connections/${encodeURIComponent(name)}`);
|
|
389
|
+
if (res)
|
|
390
|
+
this.snapshot = res;
|
|
391
|
+
return this;
|
|
392
|
+
},
|
|
393
|
+
/**
|
|
394
|
+
* Each connection with the account it uses and whether that account works
|
|
395
|
+
* now: DECLARED IS NOT CONNECTED. An agent can name an account nobody has
|
|
396
|
+
* finished signing in to, or one that was disconnected since, and that is
|
|
397
|
+
* the state a setup checklist has to show.
|
|
398
|
+
*
|
|
399
|
+
* Includes what an unpublished draft adds (`inDraft`) and removes
|
|
400
|
+
* (`removedInDraft`), so it matches the portal.
|
|
401
|
+
*/
|
|
402
|
+
list: async () => {
|
|
403
|
+
const res = await this.#http.get(`${this.base}/connections`);
|
|
404
|
+
return res.connections ?? [];
|
|
405
|
+
},
|
|
406
|
+
};
|
|
407
|
+
// ------------------------------------------------------ secrets, agent tier
|
|
408
|
+
/**
|
|
409
|
+
* Write secret VALUES to this agent's own scope: readable by this agent and
|
|
410
|
+
* nothing else in the workspace, overriding a workspace secret of the same
|
|
411
|
+
* name. Writing one twice replaces the value rather than failing, because
|
|
412
|
+
* attaching means "this is the credential now".
|
|
413
|
+
*
|
|
414
|
+
* This used to POST the whole object to `/v1/agents/:id/bindings`, which
|
|
415
|
+
* answers only to `{kind, decl}` and records a DECLARATION, never a value. So
|
|
416
|
+
* the call 400'd, and had it not, it would have stored the fact that a secret
|
|
417
|
+
* is wanted rather than the secret.
|
|
178
418
|
*/
|
|
179
419
|
async #applyBindings(bindings) {
|
|
180
420
|
for (const [name, value] of Object.entries(bindings.secrets ?? {})) {
|
|
@@ -185,144 +425,78 @@ export class Agent {
|
|
|
185
425
|
else
|
|
186
426
|
await this.#http.post('/v1/secrets', { name, value, agentId: this.id });
|
|
187
427
|
}
|
|
188
|
-
for (const [provider, connectionId] of Object.entries(bindings.connections ?? {})) {
|
|
189
|
-
await this.#http.post(`${this.base}/bindings`, {
|
|
190
|
-
kind: 'connection',
|
|
191
|
-
decl: { provider, connectionId, scope: 'fixed' },
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
428
|
const data = await this.get();
|
|
195
429
|
this.snapshot = data;
|
|
196
430
|
return new _a(this.#http, this.id, data);
|
|
197
431
|
}
|
|
198
432
|
/**
|
|
199
|
-
* Give this agent
|
|
433
|
+
* Give this agent secret values, so every conversation with it can use them.
|
|
200
434
|
*
|
|
201
435
|
* ```ts
|
|
202
|
-
* await agent.attach(
|
|
203
|
-
* await agent.attach([gmail, calendar]); // several
|
|
204
|
-
* await agent.attach({ secrets: { KEY: value } }); // a secret VALUE
|
|
436
|
+
* await agent.attach({ secrets: { APOLLO_API_KEY: value } });
|
|
205
437
|
* ```
|
|
206
438
|
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
* "Attach" rather than "bind" because it is the plainer word, it is the one
|
|
213
|
-
* that pairs with `detach`, and `knowledgeBases.attach/detach` already uses
|
|
214
|
-
* it for the same shape of operation.
|
|
215
|
-
*
|
|
216
|
-
* ACCOUNTS TAKE THE OBJECT, SECRETS TAKE A VALUE, and that asymmetry is in
|
|
217
|
-
* the product rather than in the naming. An account has an id you can hand
|
|
218
|
-
* over; a secret does not, because binding one means supplying the value
|
|
219
|
-
* itself. Hiding that behind one shape would not make it go away.
|
|
220
|
-
*
|
|
221
|
-
* The counterpart to `authorize()`, which creates an account AND attaches it
|
|
222
|
-
* in one step. Use this when the account already exists: one team mailbox
|
|
223
|
-
* connected once and attached to nine agents, rather than nine sign-ins.
|
|
224
|
-
*/
|
|
225
|
-
async attach(what) {
|
|
226
|
-
if (isBindings(what))
|
|
227
|
-
return this.#applyBindings(what);
|
|
228
|
-
const split = splitAttachments(what);
|
|
229
|
-
const connections = await providersOf(split.connections);
|
|
230
|
-
const secretIds = await secretIdsOf(split.secrets);
|
|
231
|
-
return this.#applyBindings({
|
|
232
|
-
...(connections && { connections }),
|
|
233
|
-
...(secretIds && { secretIds }),
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
/** What each declared provider's actual state is: DECLARED IS NOT CONNECTED.
|
|
237
|
-
*
|
|
238
|
-
* An agent can require Gmail with nobody signed in, and that is the state a
|
|
239
|
-
* setup checklist has to show. Without this the only way to find the gap is
|
|
240
|
-
* to start a conversation and watch the agent fail. */
|
|
241
|
-
async connections() {
|
|
242
|
-
// The MERGED view, not the raw connection rows. /v1/connections?agentId=
|
|
243
|
-
// lists rows that exist, so a provider the agent declares and nobody has
|
|
244
|
-
// connected is simply absent from it, which is the one case this method is
|
|
245
|
-
// most often asked about. The server merges declared against live with the
|
|
246
|
-
// same function the agent's own check_connections tool uses.
|
|
247
|
-
const res = await this.#http.get(`${this.base}/connections`);
|
|
248
|
-
return res.connections ?? [];
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Attach a third-party account to this agent, at `fixed` scope: one account,
|
|
252
|
-
* used by every conversation.
|
|
253
|
-
*
|
|
254
|
-
* Returns a url to send a person to. They sign in there, in their own
|
|
255
|
-
* browser, and the account attaches when they finish. YOU NEVER SEE A TOKEN,
|
|
256
|
-
* which is the entire point: nobody should be pasting a Gmail credential into
|
|
257
|
-
* a config field.
|
|
439
|
+
* SECRETS ONLY, since 0.44. Accounts used to be attached here, and a turn
|
|
440
|
+
* then had to guess which of the accounts attached and owned around it to
|
|
441
|
+
* use. The agent's setup now names them: `agent.connections.add()`. One
|
|
442
|
+
* handed in here is refused by name (`not_a_secret`) rather than dropped.
|
|
258
443
|
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
444
|
+
* A stored `Secret` is not attached to an agent either. It never was: one
|
|
445
|
+
* was accepted here and nothing was done with it. Store one for the agent
|
|
446
|
+
* with `ws.secrets.create({ name, value, agentId })`, or attach one of your
|
|
447
|
+
* end user's to their conversation with `conversation.attach(secret)`.
|
|
262
448
|
*/
|
|
263
|
-
async
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const res = await this.#http.get('/v1/connections', {
|
|
276
|
-
agentId: this.id,
|
|
277
|
-
});
|
|
278
|
-
const match = (res.connections ?? [])
|
|
279
|
-
.map(toConnectionStatus)
|
|
280
|
-
.find((c) => c.provider === provider && c.connected);
|
|
281
|
-
// Nothing connected is not an error: the caller wanted this provider
|
|
282
|
-
// detached, and it is.
|
|
283
|
-
if (!match?.id)
|
|
284
|
-
return;
|
|
285
|
-
await this.#http.post(`/v1/connections/${encodeURIComponent(match.id)}/revoke`);
|
|
449
|
+
async attach(bindings) {
|
|
450
|
+
if (!isBindings(bindings)) {
|
|
451
|
+
throw new AgentNavaError({
|
|
452
|
+
message: 'agent.attach takes { secrets: { NAME: value } }. Accounts go in the agent\'s setup ' +
|
|
453
|
+
'(agent.connections.add), and a stored secret attaches to a conversation ' +
|
|
454
|
+
'(conversation.attach).',
|
|
455
|
+
code: 'not_a_secret',
|
|
456
|
+
status: 400,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
refuseConnections(bindings);
|
|
460
|
+
return this.#applyBindings(bindings);
|
|
286
461
|
}
|
|
287
462
|
// ----------------------------------------------------------- conversations
|
|
288
|
-
/** Start a conversation. It pins the agent's version now and keeps it.
|
|
463
|
+
/** Start a conversation. It pins the agent's version now and keeps it.
|
|
464
|
+
*
|
|
465
|
+
* Nothing about accounts is passed here: the version's connections name
|
|
466
|
+
* them, and a connection to the end user's account uses the account of
|
|
467
|
+
* the person named by `endUserId`. */
|
|
289
468
|
async start(options) {
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
const
|
|
295
|
-
const
|
|
296
|
-
const secretIds = await secretIdsOf(split.secrets);
|
|
297
|
-
const bindings = options.secrets || connections || secretIds
|
|
469
|
+
const endUserId = requireEndUserId(options?.endUserId);
|
|
470
|
+
// Resolved BEFORE the launch: a Secret is named on the wire by the
|
|
471
|
+
// declared NAME it fills, and a handle addressed by id may not know its
|
|
472
|
+
// own yet. Anything that is not a Secret is refused here, by name.
|
|
473
|
+
const secretIds = await secretIdsOf(secretsIn(options.attach));
|
|
474
|
+
const bindings = options.secrets || secretIds
|
|
298
475
|
? {
|
|
299
476
|
...(options.secrets && { secrets: options.secrets }),
|
|
300
477
|
...(secretIds && { secretIds }),
|
|
301
|
-
...(connections && { connections }),
|
|
302
478
|
}
|
|
303
479
|
: undefined;
|
|
304
480
|
const data = await this.#http.post('/v1/conversations', {
|
|
305
481
|
agentId: this.id,
|
|
306
482
|
...(options.version !== undefined && { version: options.version }),
|
|
307
483
|
...(options.parameters && { parameters: options.parameters }),
|
|
308
|
-
// `sessionBindings`, NOT `secrets`
|
|
484
|
+
// `sessionBindings`, NOT `secrets` at the top level.
|
|
309
485
|
//
|
|
310
486
|
// This sent them at the top level and the launch route reads
|
|
311
487
|
// `body.sessionBindings`, so they were dropped in silence: the route's
|
|
312
|
-
// own body type has no top-level `secrets`
|
|
313
|
-
//
|
|
314
|
-
//
|
|
488
|
+
// own body type has no top-level `secrets` to read. A conversation
|
|
489
|
+
// started with a credential began without it, and the first turn
|
|
490
|
+
// failed somewhere else entirely.
|
|
315
491
|
//
|
|
316
492
|
// Third place this exact envelope mistake was found in one afternoon,
|
|
317
493
|
// after `agent.bind` and `conversation.bind`. The shape of the bug is
|
|
318
494
|
// always the same: the SDK agrees with itself, the route agrees with
|
|
319
495
|
// itself, and nothing compares the two.
|
|
320
496
|
...(bindings && { sessionBindings: bindings }),
|
|
321
|
-
//
|
|
322
|
-
//
|
|
323
|
-
|
|
324
|
-
// which is neither "nobody" nor a person.
|
|
325
|
-
...(options.endUserId != null && { user: { id: options.endUserId } }),
|
|
497
|
+
// Always present: requireEndUserId() above has already refused
|
|
498
|
+
// anything that is not a non-empty string.
|
|
499
|
+
user: { id: endUserId },
|
|
326
500
|
// ALWAYS a new conversation, even when a user is named.
|
|
327
501
|
//
|
|
328
502
|
// The route resumes that user's most recent conversation when it is
|
|
@@ -389,6 +563,17 @@ export class Agent {
|
|
|
389
563
|
}
|
|
390
564
|
}
|
|
391
565
|
_a = Agent;
|
|
392
|
-
/**
|
|
393
|
-
|
|
566
|
+
/** The end user a conversation belongs to: a non-empty string, or a thrown error before any request. */
|
|
567
|
+
function requireEndUserId(value) {
|
|
568
|
+
// Checked at RUNTIME as well as in the type: a JavaScript caller, or a
|
|
569
|
+
// TypeScript one reading a nullable column, can still pass null, a number or
|
|
570
|
+
// ''. Each used to start an ownerless conversation that looked fine.
|
|
571
|
+
if (typeof value === 'string' && value.trim())
|
|
572
|
+
return value.trim();
|
|
573
|
+
throw new AgentNavaError({
|
|
574
|
+
message: 'agent.start() needs endUserId: a non-empty string naming who the conversation is for (your user id, a visitor id, or a job name).',
|
|
575
|
+
code: 'end_user_required',
|
|
576
|
+
status: 400,
|
|
577
|
+
});
|
|
578
|
+
}
|
|
394
579
|
//# sourceMappingURL=agent.js.map
|