@cerebro-labs/agentnava-sdk 0.26.0 → 0.44.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 +204 -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 +217 -88
  6. package/agent.js +295 -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 +48 -33
  33. package/import-tools.js +281 -127
  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 +409 -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,87 @@ 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
+ // ------------------------------------------------------------ website chat
107
+ /**
108
+ * The agent's website chat: a public chat page, and the embed script that
109
+ * puts it on your website. ON from the moment the agent is created, for
110
+ * every agent, however it was made (here, through the API, or in the portal),
111
+ * until you turn it off. The one exception is an agent that needs each
112
+ * user's own account or secret, which a visitor cannot bring: it has none.
113
+ *
114
+ * You never need it to use the agent from code: the agent answers your API
115
+ * key, and the portal's Test, as soon as it exists.
116
+ *
117
+ * Anyone with the page link can talk to the agent, and each of their turns is
118
+ * charged to your workspace, so turn it off for an agent only your own code
119
+ * should reach. `turnOn()` on a new agent changes nothing and hands back what
120
+ * to put on a website. The portal has no switch for it; its Website chat panel
121
+ * holds the share link and the allowed websites (`allowedWebsites` below).
122
+ * Publishing a newer version does not move the page: call `turnOn()` while it
123
+ * is on to serve the newer one. Turned off and on again, it serves the
124
+ * version it served before.
125
+ */
126
+ websiteChat = {
127
+ /** Turn it on and get what to put on a website. The first time, and when
128
+ * it is already on, the page serves the current version; after
129
+ * `turnOff()`, the version it served before. Refused (400) for an agent
130
+ * that needs each user's own account or secret, which a visitor cannot
131
+ * bring. */
132
+ turnOn: async () => {
133
+ const res = await this.#http.post(`${this.base}/activate`, {});
134
+ return {
135
+ chatUrl: res.chatUrl,
136
+ publicToken: res.publicToken,
137
+ embedAgentId: res.agentId,
138
+ versionNumber: res.versionNumber,
139
+ };
140
+ },
141
+ /** Turn it off. Visitors are refused from the next message, follow-ups a
142
+ * visitor's conversation booked are cancelled, and nothing more is
143
+ * charged for them. The agent keeps answering your API key, its schedules
144
+ * and its webhooks. The publishable token is kept, so turning it on again
145
+ * revives a snippet already on a website. Turning off a chat that is
146
+ * already off is not an error. */
147
+ turnOff: async () => {
148
+ await this.#http.post(`${this.base}/deactivate`, {});
149
+ },
150
+ /** The websites the chat may be shown on through the embed script. A
151
+ * domain covers its subdomains; an empty list means no other website.
152
+ * The share link opened directly always works. */
153
+ allowedWebsites: {
154
+ /** The list as it stands. */
155
+ get: async () => (await this.#http.get(`${this.base}/allowed-websites`)).websites,
156
+ /** Replace the list. Each entry is normalised ("https://acme.com/page"
157
+ * becomes "acme.com"; "localhost" is allowed for testing), and one that
158
+ * is not a website is refused (400). Returns the list as stored. */
159
+ set: async (websites) => (await this.#http.put(`${this.base}/allowed-websites`, { websites })).websites,
160
+ },
161
+ };
81
162
  // --------------------------------------------------------------- knowledge
163
+ #readKnowledgeResponse(path) {
164
+ return this.#http.send(`${this.base}/knowledge/files/content`, { query: { path } });
165
+ }
82
166
  /**
83
167
  * The agent's knowledge: files every conversation can read.
84
168
  *
@@ -95,6 +179,16 @@ export class Agent {
95
179
  query: { path },
96
180
  ...(contentType && { contentType }),
97
181
  }),
182
+ /** Read an agent-owned knowledge file as bytes, without starting a turn. */
183
+ get: async (path) => {
184
+ const res = await this.#readKnowledgeResponse(path);
185
+ return new Uint8Array(await res.arrayBuffer());
186
+ },
187
+ /** Read UTF-8 text. JSON files remain text rather than being parsed. */
188
+ text: async (path) => {
189
+ const res = await this.#readKnowledgeResponse(path);
190
+ return res.text();
191
+ },
98
192
  list: async (path = '/') => {
99
193
  // The route returns EVERY knowledge file for the agent; it takes no
100
194
  // directory parameter. Filtering here rather than sending one the server
@@ -157,24 +251,155 @@ export class Agent {
157
251
  return current.knowledgeBases ?? [];
158
252
  },
159
253
  };
160
- // ------------------------------------------------- credentials, agent tier
254
+ // -------------------------------------------------------------- databases
161
255
  /**
162
- * Supply the credentials declared at 'fixed' scope: set once, used by every
163
- * conversation. This does NOT satisfy a declaration scoped 'conversation'.
256
+ * The WORKSPACE databases this agent may query, by key.
164
257
  *
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".
258
+ * Register a database first with `ws.databases.create()`, which gives it its
259
+ * key; attaching names that key in this agent's configuration. Naming it is
260
+ * the grant: the database answers this agent's `sql_*` calls and no agent
261
+ * that does not name it, and the agent's prompt lists it by key.
169
262
  *
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.
263
+ * ATTACHING PUBLISHES A VERSION, like a knowledge base: which databases an
264
+ * agent reaches is part of its configuration, so the change is diffable and
265
+ * reversible. Detaching takes effect on the agent's next query, including in
266
+ * a conversation already under way.
267
+ */
268
+ databases = {
269
+ /** Attach one database by key. The server appends, so two attaches cannot
270
+ * lose each other. A key that names no database is refused; one already
271
+ * attached is a no-op. */
272
+ attach: async (key) => {
273
+ const res = await this.#http.post(`${this.base}/databases`, { key });
274
+ this.snapshot = res;
275
+ return this;
276
+ },
277
+ /** Stop this agent querying one database. The database stays registered
278
+ * for every other agent that names it. */
279
+ detach: async (key) => {
280
+ const res = await this.#http.delete(`${this.base}/databases/${encodeURIComponent(key)}`);
281
+ if (res)
282
+ this.snapshot = res;
283
+ return this;
284
+ },
285
+ /** The keys this agent may query, in configuration order. */
286
+ list: async () => {
287
+ const current = this.snapshot ?? (await this.get());
288
+ return current.databases ?? [];
289
+ },
290
+ };
291
+ // ------------------------------------------------------------ connections
292
+ /**
293
+ * The apps this agent uses: each a NAMED connection that says WHOSE ACCOUNT
294
+ * it uses, built the way the portal's Add a connection screen asks.
173
295
  *
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.
296
+ * | built with | whose account | the person signs in |
297
+ * |---|---|---|
298
+ * | `Connection.creatorAccount(app, account)` | the agent creator's, from `ws.accounts.connect()` | once, and every conversation uses it |
299
+ * | `Connection.endUserAccount(app)` | the end user's: each person talking to the agent | once each, remembered for them |
300
+ *
301
+ * ```ts
302
+ * const { account: work } = await ws.accounts.connect({ app: App.GMAIL, nickname: 'Work' });
303
+ * await agent.connections.add(Connection.creatorAccount(App.GMAIL, work, { name: 'work-gmail' }));
304
+ * await agent.connections.add(Connection.endUserAccount(App.GOOGLE_CALENDAR));
305
+ * ```
306
+ *
307
+ * A turn uses EXACTLY the account the setup names, and never another one.
308
+ * There is no fallback to whichever account of that app happens to exist:
309
+ * that fallback is what this replaced, and it meant reading an agent's code
310
+ * told you nothing about whose mailbox it read.
311
+ *
312
+ * EVERY CHANGE PUBLISHES A VERSION, like `databases`: which accounts an
313
+ * agent reaches is part of its configuration, so the change is diffable and
314
+ * reversible. A conversation uses the connections of the version it runs.
315
+ * The ACCOUNTS are not versioned: signing one in again reaches every agent
316
+ * that names it, from the next turn.
317
+ *
318
+ * Removing a connection never touches the account. Disconnecting an account
319
+ * is the other operation, on the account: `account.disconnect()`.
320
+ */
321
+ connections = {
322
+ /**
323
+ * Add one connection. One call that publishes, not read-modify-write, so
324
+ * two adds racing cannot lose each other.
325
+ *
326
+ * Takes what `Connection.creatorAccount()` or `Connection.endUserAccount()`
327
+ * builds, or the same plain object. Refused, naming the problem, when the
328
+ * name is taken on this agent (`connection_name_taken`), when the account
329
+ * is of a different app (`account_app_mismatch`), or when no account has
330
+ * that id (`account_not_found`).
331
+ */
332
+ add: async (connection) => {
333
+ const res = await this.#http.post(`${this.base}/connections`, connectionForWire(connection));
334
+ this.snapshot = res;
335
+ return this;
336
+ },
337
+ /**
338
+ * Point one connection at another account: `{ account }` for the agent
339
+ * creator's (the `Account` you hold, or its id), or `{ endUser: true }`
340
+ * for each end user's own. The name and the app stay; an account of a
341
+ * different app is refused (`account_app_mismatch`).
342
+ *
343
+ * READ-MODIFY-WRITE, unlike `add` and `remove`, because there is no
344
+ * route that changes one connection in place: this reads the current
345
+ * version and publishes it again with the one connection changed. Two
346
+ * changes racing can therefore lose one, so make them one after the
347
+ * other. A name the agent does not have throws
348
+ * (`connection_not_found`) before anything is sent.
349
+ */
350
+ change: async (name, change) => {
351
+ // Read FRESH, never from the snapshot. A stale list sent back whole
352
+ // would silently undo whatever changed since it was read.
353
+ const current = (await this.get()).connections ?? [];
354
+ if (!current.some((c) => c.name === name)) {
355
+ throw new AgentNavaError({
356
+ message: `This agent has no connection named "${name}". It has: ${current.map((c) => c.name).join(', ') || 'none'}.`,
357
+ code: 'connection_not_found',
358
+ status: 404,
359
+ });
360
+ }
361
+ const whose = change.endUser === true ? { endUser: true } : { account: accountIdOf(change.account) };
362
+ // Every other connection goes back exactly as it was read: this changes
363
+ // one, and must not reinterpret the rest.
364
+ const connections = current.map((c) => (c.name === name ? { name: c.name, app: c.app, ...whose } : c));
365
+ const data = await this.#http.post(`${this.base}/versions`, { connections });
366
+ this.snapshot = data;
367
+ return this;
368
+ },
369
+ /** Remove one connection by name. Publishes a version. NEVER touches the
370
+ * account: it stays connected for every other agent that names it, and
371
+ * for this one if you add it back. */
372
+ remove: async (name) => {
373
+ const res = await this.#http.delete(`${this.base}/connections/${encodeURIComponent(name)}`);
374
+ if (res)
375
+ this.snapshot = res;
376
+ return this;
377
+ },
378
+ /**
379
+ * Each connection with the account it uses and whether that account works
380
+ * now: DECLARED IS NOT CONNECTED. An agent can name an account nobody has
381
+ * finished signing in to, or one that was disconnected since, and that is
382
+ * the state a setup checklist has to show.
383
+ *
384
+ * Includes what an unpublished draft adds (`inDraft`) and removes
385
+ * (`removedInDraft`), so it matches the portal.
386
+ */
387
+ list: async () => {
388
+ const res = await this.#http.get(`${this.base}/connections`);
389
+ return res.connections ?? [];
390
+ },
391
+ };
392
+ // ------------------------------------------------------ secrets, agent tier
393
+ /**
394
+ * Write secret VALUES to this agent's own scope: readable by this agent and
395
+ * nothing else in the workspace, overriding a workspace secret of the same
396
+ * name. Writing one twice replaces the value rather than failing, because
397
+ * attaching means "this is the credential now".
398
+ *
399
+ * This used to POST the whole object to `/v1/agents/:id/bindings`, which
400
+ * answers only to `{kind, decl}` and records a DECLARATION, never a value. So
401
+ * the call 400'd, and had it not, it would have stored the fact that a secret
402
+ * is wanted rather than the secret.
178
403
  */
179
404
  async #applyBindings(bindings) {
180
405
  for (const [name, value] of Object.entries(bindings.secrets ?? {})) {
@@ -185,144 +410,78 @@ export class Agent {
185
410
  else
186
411
  await this.#http.post('/v1/secrets', { name, value, agentId: this.id });
187
412
  }
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
413
  const data = await this.get();
195
414
  this.snapshot = data;
196
415
  return new _a(this.#http, this.id, data);
197
416
  }
198
417
  /**
199
- * Give this agent a credential, so every conversation with it can use one.
418
+ * Give this agent secret values, so every conversation with it can use them.
200
419
  *
201
420
  * ```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
421
+ * await agent.attach({ secrets: { APOLLO_API_KEY: value } });
205
422
  * ```
206
423
  *
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.
424
+ * SECRETS ONLY, since 0.44. Accounts used to be attached here, and a turn
425
+ * then had to guess which of the accounts attached and owned around it to
426
+ * use. The agent's setup now names them: `agent.connections.add()`. One
427
+ * handed in here is refused by name (`not_a_secret`) rather than dropped.
211
428
  *
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.
429
+ * A stored `Secret` is not attached to an agent either. It never was: one
430
+ * was accepted here and nothing was done with it. Store one for the agent
431
+ * with `ws.secrets.create({ name, value, agentId })`, or attach one of your
432
+ * end user's to their conversation with `conversation.attach(secret)`.
224
433
  */
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.
258
- *
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.
262
- */
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`);
434
+ async attach(bindings) {
435
+ if (!isBindings(bindings)) {
436
+ throw new AgentNavaError({
437
+ message: 'agent.attach takes { secrets: { NAME: value } }. Accounts go in the agent\'s setup ' +
438
+ '(agent.connections.add), and a stored secret attaches to a conversation ' +
439
+ '(conversation.attach).',
440
+ code: 'not_a_secret',
441
+ status: 400,
442
+ });
443
+ }
444
+ refuseConnections(bindings);
445
+ return this.#applyBindings(bindings);
286
446
  }
287
447
  // ----------------------------------------------------------- conversations
288
- /** Start a conversation. It pins the agent's version now and keeps it. */
448
+ /** Start a conversation. It pins the agent's version now and keeps it.
449
+ *
450
+ * Nothing about accounts is passed here: the version's connections name
451
+ * them, and a connection to the end user's account uses the account of
452
+ * the person named by `endUserId`. */
289
453
  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
454
+ const endUserId = requireEndUserId(options?.endUserId);
455
+ // Resolved BEFORE the launch: a Secret is named on the wire by the
456
+ // declared NAME it fills, and a handle addressed by id may not know its
457
+ // own yet. Anything that is not a Secret is refused here, by name.
458
+ const secretIds = await secretIdsOf(secretsIn(options.attach));
459
+ const bindings = options.secrets || secretIds
298
460
  ? {
299
461
  ...(options.secrets && { secrets: options.secrets }),
300
462
  ...(secretIds && { secretIds }),
301
- ...(connections && { connections }),
302
463
  }
303
464
  : undefined;
304
465
  const data = await this.#http.post('/v1/conversations', {
305
466
  agentId: this.id,
306
467
  ...(options.version !== undefined && { version: options.version }),
307
468
  ...(options.parameters && { parameters: options.parameters }),
308
- // `sessionBindings`, NOT `secrets` and `connections` at the top level.
469
+ // `sessionBindings`, NOT `secrets` at the top level.
309
470
  //
310
471
  // This sent them at the top level and the launch route reads
311
472
  // `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.
473
+ // own body type has no top-level `secrets` to read. A conversation
474
+ // started with a credential began without it, and the first turn
475
+ // failed somewhere else entirely.
315
476
  //
316
477
  // Third place this exact envelope mistake was found in one afternoon,
317
478
  // after `agent.bind` and `conversation.bind`. The shape of the bug is
318
479
  // always the same: the SDK agrees with itself, the route agrees with
319
480
  // itself, and nothing compares the two.
320
481
  ...(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 } }),
482
+ // Always present: requireEndUserId() above has already refused
483
+ // anything that is not a non-empty string.
484
+ user: { id: endUserId },
326
485
  // ALWAYS a new conversation, even when a user is named.
327
486
  //
328
487
  // The route resumes that user's most recent conversation when it is
@@ -389,6 +548,17 @@ export class Agent {
389
548
  }
390
549
  }
391
550
  _a = Agent;
392
- /** A connection row as `GET /v1/connections` returns it (toConnectionMeta in
393
- * apps/api/src/connections/store.ts). */
551
+ /** The end user a conversation belongs to: a non-empty string, or a thrown error before any request. */
552
+ function requireEndUserId(value) {
553
+ // Checked at RUNTIME as well as in the type: a JavaScript caller, or a
554
+ // TypeScript one reading a nullable column, can still pass null, a number or
555
+ // ''. Each used to start an ownerless conversation that looked fine.
556
+ if (typeof value === 'string' && value.trim())
557
+ return value.trim();
558
+ throw new AgentNavaError({
559
+ 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).',
560
+ code: 'end_user_required',
561
+ status: 400,
562
+ });
563
+ }
394
564
  //# sourceMappingURL=agent.js.map