@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.
Files changed (62) hide show
  1. package/README.md +212 -23
  2. package/account.d.ts +154 -0
  3. package/account.js +219 -0
  4. package/account.js.map +1 -0
  5. package/agent.d.ts +229 -87
  6. package/agent.js +310 -125
  7. package/agent.js.map +1 -1
  8. package/apps.generated.d.ts +154 -0
  9. package/apps.generated.js +152 -0
  10. package/apps.generated.js.map +1 -0
  11. package/attach.d.ts +25 -26
  12. package/attach.js +53 -66
  13. package/attach.js.map +1 -1
  14. package/connection.d.ts +79 -82
  15. package/connection.js +67 -134
  16. package/connection.js.map +1 -1
  17. package/conversation.d.ts +55 -35
  18. package/conversation.js +70 -50
  19. package/conversation.js.map +1 -1
  20. package/database.d.ts +53 -0
  21. package/database.js +69 -0
  22. package/database.js.map +1 -0
  23. package/end-user.d.ts +58 -47
  24. package/end-user.js +62 -72
  25. package/end-user.js.map +1 -1
  26. package/errors.d.ts +2 -1
  27. package/errors.js +2 -1
  28. package/errors.js.map +1 -1
  29. package/http.d.ts +5 -0
  30. package/http.js +109 -8
  31. package/http.js.map +1 -1
  32. package/import-tools.d.ts +54 -33
  33. package/import-tools.js +526 -124
  34. package/import-tools.js.map +1 -1
  35. package/index.d.ts +5 -2
  36. package/index.js +9 -2
  37. package/index.js.map +1 -1
  38. package/load-agent-dir.js +28 -5
  39. package/load-agent-dir.js.map +1 -1
  40. package/package.json +5 -1
  41. package/secret.d.ts +1 -1
  42. package/secret.js +1 -1
  43. package/secret.js.map +1 -1
  44. package/templates-inline.js +1 -1
  45. package/templates-inline.js.map +1 -1
  46. package/turn.d.ts +1 -1
  47. package/turn.js +77 -29
  48. package/turn.js.map +1 -1
  49. package/types.d.ts +462 -83
  50. package/types.js.map +1 -1
  51. package/version.d.ts +1 -1
  52. package/version.js +1 -1
  53. package/version.js.map +1 -1
  54. package/workflows.d.ts +10 -0
  55. package/workflows.js +16 -0
  56. package/workflows.js.map +1 -1
  57. package/workspace.d.ts +80 -62
  58. package/workspace.js +92 -75
  59. package/workspace.js.map +1 -1
  60. package/connection-status.d.ts +0 -28
  61. package/connection-status.js +0 -61
  62. 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 { isBindings, providersOf, secretIdsOf, splitAttachments } from "./attach.js";
4
- import { toConnectionStatus } from "./connection-status.js";
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
- // ------------------------------------------------- credentials, agent tier
269
+ // -------------------------------------------------------------- databases
161
270
  /**
162
- * Supply the credentials declared at 'fixed' scope: set once, used by every
163
- * conversation. This does NOT satisfy a declaration scoped 'conversation'.
271
+ * The WORKSPACE databases this agent may query, by key.
164
272
  *
165
- * A secret is written to this agent's own scope, so it is readable by this
166
- * agent and nothing else in the workspace, and it overrides a workspace
167
- * secret of the same name. Writing one twice replaces the value rather than
168
- * failing, because `bind` means "this is the credential now".
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
- * A connection value is the id of an account already attached to the
171
- * workspace; use `authorize()` when there is no account yet, since that is
172
- * the flow where a person signs in and no token passes through your code.
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
- * This used to POST the whole `{secrets, connections}` object to
175
- * `/v1/agents/:id/bindings`, which answers only to `{kind, decl}` and records
176
- * a DECLARATION, never a value. So the call 400'd, and had it not, it would
177
- * have stored the fact that a secret is wanted rather than the secret.
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 a credential, so every conversation with it can use one.
433
+ * Give this agent secret values, so every conversation with it can use them.
200
434
  *
201
435
  * ```ts
202
- * await agent.attach(gmail); // one account
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
- * ONE VERB, NOT TWO. This and `bind` used to both exist and do the same job
208
- * through different shapes, which is how a surface becomes hard to learn:
209
- * two names for one idea means every reader has to work out whether the
210
- * difference is meaningful. It is not, so `bind` is gone.
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
- * This does NOT satisfy a declaration scoped 'conversation'. Use
260
- * `conversation.authorize()` for those, which attaches the account to that
261
- * one conversation and cannot overwrite this shared one.
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 authorize(provider) {
264
- const res = await this.#http.post('/v1/connections/connect', { provider, agentId: this.id });
265
- // `url` on the way out, `authUrl` on the wire. The SDK's name is the one
266
- // the reference documents and reads better at a call site.
267
- return { url: res.authUrl, connectionId: res.connectionId };
268
- }
269
- /** Detach the account connected for this provider.
270
- *
271
- * Reads the raw ROWS rather than the merged status, because revoking needs
272
- * the connection's id and the merged view is keyed by provider: it answers
273
- * "is Gmail connected", not "which row is it". */
274
- async disconnect(provider) {
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
- // Resolved BEFORE the launch, because an account is named on the wire by
291
- // its provider and a handle addressed by id may not know its own yet.
292
- // Split first: a Connection is named on the wire by its PROVIDER and a
293
- // Secret by the declared NAME it fills, so the two cannot share a map.
294
- const split = splitAttachments(options.attach);
295
- const connections = await providersOf(split.connections);
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` and `connections` at the top level.
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` or `connections` to read.
313
- // A conversation started with a credential began without it, and the
314
- // first turn failed somewhere else entirely.
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
- // `!= null`, catching undefined as well as null. The field is REQUIRED
322
- // in TypeScript, but a JavaScript caller can still omit it, and
323
- // `!== null` alone then sent `user: {}`: an owner object with no id,
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
- /** A connection row as `GET /v1/connections` returns it (toConnectionMeta in
393
- * apps/api/src/connections/store.ts). */
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