@cerebro-labs/agentnava-sdk 0.26.0 → 0.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +212 -23
  2. package/account.d.ts +154 -0
  3. package/account.js +219 -0
  4. package/account.js.map +1 -0
  5. package/agent.d.ts +229 -87
  6. package/agent.js +310 -125
  7. package/agent.js.map +1 -1
  8. package/apps.generated.d.ts +154 -0
  9. package/apps.generated.js +152 -0
  10. package/apps.generated.js.map +1 -0
  11. package/attach.d.ts +25 -26
  12. package/attach.js +53 -66
  13. package/attach.js.map +1 -1
  14. package/connection.d.ts +79 -82
  15. package/connection.js +67 -134
  16. package/connection.js.map +1 -1
  17. package/conversation.d.ts +55 -35
  18. package/conversation.js +70 -50
  19. package/conversation.js.map +1 -1
  20. package/database.d.ts +53 -0
  21. package/database.js +69 -0
  22. package/database.js.map +1 -0
  23. package/end-user.d.ts +58 -47
  24. package/end-user.js +62 -72
  25. package/end-user.js.map +1 -1
  26. package/errors.d.ts +2 -1
  27. package/errors.js +2 -1
  28. package/errors.js.map +1 -1
  29. package/http.d.ts +5 -0
  30. package/http.js +109 -8
  31. package/http.js.map +1 -1
  32. package/import-tools.d.ts +54 -33
  33. package/import-tools.js +526 -124
  34. package/import-tools.js.map +1 -1
  35. package/index.d.ts +5 -2
  36. package/index.js +9 -2
  37. package/index.js.map +1 -1
  38. package/load-agent-dir.js +28 -5
  39. package/load-agent-dir.js.map +1 -1
  40. package/package.json +5 -1
  41. package/secret.d.ts +1 -1
  42. package/secret.js +1 -1
  43. package/secret.js.map +1 -1
  44. package/templates-inline.js +1 -1
  45. package/templates-inline.js.map +1 -1
  46. package/turn.d.ts +1 -1
  47. package/turn.js +77 -29
  48. package/turn.js.map +1 -1
  49. package/types.d.ts +462 -83
  50. package/types.js.map +1 -1
  51. package/version.d.ts +1 -1
  52. package/version.js +1 -1
  53. package/version.js.map +1 -1
  54. package/workflows.d.ts +10 -0
  55. package/workflows.js +16 -0
  56. package/workflows.js.map +1 -1
  57. package/workspace.d.ts +80 -62
  58. package/workspace.js +92 -75
  59. package/workspace.js.map +1 -1
  60. package/connection-status.d.ts +0 -28
  61. package/connection-status.js +0 -61
  62. package/connection-status.js.map +0 -1
package/database.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * A database your agents can query: registered once in the workspace, under a
3
+ * key, then named in each agent configuration that should reach it.
4
+ *
5
+ * The order matters and is the whole design. `ws.databases.create()` logs in
6
+ * before it saves anything, so a wrong password fails there and not in the
7
+ * middle of a conversation; an agent then names the key in its `databases`, and
8
+ * only an agent that names it can query it. The agent's prompt lists its
9
+ * databases by key, and its `sql_*` tools take that key.
10
+ *
11
+ * ```ts
12
+ * const shop = await ws.databases.create({ key: 'shop', name: 'Shop DB', url: process.env.SHOP_DB_URL! });
13
+ * await shop.tables(); // [{ schema: 'public', name: 'orders', kind: 'table' }]
14
+ * await ws.agents.create({ name: 'Ops', instructions, databases: ['shop'] });
15
+ * ```
16
+ *
17
+ * The login is stored encrypted and never read back: this handle reports where
18
+ * the database is, never how to log in to it.
19
+ */
20
+ export class Database {
21
+ /** Workspace-unique, and what an agent configuration stores. Not editable:
22
+ * renaming it would silently cut off every agent that names it. */
23
+ key;
24
+ #http;
25
+ snapshot;
26
+ constructor(http, key, snapshot) {
27
+ this.#http = http;
28
+ this.key = key;
29
+ if (snapshot)
30
+ this.snapshot = snapshot;
31
+ }
32
+ get base() {
33
+ return `/v1/databases/${encodeURIComponent(this.key)}`;
34
+ }
35
+ /** The database's current record: label, engine, host, port, name. */
36
+ async get() {
37
+ const res = await this.#http.get(this.base);
38
+ this.snapshot = res.database;
39
+ return res.database;
40
+ }
41
+ /** What was known when this handle was made, without a round trip.
42
+ * Undefined on a handle built from a bare key. */
43
+ get data() {
44
+ return this.snapshot;
45
+ }
46
+ /** The tables and views it holds, optionally within one schema. Logs in to
47
+ * ask, so this is also the quickest way to check the login still works. */
48
+ async tables(options = {}) {
49
+ const res = await this.#http.get(`${this.base}/tables`, options.schema ? { schema: options.schema } : undefined);
50
+ return res.tables ?? [];
51
+ }
52
+ /** One table's columns, with their types. */
53
+ async describe(table, options = {}) {
54
+ const res = await this.#http.get(`${this.base}/tables/${encodeURIComponent(table)}`, options.schema ? { schema: options.schema } : undefined);
55
+ return res.columns ?? [];
56
+ }
57
+ /**
58
+ * Remove the database from the workspace, and its stored login with it.
59
+ *
60
+ * REFUSED while any agent's configuration still names it, with an error
61
+ * naming those agents. That refusal is what makes naming a key safe: an agent
62
+ * published against `shop` would otherwise find it gone the next morning and
63
+ * report that it has no data.
64
+ */
65
+ async delete() {
66
+ await this.#http.delete(this.base);
67
+ }
68
+ }
69
+ //# sourceMappingURL=database.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"database.js","sourceRoot":"","sources":["../src/database.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,QAAQ;IACpB;wEACoE;IAC3D,GAAG,CAAS;IAEZ,KAAK,CAAO;IACb,QAAQ,CAAgB;IAEhC,YAAY,IAAU,EAAE,GAAW,EAAE,QAAuB;QAC3D,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC;QAClB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,QAAQ;YAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACxC,CAAC;IAED,IAAY,IAAI;QACf,OAAO,iBAAiB,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACxD,CAAC;IAED,sEAAsE;IACtE,KAAK,CAAC,GAAG;QACR,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAA6B,IAAI,CAAC,IAAI,CAAC,CAAC;QACxE,IAAI,CAAC,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC;QAC7B,OAAO,GAAG,CAAC,QAAQ,CAAC;IACrB,CAAC;IAED;uDACmD;IACnD,IAAI,IAAI;QACP,OAAO,IAAI,CAAC,QAAQ,CAAC;IACtB,CAAC;IAED;gFAC4E;IAC5E,KAAK,CAAC,MAAM,CAAC,UAA+B,EAAE;QAC7C,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAC/B,GAAG,IAAI,CAAC,IAAI,SAAS,EACrB,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CACvD,CAAC;QACF,OAAO,GAAG,CAAC,MAAM,IAAI,EAAE,CAAC;IACzB,CAAC;IAED,6CAA6C;IAC7C,KAAK,CAAC,QAAQ,CAAC,KAAa,EAAE,UAA+B,EAAE;QAC9D,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAC/B,GAAG,IAAI,CAAC,IAAI,WAAW,kBAAkB,CAAC,KAAK,CAAC,EAAE,EAClD,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CACvD,CAAC;QACF,OAAO,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC;IAC1B,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,MAAM;QACX,MAAM,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACpC,CAAC;CACD","sourcesContent":["import type { Http } from './http.ts';\nimport type { DatabaseColumn, DatabaseData, DatabaseTable } from './types.ts';\n\n/**\n * A database your agents can query: registered once in the workspace, under a\n * key, then named in each agent configuration that should reach it.\n *\n * The order matters and is the whole design. `ws.databases.create()` logs in\n * before it saves anything, so a wrong password fails there and not in the\n * middle of a conversation; an agent then names the key in its `databases`, and\n * only an agent that names it can query it. The agent's prompt lists its\n * databases by key, and its `sql_*` tools take that key.\n *\n * ```ts\n * const shop = await ws.databases.create({ key: 'shop', name: 'Shop DB', url: process.env.SHOP_DB_URL! });\n * await shop.tables(); // [{ schema: 'public', name: 'orders', kind: 'table' }]\n * await ws.agents.create({ name: 'Ops', instructions, databases: ['shop'] });\n * ```\n *\n * The login is stored encrypted and never read back: this handle reports where\n * the database is, never how to log in to it.\n */\nexport class Database {\n\t/** Workspace-unique, and what an agent configuration stores. Not editable:\n\t * renaming it would silently cut off every agent that names it. */\n\treadonly key: string;\n\n\treadonly #http: Http;\n\tprivate snapshot?: DatabaseData;\n\n\tconstructor(http: Http, key: string, snapshot?: DatabaseData) {\n\t\tthis.#http = http;\n\t\tthis.key = key;\n\t\tif (snapshot) this.snapshot = snapshot;\n\t}\n\n\tprivate get base(): string {\n\t\treturn `/v1/databases/${encodeURIComponent(this.key)}`;\n\t}\n\n\t/** The database's current record: label, engine, host, port, name. */\n\tasync get(): Promise<DatabaseData> {\n\t\tconst res = await this.#http.get<{ database: DatabaseData }>(this.base);\n\t\tthis.snapshot = res.database;\n\t\treturn res.database;\n\t}\n\n\t/** What was known when this handle was made, without a round trip.\n\t * Undefined on a handle built from a bare key. */\n\tget data(): DatabaseData | undefined {\n\t\treturn this.snapshot;\n\t}\n\n\t/** The tables and views it holds, optionally within one schema. Logs in to\n\t * ask, so this is also the quickest way to check the login still works. */\n\tasync tables(options: { schema?: string } = {}): Promise<DatabaseTable[]> {\n\t\tconst res = await this.#http.get<{ tables: DatabaseTable[] }>(\n\t\t\t`${this.base}/tables`,\n\t\t\toptions.schema ? { schema: options.schema } : undefined,\n\t\t);\n\t\treturn res.tables ?? [];\n\t}\n\n\t/** One table's columns, with their types. */\n\tasync describe(table: string, options: { schema?: string } = {}): Promise<DatabaseColumn[]> {\n\t\tconst res = await this.#http.get<{ columns: DatabaseColumn[] }>(\n\t\t\t`${this.base}/tables/${encodeURIComponent(table)}`,\n\t\t\toptions.schema ? { schema: options.schema } : undefined,\n\t\t);\n\t\treturn res.columns ?? [];\n\t}\n\n\t/**\n\t * Remove the database from the workspace, and its stored login with it.\n\t *\n\t * REFUSED while any agent's configuration still names it, with an error\n\t * naming those agents. That refusal is what makes naming a key safe: an agent\n\t * published against `shop` would otherwise find it gone the next morning and\n\t * report that it has no data.\n\t */\n\tasync delete(): Promise<void> {\n\t\tawait this.#http.delete(this.base);\n\t}\n}\n"]}
package/end-user.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { Connection } from './connection.ts';
1
+ import { Account } from './account.ts';
2
2
  import { Secret } from './secret.ts';
3
3
  import { Conversation } from './conversation.ts';
4
4
  import type { Http } from './http.ts';
5
+ import type { AppId } from './apps.generated.ts';
5
6
  /**
6
7
  * One of YOUR end users, as this platform sees them.
7
8
  *
@@ -34,32 +35,29 @@ import type { Http } from './http.ts';
34
35
  * do the other ten thousand. Every call you make is on behalf of one of them.
35
36
  *
36
37
  * Without this, the person is an argument: `endUserId` threaded through
37
- * `connections.create`, `secrets.create`, `conversations` and `start`, and
38
+ * `accounts.connect`, `secrets.create`, `conversations` and `start`, and
38
39
  * forgotten in exactly one of them one day. With it, you say who once and the
39
40
  * rest follows:
40
41
  *
41
42
  * ```ts
42
- * const ananya = ws.endUser(row.userId); // no request
43
+ * // once, in the agent's setup: each person uses their own Gmail
44
+ * await agent.connections.add(Connection.endUserAccount(App.GMAIL));
43
45
  *
44
- * const gmail = await ananya.connections.create({ provider: 'gmail' });
45
- * showToUser(gmail.url);
46
- * await gmail.waitUntilConnected();
46
+ * const ananya = ws.endUser(row.userId); // no request
47
+ * const { url } = await ananya.accounts.connect({ app: App.GMAIL });
48
+ * showToUser(url); // she signs in ONCE
47
49
  *
48
50
  * const conversation = await agent.start({ endUserId: ananya.id });
49
- * await conversation.attach(gmail); // EXPLICIT. Nothing is implied.
50
51
  * await conversation.ask('what did I miss this morning?');
51
52
  *
52
53
  * await ananya.conversations(); // everything of hers, any agent
53
54
  * ```
54
55
  *
55
- * NAMING AN END USER GRANTS NOTHING. `start({ endUserId })` labels the
56
- * conversation so it can be listed back to them; it does not hand the agent
57
- * their accounts. The `attach` is the grant, and it is a line you can see.
58
- *
59
- * An earlier version resolved a person's credentials automatically from that
60
- * label. It was wrong even when it picked correctly: reading the code told you
61
- * nothing about what the agent could reach, and one mistyped id silently swapped
62
- * whose mailbox a turn read.
56
+ * WHAT NAMING AN END USER DOES, exactly. `start({ endUserId })` labels the
57
+ * conversation so it can be listed back to them, and it decides whose account
58
+ * a connection to the end user's account uses: this person's, for that app,
59
+ * and nobody else's. The grant is the agent's setup, which you can read; it never
60
+ * falls back to the workspace's account or to anybody else's.
63
61
  *
64
62
  * THE ID IS YOURS. A row id from your database, opaque to us, and we never
65
63
  * authenticate it. We are not an identity provider and do not want to be. It
@@ -72,38 +70,60 @@ export declare class EndUser {
72
70
  #private;
73
71
  readonly id: string;
74
72
  constructor(http: Http, id: string);
73
+ private get base();
75
74
  /**
76
- * This person's own third-party accounts.
75
+ * This person's own accounts: the "end user's account" that a connection
76
+ * built with `Connection.endUserAccount(app)` uses.
77
77
  *
78
- * Connected ONCE, in their browser. After that the account is theirs and
79
- * stays theirs: you attach it to whichever conversations should use it,
80
- * rather than sending them through a sign-in again per thread. That is the
81
- * saving. The attach is not.
78
+ * Connected ONCE, on the app's own page. After that the account is theirs
79
+ * and stays theirs: every conversation of theirs uses it, with any agent
80
+ * whose setup asks for the end user's account of that app, and nobody is
81
+ * sent through a sign-in again per thread. Nothing is attached per
82
+ * conversation.
82
83
  *
83
- * Listing here is the CALLER'S view, for showing someone what they have
84
- * connected and for revoking it when they leave. It is not what a turn
85
- * consults: a turn sees only what was attached to it.
84
+ * An agent never reaches these any other way: not through the workspace's
85
+ * own account of the same app, and not in another person's conversation.
86
86
  */
87
- readonly connections: {
88
- /** Begin connecting. Returns a `Connection` carrying the url to send
89
- * them to; `waitUntilConnected()` tells you when they are done. */
90
- create: (input: {
91
- provider: string;
92
- }) => Promise<Connection>;
93
- /** Theirs alone. The shared workspace and agent accounts are not here;
94
- * `ws.connections.list()` is the view that shows those. */
95
- list: () => Promise<Connection[]>;
87
+ readonly accounts: {
88
+ /** Begin connecting one of their accounts. Returns it, still `pending`,
89
+ * and the url to send them to. It is an ordinary `Account`:
90
+ * `account.waitUntilConnected()` tells you when they are done, and
91
+ * `ws.account(id)` reaches it later. `conversation.accountsNeeded()`
92
+ * hands you the same kind of link for whatever a conversation is still
93
+ * missing. */
94
+ connect: (input: {
95
+ app: AppId;
96
+ }) => Promise<{
97
+ account: Account;
98
+ url: string;
99
+ }>;
100
+ /** Theirs alone. The agent creators' accounts are not here:
101
+ * `ws.accounts.list()` is the view that shows those. */
102
+ list: () => Promise<Account[]>;
103
+ /**
104
+ * Disconnect every account they connected, in one call: what
105
+ * off-boarding calls, so it is not a loop the caller has to remember to
106
+ * write. Every conversation of theirs stops reaching those apps from the
107
+ * next turn, and nothing is kept. Returns how many were disconnected.
108
+ */
109
+ disconnectAll: () => Promise<{
110
+ disconnected: number;
111
+ }>;
96
112
  };
97
113
  /**
98
114
  * Credentials that belong to THIS PERSON: their API key for a system you
99
115
  * integrate with, their token for a service only they have an account on.
100
116
  *
101
- * THIS WAS ABSENT UNTIL THERE WAS A WAY TO USE IT. A credential reaches a
102
- * turn only by being ATTACHED. A connection could be, by id; a secret could
103
- * not, because binding one took its VALUE, and you do not hold the value of a
104
- * secret the platform stored for you. So a per-person secret would have been
105
- * a surface that accepted writes, listed them back, and was read by nothing.
106
- * It was left out rather than shipped looking like a feature.
117
+ * THIS WAS ABSENT UNTIL THERE WAS A WAY TO USE IT. A secret reaches a turn
118
+ * only by being ATTACHED, and for a long time it could not be, because
119
+ * binding one took its VALUE, and you do not hold the value of a secret the
120
+ * platform stored for you. So a per-person secret would have been a surface
121
+ * that accepted writes, listed them back, and was read by nothing. It was
122
+ * left out rather than shipped looking like a feature.
123
+ *
124
+ * Secrets are still attached; accounts are not (0.44). A person's account is
125
+ * reached through the agent's setup (`Connection.endUserAccount`), so there is
126
+ * nothing to attach for it.
107
127
  *
108
128
  * `sessionBindings.secretIds` is what closed it, and attaching is still an
109
129
  * explicit line:
@@ -147,13 +167,4 @@ export declare class EndUser {
147
167
  limit?: number;
148
168
  before?: string;
149
169
  }): Promise<Conversation[]>;
150
- /**
151
- * Cut this person off.
152
- *
153
- * Revokes every account they connected, so every conversation of theirs
154
- * stops acting as them from the next turn. What off-boarding calls, and the
155
- * reason it is one call rather than a loop the caller has to remember to
156
- * write.
157
- */
158
- revokeConnections(): Promise<void>;
159
170
  }
package/end-user.js CHANGED
@@ -1,6 +1,5 @@
1
1
  // One of YOUR end users: a person your application serves, who never signs in here.
2
- import { Connection } from "./connection.js";
3
- import { toConnectionStatus } from "./connection-status.js";
2
+ import { Account, accountIn } from "./account.js";
4
3
  import { Secret } from "./secret.js";
5
4
  import { Conversation } from "./conversation.js";
6
5
  /**
@@ -35,32 +34,29 @@ import { Conversation } from "./conversation.js";
35
34
  * do the other ten thousand. Every call you make is on behalf of one of them.
36
35
  *
37
36
  * Without this, the person is an argument: `endUserId` threaded through
38
- * `connections.create`, `secrets.create`, `conversations` and `start`, and
37
+ * `accounts.connect`, `secrets.create`, `conversations` and `start`, and
39
38
  * forgotten in exactly one of them one day. With it, you say who once and the
40
39
  * rest follows:
41
40
  *
42
41
  * ```ts
43
- * const ananya = ws.endUser(row.userId); // no request
42
+ * // once, in the agent's setup: each person uses their own Gmail
43
+ * await agent.connections.add(Connection.endUserAccount(App.GMAIL));
44
44
  *
45
- * const gmail = await ananya.connections.create({ provider: 'gmail' });
46
- * showToUser(gmail.url);
47
- * await gmail.waitUntilConnected();
45
+ * const ananya = ws.endUser(row.userId); // no request
46
+ * const { url } = await ananya.accounts.connect({ app: App.GMAIL });
47
+ * showToUser(url); // she signs in ONCE
48
48
  *
49
49
  * const conversation = await agent.start({ endUserId: ananya.id });
50
- * await conversation.attach(gmail); // EXPLICIT. Nothing is implied.
51
50
  * await conversation.ask('what did I miss this morning?');
52
51
  *
53
52
  * await ananya.conversations(); // everything of hers, any agent
54
53
  * ```
55
54
  *
56
- * NAMING AN END USER GRANTS NOTHING. `start({ endUserId })` labels the
57
- * conversation so it can be listed back to them; it does not hand the agent
58
- * their accounts. The `attach` is the grant, and it is a line you can see.
59
- *
60
- * An earlier version resolved a person's credentials automatically from that
61
- * label. It was wrong even when it picked correctly: reading the code told you
62
- * nothing about what the agent could reach, and one mistyped id silently swapped
63
- * whose mailbox a turn read.
55
+ * WHAT NAMING AN END USER DOES, exactly. `start({ endUserId })` labels the
56
+ * conversation so it can be listed back to them, and it decides whose account
57
+ * a connection to the end user's account uses: this person's, for that app,
58
+ * and nobody else's. The grant is the agent's setup, which you can read; it never
59
+ * falls back to the workspace's account or to anybody else's.
64
60
  *
65
61
  * THE ID IS YOURS. A row id from your database, opaque to us, and we never
66
62
  * authenticate it. We are not an identity provider and do not want to be. It
@@ -76,61 +72,68 @@ export class EndUser {
76
72
  this.#http = http;
77
73
  this.id = id;
78
74
  }
75
+ get base() {
76
+ return `/v1/end-users/${encodeURIComponent(this.id)}/accounts`;
77
+ }
79
78
  /**
80
- * This person's own third-party accounts.
79
+ * This person's own accounts: the "end user's account" that a connection
80
+ * built with `Connection.endUserAccount(app)` uses.
81
81
  *
82
- * Connected ONCE, in their browser. After that the account is theirs and
83
- * stays theirs: you attach it to whichever conversations should use it,
84
- * rather than sending them through a sign-in again per thread. That is the
85
- * saving. The attach is not.
82
+ * Connected ONCE, on the app's own page. After that the account is theirs
83
+ * and stays theirs: every conversation of theirs uses it, with any agent
84
+ * whose setup asks for the end user's account of that app, and nobody is
85
+ * sent through a sign-in again per thread. Nothing is attached per
86
+ * conversation.
86
87
  *
87
- * Listing here is the CALLER'S view, for showing someone what they have
88
- * connected and for revoking it when they leave. It is not what a turn
89
- * consults: a turn sees only what was attached to it.
88
+ * An agent never reaches these any other way: not through the workspace's
89
+ * own account of the same app, and not in another person's conversation.
90
90
  */
91
- connections = {
92
- /** Begin connecting. Returns a `Connection` carrying the url to send
93
- * them to; `waitUntilConnected()` tells you when they are done. */
94
- create: async (input) => {
95
- const res = await this.#http.post('/v1/connections/connect', { provider: input.provider, endUserId: this.id });
96
- return new Connection(this.#http, res.connectionId,
97
- // NOT `as never`. That cast was here to let `scope: 'user'` through,
98
- // a value outside ConnectionStatus's own type, and it disabled
99
- // checking on every other field at the same time.
100
- {
101
- id: res.connectionId,
102
- provider: input.provider,
103
- connected: false,
104
- declared: true,
105
- required: false,
106
- endUserId: this.id,
107
- }, res.authUrl);
91
+ accounts = {
92
+ /** Begin connecting one of their accounts. Returns it, still `pending`,
93
+ * and the url to send them to. It is an ordinary `Account`:
94
+ * `account.waitUntilConnected()` tells you when they are done, and
95
+ * `ws.account(id)` reaches it later. `conversation.accountsNeeded()`
96
+ * hands you the same kind of link for whatever a conversation is still
97
+ * missing. */
98
+ connect: async (input) => {
99
+ // FIELDS PICKED: the person comes from the path, never from the body.
100
+ const res = await this.#http.post(`${this.base}/connect`, {
101
+ app: input.app,
102
+ });
103
+ const account = accountIn(res, `${this.base}/connect`);
104
+ return { account: new Account(this.#http, account.id, account), url: res.url };
108
105
  },
109
- /** Theirs alone. The shared workspace and agent accounts are not here;
110
- * `ws.connections.list()` is the view that shows those. */
106
+ /** Theirs alone. The agent creators' accounts are not here:
107
+ * `ws.accounts.list()` is the view that shows those. */
111
108
  list: async () => {
112
- // WireConnection, not ConnectionStatus. The endpoint sends the wire
113
- // shape, and typing it as the SDK shape is what hid the whole bug:
114
- // `connected` is never sent, so every account here reported
115
- // `connected === undefined` while type-checking as a boolean.
116
- const res = await this.#http.get('/v1/connections', {
117
- endUserId: this.id,
118
- });
119
- return (res.connections ?? [])
120
- .filter((c) => c.id)
121
- .map((c) => new Connection(this.#http, c.id, toConnectionStatus(c)));
109
+ const res = await this.#http.get(this.base);
110
+ return (res.accounts ?? []).map((a) => new Account(this.#http, a.id, a));
111
+ },
112
+ /**
113
+ * Disconnect every account they connected, in one call: what
114
+ * off-boarding calls, so it is not a loop the caller has to remember to
115
+ * write. Every conversation of theirs stops reaching those apps from the
116
+ * next turn, and nothing is kept. Returns how many were disconnected.
117
+ */
118
+ disconnectAll: async () => {
119
+ const res = await this.#http.delete(this.base);
120
+ return { disconnected: res?.disconnected ?? 0 };
122
121
  },
123
122
  };
124
123
  /**
125
124
  * Credentials that belong to THIS PERSON: their API key for a system you
126
125
  * integrate with, their token for a service only they have an account on.
127
126
  *
128
- * THIS WAS ABSENT UNTIL THERE WAS A WAY TO USE IT. A credential reaches a
129
- * turn only by being ATTACHED. A connection could be, by id; a secret could
130
- * not, because binding one took its VALUE, and you do not hold the value of a
131
- * secret the platform stored for you. So a per-person secret would have been
132
- * a surface that accepted writes, listed them back, and was read by nothing.
133
- * It was left out rather than shipped looking like a feature.
127
+ * THIS WAS ABSENT UNTIL THERE WAS A WAY TO USE IT. A secret reaches a turn
128
+ * only by being ATTACHED, and for a long time it could not be, because
129
+ * binding one took its VALUE, and you do not hold the value of a secret the
130
+ * platform stored for you. So a per-person secret would have been a surface
131
+ * that accepted writes, listed them back, and was read by nothing. It was
132
+ * left out rather than shipped looking like a feature.
133
+ *
134
+ * Secrets are still attached; accounts are not (0.44). A person's account is
135
+ * reached through the agent's setup (`Connection.endUserAccount`), so there is
136
+ * nothing to attach for it.
134
137
  *
135
138
  * `sessionBindings.secretIds` is what closed it, and attaching is still an
136
139
  * explicit line:
@@ -192,18 +195,5 @@ export class EndUser {
192
195
  });
193
196
  return (res.conversations ?? []).map((c) => new Conversation(this.#http, c.id, c));
194
197
  }
195
- /**
196
- * Cut this person off.
197
- *
198
- * Revokes every account they connected, so every conversation of theirs
199
- * stops acting as them from the next turn. What off-boarding calls, and the
200
- * reason it is one call rather than a loop the caller has to remember to
201
- * write.
202
- */
203
- async revokeConnections() {
204
- for (const connection of await this.connections.list()) {
205
- await connection.revoke();
206
- }
207
- }
208
198
  }
209
199
  //# sourceMappingURL=end-user.js.map
package/end-user.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"end-user.js","sourceRoot":"","sources":["../src/end-user.ts"],"names":[],"mappings":"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","sourcesContent":["// One of YOUR end users: a person your application serves, who never signs in here.\n\nimport { Connection } from './connection.ts';\nimport { toConnectionStatus, type WireConnection } from './connection-status.ts';\nimport { Secret } from './secret.ts';\nimport { Conversation } from './conversation.ts';\nimport type { Http } from './http.ts';\nimport type { AgentConversationData, ConnectionStatus, SecretMeta } from './types.ts';\n\n/**\n * One of YOUR end users, as this platform sees them.\n *\n * WHY THE NAME IS `EndUser` AND NOT `User`. There are two populations of people\n * in this product, and calling both of them \"user\" was a bug waiting to be\n * written:\n *\n * | | workspace member | END USER |\n * |---|---|---|\n * | who | your colleague, on a seat | the person your application serves |\n * | signs in to this platform | yes | NEVER |\n * | who issues the id | we do | YOUR database |\n * | do we authenticate it | yes | no, deliberately |\n * | in this SDK | `ws.members()` | `ws.endUser(id)` |\n *\n * They were both spelled `userId`, so `ws.members()` handed you an id that\n * `agent.start()` would accept without complaint and label a conversation with\n * a teammate's internal platform id. It could never have errored, because an\n * end-user id is never checked against anything. The two names now make that\n * call impossible to write by accident.\n *\n * NOT `Customer`, which was tried and reverted. Two reasons. \"Customer\" already\n * means the party that BUYS this platform, in our own strategy documents and in\n * `subscriptions.stripe_customer_id`, so the word points the other way. And it\n * is wrong for the common case: an enterprise deploying an internal workflow\n * agent serves its own EMPLOYEES, who are not anybody's customers.\n *\n * WHY THIS OBJECT EXISTS. You are building an application for many people.\n * Ananya has her own Gmail, her own conversations and her own history, and so\n * do the other ten thousand. Every call you make is on behalf of one of them.\n *\n * Without this, the person is an argument: `endUserId` threaded through\n * `connections.create`, `secrets.create`, `conversations` and `start`, and\n * forgotten in exactly one of them one day. With it, you say who once and the\n * rest follows:\n *\n * ```ts\n * const ananya = ws.endUser(row.userId); // no request\n *\n * const gmail = await ananya.connections.create({ provider: 'gmail' });\n * showToUser(gmail.url);\n * await gmail.waitUntilConnected();\n *\n * const conversation = await agent.start({ endUserId: ananya.id });\n * await conversation.attach(gmail); // EXPLICIT. Nothing is implied.\n * await conversation.ask('what did I miss this morning?');\n *\n * await ananya.conversations(); // everything of hers, any agent\n * ```\n *\n * NAMING AN END USER GRANTS NOTHING. `start({ endUserId })` labels the\n * conversation so it can be listed back to them; it does not hand the agent\n * their accounts. The `attach` is the grant, and it is a line you can see.\n *\n * An earlier version resolved a person's credentials automatically from that\n * label. It was wrong even when it picked correctly: reading the code told you\n * nothing about what the agent could reach, and one mistyped id silently swapped\n * whose mailbox a turn read.\n *\n * THE ID IS YOURS. A row id from your database, opaque to us, and we never\n * authenticate it. We are not an identity provider and do not want to be. It\n * also means passing the wrong one hands that person somebody else's mailbox,\n * which is a sharper consequence than the same mistake has for a chat list.\n *\n * Holding one makes NO request.\n */\nexport class EndUser {\n\treadonly id: string;\n\treadonly #http: Http;\n\n\tconstructor(http: Http, id: string) {\n\t\tthis.#http = http;\n\t\tthis.id = id;\n\t}\n\n\t/**\n\t * This person's own third-party accounts.\n\t *\n\t * Connected ONCE, in their browser. After that the account is theirs and\n\t * stays theirs: you attach it to whichever conversations should use it,\n\t * rather than sending them through a sign-in again per thread. That is the\n\t * saving. The attach is not.\n\t *\n\t * Listing here is the CALLER'S view, for showing someone what they have\n\t * connected and for revoking it when they leave. It is not what a turn\n\t * consults: a turn sees only what was attached to it.\n\t */\n\treadonly connections = {\n\t\t/** Begin connecting. Returns a `Connection` carrying the url to send\n\t\t * them to; `waitUntilConnected()` tells you when they are done. */\n\t\tcreate: async (input: { provider: string }): Promise<Connection> => {\n\t\t\tconst res = await this.#http.post<{ authUrl: string; connectionId: string }>(\n\t\t\t\t'/v1/connections/connect',\n\t\t\t\t{ provider: input.provider, endUserId: this.id },\n\t\t\t);\n\t\t\treturn new Connection(\n\t\t\t\tthis.#http,\n\t\t\t\tres.connectionId,\n\t\t\t\t// NOT `as never`. That cast was here to let `scope: 'user'` through,\n\t\t\t\t// a value outside ConnectionStatus's own type, and it disabled\n\t\t\t\t// checking on every other field at the same time.\n\t\t\t\t{\n\t\t\t\t\tid: res.connectionId,\n\t\t\t\t\tprovider: input.provider,\n\t\t\t\t\tconnected: false,\n\t\t\t\t\tdeclared: true,\n\t\t\t\t\trequired: false,\n\t\t\t\t\tendUserId: this.id,\n\t\t\t\t},\n\t\t\t\tres.authUrl,\n\t\t\t);\n\t\t},\n\n\t\t/** Theirs alone. The shared workspace and agent accounts are not here;\n\t\t * `ws.connections.list()` is the view that shows those. */\n\t\tlist: async (): Promise<Connection[]> => {\n\t\t\t// WireConnection, not ConnectionStatus. The endpoint sends the wire\n\t\t\t// shape, and typing it as the SDK shape is what hid the whole bug:\n\t\t\t// `connected` is never sent, so every account here reported\n\t\t\t// `connected === undefined` while type-checking as a boolean.\n\t\t\tconst res = await this.#http.get<{ connections: WireConnection[] }>('/v1/connections', {\n\t\t\t\tendUserId: this.id,\n\t\t\t});\n\t\t\treturn (res.connections ?? [])\n\t\t\t\t.filter((c) => c.id)\n\t\t\t\t.map((c) => new Connection(this.#http, c.id, toConnectionStatus(c)));\n\t\t},\n\t};\n\n\t/**\n\t * Credentials that belong to THIS PERSON: their API key for a system you\n\t * integrate with, their token for a service only they have an account on.\n\t *\n\t * THIS WAS ABSENT UNTIL THERE WAS A WAY TO USE IT. A credential reaches a\n\t * turn only by being ATTACHED. A connection could be, by id; a secret could\n\t * not, because binding one took its VALUE, and you do not hold the value of a\n\t * secret the platform stored for you. So a per-person secret would have been\n\t * a surface that accepted writes, listed them back, and was read by nothing.\n\t * It was left out rather than shipped looking like a feature.\n\t *\n\t * `sessionBindings.secretIds` is what closed it, and attaching is still an\n\t * explicit line:\n\t *\n\t * ```ts\n\t * const ananya = ws.endUser(row.userId);\n\t * const key = await ananya.secrets.create({ name: 'ZENDESK_TOKEN', value: token });\n\t *\n\t * const conversation = await agent.start({ endUserId: ananya.id });\n\t * await conversation.attach(key); // EXPLICIT. Nothing is implied.\n\t * ```\n\t *\n\t * TWO THINGS THE PLATFORM ENFORCES, neither of which you can turn off:\n\t * the agent must DECLARE that secret name at session scope, so attaching by\n\t * id is not a way around the allowlist; and a secret belonging to one end\n\t * user can only be attached to a conversation launched for that same person,\n\t * so one mistyped id cannot hand Ananya's key to somebody else's turn.\n\t */\n\treadonly secrets = {\n\t\t/** Store a credential for this person. The value is encrypted on arrival\n\t\t * and never handed back, which is why attaching uses the id. */\n\t\tcreate: async (input: {\n\t\t\tname: string;\n\t\t\tvalue: string;\n\t\t\tdescription?: string;\n\t\t\tagentId?: string;\n\t\t}): Promise<Secret> => {\n\t\t\t// FIELDS PICKED, never the input object forwarded. Removing a field\n\t\t\t// from a type does not stop a JavaScript caller sending it, and\n\t\t\t// forwarding wholesale is how `endUserId` kept reaching the API after\n\t\t\t// it had supposedly been removed.\n\t\t\tconst res = await this.#http.post<{ secret?: SecretMeta } & SecretMeta>('/v1/secrets', {\n\t\t\t\tname: input.name,\n\t\t\t\tvalue: input.value,\n\t\t\t\tendUserId: this.id,\n\t\t\t\t...(input.description !== undefined && { description: input.description }),\n\t\t\t\t...(input.agentId !== undefined && { agentId: input.agentId }),\n\t\t\t});\n\t\t\tconst meta = res.secret ?? res;\n\t\t\treturn new Secret(this.#http, meta.id, meta);\n\t\t},\n\t\t/** This person's secrets, values excluded. The workspace's own are not in\n\t\t * here: `ws.secrets.list()` answers that. */\n\t\tlist: async (): Promise<Secret[]> => {\n\t\t\tconst res = await this.#http.get<{ secrets: SecretMeta[] }>('/v1/secrets', {\n\t\t\t\tend_user_id: this.id,\n\t\t\t});\n\t\t\treturn (res.secrets ?? []).map((m) => new Secret(this.#http, m.id, m));\n\t\t},\n\t};\n\n\t/**\n\t * Everything this person has said, ACROSS EVERY AGENT, newest first.\n\t *\n\t * `agent.conversations({ endUser })` answers the narrower question, one agent\n\t * at a time. This is the one an application renders as \"your chats\", and it\n\t * is the only listing in this SDK with a real cursor: pass `before` from the\n\t * previous page to keep going, rather than being capped at 200.\n\t */\n\tasync conversations(options: { limit?: number; before?: string } = {}): Promise<Conversation[]> {\n\t\tconst res = await this.#http.get<{ conversations: AgentConversationData[] }>('/v1/conversations', {\n\t\t\tend_user_id: this.id,\n\t\t\t...(options.limit !== undefined && { limit: options.limit }),\n\t\t\t...(options.before && { before: options.before }),\n\t\t});\n\t\treturn (res.conversations ?? []).map((c) => new Conversation(this.#http, c.id, c));\n\t}\n\n\t/**\n\t * Cut this person off.\n\t *\n\t * Revokes every account they connected, so every conversation of theirs\n\t * stops acting as them from the next turn. What off-boarding calls, and the\n\t * reason it is one call rather than a loop the caller has to remember to\n\t * write.\n\t */\n\tasync revokeConnections(): Promise<void> {\n\t\tfor (const connection of await this.connections.list()) {\n\t\t\tawait connection.revoke();\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"end-user.js","sourceRoot":"","sources":["../src/end-user.ts"],"names":[],"mappings":"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","sourcesContent":["// One of YOUR end users: a person your application serves, who never signs in here.\n\nimport { Account, accountIn } from './account.ts';\nimport { Secret } from './secret.ts';\nimport { Conversation } from './conversation.ts';\nimport type { Http } from './http.ts';\nimport type { AppId } from './apps.generated.ts';\nimport type { AccountData, AgentConversationData, SecretMeta } from './types.ts';\n\n/**\n * One of YOUR end users, as this platform sees them.\n *\n * WHY THE NAME IS `EndUser` AND NOT `User`. There are two populations of people\n * in this product, and calling both of them \"user\" was a bug waiting to be\n * written:\n *\n * | | workspace member | END USER |\n * |---|---|---|\n * | who | your colleague, on a seat | the person your application serves |\n * | signs in to this platform | yes | NEVER |\n * | who issues the id | we do | YOUR database |\n * | do we authenticate it | yes | no, deliberately |\n * | in this SDK | `ws.members()` | `ws.endUser(id)` |\n *\n * They were both spelled `userId`, so `ws.members()` handed you an id that\n * `agent.start()` would accept without complaint and label a conversation with\n * a teammate's internal platform id. It could never have errored, because an\n * end-user id is never checked against anything. The two names now make that\n * call impossible to write by accident.\n *\n * NOT `Customer`, which was tried and reverted. Two reasons. \"Customer\" already\n * means the party that BUYS this platform, in our own strategy documents and in\n * `subscriptions.stripe_customer_id`, so the word points the other way. And it\n * is wrong for the common case: an enterprise deploying an internal workflow\n * agent serves its own EMPLOYEES, who are not anybody's customers.\n *\n * WHY THIS OBJECT EXISTS. You are building an application for many people.\n * Ananya has her own Gmail, her own conversations and her own history, and so\n * do the other ten thousand. Every call you make is on behalf of one of them.\n *\n * Without this, the person is an argument: `endUserId` threaded through\n * `accounts.connect`, `secrets.create`, `conversations` and `start`, and\n * forgotten in exactly one of them one day. With it, you say who once and the\n * rest follows:\n *\n * ```ts\n * // once, in the agent's setup: each person uses their own Gmail\n * await agent.connections.add(Connection.endUserAccount(App.GMAIL));\n *\n * const ananya = ws.endUser(row.userId); // no request\n * const { url } = await ananya.accounts.connect({ app: App.GMAIL });\n * showToUser(url); // she signs in ONCE\n *\n * const conversation = await agent.start({ endUserId: ananya.id });\n * await conversation.ask('what did I miss this morning?');\n *\n * await ananya.conversations(); // everything of hers, any agent\n * ```\n *\n * WHAT NAMING AN END USER DOES, exactly. `start({ endUserId })` labels the\n * conversation so it can be listed back to them, and it decides whose account\n * a connection to the end user's account uses: this person's, for that app,\n * and nobody else's. The grant is the agent's setup, which you can read; it never\n * falls back to the workspace's account or to anybody else's.\n *\n * THE ID IS YOURS. A row id from your database, opaque to us, and we never\n * authenticate it. We are not an identity provider and do not want to be. It\n * also means passing the wrong one hands that person somebody else's mailbox,\n * which is a sharper consequence than the same mistake has for a chat list.\n *\n * Holding one makes NO request.\n */\nexport class EndUser {\n\treadonly id: string;\n\treadonly #http: Http;\n\n\tconstructor(http: Http, id: string) {\n\t\tthis.#http = http;\n\t\tthis.id = id;\n\t}\n\n\tprivate get base(): string {\n\t\treturn `/v1/end-users/${encodeURIComponent(this.id)}/accounts`;\n\t}\n\n\t/**\n\t * This person's own accounts: the \"end user's account\" that a connection\n\t * built with `Connection.endUserAccount(app)` uses.\n\t *\n\t * Connected ONCE, on the app's own page. After that the account is theirs\n\t * and stays theirs: every conversation of theirs uses it, with any agent\n\t * whose setup asks for the end user's account of that app, and nobody is\n\t * sent through a sign-in again per thread. Nothing is attached per\n\t * conversation.\n\t *\n\t * An agent never reaches these any other way: not through the workspace's\n\t * own account of the same app, and not in another person's conversation.\n\t */\n\treadonly accounts = {\n\t\t/** Begin connecting one of their accounts. Returns it, still `pending`,\n\t\t * and the url to send them to. It is an ordinary `Account`:\n\t\t * `account.waitUntilConnected()` tells you when they are done, and\n\t\t * `ws.account(id)` reaches it later. `conversation.accountsNeeded()`\n\t\t * hands you the same kind of link for whatever a conversation is still\n\t\t * missing. */\n\t\tconnect: async (input: { app: AppId }): Promise<{ account: Account; url: string }> => {\n\t\t\t// FIELDS PICKED: the person comes from the path, never from the body.\n\t\t\tconst res = await this.#http.post<{ account?: AccountData; url: string }>(`${this.base}/connect`, {\n\t\t\t\tapp: input.app,\n\t\t\t});\n\t\t\tconst account = accountIn(res, `${this.base}/connect`);\n\t\t\treturn { account: new Account(this.#http, account.id, account), url: res.url };\n\t\t},\n\n\t\t/** Theirs alone. The agent creators' accounts are not here:\n\t\t * `ws.accounts.list()` is the view that shows those. */\n\t\tlist: async (): Promise<Account[]> => {\n\t\t\tconst res = await this.#http.get<{ accounts: AccountData[] }>(this.base);\n\t\t\treturn (res.accounts ?? []).map((a) => new Account(this.#http, a.id, a));\n\t\t},\n\n\t\t/**\n\t\t * Disconnect every account they connected, in one call: what\n\t\t * off-boarding calls, so it is not a loop the caller has to remember to\n\t\t * write. Every conversation of theirs stops reaching those apps from the\n\t\t * next turn, and nothing is kept. Returns how many were disconnected.\n\t\t */\n\t\tdisconnectAll: async (): Promise<{ disconnected: number }> => {\n\t\t\tconst res = await this.#http.delete<{ disconnected?: number }>(this.base);\n\t\t\treturn { disconnected: res?.disconnected ?? 0 };\n\t\t},\n\t};\n\n\t/**\n\t * Credentials that belong to THIS PERSON: their API key for a system you\n\t * integrate with, their token for a service only they have an account on.\n\t *\n\t * THIS WAS ABSENT UNTIL THERE WAS A WAY TO USE IT. A secret reaches a turn\n\t * only by being ATTACHED, and for a long time it could not be, because\n\t * binding one took its VALUE, and you do not hold the value of a secret the\n\t * platform stored for you. So a per-person secret would have been a surface\n\t * that accepted writes, listed them back, and was read by nothing. It was\n\t * left out rather than shipped looking like a feature.\n\t *\n\t * Secrets are still attached; accounts are not (0.44). A person's account is\n\t * reached through the agent's setup (`Connection.endUserAccount`), so there is\n\t * nothing to attach for it.\n\t *\n\t * `sessionBindings.secretIds` is what closed it, and attaching is still an\n\t * explicit line:\n\t *\n\t * ```ts\n\t * const ananya = ws.endUser(row.userId);\n\t * const key = await ananya.secrets.create({ name: 'ZENDESK_TOKEN', value: token });\n\t *\n\t * const conversation = await agent.start({ endUserId: ananya.id });\n\t * await conversation.attach(key); // EXPLICIT. Nothing is implied.\n\t * ```\n\t *\n\t * TWO THINGS THE PLATFORM ENFORCES, neither of which you can turn off:\n\t * the agent must DECLARE that secret name at session scope, so attaching by\n\t * id is not a way around the allowlist; and a secret belonging to one end\n\t * user can only be attached to a conversation launched for that same person,\n\t * so one mistyped id cannot hand Ananya's key to somebody else's turn.\n\t */\n\treadonly secrets = {\n\t\t/** Store a credential for this person. The value is encrypted on arrival\n\t\t * and never handed back, which is why attaching uses the id. */\n\t\tcreate: async (input: {\n\t\t\tname: string;\n\t\t\tvalue: string;\n\t\t\tdescription?: string;\n\t\t\tagentId?: string;\n\t\t}): Promise<Secret> => {\n\t\t\t// FIELDS PICKED, never the input object forwarded. Removing a field\n\t\t\t// from a type does not stop a JavaScript caller sending it, and\n\t\t\t// forwarding wholesale is how `endUserId` kept reaching the API after\n\t\t\t// it had supposedly been removed.\n\t\t\tconst res = await this.#http.post<{ secret?: SecretMeta } & SecretMeta>('/v1/secrets', {\n\t\t\t\tname: input.name,\n\t\t\t\tvalue: input.value,\n\t\t\t\tendUserId: this.id,\n\t\t\t\t...(input.description !== undefined && { description: input.description }),\n\t\t\t\t...(input.agentId !== undefined && { agentId: input.agentId }),\n\t\t\t});\n\t\t\tconst meta = res.secret ?? res;\n\t\t\treturn new Secret(this.#http, meta.id, meta);\n\t\t},\n\t\t/** This person's secrets, values excluded. The workspace's own are not in\n\t\t * here: `ws.secrets.list()` answers that. */\n\t\tlist: async (): Promise<Secret[]> => {\n\t\t\tconst res = await this.#http.get<{ secrets: SecretMeta[] }>('/v1/secrets', {\n\t\t\t\tend_user_id: this.id,\n\t\t\t});\n\t\t\treturn (res.secrets ?? []).map((m) => new Secret(this.#http, m.id, m));\n\t\t},\n\t};\n\n\t/**\n\t * Everything this person has said, ACROSS EVERY AGENT, newest first.\n\t *\n\t * `agent.conversations({ endUser })` answers the narrower question, one agent\n\t * at a time. This is the one an application renders as \"your chats\", and it\n\t * is the only listing in this SDK with a real cursor: pass `before` from the\n\t * previous page to keep going, rather than being capped at 200.\n\t */\n\tasync conversations(options: { limit?: number; before?: string } = {}): Promise<Conversation[]> {\n\t\tconst res = await this.#http.get<{ conversations: AgentConversationData[] }>('/v1/conversations', {\n\t\t\tend_user_id: this.id,\n\t\t\t...(options.limit !== undefined && { limit: options.limit }),\n\t\t\t...(options.before && { before: options.before }),\n\t\t});\n\t\treturn (res.conversations ?? []).map((c) => new Conversation(this.#http, c.id, c));\n\t}\n}\n"]}
package/errors.d.ts CHANGED
@@ -18,7 +18,8 @@ export declare class AgentNavaError extends Error {
18
18
  readonly status: number;
19
19
  /** Whether trying the identical request again could succeed.
20
20
  *
21
- * True for 5xx and 429 only. A 400 will fail the same way forever, and a
21
+ * True for 5xx and 429 unless a turn may already be accepted or the server
22
+ * says retrying is unsafe. A 400 will fail the same way forever, and a
22
23
  * 404 will not become found, so retrying either just turns one error into
23
24
  * several. This is the only thing worth automating on. */
24
25
  readonly retryable: boolean;
package/errors.js CHANGED
@@ -18,7 +18,8 @@ export class AgentNavaError extends Error {
18
18
  status;
19
19
  /** Whether trying the identical request again could succeed.
20
20
  *
21
- * True for 5xx and 429 only. A 400 will fail the same way forever, and a
21
+ * True for 5xx and 429 unless a turn may already be accepted or the server
22
+ * says retrying is unsafe. A 400 will fail the same way forever, and a
22
23
  * 404 will not become found, so retrying either just turns one error into
23
24
  * several. This is the only thing worth automating on. */
24
25
  retryable;
package/errors.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,OAAO,cAAe,SAAQ,KAAK;IACxC;;;8EAG0E;IACjE,IAAI,CAAS;IAEtB;;uDAEmD;IAC1C,MAAM,CAAS;IAExB;;;;+DAI2D;IAClD,SAAS,CAAU;IAE5B,8CAA8C;IACrC,UAAU,CAAU;IAE7B;qDACiD;IACxC,SAAS,CAAU;IAE5B,YAAY,IAMX;QACA,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC;QAC3D,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;YAAE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QACrE,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;YAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACnE,CAAC;CACD;AAED;;;;mFAImF;AACnF,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,KAAe;IAC5D,MAAM,GAAG,GAAG,IAAI,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9E,gEAAgE;IAChE,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3E,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/E,OAAO,GAAG,CAAC;AACZ,CAAC","sourcesContent":["/**\n * Every failure from this SDK is an AgentNavaError.\n *\n * One error type, not a hierarchy. A caller who wants to branch reads `code`,\n * and a caller who wants to retry reads `retryable`; neither is helped by\n * `instanceof NotFoundError`, and both are hurt by having to know which of nine\n * classes a given call can throw.\n */\nexport class AgentNavaError extends Error {\n\t/** Stable machine-readable reason, e.g. 'agent_not_found'.\n\t *\n\t * BRANCH ON THIS, never on `message`. Messages are written for a person\n\t * reading a log and will be reworded; codes are part of the contract. */\n\treadonly code: string;\n\n\t/** The HTTP status. 404 covers both \"no such thing\" and \"not yours\", so a\n\t * caller probing ids cannot tell another customer's agent from one that\n\t * never existed. There is deliberately no 403. */\n\treadonly status: number;\n\n\t/** Whether trying the identical request again could succeed.\n\t *\n\t * True for 5xx and 429 only. A 400 will fail the same way forever, and a\n\t * 404 will not become found, so retrying either just turns one error into\n\t * several. This is the only thing worth automating on. */\n\treadonly retryable: boolean;\n\n\t/** Seconds to wait, on a 429 that said so. */\n\treadonly retryAfter?: number;\n\n\t/** The request id from the response, when there was one. Quote it to us and\n\t * we can find the exact request in our logs. */\n\treadonly requestId?: string;\n\n\tconstructor(init: {\n\t\tmessage: string;\n\t\tcode: string;\n\t\tstatus: number;\n\t\tretryAfter?: number;\n\t\trequestId?: string;\n\t}) {\n\t\tsuper(init.message);\n\t\tthis.name = 'AgentNavaError';\n\t\tthis.code = init.code;\n\t\tthis.status = init.status;\n\t\tthis.retryable = init.status >= 500 || init.status === 429;\n\t\tif (init.retryAfter !== undefined) this.retryAfter = init.retryAfter;\n\t\tif (init.requestId !== undefined) this.requestId = init.requestId;\n\t}\n}\n\n/** A failure with no HTTP response behind it: DNS, a dropped socket, a timeout.\n *\n * Reported as status 0 rather than invented as a 5xx, because a request that\n * never arrived is a different thing from one the server failed. It is\n * retryable for the same reason a 5xx is: nothing about the request was wrong. */\nexport function networkError(message: string, cause?: unknown): AgentNavaError {\n\tconst err = new AgentNavaError({ message, code: 'network_error', status: 0 });\n\t// status 0 is not >= 500, so retryable has to be asserted here.\n\tObject.defineProperty(err, 'retryable', { value: true, enumerable: true });\n\tif (cause !== undefined) Object.defineProperty(err, 'cause', { value: cause });\n\treturn err;\n}\n"]}
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,MAAM,OAAO,cAAe,SAAQ,KAAK;IACxC;;;8EAG0E;IACjE,IAAI,CAAS;IAEtB;;uDAEmD;IAC1C,MAAM,CAAS;IAExB;;;;;+DAK2D;IAClD,SAAS,CAAU;IAE5B,8CAA8C;IACrC,UAAU,CAAU;IAE7B;qDACiD;IACxC,SAAS,CAAU;IAE5B,YAAY,IAMX;QACA,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACpB,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC;QACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC;QAC1B,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,CAAC;QAC3D,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS;YAAE,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC;QACrE,IAAI,IAAI,CAAC,SAAS,KAAK,SAAS;YAAE,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC;IACnE,CAAC;CACD;AAED;;;;mFAImF;AACnF,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,KAAe;IAC5D,MAAM,GAAG,GAAG,IAAI,cAAc,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;IAC9E,gEAAgE;IAChE,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3E,IAAI,KAAK,KAAK,SAAS;QAAE,MAAM,CAAC,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/E,OAAO,GAAG,CAAC;AACZ,CAAC","sourcesContent":["/**\n * Every failure from this SDK is an AgentNavaError.\n *\n * One error type, not a hierarchy. A caller who wants to branch reads `code`,\n * and a caller who wants to retry reads `retryable`; neither is helped by\n * `instanceof NotFoundError`, and both are hurt by having to know which of nine\n * classes a given call can throw.\n */\nexport class AgentNavaError extends Error {\n\t/** Stable machine-readable reason, e.g. 'agent_not_found'.\n\t *\n\t * BRANCH ON THIS, never on `message`. Messages are written for a person\n\t * reading a log and will be reworded; codes are part of the contract. */\n\treadonly code: string;\n\n\t/** The HTTP status. 404 covers both \"no such thing\" and \"not yours\", so a\n\t * caller probing ids cannot tell another customer's agent from one that\n\t * never existed. There is deliberately no 403. */\n\treadonly status: number;\n\n\t/** Whether trying the identical request again could succeed.\n\t *\n\t * True for 5xx and 429 unless a turn may already be accepted or the server\n\t * says retrying is unsafe. A 400 will fail the same way forever, and a\n\t * 404 will not become found, so retrying either just turns one error into\n\t * several. This is the only thing worth automating on. */\n\treadonly retryable: boolean;\n\n\t/** Seconds to wait, on a 429 that said so. */\n\treadonly retryAfter?: number;\n\n\t/** The request id from the response, when there was one. Quote it to us and\n\t * we can find the exact request in our logs. */\n\treadonly requestId?: string;\n\n\tconstructor(init: {\n\t\tmessage: string;\n\t\tcode: string;\n\t\tstatus: number;\n\t\tretryAfter?: number;\n\t\trequestId?: string;\n\t}) {\n\t\tsuper(init.message);\n\t\tthis.name = 'AgentNavaError';\n\t\tthis.code = init.code;\n\t\tthis.status = init.status;\n\t\tthis.retryable = init.status >= 500 || init.status === 429;\n\t\tif (init.retryAfter !== undefined) this.retryAfter = init.retryAfter;\n\t\tif (init.requestId !== undefined) this.requestId = init.requestId;\n\t}\n}\n\n/** A failure with no HTTP response behind it: DNS, a dropped socket, a timeout.\n *\n * Reported as status 0 rather than invented as a 5xx, because a request that\n * never arrived is a different thing from one the server failed. It is\n * retryable for the same reason a 5xx is: nothing about the request was wrong. */\nexport function networkError(message: string, cause?: unknown): AgentNavaError {\n\tconst err = new AgentNavaError({ message, code: 'network_error', status: 0 });\n\t// status 0 is not >= 500, so retryable has to be asserted here.\n\tObject.defineProperty(err, 'retryable', { value: true, enumerable: true });\n\tif (cause !== undefined) Object.defineProperty(err, 'cause', { value: cause });\n\treturn err;\n}\n"]}
package/http.d.ts CHANGED
@@ -10,6 +10,8 @@ export interface ClientOptions {
10
10
  }
11
11
  export interface RequestOptions {
12
12
  method?: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';
13
+ /** Internal turn submission identity, minted once by Conversation.ask. */
14
+ idempotencyKey?: string;
13
15
  /** Sent as a JSON body. Mutually exclusive with `bytes`. */
14
16
  body?: unknown;
15
17
  /** Sent as the raw body, for file writes. Mutually exclusive with `body`. */
@@ -27,6 +29,9 @@ export declare class Http {
27
29
  request<T>(path: string, options?: RequestOptions): Promise<T>;
28
30
  get<T>(path: string, query?: RequestOptions['query']): Promise<T>;
29
31
  post<T>(path: string, body?: unknown, query?: RequestOptions['query']): Promise<T>;
32
+ put<T>(path: string, body?: unknown): Promise<T>;
30
33
  patch<T>(path: string, body?: unknown): Promise<T>;
31
34
  delete<T>(path: string, query?: RequestOptions['query']): Promise<T>;
32
35
  }
36
+ /** Kept in errors because a dropped response may not have returned a turn ID. */
37
+ export declare function turnRecoveryMessage(path: string, idempotencyKey: string): string;