@cerebro-labs/agentnava-sdk 0.26.0 → 0.46.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +212 -23
- package/account.d.ts +154 -0
- package/account.js +219 -0
- package/account.js.map +1 -0
- package/agent.d.ts +229 -87
- package/agent.js +310 -125
- package/agent.js.map +1 -1
- package/apps.generated.d.ts +154 -0
- package/apps.generated.js +152 -0
- package/apps.generated.js.map +1 -0
- package/attach.d.ts +25 -26
- package/attach.js +53 -66
- package/attach.js.map +1 -1
- package/connection.d.ts +79 -82
- package/connection.js +67 -134
- package/connection.js.map +1 -1
- package/conversation.d.ts +55 -35
- package/conversation.js +70 -50
- package/conversation.js.map +1 -1
- package/database.d.ts +53 -0
- package/database.js +69 -0
- package/database.js.map +1 -0
- package/end-user.d.ts +58 -47
- package/end-user.js +62 -72
- package/end-user.js.map +1 -1
- package/errors.d.ts +2 -1
- package/errors.js +2 -1
- package/errors.js.map +1 -1
- package/http.d.ts +5 -0
- package/http.js +109 -8
- package/http.js.map +1 -1
- package/import-tools.d.ts +54 -33
- package/import-tools.js +526 -124
- package/import-tools.js.map +1 -1
- package/index.d.ts +5 -2
- package/index.js +9 -2
- package/index.js.map +1 -1
- package/load-agent-dir.js +28 -5
- package/load-agent-dir.js.map +1 -1
- package/package.json +5 -1
- package/secret.d.ts +1 -1
- package/secret.js +1 -1
- package/secret.js.map +1 -1
- package/templates-inline.js +1 -1
- package/templates-inline.js.map +1 -1
- package/turn.d.ts +1 -1
- package/turn.js +77 -29
- package/turn.js.map +1 -1
- package/types.d.ts +462 -83
- package/types.js.map +1 -1
- package/version.d.ts +1 -1
- package/version.js +1 -1
- package/version.js.map +1 -1
- package/workflows.d.ts +10 -0
- package/workflows.js +16 -0
- package/workflows.js.map +1 -1
- package/workspace.d.ts +80 -62
- package/workspace.js +92 -75
- package/workspace.js.map +1 -1
- package/connection-status.d.ts +0 -28
- package/connection-status.js +0 -61
- package/connection-status.js.map +0 -1
package/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"]}
|