@oberik/sdk 0.1.0 → 0.2.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/dist/cjs/index.js +322 -35
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +443 -69
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +319 -34
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -89,6 +89,19 @@ function toolSchemas(tools) {
|
|
|
89
89
|
function: { name: t.name, description: t.description, parameters: t.parameters ?? { type: "object", properties: {} } },
|
|
90
90
|
}));
|
|
91
91
|
}
|
|
92
|
+
/** The same shape as a client tool on the wire — the server builds a callable tool from
|
|
93
|
+
* it either way. What differs is what happens when the agent calls it: a client tool's
|
|
94
|
+
* result is awaited, and this one's is nothing. */
|
|
95
|
+
function uiToolSchemas(ui) {
|
|
96
|
+
return [...ui.values()].map((c) => ({
|
|
97
|
+
type: "function",
|
|
98
|
+
function: {
|
|
99
|
+
name: c.name,
|
|
100
|
+
description: c.description,
|
|
101
|
+
parameters: c.parameters ?? { type: "object", properties: {} },
|
|
102
|
+
},
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
92
105
|
/** Run each pending client tool via its handler; failures become error results
|
|
93
106
|
* (not thrown) so one bad tool doesn't abort the whole turn. */
|
|
94
107
|
async function executeToolCalls(calls, tools) {
|
|
@@ -110,6 +123,26 @@ async function executeToolCalls(calls, tools) {
|
|
|
110
123
|
return { tool_call_id: call.id, content };
|
|
111
124
|
}));
|
|
112
125
|
}
|
|
126
|
+
/** Put each approval request to the handler and shape the result for the API.
|
|
127
|
+
*
|
|
128
|
+
* A handler that throws is treated as a refusal rather than as a failure. That is the
|
|
129
|
+
* safe direction and the only defensible one: the alternative is a UI bug becoming an
|
|
130
|
+
* irreversible action. */
|
|
131
|
+
async function collectDecisions(pending, handler) {
|
|
132
|
+
const out = [];
|
|
133
|
+
for (const request of pending) {
|
|
134
|
+
let decision;
|
|
135
|
+
try {
|
|
136
|
+
const result = await handler(request);
|
|
137
|
+
decision = typeof result === "boolean" ? { approved: result } : result;
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
decision = { approved: false, note: String(e?.message ?? e) };
|
|
141
|
+
}
|
|
142
|
+
out.push({ ...decision, tool_call_id: decision.tool_call_id ?? request.tool_call_id });
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
113
146
|
/** Put each paused question batch to the handler and shape the result for the API.
|
|
114
147
|
*
|
|
115
148
|
* A handler that throws is treated as the user declining rather than as a failure:
|
|
@@ -192,6 +225,7 @@ export class AgentFramework {
|
|
|
192
225
|
opts;
|
|
193
226
|
_fetch;
|
|
194
227
|
toolRegistry = new Map();
|
|
228
|
+
uiRegistry = new Map();
|
|
195
229
|
/** Active session watchers, so a streamed turn can mark its own messages seen. */
|
|
196
230
|
watchers = new Set();
|
|
197
231
|
/** The bearer in use: `opts.token` initially, replaced on refresh. */
|
|
@@ -213,6 +247,8 @@ export class AgentFramework {
|
|
|
213
247
|
this.currentToken = opts.token;
|
|
214
248
|
for (const t of opts.tools ?? [])
|
|
215
249
|
this.toolRegistry.set(t.name, t);
|
|
250
|
+
for (const c of opts.ui ?? [])
|
|
251
|
+
this.uiRegistry.set(c.name, c);
|
|
216
252
|
}
|
|
217
253
|
/** Seconds before a token's own expiry at which we stop using it.
|
|
218
254
|
*
|
|
@@ -688,6 +724,19 @@ export class AgentFramework {
|
|
|
688
724
|
this.toolRegistry.set(t.name, t);
|
|
689
725
|
return this;
|
|
690
726
|
}
|
|
727
|
+
/** Register a UI component the agent can draw into your app.
|
|
728
|
+
*
|
|
729
|
+
* Unlike a tool, nothing is handed back: the agent calls it, your `render` runs, and
|
|
730
|
+
* the turn carries on without waiting. */
|
|
731
|
+
registerUi(component) {
|
|
732
|
+
this.uiRegistry.set(component.name, component);
|
|
733
|
+
return this;
|
|
734
|
+
}
|
|
735
|
+
registerUiComponents(components) {
|
|
736
|
+
for (const c of components)
|
|
737
|
+
this.uiRegistry.set(c.name, c);
|
|
738
|
+
return this;
|
|
739
|
+
}
|
|
691
740
|
/** Merge the client-level registry with any per-call tools (per-call wins). */
|
|
692
741
|
resolveTools(extra) {
|
|
693
742
|
const m = new Map(this.toolRegistry);
|
|
@@ -695,6 +744,29 @@ export class AgentFramework {
|
|
|
695
744
|
m.set(t.name, t);
|
|
696
745
|
return m;
|
|
697
746
|
}
|
|
747
|
+
resolveUi(extra) {
|
|
748
|
+
const m = new Map(this.uiRegistry);
|
|
749
|
+
for (const c of extra ?? [])
|
|
750
|
+
m.set(c.name, c);
|
|
751
|
+
return m;
|
|
752
|
+
}
|
|
753
|
+
/** Draw whatever the agent asked for, in order.
|
|
754
|
+
*
|
|
755
|
+
* A component that throws is logged and skipped: one broken chart must not take down
|
|
756
|
+
* the turn that drew it, and there is nothing to report back to the agent anyway. */
|
|
757
|
+
renderUi(renders, registry) {
|
|
758
|
+
for (const r of renders ?? []) {
|
|
759
|
+
const component = registry.get(r.name);
|
|
760
|
+
if (!component)
|
|
761
|
+
continue;
|
|
762
|
+
try {
|
|
763
|
+
component.render(r.args ?? {});
|
|
764
|
+
}
|
|
765
|
+
catch (e) {
|
|
766
|
+
console.warn(`[oberik] UI component "${r.name}" threw while rendering:`, e);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
}
|
|
698
770
|
// -- auth headers --------------------------------------------------------
|
|
699
771
|
/** `bearer` overrides token resolution — used to replay a request with the token a
|
|
700
772
|
* refresh just produced, instead of asking for one again. */
|
|
@@ -999,6 +1071,50 @@ export class AgentFramework {
|
|
|
999
1071
|
},
|
|
1000
1072
|
};
|
|
1001
1073
|
// -- documents -----------------------------------------------------------
|
|
1074
|
+
/** Agent Plugins — the skills this token can reach for, and the ones it may add.
|
|
1075
|
+
*
|
|
1076
|
+
* Two sources, one list: what the project published plus anything this end-user
|
|
1077
|
+
* uploaded. Uploading needs the `plugins:write` capability; reading does not, because
|
|
1078
|
+
* a project that publishes a procedure wants its agent to use it. */
|
|
1079
|
+
plugins = {
|
|
1080
|
+
list: () => this.request("GET", "/plugins"),
|
|
1081
|
+
delete: (id) => this.request("DELETE", `/plugins/${id}`),
|
|
1082
|
+
/** Publish a skill. Re-uploading a name replaces it.
|
|
1083
|
+
*
|
|
1084
|
+
* Takes whatever the customer actually has:
|
|
1085
|
+
*
|
|
1086
|
+
* - a packaged Agent Plugin (`plugin.json` + `skills/`), read as-is;
|
|
1087
|
+
* - a zipped folder of skills, or a single `SKILL.md` — a manifest is written
|
|
1088
|
+
* for them, because requiring one to publish a file of instructions is a
|
|
1089
|
+
* packaging exercise standing in front of the feature;
|
|
1090
|
+
* - a folder's files, from a directory picker, each keyed by its relative path.
|
|
1091
|
+
*
|
|
1092
|
+
* An end-user's plugin is private to them and unioned on top of the project's —
|
|
1093
|
+
* only a project key can publish to everyone. */
|
|
1094
|
+
upload: async (file, opts = {}) => {
|
|
1095
|
+
const form = new FormData();
|
|
1096
|
+
const blob = typeof Blob !== "undefined" && file instanceof Blob
|
|
1097
|
+
? file
|
|
1098
|
+
: new Blob([file], { type: "application/zip" });
|
|
1099
|
+
form.append("file", blob, opts.filename ?? file?.name ?? "plugin.zip");
|
|
1100
|
+
return this.request("POST", "/plugins", { form, signal: opts.signal });
|
|
1101
|
+
},
|
|
1102
|
+
/** Publish a folder of skills without zipping it.
|
|
1103
|
+
*
|
|
1104
|
+
* `files` is what a browser directory picker gives you. Each part is sent under
|
|
1105
|
+
* its path relative to the folder, which is all the server needs to lay the
|
|
1106
|
+
* skills out — so no zip library is needed on your side. */
|
|
1107
|
+
uploadFolder: async (files, opts = {}) => {
|
|
1108
|
+
const form = new FormData();
|
|
1109
|
+
for (const { path, content } of files) {
|
|
1110
|
+
const blob = typeof Blob !== "undefined" && content instanceof Blob
|
|
1111
|
+
? content
|
|
1112
|
+
: new Blob([content]);
|
|
1113
|
+
form.append("files", blob, path);
|
|
1114
|
+
}
|
|
1115
|
+
return this.request("POST", "/plugins", { form, signal: opts.signal });
|
|
1116
|
+
},
|
|
1117
|
+
};
|
|
1002
1118
|
documents = {
|
|
1003
1119
|
list: (query = {}) => this.request("GET", "/documents", { query }),
|
|
1004
1120
|
get: (id) => this.request("GET", `/documents/${id}`),
|
|
@@ -1089,20 +1205,32 @@ export class AgentFramework {
|
|
|
1089
1205
|
get: (id) => this.request("GET", `/tasks/${id}`),
|
|
1090
1206
|
cancel: (id) => this.request("POST", `/tasks/${id}/cancel`),
|
|
1091
1207
|
};
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
/**
|
|
1103
|
-
|
|
1104
|
-
remove: (id) => this.request("DELETE", `/sources/${id}`),
|
|
1208
|
+
/**
|
|
1209
|
+
* What the agent wrote down — remembered facts and wiki pages.
|
|
1210
|
+
*
|
|
1211
|
+
* Retrieval reads these back on every turn, so when an answer looks wrong a
|
|
1212
|
+
* remembered fact is often the reason. The ACL that governs retrieval governs this
|
|
1213
|
+
* too: a caller sees exactly what its token could have retrieved.
|
|
1214
|
+
*/
|
|
1215
|
+
memory = {
|
|
1216
|
+
/** `kind` is "memory" (facts, per end-user) or "wiki" (pages, per user or shared). */
|
|
1217
|
+
list: (opts = {}) => this.request("GET", "/memory", { query: { kind: opts.kind ?? "memory", limit: opts.limit } }),
|
|
1218
|
+
/** Forget one. The agent can write it again; this removes what is there now. */
|
|
1219
|
+
delete: (itemId) => this.request("DELETE", `/memory/${itemId}`),
|
|
1105
1220
|
};
|
|
1221
|
+
/**
|
|
1222
|
+
* What this token can actually do.
|
|
1223
|
+
*
|
|
1224
|
+
* The three gates on a turn are the platform's kill switch, the token's capability
|
|
1225
|
+
* and the per-request `enable_*` flag, and until this existed a client could read
|
|
1226
|
+
* none of them. `flags[].effective` is the useful one: false means setting that flag
|
|
1227
|
+
* changes nothing on this token — which is otherwise indistinguishable from the agent
|
|
1228
|
+
* simply choosing not to use the tool.
|
|
1229
|
+
*
|
|
1230
|
+
* Cheap and safe to call on load: a token asking what it holds is reading its own
|
|
1231
|
+
* claims back, so it needs no capability of its own.
|
|
1232
|
+
*/
|
|
1233
|
+
capabilities = () => this.request("GET", "/capabilities");
|
|
1106
1234
|
// -- governance: audit trail + right-to-be-forgotten (admin) -------------
|
|
1107
1235
|
audit = {
|
|
1108
1236
|
/** Read the tenant's audit trail (admin). Filter by action/subject. */
|
|
@@ -1155,81 +1283,86 @@ export class AgentFramework {
|
|
|
1155
1283
|
return res.blob();
|
|
1156
1284
|
},
|
|
1157
1285
|
};
|
|
1158
|
-
/**
|
|
1159
|
-
* Connect an external data source and keep it live in the agent's knowledge.
|
|
1160
|
-
* Validates the connector, runs an initial sync, and (when an interval is set)
|
|
1161
|
-
* schedules recurring auto-refresh so the data never goes stale.
|
|
1162
|
-
*
|
|
1163
|
-
* await ai.sync({ name: "orders", connector_type: "postgres",
|
|
1164
|
-
* config: { dsn, query: "select id, status, total from orders",
|
|
1165
|
-
* cursor_column: "updated_at" }, sync_interval_seconds: 900 });
|
|
1166
|
-
*/
|
|
1167
|
-
sync = (config) => this.request("POST", "/sources", { body: config });
|
|
1168
1286
|
// ==========================================================================
|
|
1169
1287
|
// Automatic client-tool dispatch
|
|
1170
1288
|
// ==========================================================================
|
|
1171
1289
|
async runWithTools(opts) {
|
|
1172
|
-
const { tools: extra, maxToolRounds, onToolCalls, onQuestion, ...body } = opts;
|
|
1290
|
+
const { tools: extra, ui: extraUi, maxToolRounds, onToolCalls, onQuestion, onApproval, ...body } = opts;
|
|
1173
1291
|
const tools = this.resolveTools(extra);
|
|
1292
|
+
const ui = this.resolveUi(extraUi);
|
|
1174
1293
|
const schemas = tools.size ? toolSchemas(tools) : undefined;
|
|
1294
|
+
const uiSchemas = ui.size ? uiToolSchemas(ui) : undefined;
|
|
1175
1295
|
const maxRounds = maxToolRounds ?? 10;
|
|
1176
1296
|
let resp = await this.request("POST", "/chat", {
|
|
1177
|
-
body: { ...body, client_tools: schemas },
|
|
1297
|
+
body: { ...body, client_tools: schemas, ui_tools: uiSchemas },
|
|
1178
1298
|
signal: opts.signal,
|
|
1179
1299
|
});
|
|
1300
|
+
this.renderUi(resp.ui, ui);
|
|
1180
1301
|
let rounds = 0;
|
|
1181
|
-
//
|
|
1182
|
-
// to
|
|
1183
|
-
// so a pathological alternation can't loop forever.
|
|
1302
|
+
// Three ways a turn pauses: work for the client to run, a question for the user to
|
|
1303
|
+
// answer, or permission to ask for. All resume the same way, and the round budget
|
|
1304
|
+
// covers them together so a pathological alternation can't loop forever.
|
|
1184
1305
|
for (;;) {
|
|
1185
1306
|
const hasTools = resp.requires_action && resp.tool_calls.length > 0;
|
|
1186
1307
|
const hasQuestions = onQuestion != null && (resp.questions?.length ?? 0) > 0;
|
|
1187
|
-
|
|
1308
|
+
const hasApprovals = onApproval != null && (resp.approvals?.length ?? 0) > 0;
|
|
1309
|
+
if (!hasTools && !hasQuestions && !hasApprovals)
|
|
1188
1310
|
return resp;
|
|
1189
1311
|
if (rounds++ >= maxRounds)
|
|
1190
1312
|
throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
|
|
1191
|
-
const next = {
|
|
1313
|
+
const next = {
|
|
1314
|
+
session_id: resp.session_id, client_tools: schemas, ui_tools: uiSchemas,
|
|
1315
|
+
};
|
|
1192
1316
|
if (hasTools) {
|
|
1193
1317
|
onToolCalls?.(resp.tool_calls);
|
|
1194
1318
|
next.tool_results = await executeToolCalls(resp.tool_calls, tools);
|
|
1195
1319
|
}
|
|
1196
1320
|
if (hasQuestions)
|
|
1197
1321
|
next.question_answers = await collectAnswers(resp.questions, onQuestion);
|
|
1322
|
+
if (hasApprovals)
|
|
1323
|
+
next.approval_decisions = await collectDecisions(resp.approvals, onApproval);
|
|
1198
1324
|
resp = await this.request("POST", "/chat", { body: next, signal: opts.signal });
|
|
1325
|
+
this.renderUi(resp.ui, ui);
|
|
1199
1326
|
}
|
|
1200
1327
|
}
|
|
1201
1328
|
/** Wrap startStream so a `done` carrying `requires_action` auto-executes the
|
|
1202
1329
|
* client tools and continues the stream (same session) until the agent ends. */
|
|
1203
1330
|
startStreamWithTools(body, handlers) {
|
|
1204
1331
|
const tools = this.resolveTools(handlers.tools);
|
|
1332
|
+
const ui = this.resolveUi(handlers.ui);
|
|
1205
1333
|
const onQuestion = handlers.onQuestion;
|
|
1206
|
-
|
|
1334
|
+
const onApproval = handlers.onApproval;
|
|
1335
|
+
if (tools.size === 0 && ui.size === 0 && onQuestion == null && onApproval == null)
|
|
1207
1336
|
return this.startStream(body, handlers);
|
|
1208
1337
|
const schemas = tools.size ? toolSchemas(tools) : undefined;
|
|
1209
1338
|
const maxRounds = handlers.maxToolRounds ?? 10;
|
|
1210
1339
|
let current;
|
|
1211
1340
|
let stopped = false;
|
|
1212
1341
|
const done = (async () => {
|
|
1213
|
-
|
|
1342
|
+
const uiSchemas = ui.size ? uiToolSchemas(ui) : undefined;
|
|
1343
|
+
let reqBody = { ...body, client_tools: schemas, ui_tools: uiSchemas };
|
|
1214
1344
|
let rounds = 0;
|
|
1215
1345
|
for (;;) {
|
|
1216
1346
|
current = this.startStream(reqBody, handlers);
|
|
1217
1347
|
const d = await current.done;
|
|
1218
1348
|
const hasTools = d.requires_action && d.tool_calls.length > 0;
|
|
1219
1349
|
const hasQuestions = onQuestion != null && (d.questions?.length ?? 0) > 0;
|
|
1220
|
-
|
|
1350
|
+
const hasApprovals = onApproval != null && (d.approvals?.length ?? 0) > 0;
|
|
1351
|
+
if (!hasTools && !hasQuestions && !hasApprovals)
|
|
1221
1352
|
return d;
|
|
1222
1353
|
if (stopped)
|
|
1223
1354
|
return d;
|
|
1224
1355
|
if (rounds++ >= maxRounds)
|
|
1225
1356
|
throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
|
|
1226
|
-
reqBody = { session_id: d.session_id, client_tools: schemas };
|
|
1357
|
+
reqBody = { session_id: d.session_id, client_tools: schemas, ui_tools: uiSchemas };
|
|
1227
1358
|
if (hasTools) {
|
|
1228
1359
|
handlers.onToolCalls?.(d.tool_calls);
|
|
1229
1360
|
reqBody.tool_results = await executeToolCalls(d.tool_calls, tools);
|
|
1230
1361
|
}
|
|
1231
1362
|
if (hasQuestions)
|
|
1232
1363
|
reqBody.question_answers = await collectAnswers(d.questions, onQuestion);
|
|
1364
|
+
if (hasApprovals)
|
|
1365
|
+
reqBody.approval_decisions = await collectDecisions(d.approvals, onApproval);
|
|
1233
1366
|
}
|
|
1234
1367
|
})();
|
|
1235
1368
|
return makeStreamHandle(done, {
|
|
@@ -1366,6 +1499,9 @@ export class AgentFramework {
|
|
|
1366
1499
|
const onOuterAbort = () => ac.abort();
|
|
1367
1500
|
handlers.signal?.addEventListener("abort", onOuterAbort);
|
|
1368
1501
|
const maxRetries = handlers.maxRetries ?? 10;
|
|
1502
|
+
// The components this stream may draw. Resolved once: the registry can be added to
|
|
1503
|
+
// between turns, and a stream should draw with what it was started with.
|
|
1504
|
+
const uiRegistry = handlers.ui || this.uiRegistry.size ? this.resolveUi(handlers.ui) : undefined;
|
|
1369
1505
|
let runId;
|
|
1370
1506
|
let lastId = -1;
|
|
1371
1507
|
let full = "";
|
|
@@ -1451,6 +1587,12 @@ export class AgentFramework {
|
|
|
1451
1587
|
case "todos":
|
|
1452
1588
|
handlers.onTodos?.(ev.data.todos);
|
|
1453
1589
|
break;
|
|
1590
|
+
case "ui":
|
|
1591
|
+
// Drawn the moment the agent calls it, mid-turn — waiting for `done`
|
|
1592
|
+
// would mean a chart appearing after the paragraph that refers to it.
|
|
1593
|
+
uiRegistry?.get(ev.data.name)?.render(ev.data.args ?? {});
|
|
1594
|
+
handlers.onUi?.(ev.data);
|
|
1595
|
+
break;
|
|
1454
1596
|
case "subagent":
|
|
1455
1597
|
subagents.set(ev.data.subagent.ref, ev.data.subagent);
|
|
1456
1598
|
handlers.onSubagents?.([...subagents.values()]);
|
|
@@ -1735,6 +1877,149 @@ function safeJson(s) {
|
|
|
1735
1877
|
return s;
|
|
1736
1878
|
}
|
|
1737
1879
|
}
|
|
1880
|
+
// ============================================================================
|
|
1881
|
+
// The server side: a project key, and everything it can do
|
|
1882
|
+
// ============================================================================
|
|
1883
|
+
/** Where projects are administered and end-user tokens are minted. A different host
|
|
1884
|
+
* from {@link DEFAULT_BASE_URL}, and a different credential — the two are not
|
|
1885
|
+
* interchangeable, which is the whole reason there are two clients. */
|
|
1886
|
+
export const DEFAULT_CONTROL_PLANE_URL = "https://oberik.com";
|
|
1887
|
+
/**
|
|
1888
|
+
* The control plane, from your backend.
|
|
1889
|
+
*
|
|
1890
|
+
* The other client in this package talks to the data plane as one end-user. This one
|
|
1891
|
+
* holds the project key and administers the project itself: minting those tokens,
|
|
1892
|
+
* setting the capability ceiling, curating the corpus.
|
|
1893
|
+
*
|
|
1894
|
+
* They are separate classes on purpose. A project key can mint a token with any
|
|
1895
|
+
* capability the project allows — and create further keys, and delete the project — so
|
|
1896
|
+
* it must never travel to the same place an end-user token does. Two types make that a
|
|
1897
|
+
* decision someone has to make rather than a field they can accidentally set.
|
|
1898
|
+
*/
|
|
1899
|
+
export class OberikProject {
|
|
1900
|
+
baseUrl;
|
|
1901
|
+
projectId;
|
|
1902
|
+
key;
|
|
1903
|
+
_fetch;
|
|
1904
|
+
constructor(opts) {
|
|
1905
|
+
if (!opts.projectId)
|
|
1906
|
+
throw new Error("projectId is required");
|
|
1907
|
+
if (!opts.projectKey)
|
|
1908
|
+
throw new Error("projectKey is required");
|
|
1909
|
+
// A project key in a browser is never right: it mints any capability the project
|
|
1910
|
+
// allows and can delete the project outright. Bundling this by accident is easy —
|
|
1911
|
+
// it is one import away from the client that DOES belong there — so it fails at
|
|
1912
|
+
// construction, where the stack trace names the file that did it, rather than
|
|
1913
|
+
// shipping and leaking the key to every visitor.
|
|
1914
|
+
if (typeof window !== "undefined" && typeof window.document !== "undefined") {
|
|
1915
|
+
throw new Error("OberikProject holds a project key and must never run in a browser — it can mint " +
|
|
1916
|
+
"any capability the project allows, create more keys, and delete the project. " +
|
|
1917
|
+
"Mint tokens on your server and send the token to the browser instead: " +
|
|
1918
|
+
"createClient({ getToken }).");
|
|
1919
|
+
}
|
|
1920
|
+
this.projectId = opts.projectId;
|
|
1921
|
+
this.key = opts.projectKey;
|
|
1922
|
+
this.baseUrl = (opts.baseUrl || DEFAULT_CONTROL_PLANE_URL).replace(/\/+$/, "");
|
|
1923
|
+
const raw = opts.fetch ?? globalThis.fetch;
|
|
1924
|
+
if (!raw)
|
|
1925
|
+
throw new Error("No fetch available; pass options.fetch");
|
|
1926
|
+
this._fetch = raw.bind(globalThis);
|
|
1927
|
+
}
|
|
1928
|
+
async request(method, path, body, form) {
|
|
1929
|
+
const res = await this._fetch(`${this.baseUrl}/api/projects/${this.projectId}${path}`, {
|
|
1930
|
+
method,
|
|
1931
|
+
headers: {
|
|
1932
|
+
"X-API-Key": this.key,
|
|
1933
|
+
...(body !== undefined ? { "content-type": "application/json" } : {}),
|
|
1934
|
+
},
|
|
1935
|
+
body: form ?? (body !== undefined ? JSON.stringify(body) : undefined),
|
|
1936
|
+
});
|
|
1937
|
+
const text = await res.text();
|
|
1938
|
+
const data = text ? safeJson(text) : null;
|
|
1939
|
+
if (!res.ok) {
|
|
1940
|
+
throw new AgentApiError(res.status, (data && (data.error || data.message)) || `HTTP ${res.status}`);
|
|
1941
|
+
}
|
|
1942
|
+
return data;
|
|
1943
|
+
}
|
|
1944
|
+
tokens = {
|
|
1945
|
+
/**
|
|
1946
|
+
* Mint a short-lived token for ONE end-user.
|
|
1947
|
+
*
|
|
1948
|
+
* const { access_token } = await oberik.tokens.mint({
|
|
1949
|
+
* subject: `${user.orgId}:${user.id}`,
|
|
1950
|
+
* scope: `${user.orgId}:${user.id}`,
|
|
1951
|
+
* capabilities: ["chat", "documents:read"],
|
|
1952
|
+
* });
|
|
1953
|
+
*/
|
|
1954
|
+
mint: (input) => this.request("POST", "/token", input),
|
|
1955
|
+
/**
|
|
1956
|
+
* The same thing shaped as the callback {@link createClient} wants, so the two
|
|
1957
|
+
* halves of this package fit together without a wrapper:
|
|
1958
|
+
*
|
|
1959
|
+
* const ai = createClient({ getToken: oberik.tokens.forUser({ subject: id }) });
|
|
1960
|
+
*
|
|
1961
|
+
* Called again whenever a token expires, so the client refreshes on its own.
|
|
1962
|
+
*/
|
|
1963
|
+
forUser: (input) => async () => (await this.request("POST", "/token", input)).access_token,
|
|
1964
|
+
};
|
|
1965
|
+
/** The capability ceiling: the maximum any token minted here may hold. */
|
|
1966
|
+
capabilities = {
|
|
1967
|
+
get: () => this.request("GET", "/capabilities"),
|
|
1968
|
+
/** Merges — send only what you want to change. */
|
|
1969
|
+
set: (caps) => this.request("PATCH", "", { capabilities: caps }),
|
|
1970
|
+
};
|
|
1971
|
+
/** Documents owned by the project rather than by any one end-user: the corpus you
|
|
1972
|
+
* curate and your users only read. */
|
|
1973
|
+
documents = {
|
|
1974
|
+
list: () => this.request("GET", "/documents"),
|
|
1975
|
+
upload: (file, opts = {}) => {
|
|
1976
|
+
const form = new FormData();
|
|
1977
|
+
form.append("file", file, opts.filename ?? file.name ?? "upload.bin");
|
|
1978
|
+
form.append("tags", (opts.tags ?? []).join(","));
|
|
1979
|
+
return this.request("POST", "/documents", undefined, form);
|
|
1980
|
+
},
|
|
1981
|
+
delete: (documentId) => this.request("DELETE", `/documents/${documentId}`),
|
|
1982
|
+
};
|
|
1983
|
+
/** Prepended to every request for this project, above anything a caller sends. */
|
|
1984
|
+
systemPrompt = {
|
|
1985
|
+
set: (systemPrompt) => this.request("PUT", "/system-prompt", { systemPrompt }),
|
|
1986
|
+
};
|
|
1987
|
+
/** Browser origins allowed to call the data plane with this project's tokens. */
|
|
1988
|
+
origins = {
|
|
1989
|
+
set: (origins) => this.request("PUT", "/origins", { origins }),
|
|
1990
|
+
};
|
|
1991
|
+
/** Tools the agent calls by URL. The signing secret comes back once, on create. */
|
|
1992
|
+
webhookTools = {
|
|
1993
|
+
list: () => this.request("GET", "/webhook-tools"),
|
|
1994
|
+
create: (tool) => this.request("POST", "/webhook-tools", tool),
|
|
1995
|
+
delete: (toolId) => this.request("DELETE", `/webhook-tools/${toolId}`),
|
|
1996
|
+
};
|
|
1997
|
+
/** Server-side keys. A created one is returned once and never again. */
|
|
1998
|
+
/**
|
|
1999
|
+
* Further project keys.
|
|
2000
|
+
*
|
|
2001
|
+
* `scope` is the important argument and it defaults to the narrow one. A `mint` key
|
|
2002
|
+
* can turn your signed-in user into an end-user token and nothing else — it cannot
|
|
2003
|
+
* read the corpus, raise the capability ceiling, issue more keys, or delete the
|
|
2004
|
+
* project. That is what almost every backend actually needs, and it is the difference
|
|
2005
|
+
* between a leaked key costing you some tokens and costing you the workspace.
|
|
2006
|
+
*/
|
|
2007
|
+
keys = {
|
|
2008
|
+
list: () => this.request("GET", "/keys"),
|
|
2009
|
+
create: (name, opts = {}) => this.request("POST", "/keys", { name, scope: opts.scope ?? "mint" }),
|
|
2010
|
+
revoke: (keyId) => this.request("DELETE", `/keys/${keyId}`),
|
|
2011
|
+
};
|
|
2012
|
+
/** Spend, requests, tokens and latency — including per end-user, since spend is
|
|
2013
|
+
* attributed to the token's subject. */
|
|
2014
|
+
usage = {
|
|
2015
|
+
summary: () => this.request("GET", "/usage"),
|
|
2016
|
+
observability: (windowSeconds = 86_400) => this.request("GET", `/observability?window=${windowSeconds}`),
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
/** Factory helper for the server-side client. */
|
|
2020
|
+
export function createProjectClient(opts) {
|
|
2021
|
+
return new OberikProject(opts);
|
|
2022
|
+
}
|
|
1738
2023
|
/** Factory helper. */
|
|
1739
2024
|
export function createClient(opts) {
|
|
1740
2025
|
return new AgentFramework(opts);
|