@oberik/sdk 0.53.1 → 0.55.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 +147 -6
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/index.d.ts +29 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +147 -6
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/index.js
CHANGED
|
@@ -426,13 +426,28 @@ class AgentApiError extends Error {
|
|
|
426
426
|
* an intermediary, which is replaced by the same summary as `message`. Keeping four
|
|
427
427
|
* kilobytes of someone else's markup here helped nobody and buried the status. */
|
|
428
428
|
detail;
|
|
429
|
-
|
|
429
|
+
/** The run that failed, when the server said which.
|
|
430
|
+
*
|
|
431
|
+
* A failed `chat.send` used to carry neither this nor {@link sessionId}, so the reason
|
|
432
|
+
* the server stores for the turn — the only durable answer to "why did this fail" —
|
|
433
|
+
* could not be looked up by the person who hit it. `chat.stream` had both all along;
|
|
434
|
+
* this is the same handle on the blocking path (OBE-142). */
|
|
435
|
+
runId;
|
|
436
|
+
/** The conversation the failed turn belongs to, when the server said which.
|
|
437
|
+
*
|
|
438
|
+
* Present even when the turn failed before producing anything: the session exists from
|
|
439
|
+
* the moment the turn is prepared, and whatever it wrote is readable with
|
|
440
|
+
* `ai.chat.sessions.messages(err.sessionId)`. */
|
|
441
|
+
sessionId;
|
|
442
|
+
constructor(status, detail, ids) {
|
|
430
443
|
const described = describeDetail(status, detail);
|
|
431
444
|
super(described);
|
|
432
445
|
this.name = "AgentApiError";
|
|
433
446
|
this.status = status;
|
|
434
447
|
this.detail =
|
|
435
448
|
typeof detail === "string" && looksLikeHtml(detail) ? described : detail;
|
|
449
|
+
this.runId = ids?.runId;
|
|
450
|
+
this.sessionId = ids?.sessionId;
|
|
436
451
|
}
|
|
437
452
|
}
|
|
438
453
|
exports.AgentApiError = AgentApiError;
|
|
@@ -874,6 +889,7 @@ class AgentFramework {
|
|
|
874
889
|
let element = null;
|
|
875
890
|
let dragging = false;
|
|
876
891
|
let lastMove = 0;
|
|
892
|
+
let focused = false;
|
|
877
893
|
const compute = () => {
|
|
878
894
|
const streaming = !!live;
|
|
879
895
|
const from = streaming ? clip : polled;
|
|
@@ -923,6 +939,7 @@ class AgentFramework {
|
|
|
923
939
|
style,
|
|
924
940
|
interactive: handoff.interactive,
|
|
925
941
|
checking,
|
|
942
|
+
focused,
|
|
926
943
|
autoDone,
|
|
927
944
|
handoff,
|
|
928
945
|
};
|
|
@@ -1105,6 +1122,14 @@ class AgentFramework {
|
|
|
1105
1122
|
return out;
|
|
1106
1123
|
};
|
|
1107
1124
|
const button = (event) => event.button === 2 ? "right" : event.button === 1 ? "middle" : "left";
|
|
1125
|
+
/** Keys that only ever modify another one. Relaying them alone presses nothing and
|
|
1126
|
+
* would burn a request per Shift. */
|
|
1127
|
+
const MODIFIER_KEYS = new Set(["Shift", "Control", "Alt", "Meta", "AltGraph", "CapsLock"]);
|
|
1128
|
+
/** Ctrl/Cmd chords that mean something INSIDE a page and are worth taking over.
|
|
1129
|
+
* Select-all before retyping a field, and undo. Everything else a chord can do here
|
|
1130
|
+
* either belongs to the viewer's own browser or acts on a clipboard the person
|
|
1131
|
+
* cannot reach — see `onKeyDown`. */
|
|
1132
|
+
const PAGE_CHORDS = new Set(["a", "z"]);
|
|
1108
1133
|
const attach = (el) => {
|
|
1109
1134
|
element = el;
|
|
1110
1135
|
const onDown = (event) => {
|
|
@@ -1117,6 +1142,10 @@ class AgentFramework {
|
|
|
1117
1142
|
// Capture, so a drag that wanders off the image still ends properly. Without it
|
|
1118
1143
|
// the pointer stays down on the page and every later click compounds the mess.
|
|
1119
1144
|
el.setPointerCapture?.(event.pointerId);
|
|
1145
|
+
// Pointing at the page is also how you start typing on it. Requiring a separate
|
|
1146
|
+
// click-to-focus would be a step nobody is told about, and the symptom of missing
|
|
1147
|
+
// it — keystrokes going nowhere — looks exactly like the bug this fixes.
|
|
1148
|
+
el.focus?.();
|
|
1120
1149
|
dragging = true;
|
|
1121
1150
|
lastMove = 0;
|
|
1122
1151
|
void send({ type: "pointer_down", ...p, button: button(event), modifiers: held(event) });
|
|
@@ -1157,12 +1186,96 @@ class AgentFramework {
|
|
|
1157
1186
|
});
|
|
1158
1187
|
};
|
|
1159
1188
|
const onContextMenu = (event) => event.preventDefault?.();
|
|
1189
|
+
/**
|
|
1190
|
+
* A keystroke, relayed into the page.
|
|
1191
|
+
*
|
|
1192
|
+
* Pointer events were relayed from the first version of this and keys never were,
|
|
1193
|
+
* so a hand-off could work a slider puzzle and could not fill in a login form —
|
|
1194
|
+
* which is the other case the tool that opens a hand-off names in its own
|
|
1195
|
+
* description. The only way to enter text was a separate box in the host UI that
|
|
1196
|
+
* typed wherever focus happened to be, so nothing could Tab between fields, correct
|
|
1197
|
+
* a typo, or submit a form.
|
|
1198
|
+
*
|
|
1199
|
+
* A printable character goes as `type` rather than `key`: a key press is a physical
|
|
1200
|
+
* key and gets the wrong result for anything the person's own layout produces with a
|
|
1201
|
+
* modifier — a Turkish `ı`, an accented character, anything behind AltGr. Text is
|
|
1202
|
+
* what they meant, and text is what the page receives.
|
|
1203
|
+
*/
|
|
1204
|
+
const onKeyDown = (event) => {
|
|
1205
|
+
if (!active || !view.interactive)
|
|
1206
|
+
return;
|
|
1207
|
+
const key = String(event.key ?? "");
|
|
1208
|
+
// A modifier on its own carries nothing; it rides along with the next key.
|
|
1209
|
+
if (!key || key === "Unidentified" || MODIFIER_KEYS.has(key))
|
|
1210
|
+
return;
|
|
1211
|
+
const chord = !!(event.ctrlKey || event.metaKey);
|
|
1212
|
+
const printable = key.length === 1 && !chord && !event.altKey;
|
|
1213
|
+
// A chord that is not one of the two worth relaying stays with the VIEWER's
|
|
1214
|
+
// browser — reload, new tab, devtools, the address bar. An allow-list rather
|
|
1215
|
+
// than a block-list, so a combination nobody thought about keeps working where
|
|
1216
|
+
// the person expects it instead of being swallowed by a picture.
|
|
1217
|
+
//
|
|
1218
|
+
// Only two are worth having, and the reason the rest are not is the clipboard:
|
|
1219
|
+
// Ctrl-C and Ctrl-X inside the page copy to the REMOTE Chrome's clipboard, which
|
|
1220
|
+
// the person cannot reach. Pasting INTO the page is the case that matters and it
|
|
1221
|
+
// is handled properly below, from the real paste event.
|
|
1222
|
+
if (chord && !PAGE_CHORDS.has(key.toLowerCase()))
|
|
1223
|
+
return;
|
|
1224
|
+
// Taken over from here: relayed and not also acted on locally, or Tab moves the
|
|
1225
|
+
// viewer's own focus away mid-form and Backspace navigates the host app back.
|
|
1226
|
+
event.preventDefault?.();
|
|
1227
|
+
event.stopPropagation?.();
|
|
1228
|
+
if (printable)
|
|
1229
|
+
void send({ type: "type", text: key });
|
|
1230
|
+
else
|
|
1231
|
+
void send({ type: "key", key, modifiers: held(event) });
|
|
1232
|
+
};
|
|
1233
|
+
/**
|
|
1234
|
+
* Paste, relayed as text.
|
|
1235
|
+
*
|
|
1236
|
+
* Not reachable as a keystroke: Ctrl-V inside the remote page pastes the remote
|
|
1237
|
+
* Chrome's clipboard, which is empty and is not the person's. Reading the real
|
|
1238
|
+
* clipboard from the real paste event and sending the text is the only version of
|
|
1239
|
+
* this that works — and it is the one that matters here, because a password out of
|
|
1240
|
+
* a password manager arrives no other way.
|
|
1241
|
+
*/
|
|
1242
|
+
const onPaste = (event) => {
|
|
1243
|
+
if (!active || !view.interactive)
|
|
1244
|
+
return;
|
|
1245
|
+
const text = event.clipboardData?.getData?.("text") ?? "";
|
|
1246
|
+
if (!text)
|
|
1247
|
+
return;
|
|
1248
|
+
event.preventDefault?.();
|
|
1249
|
+
void send({ type: "type", text });
|
|
1250
|
+
};
|
|
1251
|
+
const onFocus = () => {
|
|
1252
|
+
if (focused)
|
|
1253
|
+
return;
|
|
1254
|
+
focused = true;
|
|
1255
|
+
publish();
|
|
1256
|
+
};
|
|
1257
|
+
const onBlur = () => {
|
|
1258
|
+
if (!focused)
|
|
1259
|
+
return;
|
|
1260
|
+
focused = false;
|
|
1261
|
+
publish();
|
|
1262
|
+
};
|
|
1263
|
+
// Focusable, so keystrokes have somewhere to land. An <img> cannot hold focus on
|
|
1264
|
+
// its own, and a keydown listener on an element that can never be focused is a
|
|
1265
|
+
// listener that never fires — which is indistinguishable from the relay not
|
|
1266
|
+
// existing, and is how this looked.
|
|
1267
|
+
if (el.tabIndex === undefined || el.tabIndex < 0)
|
|
1268
|
+
el.tabIndex = 0;
|
|
1160
1269
|
el.addEventListener("pointerdown", onDown);
|
|
1161
1270
|
el.addEventListener("pointermove", onMove);
|
|
1162
1271
|
el.addEventListener("pointerup", onUp);
|
|
1163
1272
|
el.addEventListener("pointercancel", onUp);
|
|
1164
1273
|
el.addEventListener("wheel", onWheel);
|
|
1165
1274
|
el.addEventListener("contextmenu", onContextMenu);
|
|
1275
|
+
el.addEventListener("keydown", onKeyDown);
|
|
1276
|
+
el.addEventListener("paste", onPaste);
|
|
1277
|
+
el.addEventListener("focus", onFocus);
|
|
1278
|
+
el.addEventListener("blur", onBlur);
|
|
1166
1279
|
paint();
|
|
1167
1280
|
return () => {
|
|
1168
1281
|
el.removeEventListener("pointerdown", onDown);
|
|
@@ -1171,7 +1284,12 @@ class AgentFramework {
|
|
|
1171
1284
|
el.removeEventListener("pointercancel", onUp);
|
|
1172
1285
|
el.removeEventListener("wheel", onWheel);
|
|
1173
1286
|
el.removeEventListener("contextmenu", onContextMenu);
|
|
1287
|
+
el.removeEventListener("keydown", onKeyDown);
|
|
1288
|
+
el.removeEventListener("paste", onPaste);
|
|
1289
|
+
el.removeEventListener("focus", onFocus);
|
|
1290
|
+
el.removeEventListener("blur", onBlur);
|
|
1174
1291
|
dragging = false;
|
|
1292
|
+
focused = false;
|
|
1175
1293
|
if (element === el)
|
|
1176
1294
|
element = null;
|
|
1177
1295
|
};
|
|
@@ -1504,15 +1622,32 @@ class AgentFramework {
|
|
|
1504
1622
|
}
|
|
1505
1623
|
if (!res.ok) {
|
|
1506
1624
|
let detail = await res.text();
|
|
1625
|
+
// Alongside `detail`, when the server said which turn this was about. Read here
|
|
1626
|
+
// because this is the one place an API error is built from a response — every route's
|
|
1627
|
+
// refusal passes through it, so a route that starts saying which run failed needs no
|
|
1628
|
+
// change on this side.
|
|
1629
|
+
let ids;
|
|
1507
1630
|
try {
|
|
1508
|
-
|
|
1509
|
-
if (
|
|
1510
|
-
|
|
1631
|
+
const body = JSON.parse(detail);
|
|
1632
|
+
if (body && typeof body === "object") {
|
|
1633
|
+
const b = body;
|
|
1634
|
+
if ("detail" in b)
|
|
1635
|
+
detail = b.detail;
|
|
1636
|
+
else
|
|
1637
|
+
detail = body;
|
|
1638
|
+
const runId = typeof b.run_id === "string" ? b.run_id : undefined;
|
|
1639
|
+
const sessionId = typeof b.session_id === "string" ? b.session_id : undefined;
|
|
1640
|
+
if (runId || sessionId)
|
|
1641
|
+
ids = { runId, sessionId };
|
|
1642
|
+
}
|
|
1643
|
+
else {
|
|
1644
|
+
detail = body;
|
|
1645
|
+
}
|
|
1511
1646
|
}
|
|
1512
1647
|
catch {
|
|
1513
1648
|
/* leave as text */
|
|
1514
1649
|
}
|
|
1515
|
-
throw new AgentApiError(res.status, detail);
|
|
1650
|
+
throw new AgentApiError(res.status, detail, ids);
|
|
1516
1651
|
}
|
|
1517
1652
|
return res;
|
|
1518
1653
|
}
|
|
@@ -2555,7 +2690,13 @@ class AgentFramework {
|
|
|
2555
2690
|
// This used to be a bare message, so a stream that died at minute
|
|
2556
2691
|
// eighteen threw away the answer AND the only handle on where the rest
|
|
2557
2692
|
// of it had been written.
|
|
2558
|
-
|
|
2693
|
+
// The server's sentence, unprefixed. This read `"server: " + detail`, so
|
|
2694
|
+
// every streamed failure reached the customer as
|
|
2695
|
+
// `server: the upstream endpoint answered 408 …` — a lowercase field name in
|
|
2696
|
+
// front of the first word they read, and the one difference between the two
|
|
2697
|
+
// paths' otherwise identical message (OBE-140). `AgentStreamError` already
|
|
2698
|
+
// says where it came from; the label said it again, worse.
|
|
2699
|
+
throw new AgentStreamError(ev.data.detail, {
|
|
2559
2700
|
code: ev.data.code,
|
|
2560
2701
|
// The status the same refusal carries on `POST /chat` — 429 for a spend
|
|
2561
2702
|
// cap or a rate limit. Without it a streaming caller had no number to
|