@oberik/sdk 0.2.0 → 0.4.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 +347 -10
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +389 -10
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +347 -10
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -61,14 +61,66 @@ export class AgentCancelledError extends Error {
|
|
|
61
61
|
// ============================================================================
|
|
62
62
|
// Errors
|
|
63
63
|
// ============================================================================
|
|
64
|
+
/** Does this look like a page rather than an answer? */
|
|
65
|
+
function looksLikeHtml(text) {
|
|
66
|
+
return /^\s*(<!doctype html|<html|<head|<body)/i.test(text) || /<\/html>\s*$/i.test(text);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One readable sentence out of whatever an error carried.
|
|
70
|
+
*
|
|
71
|
+
* `limits-and-errors.md` promises `{"detail": "…"}` and an `AgentApiError` with
|
|
72
|
+
* `.status` and `.detail`, and two shapes broke that badly enough to cost an afternoon:
|
|
73
|
+
*
|
|
74
|
+
* * **A 502/524 from the edge is an HTML page.** The whole ~4 KB of it went into
|
|
75
|
+
* `message` and `detail`, so logging one error printed ninety lines of
|
|
76
|
+
* `<div class="cf-...">`. There is nothing programmatic in that markup — the useful
|
|
77
|
+
* part is the title and the status — so it is summarised rather than preserved.
|
|
78
|
+
* * **A 422's detail is an array of validation objects**, so the documented
|
|
79
|
+
* `` `${e.detail}` `` prints `[object Object]`. The content was there and the
|
|
80
|
+
* documented shape gave you no reason to go looking for it.
|
|
81
|
+
*/
|
|
82
|
+
function describeDetail(status, detail) {
|
|
83
|
+
if (typeof detail === "string") {
|
|
84
|
+
if (looksLikeHtml(detail)) {
|
|
85
|
+
const title = /<title[^>]*>([^<]{1,160})<\/title>/i.exec(detail)?.[1]?.trim();
|
|
86
|
+
const where = status >= 502 ? " — this came from a proxy in front of the API, not from the API" : "";
|
|
87
|
+
return `HTTP ${status}${title ? `: ${title}` : ""}${where}`;
|
|
88
|
+
}
|
|
89
|
+
return detail.slice(0, 2000) || `HTTP ${status}`;
|
|
90
|
+
}
|
|
91
|
+
if (Array.isArray(detail)) {
|
|
92
|
+
// FastAPI validation: [{loc: ["body","a","b"], msg, type}]
|
|
93
|
+
const lines = detail.map((d) => {
|
|
94
|
+
const item = d;
|
|
95
|
+
const loc = Array.isArray(item?.loc) ? item.loc.join(".") : undefined;
|
|
96
|
+
const msg = item?.msg ?? JSON.stringify(d);
|
|
97
|
+
return loc ? `${loc}: ${msg}` : String(msg);
|
|
98
|
+
});
|
|
99
|
+
return lines.join("; ").slice(0, 2000) || `HTTP ${status}`;
|
|
100
|
+
}
|
|
101
|
+
if (detail && typeof detail === "object") {
|
|
102
|
+
try {
|
|
103
|
+
return JSON.stringify(detail).slice(0, 2000);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return `HTTP ${status}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return `HTTP ${status}`;
|
|
110
|
+
}
|
|
64
111
|
export class AgentApiError extends Error {
|
|
65
112
|
status;
|
|
113
|
+
/** The server's `detail`, in whatever shape it sent — except an HTML error page from
|
|
114
|
+
* an intermediary, which is replaced by the same summary as `message`. Keeping four
|
|
115
|
+
* kilobytes of someone else's markup here helped nobody and buried the status. */
|
|
66
116
|
detail;
|
|
67
117
|
constructor(status, detail) {
|
|
68
|
-
|
|
118
|
+
const described = describeDetail(status, detail);
|
|
119
|
+
super(described);
|
|
69
120
|
this.name = "AgentApiError";
|
|
70
121
|
this.status = status;
|
|
71
|
-
this.detail =
|
|
122
|
+
this.detail =
|
|
123
|
+
typeof detail === "string" && looksLikeHtml(detail) ? described : detail;
|
|
72
124
|
}
|
|
73
125
|
}
|
|
74
126
|
export class AgentStreamError extends Error {
|
|
@@ -102,6 +154,75 @@ function uiToolSchemas(ui) {
|
|
|
102
154
|
},
|
|
103
155
|
}));
|
|
104
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Fold a later turn's payload into the running one.
|
|
159
|
+
*
|
|
160
|
+
* A pause is a new server turn. When the agent stops for a client tool, a question or an
|
|
161
|
+
* approval, resuming starts a *fresh* request, and the server builds its `done` from that
|
|
162
|
+
* turn alone — so the citations from the retrieval it did before the pause simply are not
|
|
163
|
+
* in the payload the loop finally returns.
|
|
164
|
+
*
|
|
165
|
+
* Which is what happened: an app that combined retrieval with its own tools — the app the
|
|
166
|
+
* docs tell you to build — got `citations: 0` and `sources: 0` on every turn where a tool
|
|
167
|
+
* ran, and an empty `ui` for anything drawn before the pause. Meanwhile both were streamed
|
|
168
|
+
* live and both rendered, so it worked on screen and vanished from the object. The docs
|
|
169
|
+
* promise the opposite in as many words: "the `done` payload carries the final state of
|
|
170
|
+
* everything above, so a client that ignored the incremental frames still ends up with the
|
|
171
|
+
* whole turn."
|
|
172
|
+
*
|
|
173
|
+
* Three kinds of field, and getting the kind wrong is its own bug:
|
|
174
|
+
* - accumulated: prose and lists the turn produced across all its segments
|
|
175
|
+
* - deduped: retrieval, which is very likely to repeat across segments
|
|
176
|
+
* - last-wins: what the *server* recomputes each time and is authoritative about
|
|
177
|
+
*/
|
|
178
|
+
function mergeDone(prev, next) {
|
|
179
|
+
if (!prev)
|
|
180
|
+
return next;
|
|
181
|
+
const byKey = (items, key) => {
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
const out = [];
|
|
184
|
+
for (const it of items) {
|
|
185
|
+
const k = key(it);
|
|
186
|
+
if (seen.has(k))
|
|
187
|
+
continue;
|
|
188
|
+
seen.add(k);
|
|
189
|
+
out.push(it);
|
|
190
|
+
}
|
|
191
|
+
return out;
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
...next,
|
|
195
|
+
// Accumulated: everything the agent said and drew, in order.
|
|
196
|
+
content: [prev.content, next.content].filter(Boolean).join(""),
|
|
197
|
+
// Claims accumulate, and the later segment's offsets shift by however much prose came
|
|
198
|
+
// before it. Getting this wrong points a footnote at the wrong sentence, which is worse
|
|
199
|
+
// than having no footnote.
|
|
200
|
+
claims: [
|
|
201
|
+
...(prev.claims ?? []),
|
|
202
|
+
...(next.claims ?? []).map((claim) => ({
|
|
203
|
+
...claim,
|
|
204
|
+
start: claim.start == null ? null : claim.start + prev.content.length,
|
|
205
|
+
end: claim.end == null ? null : claim.end + prev.content.length,
|
|
206
|
+
})),
|
|
207
|
+
],
|
|
208
|
+
attribution: prev.attribution === "per-claim" || next.attribution === "per-claim"
|
|
209
|
+
? "per-claim"
|
|
210
|
+
: prev.attribution === "retrieval-only" || next.attribution === "retrieval-only"
|
|
211
|
+
? "retrieval-only"
|
|
212
|
+
: "none",
|
|
213
|
+
reasoning: [prev.reasoning ?? "", next.reasoning ?? ""].filter(Boolean).join("") || undefined,
|
|
214
|
+
ui: [...(prev.ui ?? []), ...(next.ui ?? [])],
|
|
215
|
+
guard_flags: [...new Set([...(prev.guard_flags ?? []), ...(next.guard_flags ?? [])])],
|
|
216
|
+
// Deduped: the same chunk retrieved twice is one citation, and an attachment carried
|
|
217
|
+
// forward across a pause is one file.
|
|
218
|
+
citations: byKey([...(prev.citations ?? []), ...(next.citations ?? [])], (c) => `${c.document_id}:${c.chunk_index}`),
|
|
219
|
+
sources: byKey([...(prev.sources ?? []), ...(next.sources ?? [])], (s) => String(s.url ?? `${s.document_id}:${s.chunk_index ?? ""}`)),
|
|
220
|
+
attachments: byKey([...(prev.attachments ?? []), ...(next.attachments ?? [])], (a) => String(a.id ?? a.s3_key ?? a.filename)),
|
|
221
|
+
// Last-wins: the server recomputes these per turn and is right about them. `todos` is
|
|
222
|
+
// the whole current plan, not a delta; `subagents` is every subagent of the
|
|
223
|
+
// conversation; `context` describes the window as it is now.
|
|
224
|
+
};
|
|
225
|
+
}
|
|
105
226
|
/** Run each pending client tool via its handler; failures become error results
|
|
106
227
|
* (not thrown) so one bad tool doesn't abort the whole turn. */
|
|
107
228
|
async function executeToolCalls(calls, tools) {
|
|
@@ -870,13 +991,34 @@ export class AgentFramework {
|
|
|
870
991
|
};
|
|
871
992
|
// -- tools ---------------------------------------------------------------
|
|
872
993
|
tools = {
|
|
994
|
+
/** Every tool this token would actually be handed on its next request.
|
|
995
|
+
*
|
|
996
|
+
* `mcp_servers` is present when the project has any, and answers the question the
|
|
997
|
+
* listing alone cannot: "still connecting", "handshake failed" and "this server has
|
|
998
|
+
* no tools" all look identical as an absence of rows. Each entry says whether the
|
|
999
|
+
* server answered, how many tools it contributed, and why not. */
|
|
873
1000
|
list: () => this.request("GET", "/tools"),
|
|
874
1001
|
};
|
|
875
1002
|
// -- chat ----------------------------------------------------------------
|
|
876
1003
|
chat = {
|
|
877
1004
|
/** One round-trip. Returns `requires_action` + `tool_calls` for you to handle
|
|
878
|
-
* manually — use `chat.run` to auto-dispatch client tools instead.
|
|
879
|
-
|
|
1005
|
+
* manually — use `chat.run` to auto-dispatch client tools instead.
|
|
1006
|
+
*
|
|
1007
|
+
* Client *tools* stay manual here; that is the whole difference from `run`. UI
|
|
1008
|
+
* components do not, because there is no manual handling of one: the registry
|
|
1009
|
+
* passed to `createClient({ ui: [...] })` IS the handling, and `chat.stream`
|
|
1010
|
+
* already both declares it and draws from it. A blocking client that registered a
|
|
1011
|
+
* chart renderer got an empty `ui` and no error — the declarations were never sent,
|
|
1012
|
+
* so the agent was not offered the component at all; and when they were repeated by
|
|
1013
|
+
* hand as `ui_tools`, the renderer still never ran. The one wiring `tools.md`
|
|
1014
|
+
* documents produced nothing on the one call it documents it with. */
|
|
1015
|
+
send: async (body) => {
|
|
1016
|
+
const ui = this.resolveUi();
|
|
1017
|
+
const withUi = ui.size && body.ui_tools == null ? { ...body, ui_tools: uiToolSchemas(ui) } : body;
|
|
1018
|
+
const resp = await this.request("POST", "/chat", { body: withUi });
|
|
1019
|
+
this.renderUi(resp.ui, ui);
|
|
1020
|
+
return resp;
|
|
1021
|
+
},
|
|
880
1022
|
/** Blocking chat that AUTO-EXECUTES registered client tools: it sends the
|
|
881
1023
|
* message, and whenever the agent asks for client tools it runs their
|
|
882
1024
|
* handlers, submits the results, and repeats until the agent is done. */
|
|
@@ -931,7 +1073,15 @@ export class AgentFramework {
|
|
|
931
1073
|
body: { session_id: sessionId, question_answers: answers },
|
|
932
1074
|
}),
|
|
933
1075
|
sessions: {
|
|
934
|
-
|
|
1076
|
+
/** Conversations this token may see, newest first.
|
|
1077
|
+
*
|
|
1078
|
+
* Paged: `limit` defaults to 100 and is capped at 500. This used to return every
|
|
1079
|
+
* session in one unbounded response — fine for one end-user, and an admin token
|
|
1080
|
+
* on a busy project gets the whole tenant's history to render a sidebar showing
|
|
1081
|
+
* twenty. Page with `offset`. */
|
|
1082
|
+
list: (userRef, opts = {}) => this.request("GET", "/chat/sessions", {
|
|
1083
|
+
query: { user_ref: userRef, limit: opts.limit, offset: opts.offset },
|
|
1084
|
+
}),
|
|
935
1085
|
/** The agent's plan for this conversation — what a UI renders on a page load,
|
|
936
1086
|
* or between turns. A turn that touched the list also returns it directly. */
|
|
937
1087
|
todos: (sessionId) => this.request("GET", `/chat/sessions/${sessionId}/todos`),
|
|
@@ -1205,6 +1355,35 @@ export class AgentFramework {
|
|
|
1205
1355
|
get: (id) => this.request("GET", `/tasks/${id}`),
|
|
1206
1356
|
cancel: (id) => this.request("POST", `/tasks/${id}/cancel`),
|
|
1207
1357
|
};
|
|
1358
|
+
/**
|
|
1359
|
+
* Turns that start because something happened somewhere else.
|
|
1360
|
+
*
|
|
1361
|
+
* A trigger is a URL you give another system — a ticket tracker, a CI job, a Zapier step
|
|
1362
|
+
* — and the path is the credential, so it runs as a fixed subject chosen at creation.
|
|
1363
|
+
* These were the only routes in the API reference with no SDK method: every integration
|
|
1364
|
+
* hand-wrote `fetch` for them.
|
|
1365
|
+
*
|
|
1366
|
+
* The URL comes back absolute and is not a secret we can show once: the whole point is
|
|
1367
|
+
* that someone else's configuration holds it. `secret` IS shown once — with it, the
|
|
1368
|
+
* sender signs the body and the URL stops being a bearer token.
|
|
1369
|
+
*/
|
|
1370
|
+
triggers = {
|
|
1371
|
+
list: () => this.request("GET", "/triggers"),
|
|
1372
|
+
/** `prompt` may interpolate the event: `"A ticket arrived: {{ body.title }}"`. */
|
|
1373
|
+
create: (opts) => this.request("POST", "/triggers", {
|
|
1374
|
+
body: {
|
|
1375
|
+
prompt: opts.prompt,
|
|
1376
|
+
name: opts.name,
|
|
1377
|
+
system_prompt: opts.systemPrompt,
|
|
1378
|
+
session_id: opts.sessionId,
|
|
1379
|
+
signed: opts.signed ?? false,
|
|
1380
|
+
},
|
|
1381
|
+
}),
|
|
1382
|
+
delete: (triggerId) => this.request("DELETE", `/triggers/${triggerId}`),
|
|
1383
|
+
/** A new URL, with the old one alive for 24 hours — so telling the other system its new
|
|
1384
|
+
* address is not an outage. */
|
|
1385
|
+
rotate: (triggerId) => this.request("POST", `/triggers/${triggerId}/rotate`),
|
|
1386
|
+
};
|
|
1208
1387
|
/**
|
|
1209
1388
|
* What the agent wrote down — remembered facts and wiki pages.
|
|
1210
1389
|
*
|
|
@@ -1298,6 +1477,10 @@ export class AgentFramework {
|
|
|
1298
1477
|
signal: opts.signal,
|
|
1299
1478
|
});
|
|
1300
1479
|
this.renderUi(resp.ui, ui);
|
|
1480
|
+
// What the caller gets back: every segment folded together. Each pause starts a new
|
|
1481
|
+
// server turn, so the last segment's payload knows nothing about the retrieval that
|
|
1482
|
+
// happened before it.
|
|
1483
|
+
let merged = resp;
|
|
1301
1484
|
let rounds = 0;
|
|
1302
1485
|
// Three ways a turn pauses: work for the client to run, a question for the user to
|
|
1303
1486
|
// answer, or permission to ask for. All resume the same way, and the round budget
|
|
@@ -1307,7 +1490,7 @@ export class AgentFramework {
|
|
|
1307
1490
|
const hasQuestions = onQuestion != null && (resp.questions?.length ?? 0) > 0;
|
|
1308
1491
|
const hasApprovals = onApproval != null && (resp.approvals?.length ?? 0) > 0;
|
|
1309
1492
|
if (!hasTools && !hasQuestions && !hasApprovals)
|
|
1310
|
-
return
|
|
1493
|
+
return merged;
|
|
1311
1494
|
if (rounds++ >= maxRounds)
|
|
1312
1495
|
throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
|
|
1313
1496
|
const next = {
|
|
@@ -1323,6 +1506,7 @@ export class AgentFramework {
|
|
|
1323
1506
|
next.approval_decisions = await collectDecisions(resp.approvals, onApproval);
|
|
1324
1507
|
resp = await this.request("POST", "/chat", { body: next, signal: opts.signal });
|
|
1325
1508
|
this.renderUi(resp.ui, ui);
|
|
1509
|
+
merged = mergeDone(merged, resp);
|
|
1326
1510
|
}
|
|
1327
1511
|
}
|
|
1328
1512
|
/** Wrap startStream so a `done` carrying `requires_action` auto-executes the
|
|
@@ -1341,17 +1525,19 @@ export class AgentFramework {
|
|
|
1341
1525
|
const done = (async () => {
|
|
1342
1526
|
const uiSchemas = ui.size ? uiToolSchemas(ui) : undefined;
|
|
1343
1527
|
let reqBody = { ...body, client_tools: schemas, ui_tools: uiSchemas };
|
|
1528
|
+
let merged;
|
|
1344
1529
|
let rounds = 0;
|
|
1345
1530
|
for (;;) {
|
|
1346
1531
|
current = this.startStream(reqBody, handlers);
|
|
1347
1532
|
const d = await current.done;
|
|
1533
|
+
merged = mergeDone(merged, d);
|
|
1348
1534
|
const hasTools = d.requires_action && d.tool_calls.length > 0;
|
|
1349
1535
|
const hasQuestions = onQuestion != null && (d.questions?.length ?? 0) > 0;
|
|
1350
1536
|
const hasApprovals = onApproval != null && (d.approvals?.length ?? 0) > 0;
|
|
1351
1537
|
if (!hasTools && !hasQuestions && !hasApprovals)
|
|
1352
|
-
return
|
|
1538
|
+
return merged;
|
|
1353
1539
|
if (stopped)
|
|
1354
|
-
return
|
|
1540
|
+
return merged;
|
|
1355
1541
|
if (rounds++ >= maxRounds)
|
|
1356
1542
|
throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
|
|
1357
1543
|
reqBody = { session_id: d.session_id, client_tools: schemas, ui_tools: uiSchemas };
|
|
@@ -1499,6 +1685,19 @@ export class AgentFramework {
|
|
|
1499
1685
|
const onOuterAbort = () => ac.abort();
|
|
1500
1686
|
handlers.signal?.addEventListener("abort", onOuterAbort);
|
|
1501
1687
|
const maxRetries = handlers.maxRetries ?? 10;
|
|
1688
|
+
// Did WE stop this, or did the connection?
|
|
1689
|
+
//
|
|
1690
|
+
// `cancel()` posts the cancel and then aborts the reader, and the abort always won
|
|
1691
|
+
// the race against the server's `cancelled` frame — so the documented way to tell
|
|
1692
|
+
// "the user pressed stop" from "the network died" never fired, and every cancel
|
|
1693
|
+
// surfaced as the transport's raw `AbortError`, which reads as a crash. The server
|
|
1694
|
+
// event still throws the same error for the case where cancellation arrives from
|
|
1695
|
+
// somewhere else (another tab, the dashboard).
|
|
1696
|
+
//
|
|
1697
|
+
// `disconnect()` and an aborted `handlers.signal` deliberately do NOT set this: the
|
|
1698
|
+
// run carries on server-side, so calling it cancelled would be a lie.
|
|
1699
|
+
let cancelled = false;
|
|
1700
|
+
const stopReason = () => cancelled ? new AgentCancelledError() : new DOMException("aborted", "AbortError");
|
|
1502
1701
|
// The components this stream may draw. Resolved once: the registry can be added to
|
|
1503
1702
|
// between turns, and a stream should draw with what it was started with.
|
|
1504
1703
|
const uiRegistry = handlers.ui || this.uiRegistry.size ? this.resolveUi(handlers.ui) : undefined;
|
|
@@ -1531,7 +1730,7 @@ export class AgentFramework {
|
|
|
1531
1730
|
let refreshed = false; // one token refresh per connection, reset on progress
|
|
1532
1731
|
for (;;) {
|
|
1533
1732
|
if (ac.signal.aborted)
|
|
1534
|
-
throw
|
|
1733
|
+
throw stopReason();
|
|
1535
1734
|
let shouldRetry = false;
|
|
1536
1735
|
try {
|
|
1537
1736
|
let res = await openConnection();
|
|
@@ -1637,7 +1836,7 @@ export class AgentFramework {
|
|
|
1637
1836
|
}
|
|
1638
1837
|
catch (err) {
|
|
1639
1838
|
if (ac.signal.aborted)
|
|
1640
|
-
throw
|
|
1839
|
+
throw stopReason();
|
|
1641
1840
|
// Terminal errors: server 'error'/'cancelled' event, or an HTTP failure.
|
|
1642
1841
|
if (err instanceof AgentApiError ||
|
|
1643
1842
|
err instanceof AgentCancelledError ||
|
|
@@ -1657,6 +1856,10 @@ export class AgentFramework {
|
|
|
1657
1856
|
}
|
|
1658
1857
|
})().finally(() => handlers.signal?.removeEventListener("abort", onOuterAbort));
|
|
1659
1858
|
const cancel = async () => {
|
|
1859
|
+
// Set BEFORE the abort, and before the network call: the caller may already be
|
|
1860
|
+
// awaiting `done`, and whichever of the two finishes first must report a
|
|
1861
|
+
// cancellation rather than a transport error.
|
|
1862
|
+
cancelled = true;
|
|
1660
1863
|
const id = runId;
|
|
1661
1864
|
if (id) {
|
|
1662
1865
|
try {
|
|
@@ -1970,6 +2173,18 @@ export class OberikProject {
|
|
|
1970
2173
|
};
|
|
1971
2174
|
/** Documents owned by the project rather than by any one end-user: the corpus you
|
|
1972
2175
|
* curate and your users only read. */
|
|
2176
|
+
/**
|
|
2177
|
+
* The corpus you curate.
|
|
2178
|
+
*
|
|
2179
|
+
* Uploads here are stored `tenant`-visible — readable by every end-user of the project —
|
|
2180
|
+
* which is what "a corpus you curate, that users only read" means. That is the default
|
|
2181
|
+
* and the only option, deliberately: a project key is not a person, so there is no
|
|
2182
|
+
* per-user subtree for it to write into. Per-user documents go through the data-plane
|
|
2183
|
+
* client with an end-user token, where the subject IS the owner.
|
|
2184
|
+
*
|
|
2185
|
+
* The docs used to show `visibility: "tenant"` being passed here, which was neither
|
|
2186
|
+
* accepted nor needed — a recipe that worked by luck rather than by expression.
|
|
2187
|
+
*/
|
|
1973
2188
|
documents = {
|
|
1974
2189
|
list: () => this.request("GET", "/documents"),
|
|
1975
2190
|
upload: (file, opts = {}) => {
|
|
@@ -1980,6 +2195,95 @@ export class OberikProject {
|
|
|
1980
2195
|
},
|
|
1981
2196
|
delete: (documentId) => this.request("DELETE", `/documents/${documentId}`),
|
|
1982
2197
|
};
|
|
2198
|
+
/**
|
|
2199
|
+
* The models this project runs on, and the retrieval it uses.
|
|
2200
|
+
*
|
|
2201
|
+
* These had no methods at all: `project-api.md` documents them in a table and every
|
|
2202
|
+
* integration hand-rolled `fetch` for half its setup. `providers.add` is the one that
|
|
2203
|
+
* matters most — it is the step a new project cannot answer a question without, and it
|
|
2204
|
+
* finishes the rest of the setup itself (see `derived` in the response).
|
|
2205
|
+
*
|
|
2206
|
+
* `providers.catalog` is deliberately absent: it is not project-scoped, so it does not
|
|
2207
|
+
* belong on a client whose every path hangs off one project.
|
|
2208
|
+
*/
|
|
2209
|
+
providers = {
|
|
2210
|
+
list: () => this.request("GET", "/providers"),
|
|
2211
|
+
/** Name a chat model AND an embedding model: the first lets the agent answer, the
|
|
2212
|
+
* second lets it index. The response's `derived` says what was set for you. */
|
|
2213
|
+
add: (opts) => this.request("POST", "/providers", opts),
|
|
2214
|
+
edit: (credId, opts) => this.request("PATCH", `/providers/${credId}`, opts),
|
|
2215
|
+
/** Re-read the provider's catalog: a model registered before its price was published
|
|
2216
|
+
* bills nothing, so the usage cap never trips. */
|
|
2217
|
+
refresh: (credId) => this.request("POST", `/providers/${credId}/refresh`, {}),
|
|
2218
|
+
remove: (credId) => this.request("DELETE", `/providers/${credId}`),
|
|
2219
|
+
};
|
|
2220
|
+
/** The model used when a request does not name one. */
|
|
2221
|
+
defaultModel = {
|
|
2222
|
+
set: (model) => this.request("POST", "/default-model", { model }),
|
|
2223
|
+
};
|
|
2224
|
+
/** Embedding and rerank overrides. Set for you when you register an embedding model, so
|
|
2225
|
+
* this is for changing it rather than for getting started. */
|
|
2226
|
+
retrieval = {
|
|
2227
|
+
set: (opts) => this.request("PUT", "/retrieval", opts),
|
|
2228
|
+
/** How many floats a model returns, measured by embedding one word. No provider
|
|
2229
|
+
* publishes it, and a wrong one fails at the first ingest rather than here. */
|
|
2230
|
+
probe: (model) => this.request("POST", "/retrieval/probe", { model }),
|
|
2231
|
+
};
|
|
2232
|
+
/** How documents are read: the built-in parser, or a vision model you choose. */
|
|
2233
|
+
documentProcessor = {
|
|
2234
|
+
set: (opts) => this.request("PUT", "/document-processor", opts),
|
|
2235
|
+
};
|
|
2236
|
+
/** What happens when a conversation outgrows the model's window. */
|
|
2237
|
+
context = {
|
|
2238
|
+
get: () => this.request("GET", "/context"),
|
|
2239
|
+
set: (opts) => this.request("PUT", "/context", opts),
|
|
2240
|
+
};
|
|
2241
|
+
/** Spend and rate caps on this project's LLM key. Enforced by the biller, so they hold
|
|
2242
|
+
* even when the usage views cannot be read. */
|
|
2243
|
+
limits = {
|
|
2244
|
+
get: () => this.request("GET", "/limits"),
|
|
2245
|
+
set: (opts) => this.request("PUT", "/limits", opts),
|
|
2246
|
+
};
|
|
2247
|
+
/** Which models a delegate may run on, and how many may run at once. Without this the
|
|
2248
|
+
* subagents capability stays unavailable however it is granted. */
|
|
2249
|
+
subagents = {
|
|
2250
|
+
set: (opts) => this.request("PUT", "/subagents", opts),
|
|
2251
|
+
};
|
|
2252
|
+
/** Procedures you publish as Agent Plugins, and what your end-users have added. */
|
|
2253
|
+
skills = {
|
|
2254
|
+
list: () => this.request("GET", "/skills"),
|
|
2255
|
+
upload: (zip, filename) => {
|
|
2256
|
+
const form = new FormData();
|
|
2257
|
+
form.append("file", zip, filename ?? zip.name ?? "skill.zip");
|
|
2258
|
+
return this.request("POST", "/skills", undefined, form);
|
|
2259
|
+
},
|
|
2260
|
+
delete: (pluginId) => this.request("DELETE", `/skills/${pluginId}`),
|
|
2261
|
+
};
|
|
2262
|
+
/** MCP servers whose tools join this project's catalog. */
|
|
2263
|
+
mcp = {
|
|
2264
|
+
list: () => this.request("GET", "/mcp"),
|
|
2265
|
+
add: (opts) => this.request("POST", "/mcp", opts),
|
|
2266
|
+
remove: (mcpId) => this.request("DELETE", `/mcp/${mcpId}`),
|
|
2267
|
+
};
|
|
2268
|
+
/** Conversations, and what was said in them. */
|
|
2269
|
+
sessions = {
|
|
2270
|
+
list: () => this.request("GET", "/sessions"),
|
|
2271
|
+
messages: (sessionId) => this.request("GET", `/sessions/${sessionId}/messages`),
|
|
2272
|
+
};
|
|
2273
|
+
/** Scheduled work this project's end-users have created. */
|
|
2274
|
+
tasks = {
|
|
2275
|
+
list: () => this.request("GET", "/tasks"),
|
|
2276
|
+
};
|
|
2277
|
+
/** What the agent has written down: remembered facts and wiki pages. */
|
|
2278
|
+
wiki = {
|
|
2279
|
+
list: () => this.request("GET", "/wiki"),
|
|
2280
|
+
delete: (itemId) => this.request("DELETE", `/wiki/${itemId}`),
|
|
2281
|
+
};
|
|
2282
|
+
/** Live sandboxes, and what to do about one. */
|
|
2283
|
+
sandboxes = {
|
|
2284
|
+
list: () => this.request("GET", "/sandboxes"),
|
|
2285
|
+
action: (sessionId, action) => this.request("POST", `/sandboxes/${sessionId}/${action}`, {}),
|
|
2286
|
+
};
|
|
1983
2287
|
/** Prepended to every request for this project, above anything a caller sends. */
|
|
1984
2288
|
systemPrompt = {
|
|
1985
2289
|
set: (systemPrompt) => this.request("PUT", "/system-prompt", { systemPrompt }),
|
|
@@ -1995,6 +2299,39 @@ export class OberikProject {
|
|
|
1995
2299
|
delete: (toolId) => this.request("DELETE", `/webhook-tools/${toolId}`),
|
|
1996
2300
|
};
|
|
1997
2301
|
/** Server-side keys. A created one is returned once and never again. */
|
|
2302
|
+
/**
|
|
2303
|
+
* Checks on what goes into the model and what comes back.
|
|
2304
|
+
*
|
|
2305
|
+
* The enforcement has existed for a long time and there was no way to configure it — no
|
|
2306
|
+
* route, no dashboard section, no column — so a documentation page described switches
|
|
2307
|
+
* that could not be reached. `set` takes a partial: what you do not mention is left as it
|
|
2308
|
+
* is.
|
|
2309
|
+
*/
|
|
2310
|
+
guardrails = {
|
|
2311
|
+
get: () => this.request("GET", "/guardrails"),
|
|
2312
|
+
// `request(method, path, body, form)` — the payload is the THIRD positional
|
|
2313
|
+
// argument. Wrapping it as `{ body: policy }` sent `{"body":{…}}`, every field of
|
|
2314
|
+
// which is optional, so the route validated it, applied nothing, and answered with
|
|
2315
|
+
// the unchanged policy — which reads exactly like success. Worse, `onViolation`
|
|
2316
|
+
// carries a non-null default, so it WAS written: a project running `flag` that
|
|
2317
|
+
// called `set` to turn groundedness on started refusing answers instead, silently.
|
|
2318
|
+
set: (policy) => this.request("PUT", "/guardrails", policy),
|
|
2319
|
+
};
|
|
2320
|
+
/**
|
|
2321
|
+
* Whether this project can actually answer a question yet.
|
|
2322
|
+
*
|
|
2323
|
+
* A new project has no models, so it can neither answer nor index anything — and the
|
|
2324
|
+
* flag that used to be the closest thing to this (`hasLlm`) meant "a LiteLLM key was
|
|
2325
|
+
* provisioned", which is true from the moment a project exists. Every unfinished step
|
|
2326
|
+
* names what it blocks and the one call that fixes it.
|
|
2327
|
+
*
|
|
2328
|
+
* Worth calling in a deploy check: a project that is not ready fails every request with
|
|
2329
|
+
* the provider's own error, which reads as your bug rather than as missing setup.
|
|
2330
|
+
*/
|
|
2331
|
+
readiness = () => this.request("GET", "/readiness");
|
|
2332
|
+
/** The starting snippet and this project's endpoints — the same one the dashboard and the
|
|
2333
|
+
* SSH gateway show, so there is one of it rather than three. */
|
|
2334
|
+
connect = () => this.request("GET", "/connect");
|
|
1998
2335
|
/**
|
|
1999
2336
|
* Further project keys.
|
|
2000
2337
|
*
|