@cerebro-labs/agentnava-sdk 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/agent.d.ts +285 -0
- package/agent.js +394 -0
- package/agent.js.map +1 -0
- package/attach.d.ts +49 -0
- package/attach.js +115 -0
- package/attach.js.map +1 -0
- package/connection-status.d.ts +28 -0
- package/connection-status.js +61 -0
- package/connection-status.js.map +1 -0
- package/connection.d.ts +89 -0
- package/connection.js +143 -0
- package/connection.js.map +1 -0
- package/conversation.d.ts +177 -0
- package/conversation.js +348 -0
- package/conversation.js.map +1 -0
- package/end-user.d.ts +159 -0
- package/end-user.js +209 -0
- package/end-user.js.map +1 -0
- package/errors.d.ts +43 -0
- package/errors.js +55 -0
- package/errors.js.map +1 -0
- package/http.d.ts +32 -0
- package/http.js +192 -0
- package/http.js.map +1 -0
- package/import-tools.d.ts +282 -0
- package/import-tools.js +686 -0
- package/import-tools.js.map +1 -0
- package/index.d.ts +16 -0
- package/index.js +38 -0
- package/index.js.map +1 -0
- package/knowledge-base.d.ts +165 -0
- package/knowledge-base.js +274 -0
- package/knowledge-base.js.map +1 -0
- package/load-agent-dir.d.ts +17 -0
- package/load-agent-dir.js +192 -0
- package/load-agent-dir.js.map +1 -0
- package/package.json +32 -0
- package/project.d.ts +94 -0
- package/project.js +155 -0
- package/project.js.map +1 -0
- package/secret.d.ts +45 -0
- package/secret.js +69 -0
- package/secret.js.map +1 -0
- package/templates-inline.d.ts +2 -0
- package/templates-inline.js +16 -0
- package/templates-inline.js.map +1 -0
- package/templates.d.ts +16 -0
- package/templates.js +32 -0
- package/templates.js.map +1 -0
- package/turn.d.ts +11 -0
- package/turn.js +244 -0
- package/turn.js.map +1 -0
- package/types.d.ts +485 -0
- package/types.js +4 -0
- package/types.js.map +1 -0
- package/version.d.ts +1 -0
- package/version.js +5 -0
- package/version.js.map +1 -0
- package/workflows.d.ts +36 -0
- package/workflows.js +101 -0
- package/workflows.js.map +1 -0
- package/workspace.d.ts +290 -0
- package/workspace.js +390 -0
- package/workspace.js.map +1 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { type Attachable } from './attach.ts';
|
|
2
|
+
import type { Http } from './http.ts';
|
|
3
|
+
import { type Turn } from './turn.ts';
|
|
4
|
+
import type { AgentConversationData, Attachment, Bindings, FileEntry, Message, Requirement } from './types.ts';
|
|
5
|
+
export interface AskOptions {
|
|
6
|
+
attachments?: Attachment[];
|
|
7
|
+
signal?: AbortSignal;
|
|
8
|
+
}
|
|
9
|
+
export interface FileUrlOptions {
|
|
10
|
+
download?: boolean;
|
|
11
|
+
/** Seconds the link stays valid. */
|
|
12
|
+
ttl?: number;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* A conversation you can work with.
|
|
16
|
+
*
|
|
17
|
+
* EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT, reachable as `agentId` once
|
|
18
|
+
* this handle has read anything. A conversation is an agent doing a piece of
|
|
19
|
+
* work; there is no such thing as a conversation with nobody.
|
|
20
|
+
*
|
|
21
|
+
* ADDRESSED FROM ITS PARENT: `agent.conversation(id)`, or
|
|
22
|
+
* `project.conversation(id)` for one a project owns. It used to be
|
|
23
|
+
* `ws.conversation(id)`, which made the parent look optional and is how this
|
|
24
|
+
* SDK came to describe conversations as parentless in its own reference. The
|
|
25
|
+
* objection to moving it was that a caller would then store two ids; in
|
|
26
|
+
* practice the agent id is a constant in their own code, not a per-row value.
|
|
27
|
+
*
|
|
28
|
+
* Holding one makes NO request. The first call on it is the first round trip,
|
|
29
|
+
* because fetching a conversation just to send it a message would be a round
|
|
30
|
+
* trip for nothing.
|
|
31
|
+
*/
|
|
32
|
+
export declare class Conversation {
|
|
33
|
+
#private;
|
|
34
|
+
readonly id: string;
|
|
35
|
+
/** Whatever the call that produced this object already knew. Present when it
|
|
36
|
+
* came from `agent.start()`, absent when it came from `agent.conversation(id)`,
|
|
37
|
+
* which is exactly the difference between having made a request and not. */
|
|
38
|
+
private snapshot?;
|
|
39
|
+
constructor(http: Http, id: string, snapshot?: AgentConversationData);
|
|
40
|
+
private get base();
|
|
41
|
+
/**
|
|
42
|
+
* The agent whose conversation this is.
|
|
43
|
+
*
|
|
44
|
+
* EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT. That is the model, and it
|
|
45
|
+
* is worth stating on the object because the SDK used to hold this value and
|
|
46
|
+
* never show it: `authorize()` read it internally and nothing else could,
|
|
47
|
+
* which made a conversation look parentless from outside.
|
|
48
|
+
*
|
|
49
|
+
* Undefined on a conversation you addressed by id and have not read yet,
|
|
50
|
+
* for the same reason `ready` is: the handle has made no request. Call
|
|
51
|
+
* `get()` to fill it in.
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* const conversation = await agent.start({ endUserId: row.userId });
|
|
55
|
+
* conversation.agentId === agent.id; // already known, no round trip
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
get agentId(): string | undefined;
|
|
59
|
+
/** True when nothing REQUIRED is outstanding.
|
|
60
|
+
*
|
|
61
|
+
* Reads from the last snapshot rather than the network, so it is a property
|
|
62
|
+
* and not a call. It is only meaningful on a conversation you just started
|
|
63
|
+
* or just refreshed; call `get()` to bring it up to date. */
|
|
64
|
+
get ready(): boolean | undefined;
|
|
65
|
+
/** What the conversation is still missing. Same caveat as `ready`. */
|
|
66
|
+
get requires(): Requirement[];
|
|
67
|
+
get(): Promise<AgentConversationData>;
|
|
68
|
+
/**
|
|
69
|
+
* Ask, and get the reply.
|
|
70
|
+
*
|
|
71
|
+
* 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
|
|
73
|
+
* the events; doing both to the same turn throws rather than replaying.
|
|
74
|
+
*/
|
|
75
|
+
ask(text: string, options?: AskOptions): Turn;
|
|
76
|
+
messages(): Promise<Message[]>;
|
|
77
|
+
/** End it, and its drive with it. */
|
|
78
|
+
end(): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Give THIS conversation a credential, and nothing else.
|
|
81
|
+
*
|
|
82
|
+
* ```ts
|
|
83
|
+
* await conversation.attach(gmail); // one account
|
|
84
|
+
* await conversation.attach([gmail, calendar]); // several
|
|
85
|
+
* await conversation.attach({ secrets: { KEY: value } }); // a secret VALUE
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* ONE VERB. `bind` used to sit beside this doing the same job in a different
|
|
89
|
+
* shape, and is gone: two names for one idea is how a surface becomes hard
|
|
90
|
+
* to learn.
|
|
91
|
+
*
|
|
92
|
+
* The narrowest scope there is. It cannot overwrite the agent's shared
|
|
93
|
+
* account or the person's own (ADR 0017).
|
|
94
|
+
*
|
|
95
|
+
* PREFER `agent.start({ attach })` when you already know at launch. Between
|
|
96
|
+
* 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 account.
|
|
98
|
+
*/
|
|
99
|
+
attach(what: Attachable | Attachable[] | Bindings): Promise<AgentConversationData>;
|
|
100
|
+
/**
|
|
101
|
+
* Attach a third-party account to THIS conversation, at `conversation` scope.
|
|
102
|
+
*
|
|
103
|
+
* Send the person to the returned url; the conversation becomes ready when
|
|
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).
|
|
108
|
+
*
|
|
109
|
+
* `agentId` is required alongside the conversation, so this reads the
|
|
110
|
+
* conversation first when it has not already.
|
|
111
|
+
*/
|
|
112
|
+
authorize(provider: string): Promise<{
|
|
113
|
+
url: string;
|
|
114
|
+
connectionId: string;
|
|
115
|
+
}>;
|
|
116
|
+
/**
|
|
117
|
+
* Wait until this conversation has everything it needs.
|
|
118
|
+
*
|
|
119
|
+
* The companion to `authorize()`. That call gives you a URL for your user to
|
|
120
|
+
* visit; this one tells you when they are done, so you do not have to invent
|
|
121
|
+
* a polling loop or work out which field to believe.
|
|
122
|
+
*
|
|
123
|
+
* ```ts
|
|
124
|
+
* const { url } = await conversation.authorize('gmail');
|
|
125
|
+
* showToUser(url); // popup, redirect, QR code, your choice
|
|
126
|
+
* await conversation.waitUntilReady();
|
|
127
|
+
* ```
|
|
128
|
+
*
|
|
129
|
+
* Returns as soon as nothing REQUIRED is outstanding, and returns the
|
|
130
|
+
* conversation so you do not need a second call. An optional requirement
|
|
131
|
+
* left unmet does not hold this up, for the same reason it does not make
|
|
132
|
+
* `ready` false: the conversation genuinely works without it.
|
|
133
|
+
*
|
|
134
|
+
* ON TIMEOUT it throws naming what is still missing, because "timed out" on
|
|
135
|
+
* its own sends you looking in the wrong place. The code is
|
|
136
|
+
* `conversation_not_ready` and it is safe to call again: a person who has
|
|
137
|
+
* not finished signing in yet may simply need longer.
|
|
138
|
+
*
|
|
139
|
+
* This polls rather than subscribing. For a step with a human in the middle,
|
|
140
|
+
* a few seconds of latency costs nothing, and it keeps the method usable
|
|
141
|
+
* from any runtime without a socket.
|
|
142
|
+
*/
|
|
143
|
+
waitUntilReady(options?: {
|
|
144
|
+
/** Give up after this long. Default 2 minutes, which is generous for a
|
|
145
|
+
* sign-in and short enough that a stuck flow does not hang a request. */
|
|
146
|
+
timeoutMs?: number;
|
|
147
|
+
/** Gap between checks. Default 2 seconds. */
|
|
148
|
+
intervalMs?: number;
|
|
149
|
+
/** Cancel the wait, e.g. when the caller's own request is abandoned. */
|
|
150
|
+
signal?: AbortSignal;
|
|
151
|
+
}): Promise<AgentConversationData>;
|
|
152
|
+
/** List one directory. `path`, not `dir`: it is the same parameter every other
|
|
153
|
+
* file route on this API takes, and one name for one idea beats a prettier
|
|
154
|
+
* name used in one place. */
|
|
155
|
+
files(path?: string): Promise<FileEntry[]>;
|
|
156
|
+
/** Read a text file. */
|
|
157
|
+
readFile(path: string): Promise<string>;
|
|
158
|
+
readFileBytes(path: string): Promise<Uint8Array>;
|
|
159
|
+
/**
|
|
160
|
+
* Put a file into the conversation's drive.
|
|
161
|
+
*
|
|
162
|
+
* This is how an application STAGES material: write the contract in, then ask
|
|
163
|
+
* the question about it. Anywhere under the root is fine; `input/` is a
|
|
164
|
+
* convention in the docs and nothing enforces it, because the agent and your
|
|
165
|
+
* application share one namespace.
|
|
166
|
+
*/
|
|
167
|
+
writeFile(path: string, bytes: Uint8Array | ArrayBuffer, contentType?: string): Promise<FileEntry>;
|
|
168
|
+
removeFile(path: string): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* A link to one file that carries its own authorisation.
|
|
171
|
+
*
|
|
172
|
+
* A browser cannot hold an API key, so a Download button has no way to prove
|
|
173
|
+
* it is allowed. This grants that one file for `ttl` seconds and nothing
|
|
174
|
+
* else.
|
|
175
|
+
*/
|
|
176
|
+
fileUrl(path: string, options?: FileUrlOptions): Promise<string>;
|
|
177
|
+
}
|
package/conversation.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// A conversation: one continuous exchange with an agent, with its own history
|
|
2
|
+
// and its own filesystem. Two conversations share nothing.
|
|
3
|
+
import { isBindings, providersOf, secretIdsOf, splitAttachments } from "./attach.js";
|
|
4
|
+
import { AgentNavaError } from "./errors.js";
|
|
5
|
+
import { makeTurn } from "./turn.js";
|
|
6
|
+
/**
|
|
7
|
+
* A conversation you can work with.
|
|
8
|
+
*
|
|
9
|
+
* EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT, reachable as `agentId` once
|
|
10
|
+
* this handle has read anything. A conversation is an agent doing a piece of
|
|
11
|
+
* work; there is no such thing as a conversation with nobody.
|
|
12
|
+
*
|
|
13
|
+
* ADDRESSED FROM ITS PARENT: `agent.conversation(id)`, or
|
|
14
|
+
* `project.conversation(id)` for one a project owns. It used to be
|
|
15
|
+
* `ws.conversation(id)`, which made the parent look optional and is how this
|
|
16
|
+
* SDK came to describe conversations as parentless in its own reference. The
|
|
17
|
+
* objection to moving it was that a caller would then store two ids; in
|
|
18
|
+
* practice the agent id is a constant in their own code, not a per-row value.
|
|
19
|
+
*
|
|
20
|
+
* Holding one makes NO request. The first call on it is the first round trip,
|
|
21
|
+
* because fetching a conversation just to send it a message would be a round
|
|
22
|
+
* trip for nothing.
|
|
23
|
+
*/
|
|
24
|
+
export class Conversation {
|
|
25
|
+
id;
|
|
26
|
+
#http;
|
|
27
|
+
/** Whatever the call that produced this object already knew. Present when it
|
|
28
|
+
* came from `agent.start()`, absent when it came from `agent.conversation(id)`,
|
|
29
|
+
* which is exactly the difference between having made a request and not. */
|
|
30
|
+
snapshot;
|
|
31
|
+
constructor(http, id, snapshot) {
|
|
32
|
+
this.#http = http;
|
|
33
|
+
this.id = id;
|
|
34
|
+
if (snapshot)
|
|
35
|
+
this.snapshot = snapshot;
|
|
36
|
+
}
|
|
37
|
+
get base() {
|
|
38
|
+
return `/v1/conversations/${encodeURIComponent(this.id)}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The agent whose conversation this is.
|
|
42
|
+
*
|
|
43
|
+
* EVERY CONVERSATION BELONGS TO EXACTLY ONE AGENT. That is the model, and it
|
|
44
|
+
* is worth stating on the object because the SDK used to hold this value and
|
|
45
|
+
* never show it: `authorize()` read it internally and nothing else could,
|
|
46
|
+
* which made a conversation look parentless from outside.
|
|
47
|
+
*
|
|
48
|
+
* Undefined on a conversation you addressed by id and have not read yet,
|
|
49
|
+
* for the same reason `ready` is: the handle has made no request. Call
|
|
50
|
+
* `get()` to fill it in.
|
|
51
|
+
*
|
|
52
|
+
* ```ts
|
|
53
|
+
* const conversation = await agent.start({ endUserId: row.userId });
|
|
54
|
+
* conversation.agentId === agent.id; // already known, no round trip
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
get agentId() {
|
|
58
|
+
return this.snapshot?.agentId;
|
|
59
|
+
}
|
|
60
|
+
/** True when nothing REQUIRED is outstanding.
|
|
61
|
+
*
|
|
62
|
+
* Reads from the last snapshot rather than the network, so it is a property
|
|
63
|
+
* and not a call. It is only meaningful on a conversation you just started
|
|
64
|
+
* or just refreshed; call `get()` to bring it up to date. */
|
|
65
|
+
get ready() {
|
|
66
|
+
return this.snapshot?.ready;
|
|
67
|
+
}
|
|
68
|
+
/** What the conversation is still missing. Same caveat as `ready`. */
|
|
69
|
+
get requires() {
|
|
70
|
+
return this.snapshot?.requires ?? [];
|
|
71
|
+
}
|
|
72
|
+
async get() {
|
|
73
|
+
const data = await this.#http.get(this.base);
|
|
74
|
+
this.snapshot = data;
|
|
75
|
+
return data;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Ask, and get the reply.
|
|
79
|
+
*
|
|
80
|
+
* 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
|
|
82
|
+
* the events; doing both to the same turn throws rather than replaying.
|
|
83
|
+
*/
|
|
84
|
+
ask(text, options = {}) {
|
|
85
|
+
const response = this.#http.send(`${this.base}/messages`, {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
body: {
|
|
88
|
+
content: text,
|
|
89
|
+
...(options.attachments?.length && { attachments: options.attachments }),
|
|
90
|
+
},
|
|
91
|
+
...(options.signal && { signal: options.signal }),
|
|
92
|
+
});
|
|
93
|
+
return makeTurn(response);
|
|
94
|
+
}
|
|
95
|
+
async messages() {
|
|
96
|
+
const res = await this.#http.get(`${this.base}/messages`);
|
|
97
|
+
return res.messages ?? [];
|
|
98
|
+
}
|
|
99
|
+
/** End it, and its drive with it. */
|
|
100
|
+
async end() {
|
|
101
|
+
await this.#http.delete(this.base);
|
|
102
|
+
}
|
|
103
|
+
/** Supply the credentials this conversation's agent declared at
|
|
104
|
+
* 'conversation' scope. Merges: sending one secret does not clear another.
|
|
105
|
+
*
|
|
106
|
+
* WRAPPED IN `sessionBindings`, and that envelope is load-bearing. This
|
|
107
|
+
* posted the object at the top level, while `addSessionBindings` reads
|
|
108
|
+
* `body.sessionBindings`. The handler therefore saw undefined, and undefined
|
|
109
|
+
* is NOT an error there: a launch carrying no bindings is legitimate, so it
|
|
110
|
+
* normalises to `{}`.
|
|
111
|
+
*
|
|
112
|
+
* So this returned 200 and bound nothing. `conversation.attach()` sits on
|
|
113
|
+
* top of it, which meant the documented way to grant an account to one
|
|
114
|
+
* thread silently did nothing, and the failure appeared much later as an
|
|
115
|
+
* agent that could not reach a mailbox somebody had plainly connected.
|
|
116
|
+
*
|
|
117
|
+
* `agent.bind` had the same class of bug against its own route and its
|
|
118
|
+
* comment records it. This is the second one, found by reading both
|
|
119
|
+
* together. */
|
|
120
|
+
async #applyBindings(bindings) {
|
|
121
|
+
const data = await this.#http.post(`${this.base}/bindings`, {
|
|
122
|
+
sessionBindings: bindings,
|
|
123
|
+
});
|
|
124
|
+
this.snapshot = data;
|
|
125
|
+
return data;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Give THIS conversation a credential, and nothing else.
|
|
129
|
+
*
|
|
130
|
+
* ```ts
|
|
131
|
+
* await conversation.attach(gmail); // one account
|
|
132
|
+
* await conversation.attach([gmail, calendar]); // several
|
|
133
|
+
* await conversation.attach({ secrets: { KEY: value } }); // a secret VALUE
|
|
134
|
+
* ```
|
|
135
|
+
*
|
|
136
|
+
* ONE VERB. `bind` used to sit beside this doing the same job in a different
|
|
137
|
+
* shape, and is gone: two names for one idea is how a surface becomes hard
|
|
138
|
+
* to learn.
|
|
139
|
+
*
|
|
140
|
+
* The narrowest scope there is. It cannot overwrite the agent's shared
|
|
141
|
+
* account or the person's own (ADR 0017).
|
|
142
|
+
*
|
|
143
|
+
* PREFER `agent.start({ attach })` when you already know at launch. Between
|
|
144
|
+
* 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 account.
|
|
146
|
+
*/
|
|
147
|
+
async attach(what) {
|
|
148
|
+
if (isBindings(what))
|
|
149
|
+
return this.#applyBindings(what);
|
|
150
|
+
const split = splitAttachments(what);
|
|
151
|
+
const connections = await providersOf(split.connections);
|
|
152
|
+
const secretIds = await secretIdsOf(split.secrets);
|
|
153
|
+
return this.#applyBindings({
|
|
154
|
+
...(connections && { connections }),
|
|
155
|
+
...(secretIds && { secretIds }),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Attach a third-party account to THIS conversation, at `conversation` scope.
|
|
160
|
+
*
|
|
161
|
+
* Send the person to the returned url; the conversation becomes ready when
|
|
162
|
+
* they finish. The connection gets its own row and cannot overwrite the
|
|
163
|
+
* agent's shared account, so two people connecting their own Gmail to two
|
|
164
|
+
* conversations of the same agent do not clobber each other (ADR 0017: that
|
|
165
|
+
* was a real bug, found and fixed in this flow).
|
|
166
|
+
*
|
|
167
|
+
* `agentId` is required alongside the conversation, so this reads the
|
|
168
|
+
* conversation first when it has not already.
|
|
169
|
+
*/
|
|
170
|
+
async authorize(provider) {
|
|
171
|
+
// Free when this conversation came from `agent.start()`, one request when
|
|
172
|
+
// it was addressed by id. Every conversation HAS an agent; the handle
|
|
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 };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Wait until this conversation has everything it needs.
|
|
180
|
+
*
|
|
181
|
+
* The companion to `authorize()`. That call gives you a URL for your user to
|
|
182
|
+
* visit; this one tells you when they are done, so you do not have to invent
|
|
183
|
+
* a polling loop or work out which field to believe.
|
|
184
|
+
*
|
|
185
|
+
* ```ts
|
|
186
|
+
* const { url } = await conversation.authorize('gmail');
|
|
187
|
+
* showToUser(url); // popup, redirect, QR code, your choice
|
|
188
|
+
* await conversation.waitUntilReady();
|
|
189
|
+
* ```
|
|
190
|
+
*
|
|
191
|
+
* Returns as soon as nothing REQUIRED is outstanding, and returns the
|
|
192
|
+
* conversation so you do not need a second call. An optional requirement
|
|
193
|
+
* left unmet does not hold this up, for the same reason it does not make
|
|
194
|
+
* `ready` false: the conversation genuinely works without it.
|
|
195
|
+
*
|
|
196
|
+
* ON TIMEOUT it throws naming what is still missing, because "timed out" on
|
|
197
|
+
* its own sends you looking in the wrong place. The code is
|
|
198
|
+
* `conversation_not_ready` and it is safe to call again: a person who has
|
|
199
|
+
* not finished signing in yet may simply need longer.
|
|
200
|
+
*
|
|
201
|
+
* This polls rather than subscribing. For a step with a human in the middle,
|
|
202
|
+
* a few seconds of latency costs nothing, and it keeps the method usable
|
|
203
|
+
* from any runtime without a socket.
|
|
204
|
+
*/
|
|
205
|
+
async waitUntilReady(options = {}) {
|
|
206
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
207
|
+
const intervalMs = options.intervalMs ?? 2_000;
|
|
208
|
+
const deadline = Date.now() + timeoutMs;
|
|
209
|
+
// Checked BEFORE the first sleep. A conversation that is already ready
|
|
210
|
+
// should cost one request, not one request plus an interval.
|
|
211
|
+
let data = await this.get();
|
|
212
|
+
while (!data.ready) {
|
|
213
|
+
if (options.signal?.aborted) {
|
|
214
|
+
throw new AgentNavaError({
|
|
215
|
+
message: 'waitUntilReady was cancelled',
|
|
216
|
+
code: 'aborted',
|
|
217
|
+
status: 0,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
if (Date.now() >= deadline) {
|
|
221
|
+
// Name the outstanding REQUIRED items. The optional ones are not why
|
|
222
|
+
// this failed and listing them would send someone after the wrong
|
|
223
|
+
// thing.
|
|
224
|
+
const outstanding = data.requires
|
|
225
|
+
.filter((r) => r.required)
|
|
226
|
+
.map((r) => `${r.kind} ${r.name}`);
|
|
227
|
+
throw new AgentNavaError({
|
|
228
|
+
message: outstanding.length
|
|
229
|
+
? `Conversation ${this.id} is still waiting on: ${outstanding.join(', ')}`
|
|
230
|
+
: `Conversation ${this.id} did not become ready within ${timeoutMs}ms`,
|
|
231
|
+
code: 'conversation_not_ready',
|
|
232
|
+
status: 0,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
// Never sleep past the deadline, so the timeout means what it says.
|
|
236
|
+
await sleep(Math.min(intervalMs, Math.max(0, deadline - Date.now())), options.signal);
|
|
237
|
+
data = await this.get();
|
|
238
|
+
}
|
|
239
|
+
return data;
|
|
240
|
+
}
|
|
241
|
+
// ---------------------------------------------------------------- the drive
|
|
242
|
+
/** List one directory. `path`, not `dir`: it is the same parameter every other
|
|
243
|
+
* file route on this API takes, and one name for one idea beats a prettier
|
|
244
|
+
* name used in one place. */
|
|
245
|
+
async files(path = '/') {
|
|
246
|
+
const res = await this.#http.get(`${this.base}/files`, { path });
|
|
247
|
+
return (res.files ?? []).map(toFileEntry);
|
|
248
|
+
}
|
|
249
|
+
/** Read a text file. */
|
|
250
|
+
async readFile(path) {
|
|
251
|
+
// The route answers JSON `{path, content}`, not bare text. The envelope is
|
|
252
|
+
// worth keeping on the wire (the canonical path comes back with it), so the
|
|
253
|
+
// unwrapping happens here: a caller asked for the file, not for a wrapper.
|
|
254
|
+
const res = await this.#http.get(`${this.base}/files/content`, { path });
|
|
255
|
+
return res.content ?? '';
|
|
256
|
+
}
|
|
257
|
+
async readFileBytes(path) {
|
|
258
|
+
const res = await this.#http.send(`${this.base}/files/raw`, { query: { path } });
|
|
259
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Put a file into the conversation's drive.
|
|
263
|
+
*
|
|
264
|
+
* This is how an application STAGES material: write the contract in, then ask
|
|
265
|
+
* the question about it. Anywhere under the root is fine; `input/` is a
|
|
266
|
+
* convention in the docs and nothing enforces it, because the agent and your
|
|
267
|
+
* application share one namespace.
|
|
268
|
+
*/
|
|
269
|
+
async writeFile(path, bytes, contentType) {
|
|
270
|
+
return this.#http.request(`${this.base}/files`, {
|
|
271
|
+
method: 'PUT',
|
|
272
|
+
bytes,
|
|
273
|
+
query: { path },
|
|
274
|
+
...(contentType && { contentType }),
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async removeFile(path) {
|
|
278
|
+
await this.#http.delete(`${this.base}/files`, { path });
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* A link to one file that carries its own authorisation.
|
|
282
|
+
*
|
|
283
|
+
* A browser cannot hold an API key, so a Download button has no way to prove
|
|
284
|
+
* it is allowed. This grants that one file for `ttl` seconds and nothing
|
|
285
|
+
* else.
|
|
286
|
+
*/
|
|
287
|
+
async fileUrl(path, options = {}) {
|
|
288
|
+
const res = await this.#http.post(`${this.base}/files/url`, {
|
|
289
|
+
path,
|
|
290
|
+
...(options.download !== undefined && { download: options.download }),
|
|
291
|
+
...(options.ttl !== undefined && { ttl: options.ttl }),
|
|
292
|
+
});
|
|
293
|
+
return res.url;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const CONTENT_TYPES = {
|
|
297
|
+
txt: 'text/plain',
|
|
298
|
+
md: 'text/markdown',
|
|
299
|
+
json: 'application/json',
|
|
300
|
+
csv: 'text/csv',
|
|
301
|
+
html: 'text/html',
|
|
302
|
+
js: 'text/javascript',
|
|
303
|
+
ts: 'text/typescript',
|
|
304
|
+
png: 'image/png',
|
|
305
|
+
jpg: 'image/jpeg',
|
|
306
|
+
jpeg: 'image/jpeg',
|
|
307
|
+
gif: 'image/gif',
|
|
308
|
+
webp: 'image/webp',
|
|
309
|
+
svg: 'image/svg+xml',
|
|
310
|
+
pdf: 'application/pdf',
|
|
311
|
+
};
|
|
312
|
+
/**
|
|
313
|
+
* A Drive entry as the SDK's FileEntry.
|
|
314
|
+
*
|
|
315
|
+
* The Drive speaks `isDir` and `mtime`; the SDK speaks `kind` and an ISO
|
|
316
|
+
* `modifiedAt`. Translating at the edge is the SDK's job: the Drive's shape is
|
|
317
|
+
* what apps/web's DrivePane reads, and changing it to suit this would break a
|
|
318
|
+
* working screen to save a mapping function.
|
|
319
|
+
*
|
|
320
|
+
* `mimeType` is inferred from the extension, because the Drive does not store
|
|
321
|
+
* one (see apps/backend-workers/src/sandbox/drive-reader.ts, which infers it the
|
|
322
|
+
* same way on read).
|
|
323
|
+
*/
|
|
324
|
+
function toFileEntry(entry) {
|
|
325
|
+
const ext = entry.path.slice(entry.path.lastIndexOf('.') + 1).toLowerCase();
|
|
326
|
+
return {
|
|
327
|
+
path: entry.path,
|
|
328
|
+
kind: entry.isDir ? 'directory' : 'file',
|
|
329
|
+
mimeType: entry.isDir ? 'inode/directory' : (CONTENT_TYPES[ext] ?? 'application/octet-stream'),
|
|
330
|
+
size: entry.size,
|
|
331
|
+
modifiedAt: new Date(entry.mtime).toISOString(),
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
/** Wait, and stop waiting early if the caller aborts. */
|
|
335
|
+
function sleep(ms, signal) {
|
|
336
|
+
if (ms <= 0)
|
|
337
|
+
return Promise.resolve();
|
|
338
|
+
return new Promise((resolve) => {
|
|
339
|
+
const timer = setTimeout(done, ms);
|
|
340
|
+
function done() {
|
|
341
|
+
clearTimeout(timer);
|
|
342
|
+
signal?.removeEventListener('abort', done);
|
|
343
|
+
resolve();
|
|
344
|
+
}
|
|
345
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
//# sourceMappingURL=conversation.js.map
|
|
@@ -0,0 +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"]}
|