@oberik/sdk 0.6.0 → 0.8.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 +275 -14
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +92 -6
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +272 -14
- package/dist/esm/index.js.map +1 -1
- package/package.json +18 -4
package/dist/cjs/index.js
CHANGED
|
@@ -34,6 +34,9 @@
|
|
|
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;
|
|
39
|
+
exports.blockingBudgetIsHonoured = blockingBudgetIsHonoured;
|
|
37
40
|
exports.createProjectClient = createProjectClient;
|
|
38
41
|
exports.createClient = createClient;
|
|
39
42
|
// ============================================================================
|
|
@@ -68,6 +71,169 @@ exports.AgentCancelledError = AgentCancelledError;
|
|
|
68
71
|
// Errors
|
|
69
72
|
// ============================================================================
|
|
70
73
|
/** Does this look like a page rather than an answer? */
|
|
74
|
+
// ---- UI component arguments ----
|
|
75
|
+
//
|
|
76
|
+
// A UI component declares `parameters` exactly as a client tool does, and `render` draws
|
|
77
|
+
// into the customer's own product. Two schema violations of the *same* declared schema
|
|
78
|
+
// reached `render` unchecked: `bars` as a JSON string rather than an array, and array
|
|
79
|
+
// items missing a required `percent` while carrying two fields the schema never mentioned.
|
|
80
|
+
//
|
|
81
|
+
// For a client *tool* you can validate in the handler and answer the model. A UI component
|
|
82
|
+
// paints, so the check has to happen before it draws.
|
|
83
|
+
/** JSON Schema `properties`, whichever of the two shapes the app declared. */
|
|
84
|
+
function uiProps(schema) {
|
|
85
|
+
const s = schema;
|
|
86
|
+
return (s?.properties ?? s?.parameters?.properties ?? {});
|
|
87
|
+
}
|
|
88
|
+
function parseIfJson(value, want) {
|
|
89
|
+
if (typeof value !== "string")
|
|
90
|
+
return value;
|
|
91
|
+
const text = value.trim();
|
|
92
|
+
if (!text)
|
|
93
|
+
return value;
|
|
94
|
+
const opens = want === "array" ? "[" : "{";
|
|
95
|
+
if (!text.startsWith(opens))
|
|
96
|
+
return value;
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(text);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
// Passed through untouched. A cosmetic mismatch must not become a thrown render.
|
|
102
|
+
return value;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Make the arguments match the declared types, as far as that is safe.
|
|
107
|
+
*
|
|
108
|
+
* Narrow on purpose — this is the app's own schema and the values go straight back to it.
|
|
109
|
+
* A container declared as `array`/`object` that arrived as its JSON text is parsed, and
|
|
110
|
+
* that parsing recurses into array items, which is where the server's own coercion stops.
|
|
111
|
+
*/
|
|
112
|
+
function coerceUiArgs(schema, args) {
|
|
113
|
+
const props = uiProps(schema);
|
|
114
|
+
if (!Object.keys(props).length)
|
|
115
|
+
return args;
|
|
116
|
+
const out = { ...args };
|
|
117
|
+
for (const [key, spec] of Object.entries(props)) {
|
|
118
|
+
if (!(key in out))
|
|
119
|
+
continue;
|
|
120
|
+
const want = String(spec?.type ?? "");
|
|
121
|
+
if (want === "array" || want === "object")
|
|
122
|
+
out[key] = parseIfJson(out[key], want);
|
|
123
|
+
// Items of an array of objects: the same JSON-text-instead-of-a-container mistake,
|
|
124
|
+
// one level down, which is the level a chart's data actually lives at.
|
|
125
|
+
if (want === "array" && Array.isArray(out[key]) && spec?.items?.type === "object") {
|
|
126
|
+
out[key] = out[key].map((item) => parseIfJson(item, "object"));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/** How `args` fails `schema`, as sentences — empty when it does not. */
|
|
132
|
+
function uiSchemaViolations(schema, args) {
|
|
133
|
+
const props = uiProps(schema);
|
|
134
|
+
if (!Object.keys(props).length)
|
|
135
|
+
return [];
|
|
136
|
+
const required = (schema?.required ?? []);
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const key of required) {
|
|
139
|
+
if (args[key] === undefined || args[key] === null)
|
|
140
|
+
out.push(`missing required "${key}"`);
|
|
141
|
+
}
|
|
142
|
+
for (const [key, spec] of Object.entries(props)) {
|
|
143
|
+
const value = args[key];
|
|
144
|
+
if (value === undefined || value === null)
|
|
145
|
+
continue;
|
|
146
|
+
const want = String(spec?.type ?? "");
|
|
147
|
+
if (want === "array" && !Array.isArray(value)) {
|
|
148
|
+
out.push(`"${key}" should be an array, got ${typeof value}`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (want === "array" && Array.isArray(value) && spec?.items) {
|
|
152
|
+
const itemRequired = (spec.items.required ?? []);
|
|
153
|
+
value.forEach((item, i) => {
|
|
154
|
+
if (typeof item !== "object" || item === null)
|
|
155
|
+
return;
|
|
156
|
+
for (const f of itemRequired) {
|
|
157
|
+
if (item[f] === undefined)
|
|
158
|
+
out.push(`"${key}[${i}]" is missing required "${f}"`);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
if (want === "number" && typeof value !== "number") {
|
|
163
|
+
out.push(`"${key}" should be a number, got ${typeof value}`);
|
|
164
|
+
}
|
|
165
|
+
if (want === "string" && typeof value !== "string") {
|
|
166
|
+
out.push(`"${key}" should be a string, got ${typeof value}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
// ---- Node's five-minute wall ----
|
|
172
|
+
//
|
|
173
|
+
// `AbortSignal.timeout` bounds the whole request and does not touch undici's
|
|
174
|
+
// `headersTimeout`, which is 300s and fires first. So `timeoutMs: 600_000` really meant
|
|
175
|
+
// "about five minutes", and a blocking turn that ran a long sandbox job still died as
|
|
176
|
+
// `UND_ERR_HEADERS_TIMEOUT` — the exact failure this option was added to fix.
|
|
177
|
+
//
|
|
178
|
+
// The only way to move that ceiling is an undici `Agent`. It is not a dependency here (the
|
|
179
|
+
// SDK ships with none, and must load in a browser), so it is imported at runtime if the
|
|
180
|
+
// host happens to have it and skipped otherwise. Node's global `fetch` accepts a
|
|
181
|
+
// `dispatcher` on the request because it *is* undici underneath.
|
|
182
|
+
/** One agent per timeout value; building one per request would leak sockets. */
|
|
183
|
+
const _dispatchers = new Map();
|
|
184
|
+
/** Settled results, so the hot path never awaits.
|
|
185
|
+
*
|
|
186
|
+
* Awaiting the lookup on every request put each one a microtask later — invisible for a
|
|
187
|
+
* chat turn and not for a hand-off, where pointer events are relayed as they happen and
|
|
188
|
+
* the whole design is "relay the gesture, not an approximation of it". */
|
|
189
|
+
const _settled = new Map();
|
|
190
|
+
/** Node's default headers timeout. Below this, nothing needs raising. */
|
|
191
|
+
const NODE_HEADERS_TIMEOUT_MS = 300_000;
|
|
192
|
+
async function longRequestDispatcher(ms) {
|
|
193
|
+
const proc = globalThis.process;
|
|
194
|
+
if (!proc?.versions?.node)
|
|
195
|
+
return undefined; // a browser: no such ceiling
|
|
196
|
+
if (ms <= NODE_HEADERS_TIMEOUT_MS)
|
|
197
|
+
return undefined;
|
|
198
|
+
let pending = _dispatchers.get(ms);
|
|
199
|
+
if (!pending) {
|
|
200
|
+
pending = (async () => {
|
|
201
|
+
try {
|
|
202
|
+
// The specifier is built at runtime so a bundler does not try to resolve a
|
|
203
|
+
// package that is deliberately not a dependency.
|
|
204
|
+
const mod = await Promise.resolve(`${["undici"].join("")}`).then(s => require(s));
|
|
205
|
+
const Agent = mod?.Agent ?? mod?.default?.Agent;
|
|
206
|
+
return Agent ? new Agent({ headersTimeout: ms, bodyTimeout: ms }) : undefined;
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return undefined;
|
|
210
|
+
}
|
|
211
|
+
})();
|
|
212
|
+
_dispatchers.set(ms, pending);
|
|
213
|
+
pending.then((d) => _settled.set(ms, d)).catch(() => _settled.set(ms, undefined));
|
|
214
|
+
}
|
|
215
|
+
return pending;
|
|
216
|
+
}
|
|
217
|
+
/** Whether this request needs the dispatcher looked up at all.
|
|
218
|
+
*
|
|
219
|
+
* `explicit` matters: the lookup costs a dynamic import the first time, and only a
|
|
220
|
+
* caller who actually set `timeoutMs` past Node's 300s ceiling is asking for something
|
|
221
|
+
* the default cannot give them. Everyone else — including a hand-off viewer relaying
|
|
222
|
+
* pointer events, which sets no timeout at all — dispatches in the same tick it always
|
|
223
|
+
* did. */
|
|
224
|
+
function needsDispatcher(ms, explicit) {
|
|
225
|
+
const proc = globalThis.process;
|
|
226
|
+
return explicit && Boolean(proc?.versions?.node) && ms > NODE_HEADERS_TIMEOUT_MS;
|
|
227
|
+
}
|
|
228
|
+
/** Whether a long blocking budget will actually be honoured here. */
|
|
229
|
+
async function blockingBudgetIsHonoured(ms) {
|
|
230
|
+
const proc = globalThis.process;
|
|
231
|
+
if (!proc?.versions?.node)
|
|
232
|
+
return true;
|
|
233
|
+
if (ms <= NODE_HEADERS_TIMEOUT_MS)
|
|
234
|
+
return true;
|
|
235
|
+
return (await longRequestDispatcher(ms)) !== undefined;
|
|
236
|
+
}
|
|
71
237
|
function looksLikeHtml(text) {
|
|
72
238
|
return /^\s*(<!doctype html|<html|<head|<body)/i.test(text) || /<\/html>\s*$/i.test(text);
|
|
73
239
|
}
|
|
@@ -88,9 +254,11 @@ function looksLikeHtml(text) {
|
|
|
88
254
|
function describeDetail(status, detail) {
|
|
89
255
|
if (typeof detail === "string") {
|
|
90
256
|
if (looksLikeHtml(detail)) {
|
|
257
|
+
// Report what arrived, not a guess about what produced it. The API answers with
|
|
258
|
+
// JSON, so an HTML body is worth saying plainly — but naming whatever returned it
|
|
259
|
+
// would be speculation about someone else's infrastructure.
|
|
91
260
|
const title = /<title[^>]*>([^<]{1,160})<\/title>/i.exec(detail)?.[1]?.trim();
|
|
92
|
-
|
|
93
|
-
return `HTTP ${status}${title ? `: ${title}` : ""}${where}`;
|
|
261
|
+
return `HTTP ${status}${title ? `: ${title}` : ""} (the response body was HTML, not JSON)`;
|
|
94
262
|
}
|
|
95
263
|
return detail.slice(0, 2000) || `HTTP ${status}`;
|
|
96
264
|
}
|
|
@@ -930,8 +1098,25 @@ class AgentFramework {
|
|
|
930
1098
|
const component = registry.get(r.name);
|
|
931
1099
|
if (!component)
|
|
932
1100
|
continue;
|
|
1101
|
+
// Coerced and checked against the schema the app declared, because `render` paints
|
|
1102
|
+
// straight into somebody's product and the frame handed to it is the one thing you
|
|
1103
|
+
// would expect to have been checked against the schema you just supplied.
|
|
1104
|
+
//
|
|
1105
|
+
// The server coerces too, but only the top-level properties, and it checks nothing
|
|
1106
|
+
// required — so a declared `bars: array of {label, percent}` arrived as a JSON
|
|
1107
|
+
// *string* in a browser and as `[{label, value, target}]` in Node, and both were
|
|
1108
|
+
// dispatched. The page drew "? 0.0%" four times.
|
|
1109
|
+
const args = coerceUiArgs(component.parameters, r.args ?? {});
|
|
1110
|
+
const problems = uiSchemaViolations(component.parameters, args);
|
|
1111
|
+
if (problems.length) {
|
|
1112
|
+
// Warn, then draw anyway. The agent has already told the user it showed them
|
|
1113
|
+
// something, so silently skipping leaves them looking for a chart that never
|
|
1114
|
+
// appears — worse than a chart with a gap in it. Loud enough to find in a console.
|
|
1115
|
+
console.warn(`[oberik] UI component "${r.name}" was called with arguments that do not match ` +
|
|
1116
|
+
`its declared schema: ${problems.join("; ")}. Rendering anyway.`);
|
|
1117
|
+
}
|
|
933
1118
|
try {
|
|
934
|
-
component.render(
|
|
1119
|
+
component.render(args);
|
|
935
1120
|
}
|
|
936
1121
|
catch (e) {
|
|
937
1122
|
console.warn(`[oberik] UI component "${r.name}" threw while rendering:`, e);
|
|
@@ -975,10 +1160,28 @@ class AgentFramework {
|
|
|
975
1160
|
// turn that ran a long sandbox job simply died as `TypeError: fetch failed` with
|
|
976
1161
|
// nothing naming this SDK. Composed with the caller's own signal rather than
|
|
977
1162
|
// replacing it, so `opts.signal` still cancels.
|
|
1163
|
+
const explicitTimeout = this.opts.timeoutMs !== undefined;
|
|
978
1164
|
const ms = this.opts.timeoutMs ?? 600_000;
|
|
979
1165
|
const timer = ms > 0 ? AbortSignal.timeout(ms) : undefined;
|
|
980
1166
|
const signal = combineSignals(init.signal, timer);
|
|
981
|
-
|
|
1167
|
+
// The abort signal alone does not raise Node's 300s header wait; the dispatcher
|
|
1168
|
+
// does. Undefined in a browser, and on a Node without `undici` installed — where
|
|
1169
|
+
// the effective ceiling stays five minutes and streaming is the answer.
|
|
1170
|
+
//
|
|
1171
|
+
// Resolved without awaiting once it has settled, and not consulted at all where it
|
|
1172
|
+
// could not apply: in a browser this whole branch is skipped, so the request is
|
|
1173
|
+
// dispatched in the same tick it always was.
|
|
1174
|
+
let dispatcher;
|
|
1175
|
+
if (ms > 0 && needsDispatcher(ms, explicitTimeout)) {
|
|
1176
|
+
dispatcher = _settled.has(ms) ? _settled.get(ms) : await longRequestDispatcher(ms);
|
|
1177
|
+
}
|
|
1178
|
+
return this._fetch(url.toString(), {
|
|
1179
|
+
method,
|
|
1180
|
+
headers,
|
|
1181
|
+
body,
|
|
1182
|
+
signal,
|
|
1183
|
+
...(dispatcher ? { dispatcher } : {}),
|
|
1184
|
+
});
|
|
982
1185
|
};
|
|
983
1186
|
let res = await send();
|
|
984
1187
|
// Token expired mid-session: get a fresh one and replay the request once, so the
|
|
@@ -1557,8 +1760,14 @@ class AgentFramework {
|
|
|
1557
1760
|
const hasApprovals = onApproval != null && (resp.approvals?.length ?? 0) > 0;
|
|
1558
1761
|
if (!hasTools && !hasQuestions && !hasApprovals)
|
|
1559
1762
|
return merged;
|
|
1763
|
+
// Hand back the turn, don't throw it away.
|
|
1764
|
+
//
|
|
1765
|
+
// This threw, so hitting the ceiling produced no content and no `session_id` —
|
|
1766
|
+
// nothing to render and nothing to resume from — while the SERVER's ceiling on the
|
|
1767
|
+
// same loop returns the answer produced so far plus `finish_reason`, which the docs
|
|
1768
|
+
// even give a UI recipe for. Two ceilings on one loop with opposite semantics.
|
|
1560
1769
|
if (rounds++ >= maxRounds)
|
|
1561
|
-
|
|
1770
|
+
return { ...merged, finish_reason: "max_tool_rounds" };
|
|
1562
1771
|
const next = {
|
|
1563
1772
|
session_id: resp.session_id, client_tools: schemas, ui_tools: uiSchemas,
|
|
1564
1773
|
};
|
|
@@ -1604,8 +1813,10 @@ class AgentFramework {
|
|
|
1604
1813
|
return merged;
|
|
1605
1814
|
if (stopped)
|
|
1606
1815
|
return merged;
|
|
1816
|
+
// Returned rather than thrown, for the same reason as the blocking path above:
|
|
1817
|
+
// the work is done and paid for, and the caller has somewhere to resume from.
|
|
1607
1818
|
if (rounds++ >= maxRounds)
|
|
1608
|
-
|
|
1819
|
+
return { ...merged, finish_reason: "max_tool_rounds" };
|
|
1609
1820
|
reqBody = { session_id: d.session_id, client_tools: schemas, ui_tools: uiSchemas };
|
|
1610
1821
|
if (hasTools) {
|
|
1611
1822
|
handlers.onToolCalls?.(d.tool_calls);
|
|
@@ -2221,7 +2432,9 @@ class OberikProject {
|
|
|
2221
2432
|
const text = await res.text();
|
|
2222
2433
|
const data = text ? safeJson(text) : null;
|
|
2223
2434
|
if (!res.ok) {
|
|
2224
|
-
|
|
2435
|
+
// `detail` on both planes now, so an integrator writes one accessor rather than
|
|
2436
|
+
// discovering that the control plane spelled it differently.
|
|
2437
|
+
throw new AgentApiError(res.status, (data && (data.detail || data.message)) || `HTTP ${res.status}`);
|
|
2225
2438
|
}
|
|
2226
2439
|
return data;
|
|
2227
2440
|
}
|
|
@@ -2246,11 +2459,38 @@ class OberikProject {
|
|
|
2246
2459
|
*/
|
|
2247
2460
|
forUser: (input) => async () => (await this.request("POST", "/token", input)).access_token,
|
|
2248
2461
|
};
|
|
2462
|
+
/**
|
|
2463
|
+
* The project itself — its name, description, and the settings that are properties of
|
|
2464
|
+
* the project rather than permissions on a token.
|
|
2465
|
+
*
|
|
2466
|
+
* These had no SDK path at all. `doneWebhookUrl` is documented in the capability table
|
|
2467
|
+
* but is NOT a capability, so the obvious `capabilities.set({ doneWebhookUrl })` stored
|
|
2468
|
+
* nothing; the only symptom was `turn.stopped` never firing. Renaming a project, or
|
|
2469
|
+
* turning citation markers off, meant dropping to raw HTTP through `raw()`.
|
|
2470
|
+
*/
|
|
2471
|
+
project = {
|
|
2472
|
+
get: () => this.request("GET", ""),
|
|
2473
|
+
/** Merges — send only what you want to change. */
|
|
2474
|
+
set: (fields) => this.request("PATCH", "", fields),
|
|
2475
|
+
/** Whether `[2]` stays in the visible answer. Its own route, not a PATCH field.
|
|
2476
|
+
* `stripped` leaves `claims[].start/end` as the only way to place a footnote. */
|
|
2477
|
+
citations: (markers) => this.request("PUT", "/citations", { markers }),
|
|
2478
|
+
};
|
|
2249
2479
|
/** The capability ceiling: the maximum any token minted here may hold. */
|
|
2250
2480
|
capabilities = {
|
|
2251
2481
|
get: () => this.request("GET", "/capabilities"),
|
|
2252
|
-
/**
|
|
2253
|
-
|
|
2482
|
+
/**
|
|
2483
|
+
* Merges — send only what you want to change.
|
|
2484
|
+
*
|
|
2485
|
+
* Read-only fields that `get()` returns are dropped rather than rejected, so
|
|
2486
|
+
* `set(await get())` — the obvious way to flip one flag — works. Anything else
|
|
2487
|
+
* unrecognised still errors: a capability nobody enforces must not be accepted in
|
|
2488
|
+
* silence, which is the failure this whole check exists for.
|
|
2489
|
+
*/
|
|
2490
|
+
set: (caps) => {
|
|
2491
|
+
const { supportedModalities: _readOnly, ...settable } = caps;
|
|
2492
|
+
return this.request("PATCH", "", { capabilities: settable });
|
|
2493
|
+
},
|
|
2254
2494
|
};
|
|
2255
2495
|
/** Documents owned by the project rather than by any one end-user: the corpus you
|
|
2256
2496
|
* curate and your users only read. */
|
|
@@ -2267,7 +2507,11 @@ class OberikProject {
|
|
|
2267
2507
|
* accepted nor needed — a recipe that worked by luck rather than by expression.
|
|
2268
2508
|
*/
|
|
2269
2509
|
documents = {
|
|
2270
|
-
list
|
|
2510
|
+
/** Same filters as the end-user client's `documents.list`, so the two agree. */
|
|
2511
|
+
list: (query = {}) => {
|
|
2512
|
+
const qs = new URLSearchParams(Object.entries(query).filter(([, v]) => v != null)).toString();
|
|
2513
|
+
return this.request("GET", `/documents${qs ? `?${qs}` : ""}`);
|
|
2514
|
+
},
|
|
2271
2515
|
get: (documentId) => this.request("GET", `/documents/${documentId}`),
|
|
2272
2516
|
/** `file` may be a Blob/File, an ArrayBuffer or a Uint8Array — the same shapes the
|
|
2273
2517
|
* end-user client takes. It was `Blob | File` only, so the quickstart's own
|
|
@@ -2364,9 +2608,19 @@ class OberikProject {
|
|
|
2364
2608
|
/** Procedures you publish as Agent Plugins, and what your end-users have added. */
|
|
2365
2609
|
skills = {
|
|
2366
2610
|
list: () => this.request("GET", "/skills"),
|
|
2367
|
-
|
|
2611
|
+
/**
|
|
2612
|
+
* `zip` takes the same shapes as every other upload here — a Blob/File, an
|
|
2613
|
+
* ArrayBuffer or a Uint8Array — and `filename` goes in the options object.
|
|
2614
|
+
*
|
|
2615
|
+
* It was `Blob | File` with a positional filename, alone among four upload methods.
|
|
2616
|
+
* `await readFile("skill.zip")` (a Buffer) died as `parameter 2 is not of type 'Blob'`
|
|
2617
|
+
* inside undici with nothing naming Oberik in the stack — the identical failure that
|
|
2618
|
+
* was fixed for `documents.upload` and left here — and passing `{ filename }` in the
|
|
2619
|
+
* second slot, as the siblings take it, silently named the file `[object Object]`.
|
|
2620
|
+
*/
|
|
2621
|
+
upload: (zip, opts = {}) => {
|
|
2368
2622
|
const form = new FormData();
|
|
2369
|
-
form.append("file", zip, filename ?? zip.name ?? "skill.zip");
|
|
2623
|
+
form.append("file", asBlob(zip), opts.filename ?? zip.name ?? "skill.zip");
|
|
2370
2624
|
return this.request("POST", "/skills", undefined, form);
|
|
2371
2625
|
},
|
|
2372
2626
|
delete: (pluginId) => this.request("DELETE", `/skills/${pluginId}`),
|
|
@@ -2386,9 +2640,16 @@ class OberikProject {
|
|
|
2386
2640
|
tasks = {
|
|
2387
2641
|
list: () => this.request("GET", "/tasks"),
|
|
2388
2642
|
};
|
|
2389
|
-
/**
|
|
2643
|
+
/**
|
|
2644
|
+
* What the agent has written down: remembered facts and wiki pages.
|
|
2645
|
+
*
|
|
2646
|
+
* `kind` defaults to `"wiki"` — the route's default, and the reason this looked like
|
|
2647
|
+
* it "returns pages only": it does, and there was no argument here to ask for the
|
|
2648
|
+
* memories. Same shape and same type as the end-user client's `knowledge.list`, so the
|
|
2649
|
+
* two do not disagree about what a stored item is.
|
|
2650
|
+
*/
|
|
2390
2651
|
wiki = {
|
|
2391
|
-
list: () => this.request("GET", "
|
|
2652
|
+
list: (opts = {}) => this.request("GET", `/wiki?kind=${opts.kind ?? "wiki"}`),
|
|
2392
2653
|
delete: (itemId) => this.request("DELETE", `/wiki/${itemId}`),
|
|
2393
2654
|
};
|
|
2394
2655
|
/** Live sandboxes, and what to do about one. */
|