@oberik/sdk 0.6.0 → 0.7.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 +179 -11
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +71 -5
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +177 -11
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
*/
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.OberikProject = exports.DEFAULT_CONTROL_PLANE_URL = exports.AgentFramework = exports.AgentStreamError = exports.AgentApiError = exports.AgentCancelledError = exports.DEFAULT_BASE_URL = void 0;
|
|
37
|
+
exports.coerceUiArgs = coerceUiArgs;
|
|
38
|
+
exports.uiSchemaViolations = uiSchemaViolations;
|
|
37
39
|
exports.createProjectClient = createProjectClient;
|
|
38
40
|
exports.createClient = createClient;
|
|
39
41
|
// ============================================================================
|
|
@@ -68,6 +70,103 @@ exports.AgentCancelledError = AgentCancelledError;
|
|
|
68
70
|
// Errors
|
|
69
71
|
// ============================================================================
|
|
70
72
|
/** Does this look like a page rather than an answer? */
|
|
73
|
+
// ---- UI component arguments ----
|
|
74
|
+
//
|
|
75
|
+
// A UI component declares `parameters` exactly as a client tool does, and `render` draws
|
|
76
|
+
// into the customer's own product. Two schema violations of the *same* declared schema
|
|
77
|
+
// reached `render` unchecked: `bars` as a JSON string rather than an array, and array
|
|
78
|
+
// items missing a required `percent` while carrying two fields the schema never mentioned.
|
|
79
|
+
//
|
|
80
|
+
// For a client *tool* you can validate in the handler and answer the model. A UI component
|
|
81
|
+
// paints, so the check has to happen before it draws.
|
|
82
|
+
/** JSON Schema `properties`, whichever of the two shapes the app declared. */
|
|
83
|
+
function uiProps(schema) {
|
|
84
|
+
const s = schema;
|
|
85
|
+
return (s?.properties ?? s?.parameters?.properties ?? {});
|
|
86
|
+
}
|
|
87
|
+
function parseIfJson(value, want) {
|
|
88
|
+
if (typeof value !== "string")
|
|
89
|
+
return value;
|
|
90
|
+
const text = value.trim();
|
|
91
|
+
if (!text)
|
|
92
|
+
return value;
|
|
93
|
+
const opens = want === "array" ? "[" : "{";
|
|
94
|
+
if (!text.startsWith(opens))
|
|
95
|
+
return value;
|
|
96
|
+
try {
|
|
97
|
+
return JSON.parse(text);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Passed through untouched. A cosmetic mismatch must not become a thrown render.
|
|
101
|
+
return value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Make the arguments match the declared types, as far as that is safe.
|
|
106
|
+
*
|
|
107
|
+
* Narrow on purpose — this is the app's own schema and the values go straight back to it.
|
|
108
|
+
* A container declared as `array`/`object` that arrived as its JSON text is parsed, and
|
|
109
|
+
* that parsing recurses into array items, which is where the server's own coercion stops.
|
|
110
|
+
*/
|
|
111
|
+
function coerceUiArgs(schema, args) {
|
|
112
|
+
const props = uiProps(schema);
|
|
113
|
+
if (!Object.keys(props).length)
|
|
114
|
+
return args;
|
|
115
|
+
const out = { ...args };
|
|
116
|
+
for (const [key, spec] of Object.entries(props)) {
|
|
117
|
+
if (!(key in out))
|
|
118
|
+
continue;
|
|
119
|
+
const want = String(spec?.type ?? "");
|
|
120
|
+
if (want === "array" || want === "object")
|
|
121
|
+
out[key] = parseIfJson(out[key], want);
|
|
122
|
+
// Items of an array of objects: the same JSON-text-instead-of-a-container mistake,
|
|
123
|
+
// one level down, which is the level a chart's data actually lives at.
|
|
124
|
+
if (want === "array" && Array.isArray(out[key]) && spec?.items?.type === "object") {
|
|
125
|
+
out[key] = out[key].map((item) => parseIfJson(item, "object"));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
/** How `args` fails `schema`, as sentences — empty when it does not. */
|
|
131
|
+
function uiSchemaViolations(schema, args) {
|
|
132
|
+
const props = uiProps(schema);
|
|
133
|
+
if (!Object.keys(props).length)
|
|
134
|
+
return [];
|
|
135
|
+
const required = (schema?.required ?? []);
|
|
136
|
+
const out = [];
|
|
137
|
+
for (const key of required) {
|
|
138
|
+
if (args[key] === undefined || args[key] === null)
|
|
139
|
+
out.push(`missing required "${key}"`);
|
|
140
|
+
}
|
|
141
|
+
for (const [key, spec] of Object.entries(props)) {
|
|
142
|
+
const value = args[key];
|
|
143
|
+
if (value === undefined || value === null)
|
|
144
|
+
continue;
|
|
145
|
+
const want = String(spec?.type ?? "");
|
|
146
|
+
if (want === "array" && !Array.isArray(value)) {
|
|
147
|
+
out.push(`"${key}" should be an array, got ${typeof value}`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (want === "array" && Array.isArray(value) && spec?.items) {
|
|
151
|
+
const itemRequired = (spec.items.required ?? []);
|
|
152
|
+
value.forEach((item, i) => {
|
|
153
|
+
if (typeof item !== "object" || item === null)
|
|
154
|
+
return;
|
|
155
|
+
for (const f of itemRequired) {
|
|
156
|
+
if (item[f] === undefined)
|
|
157
|
+
out.push(`"${key}[${i}]" is missing required "${f}"`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
if (want === "number" && typeof value !== "number") {
|
|
162
|
+
out.push(`"${key}" should be a number, got ${typeof value}`);
|
|
163
|
+
}
|
|
164
|
+
if (want === "string" && typeof value !== "string") {
|
|
165
|
+
out.push(`"${key}" should be a string, got ${typeof value}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return out;
|
|
169
|
+
}
|
|
71
170
|
function looksLikeHtml(text) {
|
|
72
171
|
return /^\s*(<!doctype html|<html|<head|<body)/i.test(text) || /<\/html>\s*$/i.test(text);
|
|
73
172
|
}
|
|
@@ -88,9 +187,11 @@ function looksLikeHtml(text) {
|
|
|
88
187
|
function describeDetail(status, detail) {
|
|
89
188
|
if (typeof detail === "string") {
|
|
90
189
|
if (looksLikeHtml(detail)) {
|
|
190
|
+
// Report what arrived, not a guess about what produced it. The API answers with
|
|
191
|
+
// JSON, so an HTML body is worth saying plainly — but naming whatever returned it
|
|
192
|
+
// would be speculation about someone else's infrastructure.
|
|
91
193
|
const title = /<title[^>]*>([^<]{1,160})<\/title>/i.exec(detail)?.[1]?.trim();
|
|
92
|
-
|
|
93
|
-
return `HTTP ${status}${title ? `: ${title}` : ""}${where}`;
|
|
194
|
+
return `HTTP ${status}${title ? `: ${title}` : ""} (the response body was HTML, not JSON)`;
|
|
94
195
|
}
|
|
95
196
|
return detail.slice(0, 2000) || `HTTP ${status}`;
|
|
96
197
|
}
|
|
@@ -930,8 +1031,25 @@ class AgentFramework {
|
|
|
930
1031
|
const component = registry.get(r.name);
|
|
931
1032
|
if (!component)
|
|
932
1033
|
continue;
|
|
1034
|
+
// Coerced and checked against the schema the app declared, because `render` paints
|
|
1035
|
+
// straight into somebody's product and the frame handed to it is the one thing you
|
|
1036
|
+
// would expect to have been checked against the schema you just supplied.
|
|
1037
|
+
//
|
|
1038
|
+
// The server coerces too, but only the top-level properties, and it checks nothing
|
|
1039
|
+
// required — so a declared `bars: array of {label, percent}` arrived as a JSON
|
|
1040
|
+
// *string* in a browser and as `[{label, value, target}]` in Node, and both were
|
|
1041
|
+
// dispatched. The page drew "? 0.0%" four times.
|
|
1042
|
+
const args = coerceUiArgs(component.parameters, r.args ?? {});
|
|
1043
|
+
const problems = uiSchemaViolations(component.parameters, args);
|
|
1044
|
+
if (problems.length) {
|
|
1045
|
+
// Warn, then draw anyway. The agent has already told the user it showed them
|
|
1046
|
+
// something, so silently skipping leaves them looking for a chart that never
|
|
1047
|
+
// appears — worse than a chart with a gap in it. Loud enough to find in a console.
|
|
1048
|
+
console.warn(`[oberik] UI component "${r.name}" was called with arguments that do not match ` +
|
|
1049
|
+
`its declared schema: ${problems.join("; ")}. Rendering anyway.`);
|
|
1050
|
+
}
|
|
933
1051
|
try {
|
|
934
|
-
component.render(
|
|
1052
|
+
component.render(args);
|
|
935
1053
|
}
|
|
936
1054
|
catch (e) {
|
|
937
1055
|
console.warn(`[oberik] UI component "${r.name}" threw while rendering:`, e);
|
|
@@ -2221,7 +2339,9 @@ class OberikProject {
|
|
|
2221
2339
|
const text = await res.text();
|
|
2222
2340
|
const data = text ? safeJson(text) : null;
|
|
2223
2341
|
if (!res.ok) {
|
|
2224
|
-
|
|
2342
|
+
// `detail` on both planes now, so an integrator writes one accessor rather than
|
|
2343
|
+
// discovering that the control plane spelled it differently.
|
|
2344
|
+
throw new AgentApiError(res.status, (data && (data.detail || data.message)) || `HTTP ${res.status}`);
|
|
2225
2345
|
}
|
|
2226
2346
|
return data;
|
|
2227
2347
|
}
|
|
@@ -2246,11 +2366,38 @@ class OberikProject {
|
|
|
2246
2366
|
*/
|
|
2247
2367
|
forUser: (input) => async () => (await this.request("POST", "/token", input)).access_token,
|
|
2248
2368
|
};
|
|
2369
|
+
/**
|
|
2370
|
+
* The project itself — its name, description, and the settings that are properties of
|
|
2371
|
+
* the project rather than permissions on a token.
|
|
2372
|
+
*
|
|
2373
|
+
* These had no SDK path at all. `doneWebhookUrl` is documented in the capability table
|
|
2374
|
+
* but is NOT a capability, so the obvious `capabilities.set({ doneWebhookUrl })` stored
|
|
2375
|
+
* nothing; the only symptom was `turn.stopped` never firing. Renaming a project, or
|
|
2376
|
+
* turning citation markers off, meant dropping to raw HTTP through `raw()`.
|
|
2377
|
+
*/
|
|
2378
|
+
project = {
|
|
2379
|
+
get: () => this.request("GET", ""),
|
|
2380
|
+
/** Merges — send only what you want to change. */
|
|
2381
|
+
set: (fields) => this.request("PATCH", "", fields),
|
|
2382
|
+
/** Whether `[2]` stays in the visible answer. Its own route, not a PATCH field.
|
|
2383
|
+
* `stripped` leaves `claims[].start/end` as the only way to place a footnote. */
|
|
2384
|
+
citations: (markers) => this.request("PUT", "/citations", { markers }),
|
|
2385
|
+
};
|
|
2249
2386
|
/** The capability ceiling: the maximum any token minted here may hold. */
|
|
2250
2387
|
capabilities = {
|
|
2251
2388
|
get: () => this.request("GET", "/capabilities"),
|
|
2252
|
-
/**
|
|
2253
|
-
|
|
2389
|
+
/**
|
|
2390
|
+
* Merges — send only what you want to change.
|
|
2391
|
+
*
|
|
2392
|
+
* Read-only fields that `get()` returns are dropped rather than rejected, so
|
|
2393
|
+
* `set(await get())` — the obvious way to flip one flag — works. Anything else
|
|
2394
|
+
* unrecognised still errors: a capability nobody enforces must not be accepted in
|
|
2395
|
+
* silence, which is the failure this whole check exists for.
|
|
2396
|
+
*/
|
|
2397
|
+
set: (caps) => {
|
|
2398
|
+
const { supportedModalities: _readOnly, ...settable } = caps;
|
|
2399
|
+
return this.request("PATCH", "", { capabilities: settable });
|
|
2400
|
+
},
|
|
2254
2401
|
};
|
|
2255
2402
|
/** Documents owned by the project rather than by any one end-user: the corpus you
|
|
2256
2403
|
* curate and your users only read. */
|
|
@@ -2267,7 +2414,11 @@ class OberikProject {
|
|
|
2267
2414
|
* accepted nor needed — a recipe that worked by luck rather than by expression.
|
|
2268
2415
|
*/
|
|
2269
2416
|
documents = {
|
|
2270
|
-
list
|
|
2417
|
+
/** Same filters as the end-user client's `documents.list`, so the two agree. */
|
|
2418
|
+
list: (query = {}) => {
|
|
2419
|
+
const qs = new URLSearchParams(Object.entries(query).filter(([, v]) => v != null)).toString();
|
|
2420
|
+
return this.request("GET", `/documents${qs ? `?${qs}` : ""}`);
|
|
2421
|
+
},
|
|
2271
2422
|
get: (documentId) => this.request("GET", `/documents/${documentId}`),
|
|
2272
2423
|
/** `file` may be a Blob/File, an ArrayBuffer or a Uint8Array — the same shapes the
|
|
2273
2424
|
* end-user client takes. It was `Blob | File` only, so the quickstart's own
|
|
@@ -2364,9 +2515,19 @@ class OberikProject {
|
|
|
2364
2515
|
/** Procedures you publish as Agent Plugins, and what your end-users have added. */
|
|
2365
2516
|
skills = {
|
|
2366
2517
|
list: () => this.request("GET", "/skills"),
|
|
2367
|
-
|
|
2518
|
+
/**
|
|
2519
|
+
* `zip` takes the same shapes as every other upload here — a Blob/File, an
|
|
2520
|
+
* ArrayBuffer or a Uint8Array — and `filename` goes in the options object.
|
|
2521
|
+
*
|
|
2522
|
+
* It was `Blob | File` with a positional filename, alone among four upload methods.
|
|
2523
|
+
* `await readFile("skill.zip")` (a Buffer) died as `parameter 2 is not of type 'Blob'`
|
|
2524
|
+
* inside undici with nothing naming Oberik in the stack — the identical failure that
|
|
2525
|
+
* was fixed for `documents.upload` and left here — and passing `{ filename }` in the
|
|
2526
|
+
* second slot, as the siblings take it, silently named the file `[object Object]`.
|
|
2527
|
+
*/
|
|
2528
|
+
upload: (zip, opts = {}) => {
|
|
2368
2529
|
const form = new FormData();
|
|
2369
|
-
form.append("file", zip, filename ?? zip.name ?? "skill.zip");
|
|
2530
|
+
form.append("file", asBlob(zip), opts.filename ?? zip.name ?? "skill.zip");
|
|
2370
2531
|
return this.request("POST", "/skills", undefined, form);
|
|
2371
2532
|
},
|
|
2372
2533
|
delete: (pluginId) => this.request("DELETE", `/skills/${pluginId}`),
|
|
@@ -2386,9 +2547,16 @@ class OberikProject {
|
|
|
2386
2547
|
tasks = {
|
|
2387
2548
|
list: () => this.request("GET", "/tasks"),
|
|
2388
2549
|
};
|
|
2389
|
-
/**
|
|
2550
|
+
/**
|
|
2551
|
+
* What the agent has written down: remembered facts and wiki pages.
|
|
2552
|
+
*
|
|
2553
|
+
* `kind` defaults to `"wiki"` — the route's default, and the reason this looked like
|
|
2554
|
+
* it "returns pages only": it does, and there was no argument here to ask for the
|
|
2555
|
+
* memories. Same shape and same type as the end-user client's `knowledge.list`, so the
|
|
2556
|
+
* two do not disagree about what a stored item is.
|
|
2557
|
+
*/
|
|
2390
2558
|
wiki = {
|
|
2391
|
-
list: () => this.request("GET", "
|
|
2559
|
+
list: (opts = {}) => this.request("GET", `/wiki?kind=${opts.kind ?? "wiki"}`),
|
|
2392
2560
|
delete: (itemId) => this.request("DELETE", `/wiki/${itemId}`),
|
|
2393
2561
|
};
|
|
2394
2562
|
/** Live sandboxes, and what to do about one. */
|