@cerebro-labs/agentnava-sdk 0.26.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.
Files changed (67) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +12 -0
  3. package/README.md +230 -0
  4. package/agent.d.ts +285 -0
  5. package/agent.js +394 -0
  6. package/agent.js.map +1 -0
  7. package/attach.d.ts +49 -0
  8. package/attach.js +115 -0
  9. package/attach.js.map +1 -0
  10. package/connection-status.d.ts +28 -0
  11. package/connection-status.js +61 -0
  12. package/connection-status.js.map +1 -0
  13. package/connection.d.ts +89 -0
  14. package/connection.js +143 -0
  15. package/connection.js.map +1 -0
  16. package/conversation.d.ts +177 -0
  17. package/conversation.js +348 -0
  18. package/conversation.js.map +1 -0
  19. package/end-user.d.ts +159 -0
  20. package/end-user.js +209 -0
  21. package/end-user.js.map +1 -0
  22. package/errors.d.ts +43 -0
  23. package/errors.js +55 -0
  24. package/errors.js.map +1 -0
  25. package/http.d.ts +32 -0
  26. package/http.js +192 -0
  27. package/http.js.map +1 -0
  28. package/import-tools.d.ts +282 -0
  29. package/import-tools.js +686 -0
  30. package/import-tools.js.map +1 -0
  31. package/index.d.ts +16 -0
  32. package/index.js +38 -0
  33. package/index.js.map +1 -0
  34. package/knowledge-base.d.ts +165 -0
  35. package/knowledge-base.js +274 -0
  36. package/knowledge-base.js.map +1 -0
  37. package/load-agent-dir.d.ts +17 -0
  38. package/load-agent-dir.js +192 -0
  39. package/load-agent-dir.js.map +1 -0
  40. package/package.json +32 -0
  41. package/project.d.ts +94 -0
  42. package/project.js +155 -0
  43. package/project.js.map +1 -0
  44. package/secret.d.ts +45 -0
  45. package/secret.js +69 -0
  46. package/secret.js.map +1 -0
  47. package/templates-inline.d.ts +2 -0
  48. package/templates-inline.js +16 -0
  49. package/templates-inline.js.map +1 -0
  50. package/templates.d.ts +16 -0
  51. package/templates.js +32 -0
  52. package/templates.js.map +1 -0
  53. package/turn.d.ts +11 -0
  54. package/turn.js +244 -0
  55. package/turn.js.map +1 -0
  56. package/types.d.ts +485 -0
  57. package/types.js +4 -0
  58. package/types.js.map +1 -0
  59. package/version.d.ts +1 -0
  60. package/version.js +5 -0
  61. package/version.js.map +1 -0
  62. package/workflows.d.ts +36 -0
  63. package/workflows.js +101 -0
  64. package/workflows.js.map +1 -0
  65. package/workspace.d.ts +290 -0
  66. package/workspace.js +390 -0
  67. package/workspace.js.map +1 -0
package/agent.d.ts ADDED
@@ -0,0 +1,285 @@
1
+ import { type Attachable } from './attach.ts';
2
+ import { Conversation } from './conversation.ts';
3
+ import type { Http } from './http.ts';
4
+ import type { AgentData, AgentFields, Bindings, ConnectionStatus, FileEntry, Trigger, Workflow } from './types.ts';
5
+ /**
6
+ * Turn the accounts a caller is holding into the `{ provider: id }` map the
7
+ * wire speaks.
8
+ *
9
+ * Shared by `start`, `agent.attach` and `conversation.attach` so the three
10
+ * cannot drift. A handle you created carries its provider already; one you
11
+ * addressed by id has to read it, which is the only request here and only when
12
+ * it is unavoidable.
13
+ */
14
+ export interface StartOptions {
15
+ /**
16
+ * Accounts to attach AS THE CONVERSATION STARTS.
17
+ *
18
+ * The common shape for an application with many end users: the person
19
+ * connected Gmail once, and this thread should act as them from its first
20
+ * turn.
21
+ *
22
+ * ```ts
23
+ * const gmail = await ws.endUser(row.userId).connections.list();
24
+ * const c = await agent.start({ endUserId: row.userId, attach: gmail });
25
+ * ```
26
+ *
27
+ * Doing it here rather than with a second `conversation.attach()` call
28
+ * closes a real window: between `start` and the attach, the conversation
29
+ * exists and can be asked something, and it would answer without the
30
+ * account.
31
+ *
32
+ * STILL EXPLICIT. Naming `endUserId` does not attach their accounts; this
33
+ * line does, and you can read it.
34
+ *
35
+ * TAKES SECRETS TOO. A `Secret` from `ws.endUser(id).secrets.create(...)`
36
+ * attaches by id, which is the only way a stored per-person credential can
37
+ * reach a turn: you never hold its value. Mix them freely, they are split by
38
+ * type before the launch.
39
+ */
40
+ attach?: Attachable | Attachable[];
41
+ /**
42
+ * Secret VALUES for this conversation only.
43
+ *
44
+ * Not `attach`, because a secret has no id you can hand over: binding one
45
+ * takes the value itself. That asymmetry is in the product, not in this
46
+ * naming, and hiding it behind the same word would not remove it.
47
+ */
48
+ secrets?: Record<string, string>;
49
+ /** Pin a specific version. Defaults to the latest at the moment of starting.
50
+ *
51
+ * A conversation pins its version for life: publishing afterwards does not
52
+ * move a conversation that is already open. */
53
+ version?: number;
54
+ parameters?: Record<string, string | number>;
55
+ /**
56
+ * WHO this conversation is for: one of YOUR users, not one of ours.
57
+ *
58
+ * REQUIRED, and nullable. You must say, and `null` is a real answer: a
59
+ * nightly report belongs to nobody. Optional would let it be forgotten,
60
+ * which is the mistake worth designing against, because a conversation
61
+ * without an owner cannot be listed back to the person it was for.
62
+ *
63
+ * It is a LABEL on the conversation, and nothing else. It does not decide
64
+ * which conversation you get, because `start` always starts a new one.
65
+ * Later you can ask for everything belonging to that person:
66
+ *
67
+ * ```ts
68
+ * const conversation = await agent.start({ endUserId: row.userId });
69
+ * const nobody = await agent.start({ endUserId: null }); // a nightly job
70
+ * const theirs = await agent.conversations({ endUser: row.userId });
71
+ * ```
72
+ *
73
+ * To CONTINUE a conversation, keep its id and address it directly. That is
74
+ * the only way to continue one, on purpose:
75
+ *
76
+ * ```ts
77
+ * await ws.agent(AGENT).conversation(row.conversationId).ask('and the rest?');
78
+ * ```
79
+ *
80
+ * The id is YOURS and opaque to us: a row id from your database, not an
81
+ * email and not something we authenticate. We never verify it, so it is
82
+ * exactly as trustworthy as whatever holds your API key.
83
+ *
84
+ * NOT A WORKSPACE MEMBER'S ID. `ws.members()` lists the colleagues who share
85
+ * this account, and their `userId` is a platform id we issued. This field is
86
+ * a different namespace entirely. Both were once spelled `userId`, so
87
+ * passing a member id here was a call that could not fail: there is nothing
88
+ * for an end-user id to be checked against, so it just labelled the
89
+ * conversation with a colleague's internal id and looked fine.
90
+ */
91
+ endUserId: string | null;
92
+ }
93
+ /**
94
+ * An agent you can configure and start conversations from.
95
+ *
96
+ * Holding one makes NO request: `ws.agent(id)` is free and the first call on it
97
+ * is the first round trip.
98
+ *
99
+ * EVERY METHOD THAT CHANGES THE AGENT PUBLISHES A VERSION. There is no separate
100
+ * save. What you do not send is inherited from the current version, which is the
101
+ * only reason changing one field is safe: a resend that rebuilt the whole
102
+ * configuration would drop whatever the caller forgot, and that bug has shipped
103
+ * here four separate times.
104
+ */
105
+ export declare class Agent {
106
+ #private;
107
+ readonly id: string;
108
+ private snapshot?;
109
+ constructor(http: Http, id: string, snapshot?: AgentData);
110
+ private get base();
111
+ /** The last-known configuration, from whatever call produced this object.
112
+ * Undefined on an agent you addressed by id and have not read. */
113
+ get data(): AgentData | undefined;
114
+ get(options?: {
115
+ version?: number;
116
+ }): Promise<AgentData>;
117
+ /** Every version, newest first. */
118
+ versions(): Promise<AgentData[]>;
119
+ /** Publish a new version with these fields changed and everything else kept. */
120
+ update(fields: AgentFields): Promise<Agent>;
121
+ delete(): Promise<void>;
122
+ /**
123
+ * Add a workflow, or replace the one with the same name.
124
+ *
125
+ * Reads the current list and sends it back with this one in place, because
126
+ * the API replaces the whole list: a workflow set is a list, and there is no
127
+ * "merge one item" that does not have to decide what a duplicate name means.
128
+ * Deciding it here, once, is better than every caller deciding it.
129
+ */
130
+ setWorkflow(workflow: Workflow): Promise<Agent>;
131
+ removeWorkflow(name: string): Promise<Agent>;
132
+ /** Replace the trigger list. Sending `[]` removes every trigger, including
133
+ * `chat`, which is what leaves an agent that only runs on a schedule. */
134
+ setTriggers(triggers: Trigger[]): Promise<Agent>;
135
+ /**
136
+ * The agent's knowledge: files every conversation can read.
137
+ *
138
+ * BY REFERENCE, not by copy. A conversation reads the knowledge as it is now,
139
+ * so replacing a file changes what open conversations see on their next turn.
140
+ * That is the opposite of how a version works, and it is deliberate: a price
141
+ * list that could not be corrected without republishing the agent would be
142
+ * useless.
143
+ */
144
+ readonly knowledge: {
145
+ put: (path: string, bytes: Uint8Array | ArrayBuffer, contentType?: string) => Promise<FileEntry>;
146
+ list: (path?: string) => Promise<FileEntry[]>;
147
+ remove: (path: string) => Promise<void>;
148
+ };
149
+ /**
150
+ * The WORKSPACE knowledge bases this agent reads.
151
+ *
152
+ * Different from `knowledge` above, and the distinction is the whole point.
153
+ * `agent.knowledge` is files this agent owns; nobody else has them. A
154
+ * knowledge base is shared: one price list, attached to nine agents, corrected
155
+ * in one place.
156
+ *
157
+ * Each attached base is mounted read-only at `/knowledge/<key>/` when a turn
158
+ * starts, so the agent reads it with `ls`, `grep` and `cat`.
159
+ *
160
+ * ATTACHING PUBLISHES A VERSION. Which bases an agent reads is part of its
161
+ * configuration, so the change is diffable and reversible like any other
162
+ * configuration change. The CONTENTS of a base are not versioned: correcting
163
+ * a file changes what every attached agent sees on its next turn.
164
+ */
165
+ readonly knowledgeBases: {
166
+ /**
167
+ * Attach one base by key.
168
+ *
169
+ * One call, not read-modify-write, deliberately. Two attaches racing
170
+ * through a read of the current list and a write of the whole list would
171
+ * lose one of them silently, and losing an attachment looks exactly like
172
+ * an agent that cannot find its knowledge. The server appends.
173
+ *
174
+ * Attaching a key that names no base is refused, naming the key. Attaching
175
+ * one that is already attached is a no-op, not an error.
176
+ */
177
+ attach: (key: string) => Promise<Agent>;
178
+ /** Detach one base. The base itself and its files are untouched: this only
179
+ * stops THIS agent from reading it. Detaching one that is not attached is
180
+ * a no-op. */
181
+ detach: (key: string) => Promise<Agent>;
182
+ /** The keys this agent currently reads, in configuration order. */
183
+ list: () => Promise<string[]>;
184
+ };
185
+ /**
186
+ * Give this agent a credential, so every conversation with it can use one.
187
+ *
188
+ * ```ts
189
+ * await agent.attach(gmail); // one account
190
+ * await agent.attach([gmail, calendar]); // several
191
+ * await agent.attach({ secrets: { KEY: value } }); // a secret VALUE
192
+ * ```
193
+ *
194
+ * ONE VERB, NOT TWO. This and `bind` used to both exist and do the same job
195
+ * through different shapes, which is how a surface becomes hard to learn:
196
+ * two names for one idea means every reader has to work out whether the
197
+ * difference is meaningful. It is not, so `bind` is gone.
198
+ *
199
+ * "Attach" rather than "bind" because it is the plainer word, it is the one
200
+ * that pairs with `detach`, and `knowledgeBases.attach/detach` already uses
201
+ * it for the same shape of operation.
202
+ *
203
+ * ACCOUNTS TAKE THE OBJECT, SECRETS TAKE A VALUE, and that asymmetry is in
204
+ * the product rather than in the naming. An account has an id you can hand
205
+ * over; a secret does not, because binding one means supplying the value
206
+ * itself. Hiding that behind one shape would not make it go away.
207
+ *
208
+ * The counterpart to `authorize()`, which creates an account AND attaches it
209
+ * in one step. Use this when the account already exists: one team mailbox
210
+ * connected once and attached to nine agents, rather than nine sign-ins.
211
+ */
212
+ attach(what: Attachable | Attachable[] | Bindings): Promise<Agent>;
213
+ /** What each declared provider's actual state is: DECLARED IS NOT CONNECTED.
214
+ *
215
+ * An agent can require Gmail with nobody signed in, and that is the state a
216
+ * setup checklist has to show. Without this the only way to find the gap is
217
+ * to start a conversation and watch the agent fail. */
218
+ connections(): Promise<ConnectionStatus[]>;
219
+ /**
220
+ * Attach a third-party account to this agent, at `fixed` scope: one account,
221
+ * used by every conversation.
222
+ *
223
+ * Returns a url to send a person to. They sign in there, in their own
224
+ * browser, and the account attaches when they finish. YOU NEVER SEE A TOKEN,
225
+ * which is the entire point: nobody should be pasting a Gmail credential into
226
+ * a config field.
227
+ *
228
+ * This does NOT satisfy a declaration scoped 'conversation'. Use
229
+ * `conversation.authorize()` for those, which attaches the account to that
230
+ * one conversation and cannot overwrite this shared one.
231
+ */
232
+ authorize(provider: string): Promise<{
233
+ url: string;
234
+ connectionId: string;
235
+ }>;
236
+ /** Detach the account connected for this provider.
237
+ *
238
+ * Reads the raw ROWS rather than the merged status, because revoking needs
239
+ * the connection's id and the merged view is keyed by provider: it answers
240
+ * "is Gmail connected", not "which row is it". */
241
+ disconnect(provider: string): Promise<void>;
242
+ /** Start a conversation. It pins the agent's version now and keeps it. */
243
+ start(options: StartOptions): Promise<Conversation>;
244
+ /**
245
+ * Address one of this agent's conversations by id. No request.
246
+ *
247
+ * Here rather than on the workspace because a conversation belongs to an
248
+ * agent. You already hold the agent: it is a constant in your own code, not
249
+ * a per-row value, so naming it costs nothing and the ownership is visible.
250
+ *
251
+ * ```ts
252
+ * await ws.agent(AGENT).conversation(row.conversationId).ask('carry on');
253
+ * ```
254
+ */
255
+ conversation(conversationId: string): Conversation;
256
+ /**
257
+ * This agent's most recent conversations, newest first.
258
+ *
259
+ * NOT ALL OF THEM, and the previous sentence here said "every", which was
260
+ * wrong in the way that costs someone an afternoon. The server returns the
261
+ * newest 50 unless you ask for more, and refuses to return more than 200 on
262
+ * any single call (apps/api/src/state.ts listAgentSessions, and the Durable
263
+ * Object twin beside it, clamp identically).
264
+ *
265
+ * There is no cursor on this route yet, so an agent with more than 200
266
+ * conversations CANNOT be fully enumerated through this method. If you are
267
+ * counting or exporting, do not build on it; that needs a paginated route we
268
+ * have not added. `ws.endUser(id).conversations()` DOES have a cursor, if
269
+ * the question is one person's history rather than one agent's.
270
+ *
271
+ * `endUser` narrows to one of YOUR end users, which is what you want for a
272
+ * "your conversations" list. Leaving it out returns everyone's, so leave it
273
+ * out only for an operator view.
274
+ *
275
+ * ```ts
276
+ * const mine = await agent.conversations({ endUser: row.userId });
277
+ * ```
278
+ */
279
+ conversations(options?: {
280
+ limit?: number;
281
+ endUser?: string;
282
+ }): Promise<Conversation[]>;
283
+ }
284
+ /** A connection row as `GET /v1/connections` returns it (toConnectionMeta in
285
+ * apps/api/src/connections/store.ts). */
package/agent.js ADDED
@@ -0,0 +1,394 @@
1
+ // An agent: a configuration you start conversations from.
2
+ var _a;
3
+ import { isBindings, providersOf, secretIdsOf, splitAttachments } from "./attach.js";
4
+ import { toConnectionStatus } from "./connection-status.js";
5
+ import { Conversation } from "./conversation.js";
6
+ /**
7
+ * An agent you can configure and start conversations from.
8
+ *
9
+ * Holding one makes NO request: `ws.agent(id)` is free and the first call on it
10
+ * is the first round trip.
11
+ *
12
+ * EVERY METHOD THAT CHANGES THE AGENT PUBLISHES A VERSION. There is no separate
13
+ * save. What you do not send is inherited from the current version, which is the
14
+ * only reason changing one field is safe: a resend that rebuilt the whole
15
+ * configuration would drop whatever the caller forgot, and that bug has shipped
16
+ * here four separate times.
17
+ */
18
+ export class Agent {
19
+ id;
20
+ #http;
21
+ snapshot;
22
+ constructor(http, id, snapshot) {
23
+ this.#http = http;
24
+ this.id = id;
25
+ if (snapshot)
26
+ this.snapshot = snapshot;
27
+ }
28
+ get base() {
29
+ return `/v1/agents/${encodeURIComponent(this.id)}`;
30
+ }
31
+ /** The last-known configuration, from whatever call produced this object.
32
+ * Undefined on an agent you addressed by id and have not read. */
33
+ get data() {
34
+ return this.snapshot;
35
+ }
36
+ async get(options = {}) {
37
+ const data = await this.#http.get(this.base, options.version !== undefined ? { version: options.version } : undefined);
38
+ // Only cache the CURRENT configuration. Caching a historical version would
39
+ // make `agent.data` mean different things depending on the last call.
40
+ if (options.version === undefined)
41
+ this.snapshot = data;
42
+ return data;
43
+ }
44
+ /** Every version, newest first. */
45
+ async versions() {
46
+ const res = await this.#http.get(`${this.base}/versions`);
47
+ return res.versions ?? [];
48
+ }
49
+ /** Publish a new version with these fields changed and everything else kept. */
50
+ async update(fields) {
51
+ const data = await this.#http.post(`${this.base}/versions`, fields);
52
+ this.snapshot = data;
53
+ return new _a(this.#http, this.id, data);
54
+ }
55
+ async delete() {
56
+ await this.#http.delete(this.base);
57
+ }
58
+ // --------------------------------------------------------------- workflows
59
+ /**
60
+ * Add a workflow, or replace the one with the same name.
61
+ *
62
+ * Reads the current list and sends it back with this one in place, because
63
+ * the API replaces the whole list: a workflow set is a list, and there is no
64
+ * "merge one item" that does not have to decide what a duplicate name means.
65
+ * Deciding it here, once, is better than every caller deciding it.
66
+ */
67
+ async setWorkflow(workflow) {
68
+ const current = this.snapshot?.workflows ?? (await this.get()).workflows;
69
+ const next = current.filter((w) => w.name !== workflow.name).concat(workflow);
70
+ return this.update({ workflows: next });
71
+ }
72
+ async removeWorkflow(name) {
73
+ const current = this.snapshot?.workflows ?? (await this.get()).workflows;
74
+ return this.update({ workflows: current.filter((w) => w.name !== name) });
75
+ }
76
+ /** Replace the trigger list. Sending `[]` removes every trigger, including
77
+ * `chat`, which is what leaves an agent that only runs on a schedule. */
78
+ async setTriggers(triggers) {
79
+ return this.update({ triggers });
80
+ }
81
+ // --------------------------------------------------------------- knowledge
82
+ /**
83
+ * The agent's knowledge: files every conversation can read.
84
+ *
85
+ * BY REFERENCE, not by copy. A conversation reads the knowledge as it is now,
86
+ * so replacing a file changes what open conversations see on their next turn.
87
+ * That is the opposite of how a version works, and it is deliberate: a price
88
+ * list that could not be corrected without republishing the agent would be
89
+ * useless.
90
+ */
91
+ knowledge = {
92
+ put: async (path, bytes, contentType) => this.#http.request(`${this.base}/knowledge/files`, {
93
+ method: 'PUT',
94
+ bytes,
95
+ query: { path },
96
+ ...(contentType && { contentType }),
97
+ }),
98
+ list: async (path = '/') => {
99
+ // The route returns EVERY knowledge file for the agent; it takes no
100
+ // directory parameter. Filtering here rather than sending one the server
101
+ // ignores, because a call that looks filtered and is not is worse than
102
+ // no filter at all. Knowledge sets are small, so one read is right.
103
+ const res = await this.#http.get(`${this.base}/knowledge/files`);
104
+ const prefix = path === '/' ? '/' : `${path.replace(/\/+$/, '')}/`;
105
+ return (res.files ?? []).filter((f) => f.path?.startsWith(prefix));
106
+ },
107
+ remove: async (path) => {
108
+ await this.#http.delete(`${this.base}/knowledge/files`, { path });
109
+ },
110
+ };
111
+ // -------------------------------------------------------- knowledge bases
112
+ /**
113
+ * The WORKSPACE knowledge bases this agent reads.
114
+ *
115
+ * Different from `knowledge` above, and the distinction is the whole point.
116
+ * `agent.knowledge` is files this agent owns; nobody else has them. A
117
+ * knowledge base is shared: one price list, attached to nine agents, corrected
118
+ * in one place.
119
+ *
120
+ * Each attached base is mounted read-only at `/knowledge/<key>/` when a turn
121
+ * starts, so the agent reads it with `ls`, `grep` and `cat`.
122
+ *
123
+ * ATTACHING PUBLISHES A VERSION. Which bases an agent reads is part of its
124
+ * configuration, so the change is diffable and reversible like any other
125
+ * configuration change. The CONTENTS of a base are not versioned: correcting
126
+ * a file changes what every attached agent sees on its next turn.
127
+ */
128
+ knowledgeBases = {
129
+ /**
130
+ * Attach one base by key.
131
+ *
132
+ * One call, not read-modify-write, deliberately. Two attaches racing
133
+ * through a read of the current list and a write of the whole list would
134
+ * lose one of them silently, and losing an attachment looks exactly like
135
+ * an agent that cannot find its knowledge. The server appends.
136
+ *
137
+ * Attaching a key that names no base is refused, naming the key. Attaching
138
+ * one that is already attached is a no-op, not an error.
139
+ */
140
+ attach: async (key) => {
141
+ const res = await this.#http.post(`${this.base}/knowledge-bases`, { key });
142
+ this.snapshot = res;
143
+ return this;
144
+ },
145
+ /** Detach one base. The base itself and its files are untouched: this only
146
+ * stops THIS agent from reading it. Detaching one that is not attached is
147
+ * a no-op. */
148
+ detach: async (key) => {
149
+ const res = await this.#http.delete(`${this.base}/knowledge-bases/${encodeURIComponent(key)}`);
150
+ if (res)
151
+ this.snapshot = res;
152
+ return this;
153
+ },
154
+ /** The keys this agent currently reads, in configuration order. */
155
+ list: async () => {
156
+ const current = this.snapshot ?? (await this.get());
157
+ return current.knowledgeBases ?? [];
158
+ },
159
+ };
160
+ // ------------------------------------------------- credentials, agent tier
161
+ /**
162
+ * Supply the credentials declared at 'fixed' scope: set once, used by every
163
+ * conversation. This does NOT satisfy a declaration scoped 'conversation'.
164
+ *
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".
169
+ *
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.
173
+ *
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.
178
+ */
179
+ async #applyBindings(bindings) {
180
+ for (const [name, value] of Object.entries(bindings.secrets ?? {})) {
181
+ const mine = (await this.#http.get('/v1/secrets', { agentId: this.id })).secrets ?? [];
182
+ const existing = mine.find((s) => s.name === name && s.agentId);
183
+ if (existing)
184
+ await this.#http.patch(`/v1/secrets/${encodeURIComponent(existing.id)}`, { value });
185
+ else
186
+ await this.#http.post('/v1/secrets', { name, value, agentId: this.id });
187
+ }
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
+ const data = await this.get();
195
+ this.snapshot = data;
196
+ return new _a(this.#http, this.id, data);
197
+ }
198
+ /**
199
+ * Give this agent a credential, so every conversation with it can use one.
200
+ *
201
+ * ```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
205
+ * ```
206
+ *
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.
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`);
286
+ }
287
+ // ----------------------------------------------------------- conversations
288
+ /** Start a conversation. It pins the agent's version now and keeps it. */
289
+ 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
298
+ ? {
299
+ ...(options.secrets && { secrets: options.secrets }),
300
+ ...(secretIds && { secretIds }),
301
+ ...(connections && { connections }),
302
+ }
303
+ : undefined;
304
+ const data = await this.#http.post('/v1/conversations', {
305
+ agentId: this.id,
306
+ ...(options.version !== undefined && { version: options.version }),
307
+ ...(options.parameters && { parameters: options.parameters }),
308
+ // `sessionBindings`, NOT `secrets` and `connections` at the top level.
309
+ //
310
+ // This sent them at the top level and the launch route reads
311
+ // `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.
315
+ //
316
+ // Third place this exact envelope mistake was found in one afternoon,
317
+ // after `agent.bind` and `conversation.bind`. The shape of the bug is
318
+ // always the same: the SDK agrees with itself, the route agrees with
319
+ // itself, and nothing compares the two.
320
+ ...(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 } }),
326
+ // ALWAYS a new conversation, even when a user is named.
327
+ //
328
+ // The route resumes that user's most recent conversation when it is
329
+ // given an identity and not told otherwise. That default exists for
330
+ // our own web app, where a page reload must not leave a trail of empty
331
+ // conversations, and it is wrong for a method called `start`: a verb
332
+ // that sometimes does not do the thing is worse than two verbs. It is
333
+ // also ambiguous the moment a user has more than one conversation,
334
+ // since it silently picks the newest.
335
+ //
336
+ // So the SDK does not rely on that default. `start` starts. To
337
+ // continue a conversation, hold its id: `agent.conversation(id)`.
338
+ fresh: true,
339
+ });
340
+ return new Conversation(this.#http, data.id, data);
341
+ }
342
+ /**
343
+ * Address one of this agent's conversations by id. No request.
344
+ *
345
+ * Here rather than on the workspace because a conversation belongs to an
346
+ * agent. You already hold the agent: it is a constant in your own code, not
347
+ * a per-row value, so naming it costs nothing and the ownership is visible.
348
+ *
349
+ * ```ts
350
+ * await ws.agent(AGENT).conversation(row.conversationId).ask('carry on');
351
+ * ```
352
+ */
353
+ conversation(conversationId) {
354
+ return new Conversation(this.#http, conversationId);
355
+ }
356
+ /**
357
+ * This agent's most recent conversations, newest first.
358
+ *
359
+ * NOT ALL OF THEM, and the previous sentence here said "every", which was
360
+ * wrong in the way that costs someone an afternoon. The server returns the
361
+ * newest 50 unless you ask for more, and refuses to return more than 200 on
362
+ * any single call (apps/api/src/state.ts listAgentSessions, and the Durable
363
+ * Object twin beside it, clamp identically).
364
+ *
365
+ * There is no cursor on this route yet, so an agent with more than 200
366
+ * conversations CANNOT be fully enumerated through this method. If you are
367
+ * counting or exporting, do not build on it; that needs a paginated route we
368
+ * have not added. `ws.endUser(id).conversations()` DOES have a cursor, if
369
+ * the question is one person's history rather than one agent's.
370
+ *
371
+ * `endUser` narrows to one of YOUR end users, which is what you want for a
372
+ * "your conversations" list. Leaving it out returns everyone's, so leave it
373
+ * out only for an operator view.
374
+ *
375
+ * ```ts
376
+ * const mine = await agent.conversations({ endUser: row.userId });
377
+ * ```
378
+ */
379
+ async conversations(options = {}) {
380
+ const res = await this.#http.get('/v1/conversations', {
381
+ agentId: this.id,
382
+ ...(options.limit !== undefined && { limit: options.limit }),
383
+ // One end user's history. Sending this used to be silently ignored on
384
+ // this route, which answered with everyone's conversations, so an
385
+ // application rendering "your chats" showed somebody else's.
386
+ ...(options.endUser !== undefined && { end_user_id: options.endUser }),
387
+ });
388
+ return (res.conversations ?? []).map((c) => new Conversation(this.#http, c.id, c));
389
+ }
390
+ }
391
+ _a = Agent;
392
+ /** A connection row as `GET /v1/connections` returns it (toConnectionMeta in
393
+ * apps/api/src/connections/store.ts). */
394
+ //# sourceMappingURL=agent.js.map