@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.
- package/README.md +204 -23
- package/account.d.ts +154 -0
- package/account.js +219 -0
- package/account.js.map +1 -0
- package/agent.d.ts +217 -88
- package/agent.js +295 -125
- package/agent.js.map +1 -1
- package/apps.generated.d.ts +154 -0
- package/apps.generated.js +152 -0
- package/apps.generated.js.map +1 -0
- package/attach.d.ts +25 -26
- package/attach.js +53 -66
- package/attach.js.map +1 -1
- package/connection.d.ts +79 -82
- package/connection.js +67 -134
- package/connection.js.map +1 -1
- package/conversation.d.ts +55 -35
- package/conversation.js +70 -50
- package/conversation.js.map +1 -1
- package/database.d.ts +53 -0
- package/database.js +69 -0
- package/database.js.map +1 -0
- package/end-user.d.ts +58 -47
- package/end-user.js +62 -72
- package/end-user.js.map +1 -1
- package/errors.d.ts +2 -1
- package/errors.js +2 -1
- package/errors.js.map +1 -1
- package/http.d.ts +5 -0
- package/http.js +109 -8
- package/http.js.map +1 -1
- package/import-tools.d.ts +48 -33
- package/import-tools.js +281 -127
- package/import-tools.js.map +1 -1
- package/index.d.ts +5 -2
- package/index.js +9 -2
- package/index.js.map +1 -1
- package/load-agent-dir.js +28 -5
- package/load-agent-dir.js.map +1 -1
- package/package.json +5 -1
- package/secret.d.ts +1 -1
- package/secret.js +1 -1
- package/secret.js.map +1 -1
- package/templates-inline.js +1 -1
- package/templates-inline.js.map +1 -1
- package/turn.d.ts +1 -1
- package/turn.js +77 -29
- package/turn.js.map +1 -1
- package/types.d.ts +409 -83
- package/types.js.map +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
- package/workflows.d.ts +10 -0
- package/workflows.js +16 -0
- package/workflows.js.map +1 -1
- package/workspace.d.ts +80 -62
- package/workspace.js +92 -75
- package/workspace.js.map +1 -1
- package/connection-status.d.ts +0 -28
- package/connection-status.js +0 -61
- package/connection-status.js.map +0 -1
package/account.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// An account: one saved sign-in to one app.
|
|
2
|
+
import { AgentNavaError } from "./errors.js";
|
|
3
|
+
/**
|
|
4
|
+
* One saved sign-in to one app, that you hold and act on.
|
|
5
|
+
*
|
|
6
|
+
* AN ACCOUNT IS NOT A CONNECTION, and the two words used to be one, which is
|
|
7
|
+
* how an agent came to own a Gmail login that nothing else could see. An
|
|
8
|
+
* ACCOUNT is a sign-in: Gmail, signed in as support@example.com. A CONNECTION is
|
|
9
|
+
* an entry in an agent's setup that names one: "this agent uses Gmail, through
|
|
10
|
+
* that account". Many agents can name one account, and reconnecting it once
|
|
11
|
+
* fixes all of them.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const { account, url } = await ws.accounts.connect({ app: App.GMAIL, nickname: 'Support inbox' });
|
|
15
|
+
* showToUser(url); // they sign in on Google's own page
|
|
16
|
+
* await account.waitUntilConnected();
|
|
17
|
+
* await agent.connections.add(Connection.creatorAccount(App.GMAIL, account));
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* Nobody handles a token, you included: the person signs in on the app's own
|
|
21
|
+
* page, and this object says who they signed in as, never how.
|
|
22
|
+
*
|
|
23
|
+
* ONE KIND OF OBJECT FOR BOTH OWNERS. The agent creator's account and one of
|
|
24
|
+
* your end users' are the same class, reached by the same routes: every method
|
|
25
|
+
* here works on any account in the workspace, and `data.owner` says whose it
|
|
26
|
+
* is. So `ws.account(id).waitUntilConnected()` is how you wait for an end user
|
|
27
|
+
* to finish signing in, too.
|
|
28
|
+
*
|
|
29
|
+
* A CLASS, like `Database` and `Secret`: you hold it and act on it, rather than
|
|
30
|
+
* threading an id through calls. Holding one makes no request. `ws.account(id)`
|
|
31
|
+
* is free and the first call on it is the first round trip.
|
|
32
|
+
*/
|
|
33
|
+
export class Account {
|
|
34
|
+
id;
|
|
35
|
+
#http;
|
|
36
|
+
snapshot;
|
|
37
|
+
constructor(http, id, snapshot) {
|
|
38
|
+
this.#http = http;
|
|
39
|
+
this.id = id;
|
|
40
|
+
if (snapshot)
|
|
41
|
+
this.snapshot = snapshot;
|
|
42
|
+
}
|
|
43
|
+
get base() {
|
|
44
|
+
return `/v1/accounts/${encodeURIComponent(this.id)}`;
|
|
45
|
+
}
|
|
46
|
+
/** The app's catalog id: `'gmail'`, `'hubspot'`. Named for what the person
|
|
47
|
+
* signing in recognises, never for whoever we buy the sign-in from. */
|
|
48
|
+
get app() {
|
|
49
|
+
return this.snapshot?.app;
|
|
50
|
+
}
|
|
51
|
+
/** Who it is signed in as: the email or username the app reports. Null until
|
|
52
|
+
* somebody has finished signing in. */
|
|
53
|
+
get label() {
|
|
54
|
+
return this.snapshot?.label;
|
|
55
|
+
}
|
|
56
|
+
/** Your own name for it, such as `'Support inbox'`. */
|
|
57
|
+
get nickname() {
|
|
58
|
+
return this.snapshot?.nickname;
|
|
59
|
+
}
|
|
60
|
+
/** Where the sign-in stands, from the last read: `'pending'`, `'connected'`,
|
|
61
|
+
* `'needs_sign_in'` or `'error'`.
|
|
62
|
+
*
|
|
63
|
+
* Reads the snapshot rather than the network. Undefined means this handle
|
|
64
|
+
* has not read yet, NOT that the account is disconnected: call `get()`. */
|
|
65
|
+
get status() {
|
|
66
|
+
return this.snapshot?.status;
|
|
67
|
+
}
|
|
68
|
+
/** The agents whose setup names this account, from the last read. The
|
|
69
|
+
* question to ask before disconnecting it: who loses it? */
|
|
70
|
+
get usedBy() {
|
|
71
|
+
return this.snapshot?.usedBy;
|
|
72
|
+
}
|
|
73
|
+
/** The last-known record, from whatever call produced this object. */
|
|
74
|
+
get data() {
|
|
75
|
+
return this.snapshot;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* An account goes over the wire as its id, the way a `Date` goes as its ISO
|
|
79
|
+
* string: `JSON.stringify({ account })` sends `"acc_..."`.
|
|
80
|
+
*
|
|
81
|
+
* That is what lets a connection written as a plain object,
|
|
82
|
+
* `{ app, account }`, take the Account you are holding in
|
|
83
|
+
* `ws.agents.create()` and `agent.update()`. It also matches the rule for
|
|
84
|
+
* every handle in this SDK: store the id, not the object.
|
|
85
|
+
*/
|
|
86
|
+
toJSON() {
|
|
87
|
+
return this.id;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Read it back.
|
|
91
|
+
*
|
|
92
|
+
* A MISSING ACCOUNT THROWS (`account_not_found`, 404), and never comes back
|
|
93
|
+
* as an empty record. An account that does not exist and one that is not
|
|
94
|
+
* signed in yet are different answers. The connection this replaced once
|
|
95
|
+
* answered a miss with a made-up blank, so storing an id and reading it back
|
|
96
|
+
* gave you something that did not know what it was.
|
|
97
|
+
*/
|
|
98
|
+
async get() {
|
|
99
|
+
const res = await this.#http.get(this.base);
|
|
100
|
+
this.snapshot = accountIn(res, this.base);
|
|
101
|
+
return this.snapshot;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Wait until the person has finished signing in.
|
|
105
|
+
*
|
|
106
|
+
* The companion to `ws.accounts.connect()`, `ws.endUser(id).accounts
|
|
107
|
+
* .connect()` and `reconnect()`. Those hand you a url; this tells you when
|
|
108
|
+
* the person is done, so you do not write a polling loop or guess which
|
|
109
|
+
* field to believe.
|
|
110
|
+
*
|
|
111
|
+
* DONE MEANS CONNECTED AND NOT RECONNECTING. During a reconnect the account
|
|
112
|
+
* keeps working (`status` stays `'connected'`) while `reconnecting` is true,
|
|
113
|
+
* so a check on `status` alone would return before the person had signed in
|
|
114
|
+
* again. This waits for both.
|
|
115
|
+
*
|
|
116
|
+
* THROWS on the deadline (`account_not_connected`), naming the account and
|
|
117
|
+
* where it stands, rather than returning an account that is still pending.
|
|
118
|
+
* Safe to call again: somebody who has not finished may just need longer.
|
|
119
|
+
*
|
|
120
|
+
* Polls rather than subscribing: there is a person in the middle, so a few
|
|
121
|
+
* seconds costs nothing, and it works from any runtime without a socket.
|
|
122
|
+
*/
|
|
123
|
+
async waitUntilConnected(options = {}) {
|
|
124
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
125
|
+
const intervalMs = options.intervalMs ?? 2_000;
|
|
126
|
+
const deadline = Date.now() + timeoutMs;
|
|
127
|
+
// Checked before the first sleep: an account that is already connected
|
|
128
|
+
// should cost one request, not one request plus an interval.
|
|
129
|
+
let state = await this.get();
|
|
130
|
+
while (state.status !== 'connected' || state.reconnecting) {
|
|
131
|
+
if (Date.now() >= deadline) {
|
|
132
|
+
throw new AgentNavaError({
|
|
133
|
+
message: `Account ${this.id} (${state.app}) was not connected within ${timeoutMs}ms; it is still ${state.status}${state.reconnecting ? ', with a reconnect waiting for the person' : ''}`,
|
|
134
|
+
code: 'account_not_connected',
|
|
135
|
+
status: 0,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
await new Promise((r) => setTimeout(r, Math.min(intervalMs, Math.max(0, deadline - Date.now()))));
|
|
139
|
+
state = await this.get();
|
|
140
|
+
}
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Send the person to sign in again, and get the link.
|
|
145
|
+
*
|
|
146
|
+
* What to call when an account `needs_sign_in`. Every agent that names it
|
|
147
|
+
* follows, so reconnecting once fixes all of them. The sign-in that worked is
|
|
148
|
+
* kept until the new one succeeds, so an abandoned link breaks nothing:
|
|
149
|
+
* meanwhile `data.reconnecting` is true, and `waitUntilConnected()` waits
|
|
150
|
+
* for it to clear.
|
|
151
|
+
*
|
|
152
|
+
* If the person signs in as SOMEBODY ELSE, that becomes a NEW account and
|
|
153
|
+
* this one is left as it was, so no agent switches mailbox without anyone
|
|
154
|
+
* deciding it should. List accounts to find the new one, then change the
|
|
155
|
+
* connections that should use it.
|
|
156
|
+
*/
|
|
157
|
+
async reconnect() {
|
|
158
|
+
const res = await this.#http.post(`${this.base}/reconnect`);
|
|
159
|
+
return { url: res.url };
|
|
160
|
+
}
|
|
161
|
+
/** Give it your own name, such as `'Support inbox'`, shown beside who it is
|
|
162
|
+
* signed in as. The app and the sign-in do not change. */
|
|
163
|
+
async rename(nickname) {
|
|
164
|
+
const res = await this.#http.patch(this.base, { nickname });
|
|
165
|
+
this.snapshot = accountIn(res, this.base);
|
|
166
|
+
return this;
|
|
167
|
+
}
|
|
168
|
+
/** Ask the app whether the sign-in still works, and read the answer. A
|
|
169
|
+
* revoked sign-in comes back as `needs_sign_in`, not as an exception: it is
|
|
170
|
+
* the answer this call exists to get. */
|
|
171
|
+
async check() {
|
|
172
|
+
const res = await this.#http.post(`${this.base}/check`);
|
|
173
|
+
this.snapshot = accountIn(res, `${this.base}/check`);
|
|
174
|
+
return this.snapshot;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Sign it out at the app and remove it here.
|
|
178
|
+
*
|
|
179
|
+
* ALWAYS ALLOWED, unlike deleting a database that an agent still names. It
|
|
180
|
+
* returns the agents whose setup names the account, so you can tell whoever
|
|
181
|
+
* owns them: from their next turn those connections report that the account
|
|
182
|
+
* was disconnected. Nothing switches to another account in its place.
|
|
183
|
+
*
|
|
184
|
+
* Removing a connection from an agent is the other thing, and it never
|
|
185
|
+
* touches the account: `agent.connections.remove(name)`.
|
|
186
|
+
*/
|
|
187
|
+
async disconnect() {
|
|
188
|
+
// The same route for either owner. An end user's account is never named
|
|
189
|
+
// by an agent's setup (that says `endUser: true`), so for one of those
|
|
190
|
+
// the list of agents is empty, which is the true answer.
|
|
191
|
+
const res = await this.#http.delete(this.base);
|
|
192
|
+
return { agents: res?.agents ?? [] };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The account a response carries, or a thrown error. Never a blank.
|
|
197
|
+
*
|
|
198
|
+
* A 404 already throws in the transport, with the API's `account_not_found`.
|
|
199
|
+
* This covers the other way to end up with nothing: a 200 whose body has no
|
|
200
|
+
* account in it, which would otherwise become a snapshot of `undefined` that
|
|
201
|
+
* reads, at every call site, exactly like an account nobody has read yet.
|
|
202
|
+
*/
|
|
203
|
+
export function accountIn(res, path) {
|
|
204
|
+
const account = res?.account;
|
|
205
|
+
if (!account || typeof account.id !== 'string') {
|
|
206
|
+
throw new AgentNavaError({
|
|
207
|
+
message: `Expected an account from ${path}, got: ${JSON.stringify(res ?? null).slice(0, 200)}`,
|
|
208
|
+
code: 'bad_response',
|
|
209
|
+
status: 200,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return account;
|
|
213
|
+
}
|
|
214
|
+
/** The id a connection sends for its account: an `Account` by its id, an id
|
|
215
|
+
* as it is. */
|
|
216
|
+
export function accountIdOf(account) {
|
|
217
|
+
return typeof account === 'string' ? account : account?.id;
|
|
218
|
+
}
|
|
219
|
+
//# sourceMappingURL=account.js.map
|
package/account.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account.js","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"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","sourcesContent":["// An account: one saved sign-in to one app.\n\nimport { AgentNavaError } from './errors.ts';\nimport type { Http } from './http.ts';\nimport type { AccountData, AccountStatus, UsedBy } from './types.ts';\n\n/**\n * One saved sign-in to one app, that you hold and act on.\n *\n * AN ACCOUNT IS NOT A CONNECTION, and the two words used to be one, which is\n * how an agent came to own a Gmail login that nothing else could see. An\n * ACCOUNT is a sign-in: Gmail, signed in as support@example.com. A CONNECTION is\n * an entry in an agent's setup that names one: \"this agent uses Gmail, through\n * that account\". Many agents can name one account, and reconnecting it once\n * fixes all of them.\n *\n * ```ts\n * const { account, url } = await ws.accounts.connect({ app: App.GMAIL, nickname: 'Support inbox' });\n * showToUser(url); // they sign in on Google's own page\n * await account.waitUntilConnected();\n * await agent.connections.add(Connection.creatorAccount(App.GMAIL, account));\n * ```\n *\n * Nobody handles a token, you included: the person signs in on the app's own\n * page, and this object says who they signed in as, never how.\n *\n * ONE KIND OF OBJECT FOR BOTH OWNERS. The agent creator's account and one of\n * your end users' are the same class, reached by the same routes: every method\n * here works on any account in the workspace, and `data.owner` says whose it\n * is. So `ws.account(id).waitUntilConnected()` is how you wait for an end user\n * to finish signing in, too.\n *\n * A CLASS, like `Database` and `Secret`: you hold it and act on it, rather than\n * threading an id through calls. Holding one makes no request. `ws.account(id)`\n * is free and the first call on it is the first round trip.\n */\nexport class Account {\n\treadonly id: string;\n\treadonly #http: Http;\n\tprivate snapshot?: AccountData;\n\n\tconstructor(http: Http, id: string, snapshot?: AccountData) {\n\t\tthis.#http = http;\n\t\tthis.id = id;\n\t\tif (snapshot) this.snapshot = snapshot;\n\t}\n\n\tprivate get base(): string {\n\t\treturn `/v1/accounts/${encodeURIComponent(this.id)}`;\n\t}\n\n\t/** The app's catalog id: `'gmail'`, `'hubspot'`. Named for what the person\n\t * signing in recognises, never for whoever we buy the sign-in from. */\n\tget app(): string | undefined {\n\t\treturn this.snapshot?.app;\n\t}\n\n\t/** Who it is signed in as: the email or username the app reports. Null until\n\t * somebody has finished signing in. */\n\tget label(): string | null | undefined {\n\t\treturn this.snapshot?.label;\n\t}\n\n\t/** Your own name for it, such as `'Support inbox'`. */\n\tget nickname(): string | null | undefined {\n\t\treturn this.snapshot?.nickname;\n\t}\n\n\t/** Where the sign-in stands, from the last read: `'pending'`, `'connected'`,\n\t * `'needs_sign_in'` or `'error'`.\n\t *\n\t * Reads the snapshot rather than the network. Undefined means this handle\n\t * has not read yet, NOT that the account is disconnected: call `get()`. */\n\tget status(): AccountStatus | undefined {\n\t\treturn this.snapshot?.status;\n\t}\n\n\t/** The agents whose setup names this account, from the last read. The\n\t * question to ask before disconnecting it: who loses it? */\n\tget usedBy(): UsedBy[] | undefined {\n\t\treturn this.snapshot?.usedBy;\n\t}\n\n\t/** The last-known record, from whatever call produced this object. */\n\tget data(): AccountData | undefined {\n\t\treturn this.snapshot;\n\t}\n\n\t/**\n\t * An account goes over the wire as its id, the way a `Date` goes as its ISO\n\t * string: `JSON.stringify({ account })` sends `\"acc_...\"`.\n\t *\n\t * That is what lets a connection written as a plain object,\n\t * `{ app, account }`, take the Account you are holding in\n\t * `ws.agents.create()` and `agent.update()`. It also matches the rule for\n\t * every handle in this SDK: store the id, not the object.\n\t */\n\ttoJSON(): string {\n\t\treturn this.id;\n\t}\n\n\t/**\n\t * Read it back.\n\t *\n\t * A MISSING ACCOUNT THROWS (`account_not_found`, 404), and never comes back\n\t * as an empty record. An account that does not exist and one that is not\n\t * signed in yet are different answers. The connection this replaced once\n\t * answered a miss with a made-up blank, so storing an id and reading it back\n\t * gave you something that did not know what it was.\n\t */\n\tasync get(): Promise<AccountData> {\n\t\tconst res = await this.#http.get<{ account?: AccountData }>(this.base);\n\t\tthis.snapshot = accountIn(res, this.base);\n\t\treturn this.snapshot;\n\t}\n\n\t/**\n\t * Wait until the person has finished signing in.\n\t *\n\t * The companion to `ws.accounts.connect()`, `ws.endUser(id).accounts\n\t * .connect()` and `reconnect()`. Those hand you a url; this tells you when\n\t * the person is done, so you do not write a polling loop or guess which\n\t * field to believe.\n\t *\n\t * DONE MEANS CONNECTED AND NOT RECONNECTING. During a reconnect the account\n\t * keeps working (`status` stays `'connected'`) while `reconnecting` is true,\n\t * so a check on `status` alone would return before the person had signed in\n\t * again. This waits for both.\n\t *\n\t * THROWS on the deadline (`account_not_connected`), naming the account and\n\t * where it stands, rather than returning an account that is still pending.\n\t * Safe to call again: somebody who has not finished may just need longer.\n\t *\n\t * Polls rather than subscribing: there is a person in the middle, so a few\n\t * seconds costs nothing, and it works from any runtime without a socket.\n\t */\n\tasync waitUntilConnected(options: { timeoutMs?: number; intervalMs?: number } = {}): Promise<Account> {\n\t\tconst timeoutMs = options.timeoutMs ?? 120_000;\n\t\tconst intervalMs = options.intervalMs ?? 2_000;\n\t\tconst deadline = Date.now() + timeoutMs;\n\t\t// Checked before the first sleep: an account that is already connected\n\t\t// should cost one request, not one request plus an interval.\n\t\tlet state = await this.get();\n\t\twhile (state.status !== 'connected' || state.reconnecting) {\n\t\t\tif (Date.now() >= deadline) {\n\t\t\t\tthrow new AgentNavaError({\n\t\t\t\t\tmessage: `Account ${this.id} (${state.app}) was not connected within ${timeoutMs}ms; it is still ${state.status}${\n\t\t\t\t\t\tstate.reconnecting ? ', with a reconnect waiting for the person' : ''\n\t\t\t\t\t}`,\n\t\t\t\t\tcode: 'account_not_connected',\n\t\t\t\t\tstatus: 0,\n\t\t\t\t});\n\t\t\t}\n\t\t\tawait new Promise((r) => setTimeout(r, Math.min(intervalMs, Math.max(0, deadline - Date.now()))));\n\t\t\tstate = await this.get();\n\t\t}\n\t\treturn this;\n\t}\n\n\t/**\n\t * Send the person to sign in again, and get the link.\n\t *\n\t * What to call when an account `needs_sign_in`. Every agent that names it\n\t * follows, so reconnecting once fixes all of them. The sign-in that worked is\n\t * kept until the new one succeeds, so an abandoned link breaks nothing:\n\t * meanwhile `data.reconnecting` is true, and `waitUntilConnected()` waits\n\t * for it to clear.\n\t *\n\t * If the person signs in as SOMEBODY ELSE, that becomes a NEW account and\n\t * this one is left as it was, so no agent switches mailbox without anyone\n\t * deciding it should. List accounts to find the new one, then change the\n\t * connections that should use it.\n\t */\n\tasync reconnect(): Promise<{ url: string }> {\n\t\tconst res = await this.#http.post<{ url: string }>(`${this.base}/reconnect`);\n\t\treturn { url: res.url };\n\t}\n\n\t/** Give it your own name, such as `'Support inbox'`, shown beside who it is\n\t * signed in as. The app and the sign-in do not change. */\n\tasync rename(nickname: string): Promise<Account> {\n\t\tconst res = await this.#http.patch<{ account?: AccountData }>(this.base, { nickname });\n\t\tthis.snapshot = accountIn(res, this.base);\n\t\treturn this;\n\t}\n\n\t/** Ask the app whether the sign-in still works, and read the answer. A\n\t * revoked sign-in comes back as `needs_sign_in`, not as an exception: it is\n\t * the answer this call exists to get. */\n\tasync check(): Promise<AccountData> {\n\t\tconst res = await this.#http.post<{ account?: AccountData }>(`${this.base}/check`);\n\t\tthis.snapshot = accountIn(res, `${this.base}/check`);\n\t\treturn this.snapshot;\n\t}\n\n\t/**\n\t * Sign it out at the app and remove it here.\n\t *\n\t * ALWAYS ALLOWED, unlike deleting a database that an agent still names. It\n\t * returns the agents whose setup names the account, so you can tell whoever\n\t * owns them: from their next turn those connections report that the account\n\t * was disconnected. Nothing switches to another account in its place.\n\t *\n\t * Removing a connection from an agent is the other thing, and it never\n\t * touches the account: `agent.connections.remove(name)`.\n\t */\n\tasync disconnect(): Promise<{ agents: UsedBy[] }> {\n\t\t// The same route for either owner. An end user's account is never named\n\t\t// by an agent's setup (that says `endUser: true`), so for one of those\n\t\t// the list of agents is empty, which is the true answer.\n\t\tconst res = await this.#http.delete<{ disconnected?: boolean; agents?: UsedBy[] }>(this.base);\n\t\treturn { agents: res?.agents ?? [] };\n\t}\n}\n\n/**\n * The account a response carries, or a thrown error. Never a blank.\n *\n * A 404 already throws in the transport, with the API's `account_not_found`.\n * This covers the other way to end up with nothing: a 200 whose body has no\n * account in it, which would otherwise become a snapshot of `undefined` that\n * reads, at every call site, exactly like an account nobody has read yet.\n */\nexport function accountIn(res: { account?: AccountData } | undefined, path: string): AccountData {\n\tconst account = res?.account;\n\tif (!account || typeof account.id !== 'string') {\n\t\tthrow new AgentNavaError({\n\t\t\tmessage: `Expected an account from ${path}, got: ${JSON.stringify(res ?? null).slice(0, 200)}`,\n\t\t\tcode: 'bad_response',\n\t\t\tstatus: 200,\n\t\t});\n\t}\n\treturn account;\n}\n\n/** The id a connection sends for its account: an `Account` by its id, an id\n * as it is. */\nexport function accountIdOf(account: Account | string): string {\n\treturn typeof account === 'string' ? account : account?.id;\n}\n"]}
|
package/agent.d.ts
CHANGED
|
@@ -1,43 +1,34 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type Account } from './account.ts';
|
|
2
2
|
import { Conversation } from './conversation.ts';
|
|
3
3
|
import type { Http } from './http.ts';
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
*/
|
|
4
|
+
import type { Secret } from './secret.ts';
|
|
5
|
+
import { type Connection } from './connection.ts';
|
|
6
|
+
import type { AgentData, AgentFields, Bindings, ConnectionStatus, FileEntry, Trigger, WebsiteChat, Workflow } from './types.ts';
|
|
14
7
|
export interface StartOptions {
|
|
15
8
|
/**
|
|
16
|
-
*
|
|
9
|
+
* Stored secrets to attach AS THE CONVERSATION STARTS.
|
|
17
10
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
11
|
+
* A `Secret` from `ws.endUser(id).secrets.create(...)` attaches by id, which
|
|
12
|
+
* is the only way a stored per-person credential can reach a turn: you never
|
|
13
|
+
* hold its value.
|
|
21
14
|
*
|
|
22
15
|
* ```ts
|
|
23
|
-
* const
|
|
24
|
-
* const
|
|
16
|
+
* const key = await ws.endUser(row.userId).secrets.create({ name: 'ZENDESK_TOKEN', value });
|
|
17
|
+
* const conversation = await agent.start({ endUserId: row.userId, attach: key });
|
|
25
18
|
* ```
|
|
26
19
|
*
|
|
27
20
|
* Doing it here rather than with a second `conversation.attach()` call
|
|
28
21
|
* closes a real window: between `start` and the attach, the conversation
|
|
29
|
-
* exists and can be asked something, and it would answer without
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* reach a turn: you never hold its value. Mix them freely, they are split by
|
|
38
|
-
* type before the launch.
|
|
22
|
+
* exists and can be asked something, and it would answer without it.
|
|
23
|
+
*
|
|
24
|
+
* ACCOUNTS ARE NOT ATTACHED HERE any more (0.44), and one handed in is
|
|
25
|
+
* refused. The agent's setup names the account each connection uses; for a
|
|
26
|
+
* connection to the end user's account (`Connection.endUserAccount`), the
|
|
27
|
+
* turn uses the account of the person this conversation is for, found by
|
|
28
|
+
* `endUserId`. Ask
|
|
29
|
+
* `conversation.accountsNeeded()` for the ones they have not connected yet.
|
|
39
30
|
*/
|
|
40
|
-
attach?:
|
|
31
|
+
attach?: Secret | Secret[];
|
|
41
32
|
/**
|
|
42
33
|
* Secret VALUES for this conversation only.
|
|
43
34
|
*
|
|
@@ -49,24 +40,32 @@ export interface StartOptions {
|
|
|
49
40
|
/** Pin a specific version. Defaults to the latest at the moment of starting.
|
|
50
41
|
*
|
|
51
42
|
* A conversation pins its version for life: publishing afterwards does not
|
|
52
|
-
* move a conversation that is already open.
|
|
43
|
+
* move a conversation that is already open. The one exception is a schedule
|
|
44
|
+
* or webhook that names the conversation (`conversationId`): each firing
|
|
45
|
+
* moves it to the latest version, so a fix you publish reaches it. */
|
|
53
46
|
version?: number;
|
|
54
47
|
parameters?: Record<string, string | number>;
|
|
55
48
|
/**
|
|
56
49
|
* WHO this conversation is for: one of YOUR users, not one of ours.
|
|
57
50
|
*
|
|
58
|
-
* REQUIRED, and
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
51
|
+
* REQUIRED, and never null. Every conversation belongs to someone. The id
|
|
52
|
+
* is yours to choose: a user's row id, an anonymous visitor's random id, or
|
|
53
|
+
* a name for a job such as `'nightly-report'`. Only a non-empty string is
|
|
54
|
+
* accepted; anything else throws before a request is sent. (Decided
|
|
55
|
+
* 2026-09-26, AGE-161: a conversation with no owner cannot be listed back to
|
|
56
|
+
* the person it was for, and the accounts connected inside it could not be
|
|
57
|
+
* revoked with that person.)
|
|
58
|
+
*
|
|
59
|
+
* It LABELS the conversation, and it decides one thing more: a connection
|
|
60
|
+
* to the end user's account (`Connection.endUserAccount`) uses THIS
|
|
61
|
+
* person's account for that app.
|
|
62
|
+
* It does not decide which conversation you get, because `start` always
|
|
63
|
+
* starts a new one. Later you can ask for everything belonging to that
|
|
64
|
+
* person:
|
|
66
65
|
*
|
|
67
66
|
* ```ts
|
|
68
67
|
* const conversation = await agent.start({ endUserId: row.userId });
|
|
69
|
-
* const
|
|
68
|
+
* const nightly = await agent.start({ endUserId: 'nightly-report' }); // a job has an id too
|
|
70
69
|
* const theirs = await agent.conversations({ endUser: row.userId });
|
|
71
70
|
* ```
|
|
72
71
|
*
|
|
@@ -81,6 +80,13 @@ export interface StartOptions {
|
|
|
81
80
|
* email and not something we authenticate. We never verify it, so it is
|
|
82
81
|
* exactly as trustworthy as whatever holds your API key.
|
|
83
82
|
*
|
|
83
|
+
* The platform's prefixes are refused with a 400 (`end_user_reserved`):
|
|
84
|
+
* `member:`, `visitor:` and `job:`, which name a member in the web app, an
|
|
85
|
+
* embed visitor and work nobody is typing, and the older `webhook:`,
|
|
86
|
+
* `builder:` and `librarian:`. Case and spacing do not matter to the check
|
|
87
|
+
* (`MEMBER:x` is refused too). An id you chose must never be mistaken for
|
|
88
|
+
* one of ours.
|
|
89
|
+
*
|
|
84
90
|
* NOT A WORKSPACE MEMBER'S ID. `ws.members()` lists the colleagues who share
|
|
85
91
|
* this account, and their `userId` is a platform id we issued. This field is
|
|
86
92
|
* a different namespace entirely. Both were once spelled `userId`, so
|
|
@@ -88,7 +94,7 @@ export interface StartOptions {
|
|
|
88
94
|
* for an end-user id to be checked against, so it just labelled the
|
|
89
95
|
* conversation with a colleague's internal id and looked fine.
|
|
90
96
|
*/
|
|
91
|
-
endUserId: string
|
|
97
|
+
endUserId: string;
|
|
92
98
|
}
|
|
93
99
|
/**
|
|
94
100
|
* An agent you can configure and start conversations from.
|
|
@@ -116,7 +122,9 @@ export declare class Agent {
|
|
|
116
122
|
}): Promise<AgentData>;
|
|
117
123
|
/** Every version, newest first. */
|
|
118
124
|
versions(): Promise<AgentData[]>;
|
|
119
|
-
/** Publish a new version with these fields changed and everything else kept.
|
|
125
|
+
/** Publish a new version with these fields changed and everything else kept.
|
|
126
|
+
* `connections`, when sent, replaces the whole list; an `Account` in it goes
|
|
127
|
+
* over the wire as its id (`Account.toJSON`). */
|
|
120
128
|
update(fields: AgentFields): Promise<Agent>;
|
|
121
129
|
delete(): Promise<void>;
|
|
122
130
|
/**
|
|
@@ -132,6 +140,60 @@ export declare class Agent {
|
|
|
132
140
|
/** Replace the trigger list. Sending `[]` removes every trigger, including
|
|
133
141
|
* `chat`, which is what leaves an agent that only runs on a schedule. */
|
|
134
142
|
setTriggers(triggers: Trigger[]): Promise<Agent>;
|
|
143
|
+
/**
|
|
144
|
+
* Replace the webhook's URL and return the new one.
|
|
145
|
+
*
|
|
146
|
+
* The old URL stops working the moment this returns, so update the system
|
|
147
|
+
* that calls it. Use it when a URL leaked. The webhook's `message` and who it
|
|
148
|
+
* runs for are kept. Publishes a version. Throws `no_webhook` (409) when the
|
|
149
|
+
* agent has no webhook trigger.
|
|
150
|
+
*/
|
|
151
|
+
rotateWebhookUrl(): Promise<string>;
|
|
152
|
+
/**
|
|
153
|
+
* The agent's website chat: a public chat page, and the embed script that
|
|
154
|
+
* puts it on your website. ON from the moment the agent is created, for
|
|
155
|
+
* every agent, however it was made (here, through the API, or in the portal),
|
|
156
|
+
* until you turn it off. The one exception is an agent that needs each
|
|
157
|
+
* user's own account or secret, which a visitor cannot bring: it has none.
|
|
158
|
+
*
|
|
159
|
+
* You never need it to use the agent from code: the agent answers your API
|
|
160
|
+
* key, and the portal's Test, as soon as it exists.
|
|
161
|
+
*
|
|
162
|
+
* Anyone with the page link can talk to the agent, and each of their turns is
|
|
163
|
+
* charged to your workspace, so turn it off for an agent only your own code
|
|
164
|
+
* should reach. `turnOn()` on a new agent changes nothing and hands back what
|
|
165
|
+
* to put on a website. The portal has no switch for it; its Website chat panel
|
|
166
|
+
* holds the share link and the allowed websites (`allowedWebsites` below).
|
|
167
|
+
* Publishing a newer version does not move the page: call `turnOn()` while it
|
|
168
|
+
* is on to serve the newer one. Turned off and on again, it serves the
|
|
169
|
+
* version it served before.
|
|
170
|
+
*/
|
|
171
|
+
readonly websiteChat: {
|
|
172
|
+
/** Turn it on and get what to put on a website. The first time, and when
|
|
173
|
+
* it is already on, the page serves the current version; after
|
|
174
|
+
* `turnOff()`, the version it served before. Refused (400) for an agent
|
|
175
|
+
* that needs each user's own account or secret, which a visitor cannot
|
|
176
|
+
* bring. */
|
|
177
|
+
turnOn: () => Promise<WebsiteChat>;
|
|
178
|
+
/** Turn it off. Visitors are refused from the next message, follow-ups a
|
|
179
|
+
* visitor's conversation booked are cancelled, and nothing more is
|
|
180
|
+
* charged for them. The agent keeps answering your API key, its schedules
|
|
181
|
+
* and its webhooks. The publishable token is kept, so turning it on again
|
|
182
|
+
* revives a snippet already on a website. Turning off a chat that is
|
|
183
|
+
* already off is not an error. */
|
|
184
|
+
turnOff: () => Promise<void>;
|
|
185
|
+
/** The websites the chat may be shown on through the embed script. A
|
|
186
|
+
* domain covers its subdomains; an empty list means no other website.
|
|
187
|
+
* The share link opened directly always works. */
|
|
188
|
+
allowedWebsites: {
|
|
189
|
+
/** The list as it stands. */
|
|
190
|
+
get: () => Promise<string[]>;
|
|
191
|
+
/** Replace the list. Each entry is normalised ("https://acme.com/page"
|
|
192
|
+
* becomes "acme.com"; "localhost" is allowed for testing), and one that
|
|
193
|
+
* is not a website is refused (400). Returns the list as stored. */
|
|
194
|
+
set: (websites: string[]) => Promise<string[]>;
|
|
195
|
+
};
|
|
196
|
+
};
|
|
135
197
|
/**
|
|
136
198
|
* The agent's knowledge: files every conversation can read.
|
|
137
199
|
*
|
|
@@ -143,6 +205,10 @@ export declare class Agent {
|
|
|
143
205
|
*/
|
|
144
206
|
readonly knowledge: {
|
|
145
207
|
put: (path: string, bytes: Uint8Array | ArrayBuffer, contentType?: string) => Promise<FileEntry>;
|
|
208
|
+
/** Read an agent-owned knowledge file as bytes, without starting a turn. */
|
|
209
|
+
get: (path: string) => Promise<Uint8Array>;
|
|
210
|
+
/** Read UTF-8 text. JSON files remain text rather than being parsed. */
|
|
211
|
+
text: (path: string) => Promise<string>;
|
|
146
212
|
list: (path?: string) => Promise<FileEntry[]>;
|
|
147
213
|
remove: (path: string) => Promise<void>;
|
|
148
214
|
};
|
|
@@ -183,63 +249,128 @@ export declare class Agent {
|
|
|
183
249
|
list: () => Promise<string[]>;
|
|
184
250
|
};
|
|
185
251
|
/**
|
|
186
|
-
*
|
|
252
|
+
* The WORKSPACE databases this agent may query, by key.
|
|
253
|
+
*
|
|
254
|
+
* Register a database first with `ws.databases.create()`, which gives it its
|
|
255
|
+
* key; attaching names that key in this agent's configuration. Naming it is
|
|
256
|
+
* the grant: the database answers this agent's `sql_*` calls and no agent
|
|
257
|
+
* that does not name it, and the agent's prompt lists it by key.
|
|
258
|
+
*
|
|
259
|
+
* ATTACHING PUBLISHES A VERSION, like a knowledge base: which databases an
|
|
260
|
+
* agent reaches is part of its configuration, so the change is diffable and
|
|
261
|
+
* reversible. Detaching takes effect on the agent's next query, including in
|
|
262
|
+
* a conversation already under way.
|
|
263
|
+
*/
|
|
264
|
+
readonly databases: {
|
|
265
|
+
/** Attach one database by key. The server appends, so two attaches cannot
|
|
266
|
+
* lose each other. A key that names no database is refused; one already
|
|
267
|
+
* attached is a no-op. */
|
|
268
|
+
attach: (key: string) => Promise<Agent>;
|
|
269
|
+
/** Stop this agent querying one database. The database stays registered
|
|
270
|
+
* for every other agent that names it. */
|
|
271
|
+
detach: (key: string) => Promise<Agent>;
|
|
272
|
+
/** The keys this agent may query, in configuration order. */
|
|
273
|
+
list: () => Promise<string[]>;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* The apps this agent uses: each a NAMED connection that says WHOSE ACCOUNT
|
|
277
|
+
* it uses, built the way the portal's Add a connection screen asks.
|
|
278
|
+
*
|
|
279
|
+
* | built with | whose account | the person signs in |
|
|
280
|
+
* |---|---|---|
|
|
281
|
+
* | `Connection.creatorAccount(app, account)` | the agent creator's, from `ws.accounts.connect()` | once, and every conversation uses it |
|
|
282
|
+
* | `Connection.endUserAccount(app)` | the end user's: each person talking to the agent | once each, remembered for them |
|
|
187
283
|
*
|
|
188
284
|
* ```ts
|
|
189
|
-
* await
|
|
190
|
-
* await agent.
|
|
191
|
-
* await agent.
|
|
285
|
+
* const { account: work } = await ws.accounts.connect({ app: App.GMAIL, nickname: 'Work' });
|
|
286
|
+
* await agent.connections.add(Connection.creatorAccount(App.GMAIL, work, { name: 'work-gmail' }));
|
|
287
|
+
* await agent.connections.add(Connection.endUserAccount(App.GOOGLE_CALENDAR));
|
|
192
288
|
* ```
|
|
193
289
|
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
290
|
+
* A turn uses EXACTLY the account the setup names, and never another one.
|
|
291
|
+
* There is no fallback to whichever account of that app happens to exist:
|
|
292
|
+
* that fallback is what this replaced, and it meant reading an agent's code
|
|
293
|
+
* told you nothing about whose mailbox it read.
|
|
198
294
|
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
295
|
+
* EVERY CHANGE PUBLISHES A VERSION, like `databases`: which accounts an
|
|
296
|
+
* agent reaches is part of its configuration, so the change is diffable and
|
|
297
|
+
* reversible. A conversation uses the connections of the version it runs.
|
|
298
|
+
* The ACCOUNTS are not versioned: signing one in again reaches every agent
|
|
299
|
+
* that names it, from the next turn.
|
|
202
300
|
*
|
|
203
|
-
*
|
|
204
|
-
* the
|
|
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.
|
|
301
|
+
* Removing a connection never touches the account. Disconnecting an account
|
|
302
|
+
* is the other operation, on the account: `account.disconnect()`.
|
|
211
303
|
*/
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
304
|
+
readonly connections: {
|
|
305
|
+
/**
|
|
306
|
+
* Add one connection. One call that publishes, not read-modify-write, so
|
|
307
|
+
* two adds racing cannot lose each other.
|
|
308
|
+
*
|
|
309
|
+
* Takes what `Connection.creatorAccount()` or `Connection.endUserAccount()`
|
|
310
|
+
* builds, or the same plain object. Refused, naming the problem, when the
|
|
311
|
+
* name is taken on this agent (`connection_name_taken`), when the account
|
|
312
|
+
* is of a different app (`account_app_mismatch`), or when no account has
|
|
313
|
+
* that id (`account_not_found`).
|
|
314
|
+
*/
|
|
315
|
+
add: (connection: Connection) => Promise<Agent>;
|
|
316
|
+
/**
|
|
317
|
+
* Point one connection at another account: `{ account }` for the agent
|
|
318
|
+
* creator's (the `Account` you hold, or its id), or `{ endUser: true }`
|
|
319
|
+
* for each end user's own. The name and the app stay; an account of a
|
|
320
|
+
* different app is refused (`account_app_mismatch`).
|
|
321
|
+
*
|
|
322
|
+
* READ-MODIFY-WRITE, unlike `add` and `remove`, because there is no
|
|
323
|
+
* route that changes one connection in place: this reads the current
|
|
324
|
+
* version and publishes it again with the one connection changed. Two
|
|
325
|
+
* changes racing can therefore lose one, so make them one after the
|
|
326
|
+
* other. A name the agent does not have throws
|
|
327
|
+
* (`connection_not_found`) before anything is sent.
|
|
328
|
+
*/
|
|
329
|
+
change: (name: string, change: {
|
|
330
|
+
account: Account | string;
|
|
331
|
+
endUser?: never;
|
|
332
|
+
} | {
|
|
333
|
+
endUser: true;
|
|
334
|
+
account?: never;
|
|
335
|
+
}) => Promise<Agent>;
|
|
336
|
+
/** Remove one connection by name. Publishes a version. NEVER touches the
|
|
337
|
+
* account: it stays connected for every other agent that names it, and
|
|
338
|
+
* for this one if you add it back. */
|
|
339
|
+
remove: (name: string) => Promise<Agent>;
|
|
340
|
+
/**
|
|
341
|
+
* Each connection with the account it uses and whether that account works
|
|
342
|
+
* now: DECLARED IS NOT CONNECTED. An agent can name an account nobody has
|
|
343
|
+
* finished signing in to, or one that was disconnected since, and that is
|
|
344
|
+
* the state a setup checklist has to show.
|
|
345
|
+
*
|
|
346
|
+
* Includes what an unpublished draft adds (`inDraft`) and removes
|
|
347
|
+
* (`removedInDraft`), so it matches the portal.
|
|
348
|
+
*/
|
|
349
|
+
list: () => Promise<ConnectionStatus[]>;
|
|
350
|
+
};
|
|
219
351
|
/**
|
|
220
|
-
*
|
|
221
|
-
* used by every conversation.
|
|
352
|
+
* Give this agent secret values, so every conversation with it can use them.
|
|
222
353
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* a config field.
|
|
354
|
+
* ```ts
|
|
355
|
+
* await agent.attach({ secrets: { APOLLO_API_KEY: value } });
|
|
356
|
+
* ```
|
|
227
357
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
*
|
|
358
|
+
* SECRETS ONLY, since 0.44. Accounts used to be attached here, and a turn
|
|
359
|
+
* then had to guess which of the accounts attached and owned around it to
|
|
360
|
+
* use. The agent's setup now names them: `agent.connections.add()`. One
|
|
361
|
+
* handed in here is refused by name (`not_a_secret`) rather than dropped.
|
|
362
|
+
*
|
|
363
|
+
* A stored `Secret` is not attached to an agent either. It never was: one
|
|
364
|
+
* was accepted here and nothing was done with it. Store one for the agent
|
|
365
|
+
* with `ws.secrets.create({ name, value, agentId })`, or attach one of your
|
|
366
|
+
* end user's to their conversation with `conversation.attach(secret)`.
|
|
231
367
|
*/
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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. */
|
|
368
|
+
attach(bindings: Bindings): Promise<Agent>;
|
|
369
|
+
/** Start a conversation. It pins the agent's version now and keeps it.
|
|
370
|
+
*
|
|
371
|
+
* Nothing about accounts is passed here: the version's connections name
|
|
372
|
+
* them, and a connection to the end user's account uses the account of
|
|
373
|
+
* the person named by `endUserId`. */
|
|
243
374
|
start(options: StartOptions): Promise<Conversation>;
|
|
244
375
|
/**
|
|
245
376
|
* Address one of this agent's conversations by id. No request.
|
|
@@ -281,5 +412,3 @@ export declare class Agent {
|
|
|
281
412
|
endUser?: string;
|
|
282
413
|
}): Promise<Conversation[]>;
|
|
283
414
|
}
|
|
284
|
-
/** A connection row as `GET /v1/connections` returns it (toConnectionMeta in
|
|
285
|
-
* apps/api/src/connections/store.ts). */
|