@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/connection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"AAAA,0EAA0E;AAE1E,OAAO,EAAgB,WAAW,EAAE,MAAM,cAAc,CAAC;AAEzD,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAiC7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG;IACzB;uEACmE;IACnE,cAAc,CAAC,GAAU,EAAE,OAAyB,EAAE,UAA6B,EAAE;QAGpF,MAAM,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;QAChC,yEAAyE;QACzE,mDAAmD;QACnD,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,IAAI,cAAc,CAAC;gBACxB,OAAO,EAAE,6BAA6B,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,sCAAsC;gBAC/F,IAAI,EAAE,kBAAkB;gBACxB,MAAM,EAAE,GAAG;aACX,CAAC,CAAC;QACJ,CAAC;QACD,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IACxF,CAAC;IAED;;2CAEuC;IACvC,cAAc,CAAC,GAAU,EAAE,UAA6B,EAAE;QACzD,OAAO,EAAE,GAAG,CAAC,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;IAC1F,CAAC;CACD,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,UAAsB;IACvD,MAAM,CAAC,GAAG,UAA0F,CAAC;IACrG,OAAO;QACN,GAAG,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QAC7C,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,WAAW,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;QACnE,GAAG,CAAC,CAAC,CAAC,OAAO,KAAK,SAAS,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC;KACtD,CAAC;AACH,CAAC","sourcesContent":["// A connection: one entry in an agent's setup. An app, and whose account.\n\nimport { type Account, accountIdOf } from './account.ts';\nimport type { AppId } from './apps.generated.ts';\nimport { AgentNavaError } from './errors.ts';\n\n/** A connection that uses the AGENT CREATOR'S ACCOUNT: one account in this\n * workspace, signed in once, used by every conversation. `account` is the\n * `Account` you hold or its id; an `Account` goes over the wire as its id. */\nexport interface CreatorAccountConnection {\n\tname?: string;\n\tapp: AppId;\n\taccount: Account | string;\n\tendUser?: never;\n}\n\n/** A connection that uses the END USER'S ACCOUNT: each person talking to the\n * agent signs in with their own, once, and it is remembered for them. */\nexport interface EndUserAccountConnection {\n\tname?: string;\n\tapp: AppId;\n\tendUser: true;\n\taccount?: never;\n}\n\n/**\n * One connection, as you hand it to `ws.agents.create()`, `agent.update()` or\n * `agent.connections.add()`: an app, and whose account.\n *\n * Build it with `Connection.creatorAccount()` or `Connection.endUserAccount()`,\n * which ask the same question the portal's Add a connection screen asks. Or\n * write the object yourself: `{ app, account }` or `{ app, endUser: true }`.\n * Exactly one of the two, and the type says so, so a connection with both, or\n * with neither, does not compile.\n */\nexport type Connection = CreatorAccountConnection | EndUserAccountConnection;\n\n/**\n * Build the entries of an agent's setup.\n *\n * ```ts\n * import { App, Connection } from '@cerebro-labs/agentnava-sdk';\n *\n * await ws.agents.create({\n * name: 'Inbox helper',\n * instructions: '...',\n * connections: [\n * Connection.creatorAccount(App.GMAIL, work, { name: 'work-gmail' }),\n * Connection.creatorAccount(App.GMAIL, personal, { name: 'personal-gmail' }),\n * Connection.endUserAccount(App.GOOGLE_CALENDAR),\n * ],\n * });\n * ```\n *\n * WHY BUILDERS. Whose account a connection uses is the one decision that\n * matters, and it used to be spelled `scope: 'fixed'` or `scope:\n * 'conversation'`, words that answered a different question. These two names\n * are the question itself, as the screen asks it: the agent creator's account,\n * or the end user's account.\n *\n * NOT THE 0.43 `Connection`. That was a handle on a signed-in account and is\n * now `Account`. This name builds an entry for an agent's setup; it holds\n * nothing and makes no request.\n *\n * `name` tells two connections of one app apart, such as two Gmail accounts.\n * Lowercase letters, digits and dashes, up to 64 characters, unique on the\n * agent. Left out, it is the app's id.\n */\nexport const Connection = {\n\t/** A connection that uses the agent creator's account: the `Account` you\n\t * hold, or its id. Produces `{ name?, app, account: '<id>' }`. */\n\tcreatorAccount(app: AppId, account: Account | string, options: { name?: string } = {}): CreatorAccountConnection & {\n\t\taccount: string;\n\t} {\n\t\tconst id = accountIdOf(account);\n\t\t// Thrown here, naming the mistake, rather than sent as a connection with\n\t\t// no account for the API to refuse in other words.\n\t\tif (typeof id !== 'string' || !id) {\n\t\t\tthrow new AgentNavaError({\n\t\t\t\tmessage: `Connection.creatorAccount(${JSON.stringify(app)}, ...) needs the Account, or its id.`,\n\t\t\t\tcode: 'account_required',\n\t\t\t\tstatus: 400,\n\t\t\t});\n\t\t}\n\t\treturn { ...(options.name !== undefined && { name: options.name }), app, account: id };\n\t},\n\n\t/** A connection that uses the end user's account: whichever account of\n\t * `app` the person each conversation is for has connected. Produces\n\t * `{ name?, app, endUser: true }`. */\n\tendUserAccount(app: AppId, options: { name?: string } = {}): EndUserAccountConnection {\n\t\treturn { ...(options.name !== undefined && { name: options.name }), app, endUser: true };\n\t},\n};\n\n/**\n * One connection as `POST /v1/agents/:id/connections` takes it: the fields it\n * declares, picked by name, and an `Account` by its id.\n *\n * Picked, never forwarded: a JavaScript caller holding 0.43 code could pass\n * `provider` or `scope`, and those must not reach the route as if they meant\n * something. Neither `account` nor `endUser` is invented here: whatever was\n * given goes, and the API names the problem when it is not exactly one.\n */\nexport function connectionForWire(connection: Connection): Record<string, unknown> {\n\tconst c = connection as { name?: string; app: AppId; account?: Account | string; endUser?: boolean };\n\treturn {\n\t\t...(c.name !== undefined && { name: c.name }),\n\t\tapp: c.app,\n\t\t...(c.account !== undefined && { account: accountIdOf(c.account) }),\n\t\t...(c.endUser !== undefined && { endUser: c.endUser }),\n\t};\n}\n"]}
|
package/conversation.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
import { type
|
|
2
|
-
import type {
|
|
1
|
+
import { type Http } from './http.ts';
|
|
2
|
+
import type { Secret } from './secret.ts';
|
|
3
3
|
import { type Turn } from './turn.ts';
|
|
4
|
-
import type { AgentConversationData, Attachment, Bindings, FileEntry, Message, Requirement } from './types.ts';
|
|
4
|
+
import type { AccountNeeded, AgentConversationData, Attachment, Bindings, FileEntry, Message, Requirement } from './types.ts';
|
|
5
5
|
export interface AskOptions {
|
|
6
6
|
attachments?: Attachment[];
|
|
7
|
+
/**
|
|
8
|
+
* Stop receiving this turn's response. An already-aborted signal prevents
|
|
9
|
+
* submission, so no turn is accepted or charged for this call. After the
|
|
10
|
+
* server accepts a turn, aborting stops observation: accepted work can
|
|
11
|
+
* continue and normal billing applies, with no automatic refund or server
|
|
12
|
+
* cancellation. Read conversation.messages() before asking again.
|
|
13
|
+
*/
|
|
7
14
|
signal?: AbortSignal;
|
|
15
|
+
/** Stable identity for one submission. Reuse it only for the same content
|
|
16
|
+
* after a lost response; a new intentional turn needs a new key. Defaults to
|
|
17
|
+
* a generated key. Use 1–128 letters, digits, dots, underscores, colons or hyphens. */
|
|
18
|
+
idempotencyKey?: string;
|
|
8
19
|
}
|
|
9
20
|
export interface FileUrlOptions {
|
|
10
21
|
download?: boolean;
|
|
@@ -43,8 +54,8 @@ export declare class Conversation {
|
|
|
43
54
|
*
|
|
44
55
|
* EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT. That is the model, and it
|
|
45
56
|
* is worth stating on the object because the SDK used to hold this value and
|
|
46
|
-
* never show it:
|
|
47
|
-
*
|
|
57
|
+
* never show it: a method read it internally and nothing else could, which
|
|
58
|
+
* made a conversation look parentless from outside.
|
|
48
59
|
*
|
|
49
60
|
* Undefined on a conversation you addressed by id and have not read yet,
|
|
50
61
|
* for the same reason `ready` is: the handle has made no request. Call
|
|
@@ -64,12 +75,16 @@ export declare class Conversation {
|
|
|
64
75
|
get ready(): boolean | undefined;
|
|
65
76
|
/** What the conversation is still missing. Same caveat as `ready`. */
|
|
66
77
|
get requires(): Requirement[];
|
|
78
|
+
/** The number of the agent version it runs, from the last snapshot. Same
|
|
79
|
+
* caveat as `ready`: a schedule naming this conversation moves it to the
|
|
80
|
+
* latest version, and only `get()` sees that. */
|
|
81
|
+
get version(): number | undefined;
|
|
67
82
|
get(): Promise<AgentConversationData>;
|
|
68
83
|
/**
|
|
69
84
|
* Ask, and get the reply.
|
|
70
85
|
*
|
|
71
86
|
* The message is sent HERE, not when you await or iterate the result, so
|
|
72
|
-
* calling this twice asks twice. Await it for the answer or iterate it for
|
|
87
|
+
* calling this twice with new keys asks twice. Await it for the answer or iterate it for
|
|
73
88
|
* the events; doing both to the same turn throws rather than replaying.
|
|
74
89
|
*/
|
|
75
90
|
ask(text: string, options?: AskOptions): Turn;
|
|
@@ -77,52 +92,57 @@ export declare class Conversation {
|
|
|
77
92
|
/** End it, and its drive with it. */
|
|
78
93
|
end(): Promise<void>;
|
|
79
94
|
/**
|
|
80
|
-
* Give THIS conversation a
|
|
95
|
+
* Give THIS conversation a secret, and nothing else.
|
|
81
96
|
*
|
|
82
97
|
* ```ts
|
|
83
|
-
* await conversation.attach(
|
|
84
|
-
* await conversation.attach(
|
|
85
|
-
* await conversation.attach({ secrets: { KEY: value } }); // a secret VALUE
|
|
98
|
+
* await conversation.attach(key); // a stored Secret, by id
|
|
99
|
+
* await conversation.attach({ secrets: { KEY: value } }); // a secret VALUE
|
|
86
100
|
* ```
|
|
87
101
|
*
|
|
88
|
-
*
|
|
89
|
-
* shape, and is gone: two names for one idea is how a surface becomes hard
|
|
90
|
-
* to learn.
|
|
102
|
+
* The narrowest scope there is: it reaches this conversation only.
|
|
91
103
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
104
|
+
* SECRETS ONLY, since 0.44. An account is never attached to a conversation
|
|
105
|
+
* any more: the agent's setup names whose account each connection uses, and
|
|
106
|
+
* for the end user's account the turn uses the account of the person this
|
|
107
|
+
* conversation is for. One handed in here is refused by name
|
|
108
|
+
* (`not_a_secret`) rather than dropped. `accountsNeeded()` says which of
|
|
109
|
+
* that person's accounts are still missing.
|
|
94
110
|
*
|
|
95
111
|
* PREFER `agent.start({ attach })` when you already know at launch. Between
|
|
96
112
|
* a start and a later attach there is a window in which the conversation
|
|
97
|
-
* exists, can be asked something, and would answer without the
|
|
113
|
+
* exists, can be asked something, and would answer without the secret.
|
|
98
114
|
*/
|
|
99
|
-
attach(what:
|
|
115
|
+
attach(what: Secret | Secret[] | Bindings): Promise<AgentConversationData>;
|
|
100
116
|
/**
|
|
101
|
-
*
|
|
117
|
+
* The end user's accounts this conversation still needs, each with the link
|
|
118
|
+
* that person signs in at.
|
|
119
|
+
*
|
|
120
|
+
* One for every connection to the end user's account (`endUser: true`)
|
|
121
|
+
* whose app the person this conversation is for has not connected yet.
|
|
122
|
+
* Empty when nothing is missing. They sign in ONCE: the account is remembered for
|
|
123
|
+
* them, so their next conversation does not ask again.
|
|
124
|
+
*
|
|
125
|
+
* ```ts
|
|
126
|
+
* const conversation = await agent.start({ endUserId: user.id });
|
|
127
|
+
* for (const { app, url } of await conversation.accountsNeeded()) {
|
|
128
|
+
* showToUser(app, url); // popup, redirect, email, your choice
|
|
129
|
+
* }
|
|
130
|
+
* ```
|
|
102
131
|
*
|
|
103
|
-
*
|
|
104
|
-
* they finish. The connection gets its own row and cannot overwrite the
|
|
105
|
-
* agent's shared account, so two people connecting their own Gmail to two
|
|
106
|
-
* conversations of the same agent do not clobber each other (ADR 0017: that
|
|
107
|
-
* was a real bug, found and fixed in this flow).
|
|
132
|
+
* Ask again once they are back: an empty list means nothing is missing.
|
|
108
133
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
134
|
+
* A conversation is never refused for a missing account. It starts, and
|
|
135
|
+
* the agent says what it could not reach; this is how your application
|
|
136
|
+
* finds out first.
|
|
111
137
|
*/
|
|
112
|
-
|
|
113
|
-
url: string;
|
|
114
|
-
connectionId: string;
|
|
115
|
-
}>;
|
|
138
|
+
accountsNeeded(): Promise<AccountNeeded[]>;
|
|
116
139
|
/**
|
|
117
|
-
* Wait until this conversation has everything it needs
|
|
140
|
+
* Wait until this conversation has everything it needs: until `ready`.
|
|
118
141
|
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
* a polling loop or work out which field to believe.
|
|
142
|
+
* So you do not have to invent a polling loop or work out which field to
|
|
143
|
+
* believe.
|
|
122
144
|
*
|
|
123
145
|
* ```ts
|
|
124
|
-
* const { url } = await conversation.authorize('gmail');
|
|
125
|
-
* showToUser(url); // popup, redirect, QR code, your choice
|
|
126
146
|
* await conversation.waitUntilReady();
|
|
127
147
|
* ```
|
|
128
148
|
*
|
package/conversation.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// A conversation: one continuous exchange with an agent, with its own history
|
|
2
2
|
// and its own filesystem. Two conversations share nothing.
|
|
3
|
-
import { isBindings,
|
|
3
|
+
import { isBindings, refuseConnections, secretIdsOf, secretsIn } from "./attach.js";
|
|
4
4
|
import { AgentNavaError } from "./errors.js";
|
|
5
|
+
import { turnRecoveryMessage } from "./http.js";
|
|
5
6
|
import { makeTurn } from "./turn.js";
|
|
6
7
|
/**
|
|
7
8
|
* A conversation you can work with.
|
|
@@ -42,8 +43,8 @@ export class Conversation {
|
|
|
42
43
|
*
|
|
43
44
|
* EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT. That is the model, and it
|
|
44
45
|
* is worth stating on the object because the SDK used to hold this value and
|
|
45
|
-
* never show it:
|
|
46
|
-
*
|
|
46
|
+
* never show it: a method read it internally and nothing else could, which
|
|
47
|
+
* made a conversation look parentless from outside.
|
|
47
48
|
*
|
|
48
49
|
* Undefined on a conversation you addressed by id and have not read yet,
|
|
49
50
|
* for the same reason `ready` is: the handle has made no request. Call
|
|
@@ -69,6 +70,12 @@ export class Conversation {
|
|
|
69
70
|
get requires() {
|
|
70
71
|
return this.snapshot?.requires ?? [];
|
|
71
72
|
}
|
|
73
|
+
/** The number of the agent version it runs, from the last snapshot. Same
|
|
74
|
+
* caveat as `ready`: a schedule naming this conversation moves it to the
|
|
75
|
+
* latest version, and only `get()` sees that. */
|
|
76
|
+
get version() {
|
|
77
|
+
return this.snapshot?.version;
|
|
78
|
+
}
|
|
72
79
|
async get() {
|
|
73
80
|
const data = await this.#http.get(this.base);
|
|
74
81
|
this.snapshot = data;
|
|
@@ -78,19 +85,27 @@ export class Conversation {
|
|
|
78
85
|
* Ask, and get the reply.
|
|
79
86
|
*
|
|
80
87
|
* The message is sent HERE, not when you await or iterate the result, so
|
|
81
|
-
* calling this twice asks twice. Await it for the answer or iterate it for
|
|
88
|
+
* calling this twice with new keys asks twice. Await it for the answer or iterate it for
|
|
82
89
|
* the events; doing both to the same turn throws rather than replaying.
|
|
83
90
|
*/
|
|
84
91
|
ask(text, options = {}) {
|
|
92
|
+
// One key for this submission, retained in the transport request if a
|
|
93
|
+
// fetch implementation retries it. A caller may retain their own key for
|
|
94
|
+
// an explicit retry after losing the first response.
|
|
95
|
+
const idempotencyKey = options.idempotencyKey ?? newTurnKey();
|
|
96
|
+
if (!/^[A-Za-z0-9._:-]{1,128}$/.test(idempotencyKey)) {
|
|
97
|
+
throw new AgentNavaError({ message: 'idempotencyKey must contain 1–128 letters, digits, dots, underscores, colons or hyphens.', code: 'invalid_idempotency_key', status: 400 });
|
|
98
|
+
}
|
|
85
99
|
const response = this.#http.send(`${this.base}/messages`, {
|
|
86
100
|
method: 'POST',
|
|
101
|
+
idempotencyKey,
|
|
87
102
|
body: {
|
|
88
103
|
content: text,
|
|
89
104
|
...(options.attachments?.length && { attachments: options.attachments }),
|
|
90
105
|
},
|
|
91
106
|
...(options.signal && { signal: options.signal }),
|
|
92
107
|
});
|
|
93
|
-
return makeTurn(response);
|
|
108
|
+
return makeTurn(response, turnRecoveryMessage(`${this.base}/messages`, idempotencyKey));
|
|
94
109
|
}
|
|
95
110
|
async messages() {
|
|
96
111
|
const res = await this.#http.get(`${this.base}/messages`);
|
|
@@ -100,8 +115,8 @@ export class Conversation {
|
|
|
100
115
|
async end() {
|
|
101
116
|
await this.#http.delete(this.base);
|
|
102
117
|
}
|
|
103
|
-
/** Supply the
|
|
104
|
-
*
|
|
118
|
+
/** Supply the secrets this conversation's agent declared at 'conversation'
|
|
119
|
+
* scope. Merges: sending one secret does not clear another.
|
|
105
120
|
*
|
|
106
121
|
* WRAPPED IN `sessionBindings`, and that envelope is load-bearing. This
|
|
107
122
|
* posted the object at the top level, while `addSessionBindings` reads
|
|
@@ -109,10 +124,9 @@ export class Conversation {
|
|
|
109
124
|
* is NOT an error there: a launch carrying no bindings is legitimate, so it
|
|
110
125
|
* normalises to `{}`.
|
|
111
126
|
*
|
|
112
|
-
* So this returned 200 and bound nothing
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
* agent that could not reach a mailbox somebody had plainly connected.
|
|
127
|
+
* So this returned 200 and bound nothing, and the failure appeared much
|
|
128
|
+
* later as an agent that could not use a credential somebody had plainly
|
|
129
|
+
* supplied.
|
|
116
130
|
*
|
|
117
131
|
* `agent.bind` had the same class of bug against its own route and its
|
|
118
132
|
* comment records it. This is the second one, found by reading both
|
|
@@ -125,66 +139,67 @@ export class Conversation {
|
|
|
125
139
|
return data;
|
|
126
140
|
}
|
|
127
141
|
/**
|
|
128
|
-
* Give THIS conversation a
|
|
142
|
+
* Give THIS conversation a secret, and nothing else.
|
|
129
143
|
*
|
|
130
144
|
* ```ts
|
|
131
|
-
* await conversation.attach(
|
|
132
|
-
* await conversation.attach(
|
|
133
|
-
* await conversation.attach({ secrets: { KEY: value } }); // a secret VALUE
|
|
145
|
+
* await conversation.attach(key); // a stored Secret, by id
|
|
146
|
+
* await conversation.attach({ secrets: { KEY: value } }); // a secret VALUE
|
|
134
147
|
* ```
|
|
135
148
|
*
|
|
136
|
-
*
|
|
137
|
-
* shape, and is gone: two names for one idea is how a surface becomes hard
|
|
138
|
-
* to learn.
|
|
149
|
+
* The narrowest scope there is: it reaches this conversation only.
|
|
139
150
|
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
151
|
+
* SECRETS ONLY, since 0.44. An account is never attached to a conversation
|
|
152
|
+
* any more: the agent's setup names whose account each connection uses, and
|
|
153
|
+
* for the end user's account the turn uses the account of the person this
|
|
154
|
+
* conversation is for. One handed in here is refused by name
|
|
155
|
+
* (`not_a_secret`) rather than dropped. `accountsNeeded()` says which of
|
|
156
|
+
* that person's accounts are still missing.
|
|
142
157
|
*
|
|
143
158
|
* PREFER `agent.start({ attach })` when you already know at launch. Between
|
|
144
159
|
* a start and a later attach there is a window in which the conversation
|
|
145
|
-
* exists, can be asked something, and would answer without the
|
|
160
|
+
* exists, can be asked something, and would answer without the secret.
|
|
146
161
|
*/
|
|
147
162
|
async attach(what) {
|
|
148
|
-
if (isBindings(what))
|
|
163
|
+
if (isBindings(what)) {
|
|
164
|
+
refuseConnections(what);
|
|
149
165
|
return this.#applyBindings(what);
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
return this.#applyBindings({
|
|
154
|
-
...(connections && { connections }),
|
|
155
|
-
...(secretIds && { secretIds }),
|
|
156
|
-
});
|
|
166
|
+
}
|
|
167
|
+
const secretIds = await secretIdsOf(secretsIn(what));
|
|
168
|
+
return this.#applyBindings({ ...(secretIds && { secretIds }) });
|
|
157
169
|
}
|
|
158
170
|
/**
|
|
159
|
-
*
|
|
171
|
+
* The end user's accounts this conversation still needs, each with the link
|
|
172
|
+
* that person signs in at.
|
|
160
173
|
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
* was a real bug, found and fixed in this flow).
|
|
174
|
+
* One for every connection to the end user's account (`endUser: true`)
|
|
175
|
+
* whose app the person this conversation is for has not connected yet.
|
|
176
|
+
* Empty when nothing is missing. They sign in ONCE: the account is remembered for
|
|
177
|
+
* them, so their next conversation does not ask again.
|
|
166
178
|
*
|
|
167
|
-
*
|
|
168
|
-
* conversation
|
|
179
|
+
* ```ts
|
|
180
|
+
* const conversation = await agent.start({ endUserId: user.id });
|
|
181
|
+
* for (const { app, url } of await conversation.accountsNeeded()) {
|
|
182
|
+
* showToUser(app, url); // popup, redirect, email, your choice
|
|
183
|
+
* }
|
|
184
|
+
* ```
|
|
185
|
+
*
|
|
186
|
+
* Ask again once they are back: an empty list means nothing is missing.
|
|
187
|
+
*
|
|
188
|
+
* A conversation is never refused for a missing account. It starts, and
|
|
189
|
+
* the agent says what it could not reach; this is how your application
|
|
190
|
+
* finds out first.
|
|
169
191
|
*/
|
|
170
|
-
async
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// just may not have read it yet.
|
|
174
|
-
const agentId = this.agentId ?? (await this.get()).agentId;
|
|
175
|
-
const res = await this.#http.post('/v1/connections/connect', { provider, agentId, conversationId: this.id });
|
|
176
|
-
return { url: res.authUrl, connectionId: res.connectionId };
|
|
192
|
+
async accountsNeeded() {
|
|
193
|
+
const res = await this.#http.get(`${this.base}/accounts-needed`);
|
|
194
|
+
return res.needed ?? [];
|
|
177
195
|
}
|
|
178
196
|
/**
|
|
179
|
-
* Wait until this conversation has everything it needs
|
|
197
|
+
* Wait until this conversation has everything it needs: until `ready`.
|
|
180
198
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
* a polling loop or work out which field to believe.
|
|
199
|
+
* So you do not have to invent a polling loop or work out which field to
|
|
200
|
+
* believe.
|
|
184
201
|
*
|
|
185
202
|
* ```ts
|
|
186
|
-
* const { url } = await conversation.authorize('gmail');
|
|
187
|
-
* showToUser(url); // popup, redirect, QR code, your choice
|
|
188
203
|
* await conversation.waitUntilReady();
|
|
189
204
|
* ```
|
|
190
205
|
*
|
|
@@ -293,6 +308,11 @@ export class Conversation {
|
|
|
293
308
|
return res.url;
|
|
294
309
|
}
|
|
295
310
|
}
|
|
311
|
+
let fallbackTurnSequence = 0;
|
|
312
|
+
function newTurnKey() {
|
|
313
|
+
return globalThis.crypto?.randomUUID?.() ??
|
|
314
|
+
`turn-${Date.now().toString(36)}-${++fallbackTurnSequence}-${Math.random().toString(36).slice(2)}`;
|
|
315
|
+
}
|
|
296
316
|
const CONTENT_TYPES = {
|
|
297
317
|
txt: 'text/plain',
|
|
298
318
|
md: 'text/markdown',
|
package/conversation.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conversation.js","sourceRoot":"","sources":["../src/conversation.ts"],"names":[],"mappings":"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","sourcesContent":["// A conversation: one continuous exchange with an agent, with its own history\n// and its own filesystem. Two conversations share nothing.\n\nimport { type Attachable, isBindings, providersOf, secretIdsOf, splitAttachments } from './attach.ts';\nimport type { Connection } from './connection.ts';\nimport { AgentNavaError } from './errors.ts';\nimport type { Http } from './http.ts';\nimport { makeTurn, type Turn } from './turn.ts';\nimport type {\n\tAgentConversationData,\n\tAttachment,\n\tBindings,\n\tFileEntry,\n\tMessage,\n\tRequirement,\n} from './types.ts';\n\nexport interface AskOptions {\n\tattachments?: Attachment[];\n\tsignal?: AbortSignal;\n}\n\nexport interface FileUrlOptions {\n\tdownload?: boolean;\n\t/** Seconds the link stays valid. */\n\tttl?: number;\n}\n\n/**\n * A conversation you can work with.\n *\n * EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT, reachable as `agentId` once\n * this handle has read anything. A conversation is an agent doing a piece of\n * work; there is no such thing as a conversation with nobody.\n *\n * ADDRESSED FROM ITS PARENT: `agent.conversation(id)`, or\n * `project.conversation(id)` for one a project owns. It used to be\n * `ws.conversation(id)`, which made the parent look optional and is how this\n * SDK came to describe conversations as parentless in its own reference. The\n * objection to moving it was that a caller would then store two ids; in\n * practice the agent id is a constant in their own code, not a per-row value.\n *\n * Holding one makes NO request. The first call on it is the first round trip,\n * because fetching a conversation just to send it a message would be a round\n * trip for nothing.\n */\nexport class Conversation {\n\treadonly id: string;\n\treadonly #http: Http;\n\t/** Whatever the call that produced this object already knew. Present when it\n\t * came from `agent.start()`, absent when it came from `agent.conversation(id)`,\n\t * which is exactly the difference between having made a request and not. */\n\tprivate snapshot?: AgentConversationData;\n\n\tconstructor(http: Http, id: string, snapshot?: AgentConversationData) {\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/conversations/${encodeURIComponent(this.id)}`;\n\t}\n\n\t/**\n\t * The agent whose conversation this is.\n\t *\n\t * EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT. That is the model, and it\n\t * is worth stating on the object because the SDK used to hold this value and\n\t * never show it: `authorize()` read it internally and nothing else could,\n\t * which made a conversation look parentless from outside.\n\t *\n\t * Undefined on a conversation you addressed by id and have not read yet,\n\t * for the same reason `ready` is: the handle has made no request. Call\n\t * `get()` to fill it in.\n\t *\n\t * ```ts\n\t * const conversation = await agent.start({ endUserId: row.userId });\n\t * conversation.agentId === agent.id; // already known, no round trip\n\t * ```\n\t */\n\tget agentId(): string | undefined {\n\t\treturn this.snapshot?.agentId;\n\t}\n\n\t/** True when nothing REQUIRED is outstanding.\n\t *\n\t * Reads from the last snapshot rather than the network, so it is a property\n\t * and not a call. It is only meaningful on a conversation you just started\n\t * or just refreshed; call `get()` to bring it up to date. */\n\tget ready(): boolean | undefined {\n\t\treturn this.snapshot?.ready;\n\t}\n\n\t/** What the conversation is still missing. Same caveat as `ready`. */\n\tget requires(): Requirement[] {\n\t\treturn this.snapshot?.requires ?? [];\n\t}\n\n\tasync get(): Promise<AgentConversationData> {\n\t\tconst data = await this.#http.get<AgentConversationData>(this.base);\n\t\tthis.snapshot = data;\n\t\treturn data;\n\t}\n\n\t/**\n\t * Ask, and get the reply.\n\t *\n\t * The message is sent HERE, not when you await or iterate the result, so\n\t * calling this twice asks twice. Await it for the answer or iterate it for\n\t * the events; doing both to the same turn throws rather than replaying.\n\t */\n\task(text: string, options: AskOptions = {}): Turn {\n\t\tconst response = this.#http.send(`${this.base}/messages`, {\n\t\t\tmethod: 'POST',\n\t\t\tbody: {\n\t\t\t\tcontent: text,\n\t\t\t\t...(options.attachments?.length && { attachments: options.attachments }),\n\t\t\t},\n\t\t\t...(options.signal && { signal: options.signal }),\n\t\t});\n\t\treturn makeTurn(response);\n\t}\n\n\tasync messages(): Promise<Message[]> {\n\t\tconst res = await this.#http.get<{ messages: Message[] }>(`${this.base}/messages`);\n\t\treturn res.messages ?? [];\n\t}\n\n\t/** End it, and its drive with it. */\n\tasync end(): Promise<void> {\n\t\tawait this.#http.delete(this.base);\n\t}\n\n\t/** Supply the credentials this conversation's agent declared at\n\t * 'conversation' scope. Merges: sending one secret does not clear another.\n\t *\n\t * WRAPPED IN `sessionBindings`, and that envelope is load-bearing. This\n\t * posted the object at the top level, while `addSessionBindings` reads\n\t * `body.sessionBindings`. The handler therefore saw undefined, and undefined\n\t * is NOT an error there: a launch carrying no bindings is legitimate, so it\n\t * normalises to `{}`.\n\t *\n\t * So this returned 200 and bound nothing. `conversation.attach()` sits on\n\t * top of it, which meant the documented way to grant an account to one\n\t * thread silently did nothing, and the failure appeared much later as an\n\t * agent that could not reach a mailbox somebody had plainly connected.\n\t *\n\t * `agent.bind` had the same class of bug against its own route and its\n\t * comment records it. This is the second one, found by reading both\n\t * together. */\n\tasync #applyBindings(bindings: Bindings): Promise<AgentConversationData> {\n\t\tconst data = await this.#http.post<AgentConversationData>(`${this.base}/bindings`, {\n\t\t\tsessionBindings: bindings,\n\t\t});\n\t\tthis.snapshot = data;\n\t\treturn data;\n\t}\n\n\t/**\n\t * Give THIS conversation a credential, and nothing else.\n\t *\n\t * ```ts\n\t * await conversation.attach(gmail); // one account\n\t * await conversation.attach([gmail, calendar]); // several\n\t * await conversation.attach({ secrets: { KEY: value } }); // a secret VALUE\n\t * ```\n\t *\n\t * ONE VERB. `bind` used to sit beside this doing the same job in a different\n\t * shape, and is gone: two names for one idea is how a surface becomes hard\n\t * to learn.\n\t *\n\t * The narrowest scope there is. It cannot overwrite the agent's shared\n\t * account or the person's own (ADR 0017).\n\t *\n\t * PREFER `agent.start({ attach })` when you already know at launch. Between\n\t * a start and a later attach there is a window in which the conversation\n\t * exists, can be asked something, and would answer without the account.\n\t */\n\tasync attach(what: Attachable | Attachable[] | Bindings): Promise<AgentConversationData> {\n\t\tif (isBindings(what)) return this.#applyBindings(what as Bindings);\n\t\tconst split = splitAttachments(what as Attachable | Attachable[]);\n\t\tconst connections = await providersOf(split.connections);\n\t\tconst secretIds = await secretIdsOf(split.secrets);\n\t\treturn this.#applyBindings({\n\t\t\t...(connections && { connections }),\n\t\t\t...(secretIds && { secretIds }),\n\t\t});\n\t}\n\n\t/**\n\t * Attach a third-party account to THIS conversation, at `conversation` scope.\n\t *\n\t * Send the person to the returned url; the conversation becomes ready when\n\t * they finish. The connection gets its own row and cannot overwrite the\n\t * agent's shared account, so two people connecting their own Gmail to two\n\t * conversations of the same agent do not clobber each other (ADR 0017: that\n\t * was a real bug, found and fixed in this flow).\n\t *\n\t * `agentId` is required alongside the conversation, so this reads the\n\t * conversation first when it has not already.\n\t */\n\tasync authorize(provider: string): Promise<{ url: string; connectionId: string }> {\n\t\t// Free when this conversation came from `agent.start()`, one request when\n\t\t// it was addressed by id. Every conversation HAS an agent; the handle\n\t\t// just may not have read it yet.\n\t\tconst agentId = this.agentId ?? (await this.get()).agentId;\n\t\tconst res = await this.#http.post<{ authUrl: string; connectionId: string }>(\n\t\t\t'/v1/connections/connect',\n\t\t\t{ provider, agentId, conversationId: this.id },\n\t\t);\n\t\treturn { url: res.authUrl, connectionId: res.connectionId };\n\t}\n\n\t/**\n\t * Wait until this conversation has everything it needs.\n\t *\n\t * The companion to `authorize()`. That call gives you a URL for your user to\n\t * visit; this one tells you when they are done, so you do not have to invent\n\t * a polling loop or work out which field to believe.\n\t *\n\t * ```ts\n\t * const { url } = await conversation.authorize('gmail');\n\t * showToUser(url); // popup, redirect, QR code, your choice\n\t * await conversation.waitUntilReady();\n\t * ```\n\t *\n\t * Returns as soon as nothing REQUIRED is outstanding, and returns the\n\t * conversation so you do not need a second call. An optional requirement\n\t * left unmet does not hold this up, for the same reason it does not make\n\t * `ready` false: the conversation genuinely works without it.\n\t *\n\t * ON TIMEOUT it throws naming what is still missing, because \"timed out\" on\n\t * its own sends you looking in the wrong place. The code is\n\t * `conversation_not_ready` and it is safe to call again: a person who has\n\t * not finished signing in yet may simply need longer.\n\t *\n\t * This polls rather than subscribing. For a step with a human in the middle,\n\t * a few seconds of latency costs nothing, and it keeps the method usable\n\t * from any runtime without a socket.\n\t */\n\tasync waitUntilReady(options: {\n\t\t/** Give up after this long. Default 2 minutes, which is generous for a\n\t\t * sign-in and short enough that a stuck flow does not hang a request. */\n\t\ttimeoutMs?: number;\n\t\t/** Gap between checks. Default 2 seconds. */\n\t\tintervalMs?: number;\n\t\t/** Cancel the wait, e.g. when the caller's own request is abandoned. */\n\t\tsignal?: AbortSignal;\n\t} = {}): Promise<AgentConversationData> {\n\t\tconst timeoutMs = options.timeoutMs ?? 120_000;\n\t\tconst intervalMs = options.intervalMs ?? 2_000;\n\t\tconst deadline = Date.now() + timeoutMs;\n\n\t\t// Checked BEFORE the first sleep. A conversation that is already ready\n\t\t// should cost one request, not one request plus an interval.\n\t\tlet data = await this.get();\n\t\twhile (!data.ready) {\n\t\t\tif (options.signal?.aborted) {\n\t\t\t\tthrow new AgentNavaError({\n\t\t\t\t\tmessage: 'waitUntilReady was cancelled',\n\t\t\t\t\tcode: 'aborted',\n\t\t\t\t\tstatus: 0,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (Date.now() >= deadline) {\n\t\t\t\t// Name the outstanding REQUIRED items. The optional ones are not why\n\t\t\t\t// this failed and listing them would send someone after the wrong\n\t\t\t\t// thing.\n\t\t\t\tconst outstanding = data.requires\n\t\t\t\t\t.filter((r) => r.required)\n\t\t\t\t\t.map((r) => `${r.kind} ${r.name}`);\n\t\t\t\tthrow new AgentNavaError({\n\t\t\t\t\tmessage: outstanding.length\n\t\t\t\t\t\t? `Conversation ${this.id} is still waiting on: ${outstanding.join(', ')}`\n\t\t\t\t\t\t: `Conversation ${this.id} did not become ready within ${timeoutMs}ms`,\n\t\t\t\t\tcode: 'conversation_not_ready',\n\t\t\t\t\tstatus: 0,\n\t\t\t\t});\n\t\t\t}\n\t\t\t// Never sleep past the deadline, so the timeout means what it says.\n\t\t\tawait sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())), options.signal);\n\t\t\tdata = await this.get();\n\t\t}\n\t\treturn data;\n\t}\n\n\t// ---------------------------------------------------------------- the drive\n\n\t/** List one directory. `path`, not `dir`: it is the same parameter every other\n\t * file route on this API takes, and one name for one idea beats a prettier\n\t * name used in one place. */\n\tasync files(path = '/'): Promise<FileEntry[]> {\n\t\tconst res = await this.#http.get<{ files: DriveEntry[] }>(`${this.base}/files`, { path });\n\t\treturn (res.files ?? []).map(toFileEntry);\n\t}\n\n\t/** Read a text file. */\n\tasync readFile(path: string): Promise<string> {\n\t\t// The route answers JSON `{path, content}`, not bare text. The envelope is\n\t\t// worth keeping on the wire (the canonical path comes back with it), so the\n\t\t// unwrapping happens here: a caller asked for the file, not for a wrapper.\n\t\tconst res = await this.#http.get<{ path: string; content: string }>(\n\t\t\t`${this.base}/files/content`,\n\t\t\t{ path },\n\t\t);\n\t\treturn res.content ?? '';\n\t}\n\n\tasync readFileBytes(path: string): Promise<Uint8Array> {\n\t\tconst res = await this.#http.send(`${this.base}/files/raw`, { query: { path } });\n\t\treturn new Uint8Array(await res.arrayBuffer());\n\t}\n\n\t/**\n\t * Put a file into the conversation's drive.\n\t *\n\t * This is how an application STAGES material: write the contract in, then ask\n\t * the question about it. Anywhere under the root is fine; `input/` is a\n\t * convention in the docs and nothing enforces it, because the agent and your\n\t * application share one namespace.\n\t */\n\tasync writeFile(\n\t\tpath: string,\n\t\tbytes: Uint8Array | ArrayBuffer,\n\t\tcontentType?: string,\n\t): Promise<FileEntry> {\n\t\treturn this.#http.request<FileEntry>(`${this.base}/files`, {\n\t\t\tmethod: 'PUT',\n\t\t\tbytes,\n\t\t\tquery: { path },\n\t\t\t...(contentType && { contentType }),\n\t\t});\n\t}\n\n\tasync removeFile(path: string): Promise<void> {\n\t\tawait this.#http.delete(`${this.base}/files`, { path });\n\t}\n\n\t/**\n\t * A link to one file that carries its own authorisation.\n\t *\n\t * A browser cannot hold an API key, so a Download button has no way to prove\n\t * it is allowed. This grants that one file for `ttl` seconds and nothing\n\t * else.\n\t */\n\tasync fileUrl(path: string, options: FileUrlOptions = {}): Promise<string> {\n\t\tconst res = await this.#http.post<{ url: string }>(`${this.base}/files/url`, {\n\t\t\tpath,\n\t\t\t...(options.download !== undefined && { download: options.download }),\n\t\t\t...(options.ttl !== undefined && { ttl: options.ttl }),\n\t\t});\n\t\treturn res.url;\n\t}\n}\n\n/** One entry as the Drive returns it. */\ninterface DriveEntry {\n\tname: string;\n\tpath: string;\n\tisDir: boolean;\n\tsize: number;\n\t/** Epoch milliseconds. */\n\tmtime: number;\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n\ttxt: 'text/plain',\n\tmd: 'text/markdown',\n\tjson: 'application/json',\n\tcsv: 'text/csv',\n\thtml: 'text/html',\n\tjs: 'text/javascript',\n\tts: 'text/typescript',\n\tpng: 'image/png',\n\tjpg: 'image/jpeg',\n\tjpeg: 'image/jpeg',\n\tgif: 'image/gif',\n\twebp: 'image/webp',\n\tsvg: 'image/svg+xml',\n\tpdf: 'application/pdf',\n};\n\n/**\n * A Drive entry as the SDK's FileEntry.\n *\n * The Drive speaks `isDir` and `mtime`; the SDK speaks `kind` and an ISO\n * `modifiedAt`. Translating at the edge is the SDK's job: the Drive's shape is\n * what apps/web's DrivePane reads, and changing it to suit this would break a\n * working screen to save a mapping function.\n *\n * `mimeType` is inferred from the extension, because the Drive does not store\n * one (see apps/backend-workers/src/sandbox/drive-reader.ts, which infers it the\n * same way on read).\n */\nfunction toFileEntry(entry: DriveEntry): FileEntry {\n\tconst ext = entry.path.slice(entry.path.lastIndexOf('.') + 1).toLowerCase();\n\treturn {\n\t\tpath: entry.path,\n\t\tkind: entry.isDir ? 'directory' : 'file',\n\t\tmimeType: entry.isDir ? 'inode/directory' : (CONTENT_TYPES[ext] ?? 'application/octet-stream'),\n\t\tsize: entry.size,\n\t\tmodifiedAt: new Date(entry.mtime).toISOString(),\n\t};\n}\n\n/** Wait, and stop waiting early if the caller aborts. */\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\tif (ms <= 0) return Promise.resolve();\n\treturn new Promise((resolve) => {\n\t\tconst timer = setTimeout(done, ms);\n\t\tfunction done() {\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener('abort', done);\n\t\t\tresolve();\n\t\t}\n\t\tsignal?.addEventListener('abort', done, { once: true });\n\t});\n}\n"]}
|
|
1
|
+
{"version":3,"file":"conversation.js","sourceRoot":"","sources":["../src/conversation.ts"],"names":[],"mappings":"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","sourcesContent":["// A conversation: one continuous exchange with an agent, with its own history\n// and its own filesystem. Two conversations share nothing.\n\nimport { isBindings, refuseConnections, type SessionBindings, secretIdsOf, secretsIn } from './attach.ts';\nimport { AgentNavaError } from './errors.ts';\nimport { type Http, turnRecoveryMessage } from './http.ts';\nimport type { Secret } from './secret.ts';\nimport { makeTurn, type Turn } from './turn.ts';\nimport type {\n\tAccountNeeded,\n\tAgentConversationData,\n\tAttachment,\n\tBindings,\n\tFileEntry,\n\tMessage,\n\tRequirement,\n} from './types.ts';\n\nexport interface AskOptions {\n\tattachments?: Attachment[];\n\t/**\n\t * Stop receiving this turn's response. An already-aborted signal prevents\n\t * submission, so no turn is accepted or charged for this call. After the\n\t * server accepts a turn, aborting stops observation: accepted work can\n\t * continue and normal billing applies, with no automatic refund or server\n\t * cancellation. Read conversation.messages() before asking again.\n\t */\n\tsignal?: AbortSignal;\n\t/** Stable identity for one submission. Reuse it only for the same content\n\t * after a lost response; a new intentional turn needs a new key. Defaults to\n\t * a generated key. Use 1–128 letters, digits, dots, underscores, colons or hyphens. */\n\tidempotencyKey?: string;\n}\n\nexport interface FileUrlOptions {\n\tdownload?: boolean;\n\t/** Seconds the link stays valid. */\n\tttl?: number;\n}\n\n/**\n * A conversation you can work with.\n *\n * EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT, reachable as `agentId` once\n * this handle has read anything. A conversation is an agent doing a piece of\n * work; there is no such thing as a conversation with nobody.\n *\n * ADDRESSED FROM ITS PARENT: `agent.conversation(id)`, or\n * `project.conversation(id)` for one a project owns. It used to be\n * `ws.conversation(id)`, which made the parent look optional and is how this\n * SDK came to describe conversations as parentless in its own reference. The\n * objection to moving it was that a caller would then store two ids; in\n * practice the agent id is a constant in their own code, not a per-row value.\n *\n * Holding one makes NO request. The first call on it is the first round trip,\n * because fetching a conversation just to send it a message would be a round\n * trip for nothing.\n */\nexport class Conversation {\n\treadonly id: string;\n\treadonly #http: Http;\n\t/** Whatever the call that produced this object already knew. Present when it\n\t * came from `agent.start()`, absent when it came from `agent.conversation(id)`,\n\t * which is exactly the difference between having made a request and not. */\n\tprivate snapshot?: AgentConversationData;\n\n\tconstructor(http: Http, id: string, snapshot?: AgentConversationData) {\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/conversations/${encodeURIComponent(this.id)}`;\n\t}\n\n\t/**\n\t * The agent whose conversation this is.\n\t *\n\t * EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT. That is the model, and it\n\t * is worth stating on the object because the SDK used to hold this value and\n\t * never show it: a method read it internally and nothing else could, which\n\t * made a conversation look parentless from outside.\n\t *\n\t * Undefined on a conversation you addressed by id and have not read yet,\n\t * for the same reason `ready` is: the handle has made no request. Call\n\t * `get()` to fill it in.\n\t *\n\t * ```ts\n\t * const conversation = await agent.start({ endUserId: row.userId });\n\t * conversation.agentId === agent.id; // already known, no round trip\n\t * ```\n\t */\n\tget agentId(): string | undefined {\n\t\treturn this.snapshot?.agentId;\n\t}\n\n\t/** True when nothing REQUIRED is outstanding.\n\t *\n\t * Reads from the last snapshot rather than the network, so it is a property\n\t * and not a call. It is only meaningful on a conversation you just started\n\t * or just refreshed; call `get()` to bring it up to date. */\n\tget ready(): boolean | undefined {\n\t\treturn this.snapshot?.ready;\n\t}\n\n\t/** What the conversation is still missing. Same caveat as `ready`. */\n\tget requires(): Requirement[] {\n\t\treturn this.snapshot?.requires ?? [];\n\t}\n\n\t/** The number of the agent version it runs, from the last snapshot. Same\n\t * caveat as `ready`: a schedule naming this conversation moves it to the\n\t * latest version, and only `get()` sees that. */\n\tget version(): number | undefined {\n\t\treturn this.snapshot?.version;\n\t}\n\n\tasync get(): Promise<AgentConversationData> {\n\t\tconst data = await this.#http.get<AgentConversationData>(this.base);\n\t\tthis.snapshot = data;\n\t\treturn data;\n\t}\n\n\t/**\n\t * Ask, and get the reply.\n\t *\n\t * The message is sent HERE, not when you await or iterate the result, so\n\t * calling this twice with new keys asks twice. Await it for the answer or iterate it for\n\t * the events; doing both to the same turn throws rather than replaying.\n\t */\n\task(text: string, options: AskOptions = {}): Turn {\n\t\t// One key for this submission, retained in the transport request if a\n\t\t// fetch implementation retries it. A caller may retain their own key for\n\t\t// an explicit retry after losing the first response.\n\t\tconst idempotencyKey = options.idempotencyKey ?? newTurnKey();\n\t\tif (!/^[A-Za-z0-9._:-]{1,128}$/.test(idempotencyKey)) {\n\t\t\tthrow new AgentNavaError({ message: 'idempotencyKey must contain 1–128 letters, digits, dots, underscores, colons or hyphens.', code: 'invalid_idempotency_key', status: 400 });\n\t\t}\n\t\tconst response = this.#http.send(`${this.base}/messages`, {\n\t\t\tmethod: 'POST',\n\t\t\tidempotencyKey,\n\t\t\tbody: {\n\t\t\t\tcontent: text,\n\t\t\t\t...(options.attachments?.length && { attachments: options.attachments }),\n\t\t\t},\n\t\t\t...(options.signal && { signal: options.signal }),\n\t\t});\n\t\treturn makeTurn(response, turnRecoveryMessage(`${this.base}/messages`, idempotencyKey));\n\t}\n\n\tasync messages(): Promise<Message[]> {\n\t\tconst res = await this.#http.get<{ messages: Message[] }>(`${this.base}/messages`);\n\t\treturn res.messages ?? [];\n\t}\n\n\t/** End it, and its drive with it. */\n\tasync end(): Promise<void> {\n\t\tawait this.#http.delete(this.base);\n\t}\n\n\t/** Supply the secrets this conversation's agent declared at 'conversation'\n\t * scope. Merges: sending one secret does not clear another.\n\t *\n\t * WRAPPED IN `sessionBindings`, and that envelope is load-bearing. This\n\t * posted the object at the top level, while `addSessionBindings` reads\n\t * `body.sessionBindings`. The handler therefore saw undefined, and undefined\n\t * is NOT an error there: a launch carrying no bindings is legitimate, so it\n\t * normalises to `{}`.\n\t *\n\t * So this returned 200 and bound nothing, and the failure appeared much\n\t * later as an agent that could not use a credential somebody had plainly\n\t * supplied.\n\t *\n\t * `agent.bind` had the same class of bug against its own route and its\n\t * comment records it. This is the second one, found by reading both\n\t * together. */\n\tasync #applyBindings(bindings: SessionBindings): Promise<AgentConversationData> {\n\t\tconst data = await this.#http.post<AgentConversationData>(`${this.base}/bindings`, {\n\t\t\tsessionBindings: bindings,\n\t\t});\n\t\tthis.snapshot = data;\n\t\treturn data;\n\t}\n\n\t/**\n\t * Give THIS conversation a secret, and nothing else.\n\t *\n\t * ```ts\n\t * await conversation.attach(key); // a stored Secret, by id\n\t * await conversation.attach({ secrets: { KEY: value } }); // a secret VALUE\n\t * ```\n\t *\n\t * The narrowest scope there is: it reaches this conversation only.\n\t *\n\t * SECRETS ONLY, since 0.44. An account is never attached to a conversation\n\t * any more: the agent's setup names whose account each connection uses, and\n\t * for the end user's account the turn uses the account of the person this\n\t * conversation is for. One handed in here is refused by name\n\t * (`not_a_secret`) rather than dropped. `accountsNeeded()` says which of\n\t * that person's accounts are still missing.\n\t *\n\t * PREFER `agent.start({ attach })` when you already know at launch. Between\n\t * a start and a later attach there is a window in which the conversation\n\t * exists, can be asked something, and would answer without the secret.\n\t */\n\tasync attach(what: Secret | Secret[] | Bindings): Promise<AgentConversationData> {\n\t\tif (isBindings(what)) {\n\t\t\trefuseConnections(what);\n\t\t\treturn this.#applyBindings(what);\n\t\t}\n\t\tconst secretIds = await secretIdsOf(secretsIn(what));\n\t\treturn this.#applyBindings({ ...(secretIds && { secretIds }) });\n\t}\n\n\t/**\n\t * The end user's accounts this conversation still needs, each with the link\n\t * that person signs in at.\n\t *\n\t * One for every connection to the end user's account (`endUser: true`)\n\t * whose app the person this conversation is for has not connected yet.\n\t * Empty when nothing is missing. They sign in ONCE: the account is remembered for\n\t * them, so their next conversation does not ask again.\n\t *\n\t * ```ts\n\t * const conversation = await agent.start({ endUserId: user.id });\n\t * for (const { app, url } of await conversation.accountsNeeded()) {\n\t * showToUser(app, url); // popup, redirect, email, your choice\n\t * }\n\t * ```\n\t *\n\t * Ask again once they are back: an empty list means nothing is missing.\n\t *\n\t * A conversation is never refused for a missing account. It starts, and\n\t * the agent says what it could not reach; this is how your application\n\t * finds out first.\n\t */\n\tasync accountsNeeded(): Promise<AccountNeeded[]> {\n\t\tconst res = await this.#http.get<{ needed: AccountNeeded[] }>(`${this.base}/accounts-needed`);\n\t\treturn res.needed ?? [];\n\t}\n\n\t/**\n\t * Wait until this conversation has everything it needs: until `ready`.\n\t *\n\t * So you do not have to invent a polling loop or work out which field to\n\t * believe.\n\t *\n\t * ```ts\n\t * await conversation.waitUntilReady();\n\t * ```\n\t *\n\t * Returns as soon as nothing REQUIRED is outstanding, and returns the\n\t * conversation so you do not need a second call. An optional requirement\n\t * left unmet does not hold this up, for the same reason it does not make\n\t * `ready` false: the conversation genuinely works without it.\n\t *\n\t * ON TIMEOUT it throws naming what is still missing, because \"timed out\" on\n\t * its own sends you looking in the wrong place. The code is\n\t * `conversation_not_ready` and it is safe to call again: a person who has\n\t * not finished signing in yet may simply need longer.\n\t *\n\t * This polls rather than subscribing. For a step with a human in the middle,\n\t * a few seconds of latency costs nothing, and it keeps the method usable\n\t * from any runtime without a socket.\n\t */\n\tasync waitUntilReady(options: {\n\t\t/** Give up after this long. Default 2 minutes, which is generous for a\n\t\t * sign-in and short enough that a stuck flow does not hang a request. */\n\t\ttimeoutMs?: number;\n\t\t/** Gap between checks. Default 2 seconds. */\n\t\tintervalMs?: number;\n\t\t/** Cancel the wait, e.g. when the caller's own request is abandoned. */\n\t\tsignal?: AbortSignal;\n\t} = {}): Promise<AgentConversationData> {\n\t\tconst timeoutMs = options.timeoutMs ?? 120_000;\n\t\tconst intervalMs = options.intervalMs ?? 2_000;\n\t\tconst deadline = Date.now() + timeoutMs;\n\n\t\t// Checked BEFORE the first sleep. A conversation that is already ready\n\t\t// should cost one request, not one request plus an interval.\n\t\tlet data = await this.get();\n\t\twhile (!data.ready) {\n\t\t\tif (options.signal?.aborted) {\n\t\t\t\tthrow new AgentNavaError({\n\t\t\t\t\tmessage: 'waitUntilReady was cancelled',\n\t\t\t\t\tcode: 'aborted',\n\t\t\t\t\tstatus: 0,\n\t\t\t\t});\n\t\t\t}\n\t\t\tif (Date.now() >= deadline) {\n\t\t\t\t// Name the outstanding REQUIRED items. The optional ones are not why\n\t\t\t\t// this failed and listing them would send someone after the wrong\n\t\t\t\t// thing.\n\t\t\t\tconst outstanding = data.requires\n\t\t\t\t\t.filter((r) => r.required)\n\t\t\t\t\t.map((r) => `${r.kind} ${r.name}`);\n\t\t\t\tthrow new AgentNavaError({\n\t\t\t\t\tmessage: outstanding.length\n\t\t\t\t\t\t? `Conversation ${this.id} is still waiting on: ${outstanding.join(', ')}`\n\t\t\t\t\t\t: `Conversation ${this.id} did not become ready within ${timeoutMs}ms`,\n\t\t\t\t\tcode: 'conversation_not_ready',\n\t\t\t\t\tstatus: 0,\n\t\t\t\t});\n\t\t\t}\n\t\t\t// Never sleep past the deadline, so the timeout means what it says.\n\t\t\tawait sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())), options.signal);\n\t\t\tdata = await this.get();\n\t\t}\n\t\treturn data;\n\t}\n\n\t// ---------------------------------------------------------------- the drive\n\n\t/** List one directory. `path`, not `dir`: it is the same parameter every other\n\t * file route on this API takes, and one name for one idea beats a prettier\n\t * name used in one place. */\n\tasync files(path = '/'): Promise<FileEntry[]> {\n\t\tconst res = await this.#http.get<{ files: DriveEntry[] }>(`${this.base}/files`, { path });\n\t\treturn (res.files ?? []).map(toFileEntry);\n\t}\n\n\t/** Read a text file. */\n\tasync readFile(path: string): Promise<string> {\n\t\t// The route answers JSON `{path, content}`, not bare text. The envelope is\n\t\t// worth keeping on the wire (the canonical path comes back with it), so the\n\t\t// unwrapping happens here: a caller asked for the file, not for a wrapper.\n\t\tconst res = await this.#http.get<{ path: string; content: string }>(\n\t\t\t`${this.base}/files/content`,\n\t\t\t{ path },\n\t\t);\n\t\treturn res.content ?? '';\n\t}\n\n\tasync readFileBytes(path: string): Promise<Uint8Array> {\n\t\tconst res = await this.#http.send(`${this.base}/files/raw`, { query: { path } });\n\t\treturn new Uint8Array(await res.arrayBuffer());\n\t}\n\n\t/**\n\t * Put a file into the conversation's drive.\n\t *\n\t * This is how an application STAGES material: write the contract in, then ask\n\t * the question about it. Anywhere under the root is fine; `input/` is a\n\t * convention in the docs and nothing enforces it, because the agent and your\n\t * application share one namespace.\n\t */\n\tasync writeFile(\n\t\tpath: string,\n\t\tbytes: Uint8Array | ArrayBuffer,\n\t\tcontentType?: string,\n\t): Promise<FileEntry> {\n\t\treturn this.#http.request<FileEntry>(`${this.base}/files`, {\n\t\t\tmethod: 'PUT',\n\t\t\tbytes,\n\t\t\tquery: { path },\n\t\t\t...(contentType && { contentType }),\n\t\t});\n\t}\n\n\tasync removeFile(path: string): Promise<void> {\n\t\tawait this.#http.delete(`${this.base}/files`, { path });\n\t}\n\n\t/**\n\t * A link to one file that carries its own authorisation.\n\t *\n\t * A browser cannot hold an API key, so a Download button has no way to prove\n\t * it is allowed. This grants that one file for `ttl` seconds and nothing\n\t * else.\n\t */\n\tasync fileUrl(path: string, options: FileUrlOptions = {}): Promise<string> {\n\t\tconst res = await this.#http.post<{ url: string }>(`${this.base}/files/url`, {\n\t\t\tpath,\n\t\t\t...(options.download !== undefined && { download: options.download }),\n\t\t\t...(options.ttl !== undefined && { ttl: options.ttl }),\n\t\t});\n\t\treturn res.url;\n\t}\n}\n\nlet fallbackTurnSequence = 0;\nfunction newTurnKey(): string {\n\treturn globalThis.crypto?.randomUUID?.() ??\n\t\t`turn-${Date.now().toString(36)}-${++fallbackTurnSequence}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** One entry as the Drive returns it. */\ninterface DriveEntry {\n\tname: string;\n\tpath: string;\n\tisDir: boolean;\n\tsize: number;\n\t/** Epoch milliseconds. */\n\tmtime: number;\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n\ttxt: 'text/plain',\n\tmd: 'text/markdown',\n\tjson: 'application/json',\n\tcsv: 'text/csv',\n\thtml: 'text/html',\n\tjs: 'text/javascript',\n\tts: 'text/typescript',\n\tpng: 'image/png',\n\tjpg: 'image/jpeg',\n\tjpeg: 'image/jpeg',\n\tgif: 'image/gif',\n\twebp: 'image/webp',\n\tsvg: 'image/svg+xml',\n\tpdf: 'application/pdf',\n};\n\n/**\n * A Drive entry as the SDK's FileEntry.\n *\n * The Drive speaks `isDir` and `mtime`; the SDK speaks `kind` and an ISO\n * `modifiedAt`. Translating at the edge is the SDK's job: the Drive's shape is\n * what apps/web's DrivePane reads, and changing it to suit this would break a\n * working screen to save a mapping function.\n *\n * `mimeType` is inferred from the extension, because the Drive does not store\n * one (see apps/backend-workers/src/sandbox/drive-reader.ts, which infers it the\n * same way on read).\n */\nfunction toFileEntry(entry: DriveEntry): FileEntry {\n\tconst ext = entry.path.slice(entry.path.lastIndexOf('.') + 1).toLowerCase();\n\treturn {\n\t\tpath: entry.path,\n\t\tkind: entry.isDir ? 'directory' : 'file',\n\t\tmimeType: entry.isDir ? 'inode/directory' : (CONTENT_TYPES[ext] ?? 'application/octet-stream'),\n\t\tsize: entry.size,\n\t\tmodifiedAt: new Date(entry.mtime).toISOString(),\n\t};\n}\n\n/** Wait, and stop waiting early if the caller aborts. */\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n\tif (ms <= 0) return Promise.resolve();\n\treturn new Promise((resolve) => {\n\t\tconst timer = setTimeout(done, ms);\n\t\tfunction done() {\n\t\t\tclearTimeout(timer);\n\t\t\tsignal?.removeEventListener('abort', done);\n\t\t\tresolve();\n\t\t}\n\t\tsignal?.addEventListener('abort', done, { once: true });\n\t});\n}\n"]}
|
package/database.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { Http } from './http.ts';
|
|
2
|
+
import type { DatabaseColumn, DatabaseData, DatabaseTable } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* A database your agents can query: registered once in the workspace, under a
|
|
5
|
+
* key, then named in each agent configuration that should reach it.
|
|
6
|
+
*
|
|
7
|
+
* The order matters and is the whole design. `ws.databases.create()` logs in
|
|
8
|
+
* before it saves anything, so a wrong password fails there and not in the
|
|
9
|
+
* middle of a conversation; an agent then names the key in its `databases`, and
|
|
10
|
+
* only an agent that names it can query it. The agent's prompt lists its
|
|
11
|
+
* databases by key, and its `sql_*` tools take that key.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* const shop = await ws.databases.create({ key: 'shop', name: 'Shop DB', url: process.env.SHOP_DB_URL! });
|
|
15
|
+
* await shop.tables(); // [{ schema: 'public', name: 'orders', kind: 'table' }]
|
|
16
|
+
* await ws.agents.create({ name: 'Ops', instructions, databases: ['shop'] });
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* The login is stored encrypted and never read back: this handle reports where
|
|
20
|
+
* the database is, never how to log in to it.
|
|
21
|
+
*/
|
|
22
|
+
export declare class Database {
|
|
23
|
+
#private;
|
|
24
|
+
/** Workspace-unique, and what an agent configuration stores. Not editable:
|
|
25
|
+
* renaming it would silently cut off every agent that names it. */
|
|
26
|
+
readonly key: string;
|
|
27
|
+
private snapshot?;
|
|
28
|
+
constructor(http: Http, key: string, snapshot?: DatabaseData);
|
|
29
|
+
private get base();
|
|
30
|
+
/** The database's current record: label, engine, host, port, name. */
|
|
31
|
+
get(): Promise<DatabaseData>;
|
|
32
|
+
/** What was known when this handle was made, without a round trip.
|
|
33
|
+
* Undefined on a handle built from a bare key. */
|
|
34
|
+
get data(): DatabaseData | undefined;
|
|
35
|
+
/** The tables and views it holds, optionally within one schema. Logs in to
|
|
36
|
+
* ask, so this is also the quickest way to check the login still works. */
|
|
37
|
+
tables(options?: {
|
|
38
|
+
schema?: string;
|
|
39
|
+
}): Promise<DatabaseTable[]>;
|
|
40
|
+
/** One table's columns, with their types. */
|
|
41
|
+
describe(table: string, options?: {
|
|
42
|
+
schema?: string;
|
|
43
|
+
}): Promise<DatabaseColumn[]>;
|
|
44
|
+
/**
|
|
45
|
+
* Remove the database from the workspace, and its stored login with it.
|
|
46
|
+
*
|
|
47
|
+
* REFUSED while any agent's configuration still names it, with an error
|
|
48
|
+
* naming those agents. That refusal is what makes naming a key safe: an agent
|
|
49
|
+
* published against `shop` would otherwise find it gone the next morning and
|
|
50
|
+
* report that it has no data.
|
|
51
|
+
*/
|
|
52
|
+
delete(): Promise<void>;
|
|
53
|
+
}
|