@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
package/agent.d.ts
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { type Attachable } from './attach.ts';
|
|
2
|
+
import { Conversation } from './conversation.ts';
|
|
3
|
+
import type { Http } from './http.ts';
|
|
4
|
+
import type { AgentData, AgentFields, Bindings, ConnectionStatus, FileEntry, Trigger, Workflow } from './types.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Turn the accounts a caller is holding into the `{ provider: id }` map the
|
|
7
|
+
* wire speaks.
|
|
8
|
+
*
|
|
9
|
+
* Shared by `start`, `agent.attach` and `conversation.attach` so the three
|
|
10
|
+
* cannot drift. A handle you created carries its provider already; one you
|
|
11
|
+
* addressed by id has to read it, which is the only request here and only when
|
|
12
|
+
* it is unavoidable.
|
|
13
|
+
*/
|
|
14
|
+
export interface StartOptions {
|
|
15
|
+
/**
|
|
16
|
+
* Accounts to attach AS THE CONVERSATION STARTS.
|
|
17
|
+
*
|
|
18
|
+
* The common shape for an application with many end users: the person
|
|
19
|
+
* connected Gmail once, and this thread should act as them from its first
|
|
20
|
+
* turn.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* const gmail = await ws.endUser(row.userId).connections.list();
|
|
24
|
+
* const c = await agent.start({ endUserId: row.userId, attach: gmail });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Doing it here rather than with a second `conversation.attach()` call
|
|
28
|
+
* closes a real window: between `start` and the attach, the conversation
|
|
29
|
+
* exists and can be asked something, and it would answer without the
|
|
30
|
+
* account.
|
|
31
|
+
*
|
|
32
|
+
* STILL EXPLICIT. Naming `endUserId` does not attach their accounts; this
|
|
33
|
+
* line does, and you can read it.
|
|
34
|
+
*
|
|
35
|
+
* TAKES SECRETS TOO. A `Secret` from `ws.endUser(id).secrets.create(...)`
|
|
36
|
+
* attaches by id, which is the only way a stored per-person credential can
|
|
37
|
+
* reach a turn: you never hold its value. Mix them freely, they are split by
|
|
38
|
+
* type before the launch.
|
|
39
|
+
*/
|
|
40
|
+
attach?: Attachable | Attachable[];
|
|
41
|
+
/**
|
|
42
|
+
* Secret VALUES for this conversation only.
|
|
43
|
+
*
|
|
44
|
+
* Not `attach`, because a secret has no id you can hand over: binding one
|
|
45
|
+
* takes the value itself. That asymmetry is in the product, not in this
|
|
46
|
+
* naming, and hiding it behind the same word would not remove it.
|
|
47
|
+
*/
|
|
48
|
+
secrets?: Record<string, string>;
|
|
49
|
+
/** Pin a specific version. Defaults to the latest at the moment of starting.
|
|
50
|
+
*
|
|
51
|
+
* A conversation pins its version for life: publishing afterwards does not
|
|
52
|
+
* move a conversation that is already open. */
|
|
53
|
+
version?: number;
|
|
54
|
+
parameters?: Record<string, string | number>;
|
|
55
|
+
/**
|
|
56
|
+
* WHO this conversation is for: one of YOUR users, not one of ours.
|
|
57
|
+
*
|
|
58
|
+
* REQUIRED, and nullable. You must say, and `null` is a real answer: a
|
|
59
|
+
* nightly report belongs to nobody. Optional would let it be forgotten,
|
|
60
|
+
* which is the mistake worth designing against, because a conversation
|
|
61
|
+
* without an owner cannot be listed back to the person it was for.
|
|
62
|
+
*
|
|
63
|
+
* It is a LABEL on the conversation, and nothing else. It does not decide
|
|
64
|
+
* which conversation you get, because `start` always starts a new one.
|
|
65
|
+
* Later you can ask for everything belonging to that person:
|
|
66
|
+
*
|
|
67
|
+
* ```ts
|
|
68
|
+
* const conversation = await agent.start({ endUserId: row.userId });
|
|
69
|
+
* const nobody = await agent.start({ endUserId: null }); // a nightly job
|
|
70
|
+
* const theirs = await agent.conversations({ endUser: row.userId });
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* To CONTINUE a conversation, keep its id and address it directly. That is
|
|
74
|
+
* the only way to continue one, on purpose:
|
|
75
|
+
*
|
|
76
|
+
* ```ts
|
|
77
|
+
* await ws.agent(AGENT).conversation(row.conversationId).ask('and the rest?');
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* The id is YOURS and opaque to us: a row id from your database, not an
|
|
81
|
+
* email and not something we authenticate. We never verify it, so it is
|
|
82
|
+
* exactly as trustworthy as whatever holds your API key.
|
|
83
|
+
*
|
|
84
|
+
* NOT A WORKSPACE MEMBER'S ID. `ws.members()` lists the colleagues who share
|
|
85
|
+
* this account, and their `userId` is a platform id we issued. This field is
|
|
86
|
+
* a different namespace entirely. Both were once spelled `userId`, so
|
|
87
|
+
* passing a member id here was a call that could not fail: there is nothing
|
|
88
|
+
* for an end-user id to be checked against, so it just labelled the
|
|
89
|
+
* conversation with a colleague's internal id and looked fine.
|
|
90
|
+
*/
|
|
91
|
+
endUserId: string | null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* An agent you can configure and start conversations from.
|
|
95
|
+
*
|
|
96
|
+
* Holding one makes NO request: `ws.agent(id)` is free and the first call on it
|
|
97
|
+
* is the first round trip.
|
|
98
|
+
*
|
|
99
|
+
* EVERY METHOD THAT CHANGES THE AGENT PUBLISHES A VERSION. There is no separate
|
|
100
|
+
* save. What you do not send is inherited from the current version, which is the
|
|
101
|
+
* only reason changing one field is safe: a resend that rebuilt the whole
|
|
102
|
+
* configuration would drop whatever the caller forgot, and that bug has shipped
|
|
103
|
+
* here four separate times.
|
|
104
|
+
*/
|
|
105
|
+
export declare class Agent {
|
|
106
|
+
#private;
|
|
107
|
+
readonly id: string;
|
|
108
|
+
private snapshot?;
|
|
109
|
+
constructor(http: Http, id: string, snapshot?: AgentData);
|
|
110
|
+
private get base();
|
|
111
|
+
/** The last-known configuration, from whatever call produced this object.
|
|
112
|
+
* Undefined on an agent you addressed by id and have not read. */
|
|
113
|
+
get data(): AgentData | undefined;
|
|
114
|
+
get(options?: {
|
|
115
|
+
version?: number;
|
|
116
|
+
}): Promise<AgentData>;
|
|
117
|
+
/** Every version, newest first. */
|
|
118
|
+
versions(): Promise<AgentData[]>;
|
|
119
|
+
/** Publish a new version with these fields changed and everything else kept. */
|
|
120
|
+
update(fields: AgentFields): Promise<Agent>;
|
|
121
|
+
delete(): Promise<void>;
|
|
122
|
+
/**
|
|
123
|
+
* Add a workflow, or replace the one with the same name.
|
|
124
|
+
*
|
|
125
|
+
* Reads the current list and sends it back with this one in place, because
|
|
126
|
+
* the API replaces the whole list: a workflow set is a list, and there is no
|
|
127
|
+
* "merge one item" that does not have to decide what a duplicate name means.
|
|
128
|
+
* Deciding it here, once, is better than every caller deciding it.
|
|
129
|
+
*/
|
|
130
|
+
setWorkflow(workflow: Workflow): Promise<Agent>;
|
|
131
|
+
removeWorkflow(name: string): Promise<Agent>;
|
|
132
|
+
/** Replace the trigger list. Sending `[]` removes every trigger, including
|
|
133
|
+
* `chat`, which is what leaves an agent that only runs on a schedule. */
|
|
134
|
+
setTriggers(triggers: Trigger[]): Promise<Agent>;
|
|
135
|
+
/**
|
|
136
|
+
* The agent's knowledge: files every conversation can read.
|
|
137
|
+
*
|
|
138
|
+
* BY REFERENCE, not by copy. A conversation reads the knowledge as it is now,
|
|
139
|
+
* so replacing a file changes what open conversations see on their next turn.
|
|
140
|
+
* That is the opposite of how a version works, and it is deliberate: a price
|
|
141
|
+
* list that could not be corrected without republishing the agent would be
|
|
142
|
+
* useless.
|
|
143
|
+
*/
|
|
144
|
+
readonly knowledge: {
|
|
145
|
+
put: (path: string, bytes: Uint8Array | ArrayBuffer, contentType?: string) => Promise<FileEntry>;
|
|
146
|
+
list: (path?: string) => Promise<FileEntry[]>;
|
|
147
|
+
remove: (path: string) => Promise<void>;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* The WORKSPACE knowledge bases this agent reads.
|
|
151
|
+
*
|
|
152
|
+
* Different from `knowledge` above, and the distinction is the whole point.
|
|
153
|
+
* `agent.knowledge` is files this agent owns; nobody else has them. A
|
|
154
|
+
* knowledge base is shared: one price list, attached to nine agents, corrected
|
|
155
|
+
* in one place.
|
|
156
|
+
*
|
|
157
|
+
* Each attached base is mounted read-only at `/knowledge/<key>/` when a turn
|
|
158
|
+
* starts, so the agent reads it with `ls`, `grep` and `cat`.
|
|
159
|
+
*
|
|
160
|
+
* ATTACHING PUBLISHES A VERSION. Which bases an agent reads is part of its
|
|
161
|
+
* configuration, so the change is diffable and reversible like any other
|
|
162
|
+
* configuration change. The CONTENTS of a base are not versioned: correcting
|
|
163
|
+
* a file changes what every attached agent sees on its next turn.
|
|
164
|
+
*/
|
|
165
|
+
readonly knowledgeBases: {
|
|
166
|
+
/**
|
|
167
|
+
* Attach one base by key.
|
|
168
|
+
*
|
|
169
|
+
* One call, not read-modify-write, deliberately. Two attaches racing
|
|
170
|
+
* through a read of the current list and a write of the whole list would
|
|
171
|
+
* lose one of them silently, and losing an attachment looks exactly like
|
|
172
|
+
* an agent that cannot find its knowledge. The server appends.
|
|
173
|
+
*
|
|
174
|
+
* Attaching a key that names no base is refused, naming the key. Attaching
|
|
175
|
+
* one that is already attached is a no-op, not an error.
|
|
176
|
+
*/
|
|
177
|
+
attach: (key: string) => Promise<Agent>;
|
|
178
|
+
/** Detach one base. The base itself and its files are untouched: this only
|
|
179
|
+
* stops THIS agent from reading it. Detaching one that is not attached is
|
|
180
|
+
* a no-op. */
|
|
181
|
+
detach: (key: string) => Promise<Agent>;
|
|
182
|
+
/** The keys this agent currently reads, in configuration order. */
|
|
183
|
+
list: () => Promise<string[]>;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* Give this agent a credential, so every conversation with it can use one.
|
|
187
|
+
*
|
|
188
|
+
* ```ts
|
|
189
|
+
* await agent.attach(gmail); // one account
|
|
190
|
+
* await agent.attach([gmail, calendar]); // several
|
|
191
|
+
* await agent.attach({ secrets: { KEY: value } }); // a secret VALUE
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* ONE VERB, NOT TWO. This and `bind` used to both exist and do the same job
|
|
195
|
+
* through different shapes, which is how a surface becomes hard to learn:
|
|
196
|
+
* two names for one idea means every reader has to work out whether the
|
|
197
|
+
* difference is meaningful. It is not, so `bind` is gone.
|
|
198
|
+
*
|
|
199
|
+
* "Attach" rather than "bind" because it is the plainer word, it is the one
|
|
200
|
+
* that pairs with `detach`, and `knowledgeBases.attach/detach` already uses
|
|
201
|
+
* it for the same shape of operation.
|
|
202
|
+
*
|
|
203
|
+
* ACCOUNTS TAKE THE OBJECT, SECRETS TAKE A VALUE, and that asymmetry is in
|
|
204
|
+
* the product rather than in the naming. An account has an id you can hand
|
|
205
|
+
* over; a secret does not, because binding one means supplying the value
|
|
206
|
+
* itself. Hiding that behind one shape would not make it go away.
|
|
207
|
+
*
|
|
208
|
+
* The counterpart to `authorize()`, which creates an account AND attaches it
|
|
209
|
+
* in one step. Use this when the account already exists: one team mailbox
|
|
210
|
+
* connected once and attached to nine agents, rather than nine sign-ins.
|
|
211
|
+
*/
|
|
212
|
+
attach(what: Attachable | Attachable[] | Bindings): Promise<Agent>;
|
|
213
|
+
/** What each declared provider's actual state is: DECLARED IS NOT CONNECTED.
|
|
214
|
+
*
|
|
215
|
+
* An agent can require Gmail with nobody signed in, and that is the state a
|
|
216
|
+
* setup checklist has to show. Without this the only way to find the gap is
|
|
217
|
+
* to start a conversation and watch the agent fail. */
|
|
218
|
+
connections(): Promise<ConnectionStatus[]>;
|
|
219
|
+
/**
|
|
220
|
+
* Attach a third-party account to this agent, at `fixed` scope: one account,
|
|
221
|
+
* used by every conversation.
|
|
222
|
+
*
|
|
223
|
+
* Returns a url to send a person to. They sign in there, in their own
|
|
224
|
+
* browser, and the account attaches when they finish. YOU NEVER SEE A TOKEN,
|
|
225
|
+
* which is the entire point: nobody should be pasting a Gmail credential into
|
|
226
|
+
* a config field.
|
|
227
|
+
*
|
|
228
|
+
* This does NOT satisfy a declaration scoped 'conversation'. Use
|
|
229
|
+
* `conversation.authorize()` for those, which attaches the account to that
|
|
230
|
+
* one conversation and cannot overwrite this shared one.
|
|
231
|
+
*/
|
|
232
|
+
authorize(provider: string): Promise<{
|
|
233
|
+
url: string;
|
|
234
|
+
connectionId: string;
|
|
235
|
+
}>;
|
|
236
|
+
/** Detach the account connected for this provider.
|
|
237
|
+
*
|
|
238
|
+
* Reads the raw ROWS rather than the merged status, because revoking needs
|
|
239
|
+
* the connection's id and the merged view is keyed by provider: it answers
|
|
240
|
+
* "is Gmail connected", not "which row is it". */
|
|
241
|
+
disconnect(provider: string): Promise<void>;
|
|
242
|
+
/** Start a conversation. It pins the agent's version now and keeps it. */
|
|
243
|
+
start(options: StartOptions): Promise<Conversation>;
|
|
244
|
+
/**
|
|
245
|
+
* Address one of this agent's conversations by id. No request.
|
|
246
|
+
*
|
|
247
|
+
* Here rather than on the workspace because a conversation belongs to an
|
|
248
|
+
* agent. You already hold the agent: it is a constant in your own code, not
|
|
249
|
+
* a per-row value, so naming it costs nothing and the ownership is visible.
|
|
250
|
+
*
|
|
251
|
+
* ```ts
|
|
252
|
+
* await ws.agent(AGENT).conversation(row.conversationId).ask('carry on');
|
|
253
|
+
* ```
|
|
254
|
+
*/
|
|
255
|
+
conversation(conversationId: string): Conversation;
|
|
256
|
+
/**
|
|
257
|
+
* This agent's most recent conversations, newest first.
|
|
258
|
+
*
|
|
259
|
+
* NOT ALL OF THEM, and the previous sentence here said "every", which was
|
|
260
|
+
* wrong in the way that costs someone an afternoon. The server returns the
|
|
261
|
+
* newest 50 unless you ask for more, and refuses to return more than 200 on
|
|
262
|
+
* any single call (apps/api/src/state.ts listAgentSessions, and the Durable
|
|
263
|
+
* Object twin beside it, clamp identically).
|
|
264
|
+
*
|
|
265
|
+
* There is no cursor on this route yet, so an agent with more than 200
|
|
266
|
+
* conversations CANNOT be fully enumerated through this method. If you are
|
|
267
|
+
* counting or exporting, do not build on it; that needs a paginated route we
|
|
268
|
+
* have not added. `ws.endUser(id).conversations()` DOES have a cursor, if
|
|
269
|
+
* the question is one person's history rather than one agent's.
|
|
270
|
+
*
|
|
271
|
+
* `endUser` narrows to one of YOUR end users, which is what you want for a
|
|
272
|
+
* "your conversations" list. Leaving it out returns everyone's, so leave it
|
|
273
|
+
* out only for an operator view.
|
|
274
|
+
*
|
|
275
|
+
* ```ts
|
|
276
|
+
* const mine = await agent.conversations({ endUser: row.userId });
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
conversations(options?: {
|
|
280
|
+
limit?: number;
|
|
281
|
+
endUser?: string;
|
|
282
|
+
}): Promise<Conversation[]>;
|
|
283
|
+
}
|
|
284
|
+
/** A connection row as `GET /v1/connections` returns it (toConnectionMeta in
|
|
285
|
+
* apps/api/src/connections/store.ts). */
|
package/agent.js
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
// An agent: a configuration you start conversations from.
|
|
2
|
+
var _a;
|
|
3
|
+
import { isBindings, providersOf, secretIdsOf, splitAttachments } from "./attach.js";
|
|
4
|
+
import { toConnectionStatus } from "./connection-status.js";
|
|
5
|
+
import { Conversation } from "./conversation.js";
|
|
6
|
+
/**
|
|
7
|
+
* An agent you can configure and start conversations from.
|
|
8
|
+
*
|
|
9
|
+
* Holding one makes NO request: `ws.agent(id)` is free and the first call on it
|
|
10
|
+
* is the first round trip.
|
|
11
|
+
*
|
|
12
|
+
* EVERY METHOD THAT CHANGES THE AGENT PUBLISHES A VERSION. There is no separate
|
|
13
|
+
* save. What you do not send is inherited from the current version, which is the
|
|
14
|
+
* only reason changing one field is safe: a resend that rebuilt the whole
|
|
15
|
+
* configuration would drop whatever the caller forgot, and that bug has shipped
|
|
16
|
+
* here four separate times.
|
|
17
|
+
*/
|
|
18
|
+
export class Agent {
|
|
19
|
+
id;
|
|
20
|
+
#http;
|
|
21
|
+
snapshot;
|
|
22
|
+
constructor(http, id, snapshot) {
|
|
23
|
+
this.#http = http;
|
|
24
|
+
this.id = id;
|
|
25
|
+
if (snapshot)
|
|
26
|
+
this.snapshot = snapshot;
|
|
27
|
+
}
|
|
28
|
+
get base() {
|
|
29
|
+
return `/v1/agents/${encodeURIComponent(this.id)}`;
|
|
30
|
+
}
|
|
31
|
+
/** The last-known configuration, from whatever call produced this object.
|
|
32
|
+
* Undefined on an agent you addressed by id and have not read. */
|
|
33
|
+
get data() {
|
|
34
|
+
return this.snapshot;
|
|
35
|
+
}
|
|
36
|
+
async get(options = {}) {
|
|
37
|
+
const data = await this.#http.get(this.base, options.version !== undefined ? { version: options.version } : undefined);
|
|
38
|
+
// Only cache the CURRENT configuration. Caching a historical version would
|
|
39
|
+
// make `agent.data` mean different things depending on the last call.
|
|
40
|
+
if (options.version === undefined)
|
|
41
|
+
this.snapshot = data;
|
|
42
|
+
return data;
|
|
43
|
+
}
|
|
44
|
+
/** Every version, newest first. */
|
|
45
|
+
async versions() {
|
|
46
|
+
const res = await this.#http.get(`${this.base}/versions`);
|
|
47
|
+
return res.versions ?? [];
|
|
48
|
+
}
|
|
49
|
+
/** Publish a new version with these fields changed and everything else kept. */
|
|
50
|
+
async update(fields) {
|
|
51
|
+
const data = await this.#http.post(`${this.base}/versions`, fields);
|
|
52
|
+
this.snapshot = data;
|
|
53
|
+
return new _a(this.#http, this.id, data);
|
|
54
|
+
}
|
|
55
|
+
async delete() {
|
|
56
|
+
await this.#http.delete(this.base);
|
|
57
|
+
}
|
|
58
|
+
// --------------------------------------------------------------- workflows
|
|
59
|
+
/**
|
|
60
|
+
* Add a workflow, or replace the one with the same name.
|
|
61
|
+
*
|
|
62
|
+
* Reads the current list and sends it back with this one in place, because
|
|
63
|
+
* the API replaces the whole list: a workflow set is a list, and there is no
|
|
64
|
+
* "merge one item" that does not have to decide what a duplicate name means.
|
|
65
|
+
* Deciding it here, once, is better than every caller deciding it.
|
|
66
|
+
*/
|
|
67
|
+
async setWorkflow(workflow) {
|
|
68
|
+
const current = this.snapshot?.workflows ?? (await this.get()).workflows;
|
|
69
|
+
const next = current.filter((w) => w.name !== workflow.name).concat(workflow);
|
|
70
|
+
return this.update({ workflows: next });
|
|
71
|
+
}
|
|
72
|
+
async removeWorkflow(name) {
|
|
73
|
+
const current = this.snapshot?.workflows ?? (await this.get()).workflows;
|
|
74
|
+
return this.update({ workflows: current.filter((w) => w.name !== name) });
|
|
75
|
+
}
|
|
76
|
+
/** Replace the trigger list. Sending `[]` removes every trigger, including
|
|
77
|
+
* `chat`, which is what leaves an agent that only runs on a schedule. */
|
|
78
|
+
async setTriggers(triggers) {
|
|
79
|
+
return this.update({ triggers });
|
|
80
|
+
}
|
|
81
|
+
// --------------------------------------------------------------- knowledge
|
|
82
|
+
/**
|
|
83
|
+
* The agent's knowledge: files every conversation can read.
|
|
84
|
+
*
|
|
85
|
+
* BY REFERENCE, not by copy. A conversation reads the knowledge as it is now,
|
|
86
|
+
* so replacing a file changes what open conversations see on their next turn.
|
|
87
|
+
* That is the opposite of how a version works, and it is deliberate: a price
|
|
88
|
+
* list that could not be corrected without republishing the agent would be
|
|
89
|
+
* useless.
|
|
90
|
+
*/
|
|
91
|
+
knowledge = {
|
|
92
|
+
put: async (path, bytes, contentType) => this.#http.request(`${this.base}/knowledge/files`, {
|
|
93
|
+
method: 'PUT',
|
|
94
|
+
bytes,
|
|
95
|
+
query: { path },
|
|
96
|
+
...(contentType && { contentType }),
|
|
97
|
+
}),
|
|
98
|
+
list: async (path = '/') => {
|
|
99
|
+
// The route returns EVERY knowledge file for the agent; it takes no
|
|
100
|
+
// directory parameter. Filtering here rather than sending one the server
|
|
101
|
+
// ignores, because a call that looks filtered and is not is worse than
|
|
102
|
+
// no filter at all. Knowledge sets are small, so one read is right.
|
|
103
|
+
const res = await this.#http.get(`${this.base}/knowledge/files`);
|
|
104
|
+
const prefix = path === '/' ? '/' : `${path.replace(/\/+$/, '')}/`;
|
|
105
|
+
return (res.files ?? []).filter((f) => f.path?.startsWith(prefix));
|
|
106
|
+
},
|
|
107
|
+
remove: async (path) => {
|
|
108
|
+
await this.#http.delete(`${this.base}/knowledge/files`, { path });
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
// -------------------------------------------------------- knowledge bases
|
|
112
|
+
/**
|
|
113
|
+
* The WORKSPACE knowledge bases this agent reads.
|
|
114
|
+
*
|
|
115
|
+
* Different from `knowledge` above, and the distinction is the whole point.
|
|
116
|
+
* `agent.knowledge` is files this agent owns; nobody else has them. A
|
|
117
|
+
* knowledge base is shared: one price list, attached to nine agents, corrected
|
|
118
|
+
* in one place.
|
|
119
|
+
*
|
|
120
|
+
* Each attached base is mounted read-only at `/knowledge/<key>/` when a turn
|
|
121
|
+
* starts, so the agent reads it with `ls`, `grep` and `cat`.
|
|
122
|
+
*
|
|
123
|
+
* ATTACHING PUBLISHES A VERSION. Which bases an agent reads is part of its
|
|
124
|
+
* configuration, so the change is diffable and reversible like any other
|
|
125
|
+
* configuration change. The CONTENTS of a base are not versioned: correcting
|
|
126
|
+
* a file changes what every attached agent sees on its next turn.
|
|
127
|
+
*/
|
|
128
|
+
knowledgeBases = {
|
|
129
|
+
/**
|
|
130
|
+
* Attach one base by key.
|
|
131
|
+
*
|
|
132
|
+
* One call, not read-modify-write, deliberately. Two attaches racing
|
|
133
|
+
* through a read of the current list and a write of the whole list would
|
|
134
|
+
* lose one of them silently, and losing an attachment looks exactly like
|
|
135
|
+
* an agent that cannot find its knowledge. The server appends.
|
|
136
|
+
*
|
|
137
|
+
* Attaching a key that names no base is refused, naming the key. Attaching
|
|
138
|
+
* one that is already attached is a no-op, not an error.
|
|
139
|
+
*/
|
|
140
|
+
attach: async (key) => {
|
|
141
|
+
const res = await this.#http.post(`${this.base}/knowledge-bases`, { key });
|
|
142
|
+
this.snapshot = res;
|
|
143
|
+
return this;
|
|
144
|
+
},
|
|
145
|
+
/** Detach one base. The base itself and its files are untouched: this only
|
|
146
|
+
* stops THIS agent from reading it. Detaching one that is not attached is
|
|
147
|
+
* a no-op. */
|
|
148
|
+
detach: async (key) => {
|
|
149
|
+
const res = await this.#http.delete(`${this.base}/knowledge-bases/${encodeURIComponent(key)}`);
|
|
150
|
+
if (res)
|
|
151
|
+
this.snapshot = res;
|
|
152
|
+
return this;
|
|
153
|
+
},
|
|
154
|
+
/** The keys this agent currently reads, in configuration order. */
|
|
155
|
+
list: async () => {
|
|
156
|
+
const current = this.snapshot ?? (await this.get());
|
|
157
|
+
return current.knowledgeBases ?? [];
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
// ------------------------------------------------- credentials, agent tier
|
|
161
|
+
/**
|
|
162
|
+
* Supply the credentials declared at 'fixed' scope: set once, used by every
|
|
163
|
+
* conversation. This does NOT satisfy a declaration scoped 'conversation'.
|
|
164
|
+
*
|
|
165
|
+
* A secret is written to this agent's own scope, so it is readable by this
|
|
166
|
+
* agent and nothing else in the workspace, and it overrides a workspace
|
|
167
|
+
* secret of the same name. Writing one twice replaces the value rather than
|
|
168
|
+
* failing, because `bind` means "this is the credential now".
|
|
169
|
+
*
|
|
170
|
+
* A connection value is the id of an account already attached to the
|
|
171
|
+
* workspace; use `authorize()` when there is no account yet, since that is
|
|
172
|
+
* the flow where a person signs in and no token passes through your code.
|
|
173
|
+
*
|
|
174
|
+
* This used to POST the whole `{secrets, connections}` object to
|
|
175
|
+
* `/v1/agents/:id/bindings`, which answers only to `{kind, decl}` and records
|
|
176
|
+
* a DECLARATION, never a value. So the call 400'd, and had it not, it would
|
|
177
|
+
* have stored the fact that a secret is wanted rather than the secret.
|
|
178
|
+
*/
|
|
179
|
+
async #applyBindings(bindings) {
|
|
180
|
+
for (const [name, value] of Object.entries(bindings.secrets ?? {})) {
|
|
181
|
+
const mine = (await this.#http.get('/v1/secrets', { agentId: this.id })).secrets ?? [];
|
|
182
|
+
const existing = mine.find((s) => s.name === name && s.agentId);
|
|
183
|
+
if (existing)
|
|
184
|
+
await this.#http.patch(`/v1/secrets/${encodeURIComponent(existing.id)}`, { value });
|
|
185
|
+
else
|
|
186
|
+
await this.#http.post('/v1/secrets', { name, value, agentId: this.id });
|
|
187
|
+
}
|
|
188
|
+
for (const [provider, connectionId] of Object.entries(bindings.connections ?? {})) {
|
|
189
|
+
await this.#http.post(`${this.base}/bindings`, {
|
|
190
|
+
kind: 'connection',
|
|
191
|
+
decl: { provider, connectionId, scope: 'fixed' },
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
const data = await this.get();
|
|
195
|
+
this.snapshot = data;
|
|
196
|
+
return new _a(this.#http, this.id, data);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Give this agent a credential, so every conversation with it can use one.
|
|
200
|
+
*
|
|
201
|
+
* ```ts
|
|
202
|
+
* await agent.attach(gmail); // one account
|
|
203
|
+
* await agent.attach([gmail, calendar]); // several
|
|
204
|
+
* await agent.attach({ secrets: { KEY: value } }); // a secret VALUE
|
|
205
|
+
* ```
|
|
206
|
+
*
|
|
207
|
+
* ONE VERB, NOT TWO. This and `bind` used to both exist and do the same job
|
|
208
|
+
* through different shapes, which is how a surface becomes hard to learn:
|
|
209
|
+
* two names for one idea means every reader has to work out whether the
|
|
210
|
+
* difference is meaningful. It is not, so `bind` is gone.
|
|
211
|
+
*
|
|
212
|
+
* "Attach" rather than "bind" because it is the plainer word, it is the one
|
|
213
|
+
* that pairs with `detach`, and `knowledgeBases.attach/detach` already uses
|
|
214
|
+
* it for the same shape of operation.
|
|
215
|
+
*
|
|
216
|
+
* ACCOUNTS TAKE THE OBJECT, SECRETS TAKE A VALUE, and that asymmetry is in
|
|
217
|
+
* the product rather than in the naming. An account has an id you can hand
|
|
218
|
+
* over; a secret does not, because binding one means supplying the value
|
|
219
|
+
* itself. Hiding that behind one shape would not make it go away.
|
|
220
|
+
*
|
|
221
|
+
* The counterpart to `authorize()`, which creates an account AND attaches it
|
|
222
|
+
* in one step. Use this when the account already exists: one team mailbox
|
|
223
|
+
* connected once and attached to nine agents, rather than nine sign-ins.
|
|
224
|
+
*/
|
|
225
|
+
async attach(what) {
|
|
226
|
+
if (isBindings(what))
|
|
227
|
+
return this.#applyBindings(what);
|
|
228
|
+
const split = splitAttachments(what);
|
|
229
|
+
const connections = await providersOf(split.connections);
|
|
230
|
+
const secretIds = await secretIdsOf(split.secrets);
|
|
231
|
+
return this.#applyBindings({
|
|
232
|
+
...(connections && { connections }),
|
|
233
|
+
...(secretIds && { secretIds }),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/** What each declared provider's actual state is: DECLARED IS NOT CONNECTED.
|
|
237
|
+
*
|
|
238
|
+
* An agent can require Gmail with nobody signed in, and that is the state a
|
|
239
|
+
* setup checklist has to show. Without this the only way to find the gap is
|
|
240
|
+
* to start a conversation and watch the agent fail. */
|
|
241
|
+
async connections() {
|
|
242
|
+
// The MERGED view, not the raw connection rows. /v1/connections?agentId=
|
|
243
|
+
// lists rows that exist, so a provider the agent declares and nobody has
|
|
244
|
+
// connected is simply absent from it, which is the one case this method is
|
|
245
|
+
// most often asked about. The server merges declared against live with the
|
|
246
|
+
// same function the agent's own check_connections tool uses.
|
|
247
|
+
const res = await this.#http.get(`${this.base}/connections`);
|
|
248
|
+
return res.connections ?? [];
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Attach a third-party account to this agent, at `fixed` scope: one account,
|
|
252
|
+
* used by every conversation.
|
|
253
|
+
*
|
|
254
|
+
* Returns a url to send a person to. They sign in there, in their own
|
|
255
|
+
* browser, and the account attaches when they finish. YOU NEVER SEE A TOKEN,
|
|
256
|
+
* which is the entire point: nobody should be pasting a Gmail credential into
|
|
257
|
+
* a config field.
|
|
258
|
+
*
|
|
259
|
+
* This does NOT satisfy a declaration scoped 'conversation'. Use
|
|
260
|
+
* `conversation.authorize()` for those, which attaches the account to that
|
|
261
|
+
* one conversation and cannot overwrite this shared one.
|
|
262
|
+
*/
|
|
263
|
+
async authorize(provider) {
|
|
264
|
+
const res = await this.#http.post('/v1/connections/connect', { provider, agentId: this.id });
|
|
265
|
+
// `url` on the way out, `authUrl` on the wire. The SDK's name is the one
|
|
266
|
+
// the reference documents and reads better at a call site.
|
|
267
|
+
return { url: res.authUrl, connectionId: res.connectionId };
|
|
268
|
+
}
|
|
269
|
+
/** Detach the account connected for this provider.
|
|
270
|
+
*
|
|
271
|
+
* Reads the raw ROWS rather than the merged status, because revoking needs
|
|
272
|
+
* the connection's id and the merged view is keyed by provider: it answers
|
|
273
|
+
* "is Gmail connected", not "which row is it". */
|
|
274
|
+
async disconnect(provider) {
|
|
275
|
+
const res = await this.#http.get('/v1/connections', {
|
|
276
|
+
agentId: this.id,
|
|
277
|
+
});
|
|
278
|
+
const match = (res.connections ?? [])
|
|
279
|
+
.map(toConnectionStatus)
|
|
280
|
+
.find((c) => c.provider === provider && c.connected);
|
|
281
|
+
// Nothing connected is not an error: the caller wanted this provider
|
|
282
|
+
// detached, and it is.
|
|
283
|
+
if (!match?.id)
|
|
284
|
+
return;
|
|
285
|
+
await this.#http.post(`/v1/connections/${encodeURIComponent(match.id)}/revoke`);
|
|
286
|
+
}
|
|
287
|
+
// ----------------------------------------------------------- conversations
|
|
288
|
+
/** Start a conversation. It pins the agent's version now and keeps it. */
|
|
289
|
+
async start(options) {
|
|
290
|
+
// Resolved BEFORE the launch, because an account is named on the wire by
|
|
291
|
+
// its provider and a handle addressed by id may not know its own yet.
|
|
292
|
+
// Split first: a Connection is named on the wire by its PROVIDER and a
|
|
293
|
+
// Secret by the declared NAME it fills, so the two cannot share a map.
|
|
294
|
+
const split = splitAttachments(options.attach);
|
|
295
|
+
const connections = await providersOf(split.connections);
|
|
296
|
+
const secretIds = await secretIdsOf(split.secrets);
|
|
297
|
+
const bindings = options.secrets || connections || secretIds
|
|
298
|
+
? {
|
|
299
|
+
...(options.secrets && { secrets: options.secrets }),
|
|
300
|
+
...(secretIds && { secretIds }),
|
|
301
|
+
...(connections && { connections }),
|
|
302
|
+
}
|
|
303
|
+
: undefined;
|
|
304
|
+
const data = await this.#http.post('/v1/conversations', {
|
|
305
|
+
agentId: this.id,
|
|
306
|
+
...(options.version !== undefined && { version: options.version }),
|
|
307
|
+
...(options.parameters && { parameters: options.parameters }),
|
|
308
|
+
// `sessionBindings`, NOT `secrets` and `connections` at the top level.
|
|
309
|
+
//
|
|
310
|
+
// This sent them at the top level and the launch route reads
|
|
311
|
+
// `body.sessionBindings`, so they were dropped in silence: the route's
|
|
312
|
+
// own body type has no top-level `secrets` or `connections` to read.
|
|
313
|
+
// A conversation started with a credential began without it, and the
|
|
314
|
+
// first turn failed somewhere else entirely.
|
|
315
|
+
//
|
|
316
|
+
// Third place this exact envelope mistake was found in one afternoon,
|
|
317
|
+
// after `agent.bind` and `conversation.bind`. The shape of the bug is
|
|
318
|
+
// always the same: the SDK agrees with itself, the route agrees with
|
|
319
|
+
// itself, and nothing compares the two.
|
|
320
|
+
...(bindings && { sessionBindings: bindings }),
|
|
321
|
+
// `!= null`, catching undefined as well as null. The field is REQUIRED
|
|
322
|
+
// in TypeScript, but a JavaScript caller can still omit it, and
|
|
323
|
+
// `!== null` alone then sent `user: {}`: an owner object with no id,
|
|
324
|
+
// which is neither "nobody" nor a person.
|
|
325
|
+
...(options.endUserId != null && { user: { id: options.endUserId } }),
|
|
326
|
+
// ALWAYS a new conversation, even when a user is named.
|
|
327
|
+
//
|
|
328
|
+
// The route resumes that user's most recent conversation when it is
|
|
329
|
+
// given an identity and not told otherwise. That default exists for
|
|
330
|
+
// our own web app, where a page reload must not leave a trail of empty
|
|
331
|
+
// conversations, and it is wrong for a method called `start`: a verb
|
|
332
|
+
// that sometimes does not do the thing is worse than two verbs. It is
|
|
333
|
+
// also ambiguous the moment a user has more than one conversation,
|
|
334
|
+
// since it silently picks the newest.
|
|
335
|
+
//
|
|
336
|
+
// So the SDK does not rely on that default. `start` starts. To
|
|
337
|
+
// continue a conversation, hold its id: `agent.conversation(id)`.
|
|
338
|
+
fresh: true,
|
|
339
|
+
});
|
|
340
|
+
return new Conversation(this.#http, data.id, data);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Address one of this agent's conversations by id. No request.
|
|
344
|
+
*
|
|
345
|
+
* Here rather than on the workspace because a conversation belongs to an
|
|
346
|
+
* agent. You already hold the agent: it is a constant in your own code, not
|
|
347
|
+
* a per-row value, so naming it costs nothing and the ownership is visible.
|
|
348
|
+
*
|
|
349
|
+
* ```ts
|
|
350
|
+
* await ws.agent(AGENT).conversation(row.conversationId).ask('carry on');
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
353
|
+
conversation(conversationId) {
|
|
354
|
+
return new Conversation(this.#http, conversationId);
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* This agent's most recent conversations, newest first.
|
|
358
|
+
*
|
|
359
|
+
* NOT ALL OF THEM, and the previous sentence here said "every", which was
|
|
360
|
+
* wrong in the way that costs someone an afternoon. The server returns the
|
|
361
|
+
* newest 50 unless you ask for more, and refuses to return more than 200 on
|
|
362
|
+
* any single call (apps/api/src/state.ts listAgentSessions, and the Durable
|
|
363
|
+
* Object twin beside it, clamp identically).
|
|
364
|
+
*
|
|
365
|
+
* There is no cursor on this route yet, so an agent with more than 200
|
|
366
|
+
* conversations CANNOT be fully enumerated through this method. If you are
|
|
367
|
+
* counting or exporting, do not build on it; that needs a paginated route we
|
|
368
|
+
* have not added. `ws.endUser(id).conversations()` DOES have a cursor, if
|
|
369
|
+
* the question is one person's history rather than one agent's.
|
|
370
|
+
*
|
|
371
|
+
* `endUser` narrows to one of YOUR end users, which is what you want for a
|
|
372
|
+
* "your conversations" list. Leaving it out returns everyone's, so leave it
|
|
373
|
+
* out only for an operator view.
|
|
374
|
+
*
|
|
375
|
+
* ```ts
|
|
376
|
+
* const mine = await agent.conversations({ endUser: row.userId });
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
async conversations(options = {}) {
|
|
380
|
+
const res = await this.#http.get('/v1/conversations', {
|
|
381
|
+
agentId: this.id,
|
|
382
|
+
...(options.limit !== undefined && { limit: options.limit }),
|
|
383
|
+
// One end user's history. Sending this used to be silently ignored on
|
|
384
|
+
// this route, which answered with everyone's conversations, so an
|
|
385
|
+
// application rendering "your chats" showed somebody else's.
|
|
386
|
+
...(options.endUser !== undefined && { end_user_id: options.endUser }),
|
|
387
|
+
});
|
|
388
|
+
return (res.conversations ?? []).map((c) => new Conversation(this.#http, c.id, c));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
_a = Agent;
|
|
392
|
+
/** A connection row as `GET /v1/connections` returns it (toConnectionMeta in
|
|
393
|
+
* apps/api/src/connections/store.ts). */
|
|
394
|
+
//# sourceMappingURL=agent.js.map
|