@oberik/sdk 0.2.0 → 0.3.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 +236 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +316 -3
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +236 -3
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/esm/index.js
CHANGED
|
@@ -102,6 +102,75 @@ function uiToolSchemas(ui) {
|
|
|
102
102
|
},
|
|
103
103
|
}));
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Fold a later turn's payload into the running one.
|
|
107
|
+
*
|
|
108
|
+
* A pause is a new server turn. When the agent stops for a client tool, a question or an
|
|
109
|
+
* approval, resuming starts a *fresh* request, and the server builds its `done` from that
|
|
110
|
+
* turn alone — so the citations from the retrieval it did before the pause simply are not
|
|
111
|
+
* in the payload the loop finally returns.
|
|
112
|
+
*
|
|
113
|
+
* Which is what happened: an app that combined retrieval with its own tools — the app the
|
|
114
|
+
* docs tell you to build — got `citations: 0` and `sources: 0` on every turn where a tool
|
|
115
|
+
* ran, and an empty `ui` for anything drawn before the pause. Meanwhile both were streamed
|
|
116
|
+
* live and both rendered, so it worked on screen and vanished from the object. The docs
|
|
117
|
+
* promise the opposite in as many words: "the `done` payload carries the final state of
|
|
118
|
+
* everything above, so a client that ignored the incremental frames still ends up with the
|
|
119
|
+
* whole turn."
|
|
120
|
+
*
|
|
121
|
+
* Three kinds of field, and getting the kind wrong is its own bug:
|
|
122
|
+
* - accumulated: prose and lists the turn produced across all its segments
|
|
123
|
+
* - deduped: retrieval, which is very likely to repeat across segments
|
|
124
|
+
* - last-wins: what the *server* recomputes each time and is authoritative about
|
|
125
|
+
*/
|
|
126
|
+
function mergeDone(prev, next) {
|
|
127
|
+
if (!prev)
|
|
128
|
+
return next;
|
|
129
|
+
const byKey = (items, key) => {
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
const out = [];
|
|
132
|
+
for (const it of items) {
|
|
133
|
+
const k = key(it);
|
|
134
|
+
if (seen.has(k))
|
|
135
|
+
continue;
|
|
136
|
+
seen.add(k);
|
|
137
|
+
out.push(it);
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
...next,
|
|
143
|
+
// Accumulated: everything the agent said and drew, in order.
|
|
144
|
+
content: [prev.content, next.content].filter(Boolean).join(""),
|
|
145
|
+
// Claims accumulate, and the later segment's offsets shift by however much prose came
|
|
146
|
+
// before it. Getting this wrong points a footnote at the wrong sentence, which is worse
|
|
147
|
+
// than having no footnote.
|
|
148
|
+
claims: [
|
|
149
|
+
...(prev.claims ?? []),
|
|
150
|
+
...(next.claims ?? []).map((claim) => ({
|
|
151
|
+
...claim,
|
|
152
|
+
start: claim.start == null ? null : claim.start + prev.content.length,
|
|
153
|
+
end: claim.end == null ? null : claim.end + prev.content.length,
|
|
154
|
+
})),
|
|
155
|
+
],
|
|
156
|
+
attribution: prev.attribution === "per-claim" || next.attribution === "per-claim"
|
|
157
|
+
? "per-claim"
|
|
158
|
+
: prev.attribution === "retrieval-only" || next.attribution === "retrieval-only"
|
|
159
|
+
? "retrieval-only"
|
|
160
|
+
: "none",
|
|
161
|
+
reasoning: [prev.reasoning ?? "", next.reasoning ?? ""].filter(Boolean).join("") || undefined,
|
|
162
|
+
ui: [...(prev.ui ?? []), ...(next.ui ?? [])],
|
|
163
|
+
guard_flags: [...new Set([...(prev.guard_flags ?? []), ...(next.guard_flags ?? [])])],
|
|
164
|
+
// Deduped: the same chunk retrieved twice is one citation, and an attachment carried
|
|
165
|
+
// forward across a pause is one file.
|
|
166
|
+
citations: byKey([...(prev.citations ?? []), ...(next.citations ?? [])], (c) => `${c.document_id}:${c.chunk_index}`),
|
|
167
|
+
sources: byKey([...(prev.sources ?? []), ...(next.sources ?? [])], (s) => String(s.url ?? `${s.document_id}:${s.chunk_index ?? ""}`)),
|
|
168
|
+
attachments: byKey([...(prev.attachments ?? []), ...(next.attachments ?? [])], (a) => String(a.id ?? a.s3_key ?? a.filename)),
|
|
169
|
+
// Last-wins: the server recomputes these per turn and is right about them. `todos` is
|
|
170
|
+
// the whole current plan, not a delta; `subagents` is every subagent of the
|
|
171
|
+
// conversation; `context` describes the window as it is now.
|
|
172
|
+
};
|
|
173
|
+
}
|
|
105
174
|
/** Run each pending client tool via its handler; failures become error results
|
|
106
175
|
* (not thrown) so one bad tool doesn't abort the whole turn. */
|
|
107
176
|
async function executeToolCalls(calls, tools) {
|
|
@@ -1205,6 +1274,35 @@ export class AgentFramework {
|
|
|
1205
1274
|
get: (id) => this.request("GET", `/tasks/${id}`),
|
|
1206
1275
|
cancel: (id) => this.request("POST", `/tasks/${id}/cancel`),
|
|
1207
1276
|
};
|
|
1277
|
+
/**
|
|
1278
|
+
* Turns that start because something happened somewhere else.
|
|
1279
|
+
*
|
|
1280
|
+
* A trigger is a URL you give another system — a ticket tracker, a CI job, a Zapier step
|
|
1281
|
+
* — and the path is the credential, so it runs as a fixed subject chosen at creation.
|
|
1282
|
+
* These were the only routes in the API reference with no SDK method: every integration
|
|
1283
|
+
* hand-wrote `fetch` for them.
|
|
1284
|
+
*
|
|
1285
|
+
* The URL comes back absolute and is not a secret we can show once: the whole point is
|
|
1286
|
+
* that someone else's configuration holds it. `secret` IS shown once — with it, the
|
|
1287
|
+
* sender signs the body and the URL stops being a bearer token.
|
|
1288
|
+
*/
|
|
1289
|
+
triggers = {
|
|
1290
|
+
list: () => this.request("GET", "/triggers"),
|
|
1291
|
+
/** `prompt` may interpolate the event: `"A ticket arrived: {{ body.title }}"`. */
|
|
1292
|
+
create: (opts) => this.request("POST", "/triggers", {
|
|
1293
|
+
body: {
|
|
1294
|
+
prompt: opts.prompt,
|
|
1295
|
+
name: opts.name,
|
|
1296
|
+
system_prompt: opts.systemPrompt,
|
|
1297
|
+
session_id: opts.sessionId,
|
|
1298
|
+
signed: opts.signed ?? false,
|
|
1299
|
+
},
|
|
1300
|
+
}),
|
|
1301
|
+
delete: (triggerId) => this.request("DELETE", `/triggers/${triggerId}`),
|
|
1302
|
+
/** A new URL, with the old one alive for 24 hours — so telling the other system its new
|
|
1303
|
+
* address is not an outage. */
|
|
1304
|
+
rotate: (triggerId) => this.request("POST", `/triggers/${triggerId}/rotate`),
|
|
1305
|
+
};
|
|
1208
1306
|
/**
|
|
1209
1307
|
* What the agent wrote down — remembered facts and wiki pages.
|
|
1210
1308
|
*
|
|
@@ -1298,6 +1396,10 @@ export class AgentFramework {
|
|
|
1298
1396
|
signal: opts.signal,
|
|
1299
1397
|
});
|
|
1300
1398
|
this.renderUi(resp.ui, ui);
|
|
1399
|
+
// What the caller gets back: every segment folded together. Each pause starts a new
|
|
1400
|
+
// server turn, so the last segment's payload knows nothing about the retrieval that
|
|
1401
|
+
// happened before it.
|
|
1402
|
+
let merged = resp;
|
|
1301
1403
|
let rounds = 0;
|
|
1302
1404
|
// Three ways a turn pauses: work for the client to run, a question for the user to
|
|
1303
1405
|
// answer, or permission to ask for. All resume the same way, and the round budget
|
|
@@ -1307,7 +1409,7 @@ export class AgentFramework {
|
|
|
1307
1409
|
const hasQuestions = onQuestion != null && (resp.questions?.length ?? 0) > 0;
|
|
1308
1410
|
const hasApprovals = onApproval != null && (resp.approvals?.length ?? 0) > 0;
|
|
1309
1411
|
if (!hasTools && !hasQuestions && !hasApprovals)
|
|
1310
|
-
return
|
|
1412
|
+
return merged;
|
|
1311
1413
|
if (rounds++ >= maxRounds)
|
|
1312
1414
|
throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
|
|
1313
1415
|
const next = {
|
|
@@ -1323,6 +1425,7 @@ export class AgentFramework {
|
|
|
1323
1425
|
next.approval_decisions = await collectDecisions(resp.approvals, onApproval);
|
|
1324
1426
|
resp = await this.request("POST", "/chat", { body: next, signal: opts.signal });
|
|
1325
1427
|
this.renderUi(resp.ui, ui);
|
|
1428
|
+
merged = mergeDone(merged, resp);
|
|
1326
1429
|
}
|
|
1327
1430
|
}
|
|
1328
1431
|
/** Wrap startStream so a `done` carrying `requires_action` auto-executes the
|
|
@@ -1341,17 +1444,19 @@ export class AgentFramework {
|
|
|
1341
1444
|
const done = (async () => {
|
|
1342
1445
|
const uiSchemas = ui.size ? uiToolSchemas(ui) : undefined;
|
|
1343
1446
|
let reqBody = { ...body, client_tools: schemas, ui_tools: uiSchemas };
|
|
1447
|
+
let merged;
|
|
1344
1448
|
let rounds = 0;
|
|
1345
1449
|
for (;;) {
|
|
1346
1450
|
current = this.startStream(reqBody, handlers);
|
|
1347
1451
|
const d = await current.done;
|
|
1452
|
+
merged = mergeDone(merged, d);
|
|
1348
1453
|
const hasTools = d.requires_action && d.tool_calls.length > 0;
|
|
1349
1454
|
const hasQuestions = onQuestion != null && (d.questions?.length ?? 0) > 0;
|
|
1350
1455
|
const hasApprovals = onApproval != null && (d.approvals?.length ?? 0) > 0;
|
|
1351
1456
|
if (!hasTools && !hasQuestions && !hasApprovals)
|
|
1352
|
-
return
|
|
1457
|
+
return merged;
|
|
1353
1458
|
if (stopped)
|
|
1354
|
-
return
|
|
1459
|
+
return merged;
|
|
1355
1460
|
if (rounds++ >= maxRounds)
|
|
1356
1461
|
throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
|
|
1357
1462
|
reqBody = { session_id: d.session_id, client_tools: schemas, ui_tools: uiSchemas };
|
|
@@ -1970,6 +2075,18 @@ export class OberikProject {
|
|
|
1970
2075
|
};
|
|
1971
2076
|
/** Documents owned by the project rather than by any one end-user: the corpus you
|
|
1972
2077
|
* curate and your users only read. */
|
|
2078
|
+
/**
|
|
2079
|
+
* The corpus you curate.
|
|
2080
|
+
*
|
|
2081
|
+
* Uploads here are stored `tenant`-visible — readable by every end-user of the project —
|
|
2082
|
+
* which is what "a corpus you curate, that users only read" means. That is the default
|
|
2083
|
+
* and the only option, deliberately: a project key is not a person, so there is no
|
|
2084
|
+
* per-user subtree for it to write into. Per-user documents go through the data-plane
|
|
2085
|
+
* client with an end-user token, where the subject IS the owner.
|
|
2086
|
+
*
|
|
2087
|
+
* The docs used to show `visibility: "tenant"` being passed here, which was neither
|
|
2088
|
+
* accepted nor needed — a recipe that worked by luck rather than by expression.
|
|
2089
|
+
*/
|
|
1973
2090
|
documents = {
|
|
1974
2091
|
list: () => this.request("GET", "/documents"),
|
|
1975
2092
|
upload: (file, opts = {}) => {
|
|
@@ -1980,6 +2097,95 @@ export class OberikProject {
|
|
|
1980
2097
|
},
|
|
1981
2098
|
delete: (documentId) => this.request("DELETE", `/documents/${documentId}`),
|
|
1982
2099
|
};
|
|
2100
|
+
/**
|
|
2101
|
+
* The models this project runs on, and the retrieval it uses.
|
|
2102
|
+
*
|
|
2103
|
+
* These had no methods at all: `project-api.md` documents them in a table and every
|
|
2104
|
+
* integration hand-rolled `fetch` for half its setup. `providers.add` is the one that
|
|
2105
|
+
* matters most — it is the step a new project cannot answer a question without, and it
|
|
2106
|
+
* finishes the rest of the setup itself (see `derived` in the response).
|
|
2107
|
+
*
|
|
2108
|
+
* `providers.catalog` is deliberately absent: it is not project-scoped, so it does not
|
|
2109
|
+
* belong on a client whose every path hangs off one project.
|
|
2110
|
+
*/
|
|
2111
|
+
providers = {
|
|
2112
|
+
list: () => this.request("GET", "/providers"),
|
|
2113
|
+
/** Name a chat model AND an embedding model: the first lets the agent answer, the
|
|
2114
|
+
* second lets it index. The response's `derived` says what was set for you. */
|
|
2115
|
+
add: (opts) => this.request("POST", "/providers", opts),
|
|
2116
|
+
edit: (credId, opts) => this.request("PATCH", `/providers/${credId}`, opts),
|
|
2117
|
+
/** Re-read the provider's catalog: a model registered before its price was published
|
|
2118
|
+
* bills nothing, so the usage cap never trips. */
|
|
2119
|
+
refresh: (credId) => this.request("POST", `/providers/${credId}/refresh`, {}),
|
|
2120
|
+
remove: (credId) => this.request("DELETE", `/providers/${credId}`),
|
|
2121
|
+
};
|
|
2122
|
+
/** The model used when a request does not name one. */
|
|
2123
|
+
defaultModel = {
|
|
2124
|
+
set: (model) => this.request("POST", "/default-model", { model }),
|
|
2125
|
+
};
|
|
2126
|
+
/** Embedding and rerank overrides. Set for you when you register an embedding model, so
|
|
2127
|
+
* this is for changing it rather than for getting started. */
|
|
2128
|
+
retrieval = {
|
|
2129
|
+
set: (opts) => this.request("PUT", "/retrieval", opts),
|
|
2130
|
+
/** How many floats a model returns, measured by embedding one word. No provider
|
|
2131
|
+
* publishes it, and a wrong one fails at the first ingest rather than here. */
|
|
2132
|
+
probe: (model) => this.request("POST", "/retrieval/probe", { model }),
|
|
2133
|
+
};
|
|
2134
|
+
/** How documents are read: the built-in parser, or a vision model you choose. */
|
|
2135
|
+
documentProcessor = {
|
|
2136
|
+
set: (opts) => this.request("PUT", "/document-processor", opts),
|
|
2137
|
+
};
|
|
2138
|
+
/** What happens when a conversation outgrows the model's window. */
|
|
2139
|
+
context = {
|
|
2140
|
+
get: () => this.request("GET", "/context"),
|
|
2141
|
+
set: (opts) => this.request("PUT", "/context", opts),
|
|
2142
|
+
};
|
|
2143
|
+
/** Spend and rate caps on this project's LLM key. Enforced by the biller, so they hold
|
|
2144
|
+
* even when the usage views cannot be read. */
|
|
2145
|
+
limits = {
|
|
2146
|
+
get: () => this.request("GET", "/limits"),
|
|
2147
|
+
set: (opts) => this.request("PUT", "/limits", opts),
|
|
2148
|
+
};
|
|
2149
|
+
/** Which models a delegate may run on, and how many may run at once. Without this the
|
|
2150
|
+
* subagents capability stays unavailable however it is granted. */
|
|
2151
|
+
subagents = {
|
|
2152
|
+
set: (opts) => this.request("PUT", "/subagents", opts),
|
|
2153
|
+
};
|
|
2154
|
+
/** Procedures you publish as Agent Plugins, and what your end-users have added. */
|
|
2155
|
+
skills = {
|
|
2156
|
+
list: () => this.request("GET", "/skills"),
|
|
2157
|
+
upload: (zip, filename) => {
|
|
2158
|
+
const form = new FormData();
|
|
2159
|
+
form.append("file", zip, filename ?? zip.name ?? "skill.zip");
|
|
2160
|
+
return this.request("POST", "/skills", undefined, form);
|
|
2161
|
+
},
|
|
2162
|
+
delete: (pluginId) => this.request("DELETE", `/skills/${pluginId}`),
|
|
2163
|
+
};
|
|
2164
|
+
/** MCP servers whose tools join this project's catalog. */
|
|
2165
|
+
mcp = {
|
|
2166
|
+
list: () => this.request("GET", "/mcp"),
|
|
2167
|
+
add: (opts) => this.request("POST", "/mcp", opts),
|
|
2168
|
+
remove: (mcpId) => this.request("DELETE", `/mcp/${mcpId}`),
|
|
2169
|
+
};
|
|
2170
|
+
/** Conversations, and what was said in them. */
|
|
2171
|
+
sessions = {
|
|
2172
|
+
list: () => this.request("GET", "/sessions"),
|
|
2173
|
+
messages: (sessionId) => this.request("GET", `/sessions/${sessionId}/messages`),
|
|
2174
|
+
};
|
|
2175
|
+
/** Scheduled work this project's end-users have created. */
|
|
2176
|
+
tasks = {
|
|
2177
|
+
list: () => this.request("GET", "/tasks"),
|
|
2178
|
+
};
|
|
2179
|
+
/** What the agent has written down: remembered facts and wiki pages. */
|
|
2180
|
+
wiki = {
|
|
2181
|
+
list: () => this.request("GET", "/wiki"),
|
|
2182
|
+
delete: (itemId) => this.request("DELETE", `/wiki/${itemId}`),
|
|
2183
|
+
};
|
|
2184
|
+
/** Live sandboxes, and what to do about one. */
|
|
2185
|
+
sandboxes = {
|
|
2186
|
+
list: () => this.request("GET", "/sandboxes"),
|
|
2187
|
+
action: (sessionId, action) => this.request("POST", `/sandboxes/${sessionId}/${action}`, {}),
|
|
2188
|
+
};
|
|
1983
2189
|
/** Prepended to every request for this project, above anything a caller sends. */
|
|
1984
2190
|
systemPrompt = {
|
|
1985
2191
|
set: (systemPrompt) => this.request("PUT", "/system-prompt", { systemPrompt }),
|
|
@@ -1995,6 +2201,33 @@ export class OberikProject {
|
|
|
1995
2201
|
delete: (toolId) => this.request("DELETE", `/webhook-tools/${toolId}`),
|
|
1996
2202
|
};
|
|
1997
2203
|
/** Server-side keys. A created one is returned once and never again. */
|
|
2204
|
+
/**
|
|
2205
|
+
* Checks on what goes into the model and what comes back.
|
|
2206
|
+
*
|
|
2207
|
+
* The enforcement has existed for a long time and there was no way to configure it — no
|
|
2208
|
+
* route, no dashboard section, no column — so a documentation page described switches
|
|
2209
|
+
* that could not be reached. `set` takes a partial: what you do not mention is left as it
|
|
2210
|
+
* is.
|
|
2211
|
+
*/
|
|
2212
|
+
guardrails = {
|
|
2213
|
+
get: () => this.request("GET", "/guardrails"),
|
|
2214
|
+
set: (policy) => this.request("PUT", "/guardrails", { body: policy }),
|
|
2215
|
+
};
|
|
2216
|
+
/**
|
|
2217
|
+
* Whether this project can actually answer a question yet.
|
|
2218
|
+
*
|
|
2219
|
+
* A new project has no models, so it can neither answer nor index anything — and the
|
|
2220
|
+
* flag that used to be the closest thing to this (`hasLlm`) meant "a LiteLLM key was
|
|
2221
|
+
* provisioned", which is true from the moment a project exists. Every unfinished step
|
|
2222
|
+
* names what it blocks and the one call that fixes it.
|
|
2223
|
+
*
|
|
2224
|
+
* Worth calling in a deploy check: a project that is not ready fails every request with
|
|
2225
|
+
* the provider's own error, which reads as your bug rather than as missing setup.
|
|
2226
|
+
*/
|
|
2227
|
+
readiness = () => this.request("GET", "/readiness");
|
|
2228
|
+
/** The starting snippet and this project's endpoints — the same one the dashboard and the
|
|
2229
|
+
* SSH gateway show, so there is one of it rather than three. */
|
|
2230
|
+
connect = () => this.request("GET", "/connect");
|
|
1998
2231
|
/**
|
|
1999
2232
|
* Further project keys.
|
|
2000
2233
|
*
|