@qoniai/qoni 0.4.1 → 0.4.3
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/CHANGELOG.md +31 -0
- package/README.md +23 -1
- package/README.zh-CN.md +16 -1
- package/dist/index.cjs +209 -107
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -2
- package/dist/index.d.ts +7 -2
- package/dist/index.js +209 -107
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,37 @@ This project follows semantic versioning. Before `1.0.0`, minor versions may
|
|
|
6
6
|
include SDK API changes, while patch versions are reserved for backward
|
|
7
7
|
compatible fixes.
|
|
8
8
|
|
|
9
|
+
## 0.4.3 - 2026-09-09
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Treat malformed or empty screenshot data URIs as undecodable instead of
|
|
14
|
+
emitting invalid screenshot bytes or interrupting `wait()`. Preserve the
|
|
15
|
+
existing progress/raw fallback with screenshot capture enabled or disabled.
|
|
16
|
+
- Keep valid padded, unpadded, whitespace-containing and URL-safe base64
|
|
17
|
+
screenshots compatible.
|
|
18
|
+
|
|
19
|
+
## 0.4.2 - 2026-09-09
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Require authoritative run details when settling `wait()`: preserve detail
|
|
24
|
+
errors and reject unconfirmed terminal states instead of inventing success.
|
|
25
|
+
- Reuse terminal details read after a clean stream close and resume from every
|
|
26
|
+
wire event cursor, including frames filtered from semantic events.
|
|
27
|
+
- Propagate wait cancellation to detail and artifact-list GETs. Bound complete
|
|
28
|
+
JSON, NDJSON, binary, and HTTP error body reads by the request timeout, cancel
|
|
29
|
+
stalled readers, and discard late responses without starting replacement runs.
|
|
30
|
+
- Clean up wait timers and SSE abort listeners. Cancel idle SSE readers directly
|
|
31
|
+
and release their locks without waiting indefinitely for source cancellation.
|
|
32
|
+
- Retry HTTP 408 stream failures within the configured reconnect budget and stop
|
|
33
|
+
delivering buffered events after cancellation.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Optional abort signals on product run-detail and artifact-list wire reads;
|
|
38
|
+
existing calls without a signal remain compatible.
|
|
39
|
+
|
|
9
40
|
## Unreleased
|
|
10
41
|
|
|
11
42
|
### Breaking
|
package/README.md
CHANGED
|
@@ -161,6 +161,8 @@ console.log(result.output);
|
|
|
161
161
|
|
|
162
162
|
`run()` returns a `RunHandle` typed to the product: `qoni.deepResearch.run()` is a `RunHandle<DeepResearchEvent>`, so its events are narrowed to what Deep Research actually emits. `capture: { screenshots: true }` opts into per-step screenshots, decoded to bytes for you. `wait()` drives the run to its terminal state and settles the result (`result.raw` carries cost, step counts, token usage).
|
|
163
163
|
|
|
164
|
+
Undecodable or empty screenshot data URIs never interrupt `wait()` or invoke `onScreenshot`, regardless of capture settings. They follow the existing progress fallback when a human-readable message is available; `onWireEvent` always retains the original payload, including frames with no progress text.
|
|
165
|
+
|
|
164
166
|
> **Best practice — match `event.type` against the `QoniEventTypes` constants.**
|
|
165
167
|
> Each product handle is typed to its own event set, so in TypeScript a case
|
|
166
168
|
> for another product's event is a compile error and autocomplete shows only
|
|
@@ -512,7 +514,7 @@ const qoni = new Qoni({
|
|
|
512
514
|
| `secretKey` | `string` | Yes | Qoni secret key from Qoni Console. |
|
|
513
515
|
| `host` | `string` | No | Optional Qoni Console/SDK gateway override for private or local deployments. |
|
|
514
516
|
| `fetch` | `typeof fetch` | No | Custom transport implementation. |
|
|
515
|
-
| `timeoutMs` | `number` | No | Per-request timeout (connect +
|
|
517
|
+
| `timeoutMs` | `number` | No | Per-request timeout (connect + headers + body for one-shot requests; headers only for successful SSE connections). Defaults to `30000`. |
|
|
516
518
|
|
|
517
519
|
> **`timeoutMs` does not bound a long-running run.** It applies to individual
|
|
518
520
|
> one-shot HTTP requests (delegation, `status()`, a single POST). The live
|
|
@@ -550,6 +552,26 @@ await run.cancel(reason?) // idempotent
|
|
|
550
552
|
|
|
551
553
|
`await run.wait(...)` settles to a `RunResult`:
|
|
552
554
|
|
|
555
|
+
The run-detail response must confirm a terminal state. Detail read errors propagate
|
|
556
|
+
unchanged; `wait()` never substitutes an SSE completion payload for the authoritative
|
|
557
|
+
result. A nonterminal or incomplete detail rejects with a retryable `upstream.failed`
|
|
558
|
+
error; use `status()` or re-attach to recover.
|
|
559
|
+
|
|
560
|
+
`timeoutMs` and `signal` stop waiting for detail/artifact reads and promises returned
|
|
561
|
+
by `onEvent`, `onWireEvent`, `onInteraction`, and `onScreenshot`. They cannot preempt
|
|
562
|
+
synchronous JavaScript or stop work already started inside a callback. `onReconnect`
|
|
563
|
+
is synchronous telemetry: its return value is not awaited and returned promise
|
|
564
|
+
rejections are not handled. Do not pass an async callback there; synchronous throws
|
|
565
|
+
propagate. Reconnect backoff remains abortable and subject to the wait deadline.
|
|
566
|
+
|
|
567
|
+
Detail/artifact-list GETs receive the wait signal: aborting or timing out a wait aborts
|
|
568
|
+
the request and cancels its body reader. The client's per-request timer also covers
|
|
569
|
+
complete JSON/NDJSON and binary body reads, including HTTP error bodies. A late response
|
|
570
|
+
from a custom fetch that ignores abort has its body canceled and cannot resume settlement
|
|
571
|
+
or start subsequent run requests. Custom transports must honor `RequestInit.signal` to
|
|
572
|
+
stop their own in-flight network work. Stopping a wait neither cancels the server-side
|
|
573
|
+
run nor creates a replacement task; use `cancel()` explicitly to stop the existing run.
|
|
574
|
+
|
|
553
575
|
```ts
|
|
554
576
|
interface RunResult {
|
|
555
577
|
runId: string;
|
package/README.zh-CN.md
CHANGED
|
@@ -154,6 +154,8 @@ console.log(result.output);
|
|
|
154
154
|
|
|
155
155
|
`run()` 返回一个 `RunHandle`。核心事件(状态、动作、交互、完成)始终默认推送;`capture: { screenshots: true }` 开启逐步截图,并且 SDK 已经把图片解码成字节。`wait()` 把任务跑到终态,并从权威 run envelope 结算结果(成本、步数、token 用量都在 `result.raw` 上)。
|
|
156
156
|
|
|
157
|
+
无论是否开启 capture,不可解码或空的截图 data URI 都不会中断 `wait()`,也不会触发 `onScreenshot`。有可读消息时按既有契约降级为 progress;`onWireEvent` 始终保留原始 payload,包括没有 progress 文本的帧。
|
|
158
|
+
|
|
157
159
|
需要逐步控制时,直接消费事件流,`switch (event.type)` 用 `QoniEventTypes` 常量匹配。每个产品的 handle 按产品分型(`qoni.deepResearch.run()` 出来是 `RunHandle<DeepResearchEvent>`),TS 里写错产品的事件会编译报错、补全只列本产品的。内部细节(supervisor 编排、子 agent 点击/输入、telemetry)统一折叠成 `QoniEventTypes.Progress`;匹配到 `case` 后 `event.data` 就是该类型的固定形状。原始 wire 类型不导出。
|
|
158
160
|
|
|
159
161
|
```ts
|
|
@@ -482,7 +484,7 @@ const qoni = new Qoni({
|
|
|
482
484
|
| `secretKey` | `string` | 是 | Qoni Console 创建的 secret key。 |
|
|
483
485
|
| `host` | `string` | 否 | 私有化或本地部署时可选的 Qoni Console/SDK 网关覆盖地址。 |
|
|
484
486
|
| `fetch` | `typeof fetch` | 否 | 自定义传输实现。 |
|
|
485
|
-
| `timeoutMs` | `number` | 否 |
|
|
487
|
+
| `timeoutMs` | `number` | 否 | 单次请求超时(一次性请求覆盖连接、响应头及响应体;成功建立的 SSE 仅覆盖到响应头),默认 `30000`。 |
|
|
486
488
|
|
|
487
489
|
> **`timeoutMs` 不会限制长任务。** 它只作用于单次一次性 HTTP 请求(委托、`status()`、单个 POST)。事件流(`events()` / `wait()`)**不受它约束**——流的响应头一到,这个单次计时器就被清掉了,所以一个跑一小时的 Deep Research 在默认 30s 客户端超时下也能正常流式。要限制等待时长用 `wait({ timeoutMs })`;不传则 `wait()` 一直等到终态。`wait()` 超时**不会停止 run**——它在服务端继续跑,可以再 `attach()` 回去。
|
|
488
490
|
|
|
@@ -512,6 +514,19 @@ await run.cancel(reason?) // 幂等
|
|
|
512
514
|
|
|
513
515
|
`await run.wait(...)` 结算成一个 `RunResult`:
|
|
514
516
|
|
|
517
|
+
必须由 run-detail 响应确认终态。详情读取错误原样抛出,`wait()` 不会用 SSE 完成事件替代权威结果。
|
|
518
|
+
详情仍为非终态或缺少终态信息时,抛出可重试的 `upstream.failed`;可通过 `status()` 或重新 `attach()` 恢复。
|
|
519
|
+
|
|
520
|
+
`timeoutMs` 与 `signal` 会结束详情、交付物读取及 `onEvent`、`onWireEvent`、`onInteraction`、
|
|
521
|
+
`onScreenshot` 返回的 Promise 的等待,但不能抢占同步 JavaScript,也不能停止回调内部已启动的工作。
|
|
522
|
+
`onReconnect` 是同步遥测通知:返回值不被等待,返回的 Promise 的 rejection 也不被处理,因此不要传 async
|
|
523
|
+
回调;同步抛错会向上传播。重连退避等待仍可被 signal 或 wait 超时中断。
|
|
524
|
+
|
|
525
|
+
详情/交付物列表 GET 会接收 wait signal:wait 中止或超时时会中止请求、取消并释放响应体 reader。
|
|
526
|
+
客户端单次请求计时器也覆盖完整 JSON/NDJSON、二进制响应体及 HTTP 错误体读取。即使自定义 fetch 忽略
|
|
527
|
+
abort,SDK 也会取消其迟到响应体,不恢复结算或启动后续 run 请求;自定义传输须遵守 `RequestInit.signal`
|
|
528
|
+
才能停止其内部网络工作。停止 wait 既不会取消服务端 run,也不会创建替代任务;需要停止原 run 时须显式调用 `cancel()`。
|
|
529
|
+
|
|
515
530
|
```ts
|
|
516
531
|
interface RunResult {
|
|
517
532
|
runId: string;
|
package/dist/index.cjs
CHANGED
|
@@ -397,8 +397,14 @@ function decodeDataUri(uri) {
|
|
|
397
397
|
const match = /^data:([^;,]*)?(;base64)?,(.*)$/s.exec(uri);
|
|
398
398
|
if (!match) return void 0;
|
|
399
399
|
const [, mime, isBase64, body] = match;
|
|
400
|
-
const
|
|
401
|
-
|
|
400
|
+
const encoded = isBase64 ? body.replace(/\s/g, "") : body;
|
|
401
|
+
if (isBase64 && (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(encoded) || encoded.length % 4 === 1 || encoded.includes("=") && encoded.length % 4 !== 0)) return void 0;
|
|
402
|
+
try {
|
|
403
|
+
const bytes = isBase64 ? new Uint8Array(Buffer.from(encoded, "base64")) : new TextEncoder().encode(decodeURIComponent(encoded));
|
|
404
|
+
return bytes.length ? { bytes, mime: mime || "application/octet-stream" } : void 0;
|
|
405
|
+
} catch {
|
|
406
|
+
return void 0;
|
|
407
|
+
}
|
|
402
408
|
}
|
|
403
409
|
function asString(value) {
|
|
404
410
|
return typeof value === "string" && value ? value : void 0;
|
|
@@ -816,11 +822,11 @@ var RunHandle = class {
|
|
|
816
822
|
}
|
|
817
823
|
/** Drive the run to a terminal state and return the settled result. */
|
|
818
824
|
async wait(opts = {}) {
|
|
819
|
-
const { signal, timedOut } = withTimeout(opts.signal, opts.timeoutMs);
|
|
825
|
+
const { signal, timedOut, dispose } = withTimeout(opts.signal, opts.timeoutMs);
|
|
820
826
|
let lastEventId;
|
|
821
827
|
let eventIdAtLastClose;
|
|
822
828
|
let sawCleanClose = false;
|
|
823
|
-
let
|
|
829
|
+
let terminalDetail;
|
|
824
830
|
let terminalDoneReason;
|
|
825
831
|
let screenshotIndex = 0;
|
|
826
832
|
try {
|
|
@@ -828,24 +834,28 @@ var RunHandle = class {
|
|
|
828
834
|
for await (const event of this.events({
|
|
829
835
|
signal,
|
|
830
836
|
lastEventId,
|
|
831
|
-
onWireEvent:
|
|
837
|
+
onWireEvent: (wire) => waitWithSignal(() => {
|
|
838
|
+
if (wire.id !== void 0) lastEventId = wire.id;
|
|
839
|
+
return opts.onWireEvent?.(wire);
|
|
840
|
+
}, signal),
|
|
832
841
|
onReconnect: opts.onReconnect
|
|
833
842
|
})) {
|
|
834
|
-
|
|
835
|
-
await opts.onEvent?.(event);
|
|
843
|
+
await waitWithSignal(() => opts.onEvent?.(event), signal);
|
|
836
844
|
if (event.type === "screenshot") {
|
|
837
|
-
await opts.onScreenshot?.(event.image, screenshotIndex++);
|
|
845
|
+
await waitWithSignal(() => opts.onScreenshot?.(event.image, screenshotIndex++), signal);
|
|
838
846
|
} else if (event.type === "interaction") {
|
|
839
|
-
await opts.onInteraction?.(this.interactionHandle(event.data), event);
|
|
847
|
+
await waitWithSignal(() => opts.onInteraction?.(this.interactionHandle(event.data), event), signal);
|
|
840
848
|
}
|
|
841
849
|
if (event.isTerminal) {
|
|
842
|
-
terminalPayload = isJsonObject(event.raw.data) ? event.raw.data : void 0;
|
|
843
850
|
if (event.type === "done") terminalDoneReason = event.data.terminalReason || void 0;
|
|
844
851
|
break settled;
|
|
845
852
|
}
|
|
846
853
|
}
|
|
847
|
-
const detail2 = await this.ops.getDetail();
|
|
848
|
-
if (isTerminalRunStatus(detail2.status))
|
|
854
|
+
const detail2 = await waitWithSignal(() => this.ops.getDetail(signal), signal);
|
|
855
|
+
if (isTerminalRunStatus(normalizeRunStatus(this.id, detail2).status)) {
|
|
856
|
+
terminalDetail = detail2;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
849
859
|
if (sawCleanClose && lastEventId === eventIdAtLastClose) {
|
|
850
860
|
throw new QoniError(
|
|
851
861
|
`run event stream ended before a terminal event while the run is still "${String(detail2.status)}" \u2014 re-attach or poll status()`,
|
|
@@ -855,6 +865,11 @@ var RunHandle = class {
|
|
|
855
865
|
sawCleanClose = true;
|
|
856
866
|
eventIdAtLastClose = lastEventId;
|
|
857
867
|
}
|
|
868
|
+
const detail = terminalDetail ?? await waitWithSignal(() => this.ops.getDetail(signal), signal);
|
|
869
|
+
this.#adoptSessionRef(detail);
|
|
870
|
+
const result = settleRunResult(this.id, detail, [], terminalDoneReason);
|
|
871
|
+
result.artifacts = this.ops.listArtifacts ? await waitWithSignal(() => this.ops.listArtifacts(signal), signal) : [];
|
|
872
|
+
return result;
|
|
858
873
|
} catch (err) {
|
|
859
874
|
if (timedOut() && !opts.signal?.aborted) {
|
|
860
875
|
throw new QoniTimeoutError(
|
|
@@ -863,17 +878,9 @@ var RunHandle = class {
|
|
|
863
878
|
);
|
|
864
879
|
}
|
|
865
880
|
throw err;
|
|
881
|
+
} finally {
|
|
882
|
+
dispose();
|
|
866
883
|
}
|
|
867
|
-
let detail;
|
|
868
|
-
try {
|
|
869
|
-
detail = await this.ops.getDetail();
|
|
870
|
-
} catch (err) {
|
|
871
|
-
if (!terminalPayload) throw err;
|
|
872
|
-
detail = terminalPayload;
|
|
873
|
-
}
|
|
874
|
-
this.#adoptSessionRef(detail);
|
|
875
|
-
const artifacts = this.ops.listArtifacts ? await this.ops.listArtifacts() : [];
|
|
876
|
-
return settleRunResult(this.id, detail, artifacts, terminalDoneReason);
|
|
877
884
|
}
|
|
878
885
|
/**
|
|
879
886
|
* Wrap an {@link Interaction} (from `event.data` of an `interaction` event)
|
|
@@ -926,15 +933,12 @@ function normalizeRunStatus(id, detail) {
|
|
|
926
933
|
function settleRunResult(runId, detail, artifacts, fallbackTerminalReason) {
|
|
927
934
|
const terminalReason = (typeof detail.terminal_reason === "string" ? detail.terminal_reason : void 0) ?? fallbackTerminalReason;
|
|
928
935
|
const isTaskSuccessful = typeof detail.is_task_successful === "boolean" ? detail.is_task_successful : void 0;
|
|
929
|
-
|
|
930
|
-
if (isTerminalRunStatus(
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
status = "failed";
|
|
936
|
-
} else {
|
|
937
|
-
status = "succeeded";
|
|
936
|
+
const status = normalizeRunStatus(runId, detail).status;
|
|
937
|
+
if (!isTerminalRunStatus(status)) {
|
|
938
|
+
throw new QoniError(
|
|
939
|
+
"run detail did not confirm a terminal state \u2014 re-attach or poll status()",
|
|
940
|
+
{ code: "upstream.failed", retryable: true, body: detail }
|
|
941
|
+
);
|
|
938
942
|
}
|
|
939
943
|
return {
|
|
940
944
|
runId,
|
|
@@ -961,19 +965,40 @@ function isVideoFrame(wire) {
|
|
|
961
965
|
return type === "browser_video_frame" || type === "run.browser_video_frame";
|
|
962
966
|
}
|
|
963
967
|
function withTimeout(signal, timeoutMs) {
|
|
964
|
-
if (!timeoutMs) return { signal, timedOut: () => false
|
|
968
|
+
if (!timeoutMs) return { signal, timedOut: () => false, dispose: () => {
|
|
969
|
+
} };
|
|
965
970
|
const controller = new AbortController();
|
|
966
971
|
let fired = false;
|
|
967
972
|
const timer = setTimeout(() => {
|
|
968
973
|
fired = true;
|
|
969
974
|
controller.abort();
|
|
970
975
|
}, timeoutMs);
|
|
971
|
-
|
|
976
|
+
const onAbort = () => controller.abort(signal?.reason);
|
|
977
|
+
const dispose = () => {
|
|
978
|
+
clearTimeout(timer);
|
|
979
|
+
signal?.removeEventListener("abort", onAbort);
|
|
980
|
+
};
|
|
981
|
+
controller.signal.addEventListener("abort", dispose, { once: true });
|
|
972
982
|
if (signal) {
|
|
973
|
-
if (signal.aborted)
|
|
974
|
-
else signal.addEventListener("abort",
|
|
983
|
+
if (signal.aborted) onAbort();
|
|
984
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
975
985
|
}
|
|
976
|
-
return { signal: controller.signal, timedOut: () => fired };
|
|
986
|
+
return { signal: controller.signal, timedOut: () => fired, dispose };
|
|
987
|
+
}
|
|
988
|
+
function waitWithSignal(operation, signal) {
|
|
989
|
+
if (!signal) return Promise.resolve().then(operation);
|
|
990
|
+
return new Promise((resolve, reject) => {
|
|
991
|
+
const onAbort = () => reject(signal.reason ?? new DOMException("Aborted", "AbortError"));
|
|
992
|
+
if (signal.aborted) {
|
|
993
|
+
onAbort();
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
997
|
+
Promise.resolve().then(() => {
|
|
998
|
+
signal.throwIfAborted();
|
|
999
|
+
return operation();
|
|
1000
|
+
}).then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
1001
|
+
});
|
|
977
1002
|
}
|
|
978
1003
|
|
|
979
1004
|
// src/deep-research.ts
|
|
@@ -988,6 +1013,7 @@ function createDeepResearchNamespace(transport) {
|
|
|
988
1013
|
`/deep_research/runs/${encodeURIComponent(input.runId)}`,
|
|
989
1014
|
input.token,
|
|
990
1015
|
{
|
|
1016
|
+
signal: input.signal,
|
|
991
1017
|
requiredScopes: ["webagent.deep_research:read"]
|
|
992
1018
|
}
|
|
993
1019
|
),
|
|
@@ -1033,6 +1059,7 @@ function createDeepResearchNamespace(transport) {
|
|
|
1033
1059
|
`/deep_research/runs/${encodeURIComponent(input.runId)}/artifacts`,
|
|
1034
1060
|
input.token,
|
|
1035
1061
|
{
|
|
1062
|
+
signal: input.signal,
|
|
1036
1063
|
requiredScopes: ["webagent.deep_research:read"]
|
|
1037
1064
|
}
|
|
1038
1065
|
),
|
|
@@ -1047,7 +1074,7 @@ function createDeepResearchNamespace(transport) {
|
|
|
1047
1074
|
};
|
|
1048
1075
|
function buildOps(token, runId) {
|
|
1049
1076
|
return {
|
|
1050
|
-
getDetail: async () => asRecord((await api.get({ token, runId })).data),
|
|
1077
|
+
getDetail: async (signal) => asRecord((await api.get({ token, runId, signal })).data),
|
|
1051
1078
|
streamEvents: (opts) => api.events({
|
|
1052
1079
|
token,
|
|
1053
1080
|
runId,
|
|
@@ -1067,8 +1094,8 @@ function createDeepResearchNamespace(transport) {
|
|
|
1067
1094
|
);
|
|
1068
1095
|
},
|
|
1069
1096
|
cancel: async () => asRecord((await api.cancel({ token, runId })).data),
|
|
1070
|
-
listArtifacts: async () => {
|
|
1071
|
-
const res = await api.listArtifacts({ token, runId });
|
|
1097
|
+
listArtifacts: async (signal) => {
|
|
1098
|
+
const res = await api.listArtifacts({ token, runId, signal });
|
|
1072
1099
|
const items = Array.isArray(res.data?.items) ? res.data.items : [];
|
|
1073
1100
|
return items.filter(isJsonObject).map((item) => toArtifact(item, token, runId));
|
|
1074
1101
|
}
|
|
@@ -1188,6 +1215,7 @@ function createDoAnythingNamespace(transport) {
|
|
|
1188
1215
|
`/do_anything/runs/${encodeURIComponent(input.runId)}`,
|
|
1189
1216
|
input.token,
|
|
1190
1217
|
{
|
|
1218
|
+
signal: input.signal,
|
|
1191
1219
|
requiredScopes: ["webagent.do_anything:read"]
|
|
1192
1220
|
}
|
|
1193
1221
|
),
|
|
@@ -1245,7 +1273,7 @@ function createDoAnythingNamespace(transport) {
|
|
|
1245
1273
|
};
|
|
1246
1274
|
function buildOps(token, runId, capture) {
|
|
1247
1275
|
return {
|
|
1248
|
-
getDetail: async () => asRecord2((await api.getRun({ token, runId })).data),
|
|
1276
|
+
getDetail: async (signal) => asRecord2((await api.getRun({ token, runId, signal })).data),
|
|
1249
1277
|
streamEvents: (opts) => api.events({
|
|
1250
1278
|
token,
|
|
1251
1279
|
runId,
|
|
@@ -1839,6 +1867,7 @@ function createWebSearchNamespace(transport) {
|
|
|
1839
1867
|
`/web_search/runs/${encodeURIComponent(input.runId)}`,
|
|
1840
1868
|
input.token,
|
|
1841
1869
|
{
|
|
1870
|
+
signal: input.signal,
|
|
1842
1871
|
requiredScopes: ["webagent.web_search:read"]
|
|
1843
1872
|
}
|
|
1844
1873
|
),
|
|
@@ -1864,7 +1893,7 @@ function createWebSearchNamespace(transport) {
|
|
|
1864
1893
|
};
|
|
1865
1894
|
function buildOps(token, runId) {
|
|
1866
1895
|
return {
|
|
1867
|
-
getDetail: async () => asRecord4((await api.get({ token, runId })).data),
|
|
1896
|
+
getDetail: async (signal) => asRecord4((await api.get({ token, runId, signal })).data),
|
|
1868
1897
|
streamEvents: (opts) => api.events({
|
|
1869
1898
|
token,
|
|
1870
1899
|
runId,
|
|
@@ -2137,7 +2166,9 @@ var Qoni = class {
|
|
|
2137
2166
|
}
|
|
2138
2167
|
}
|
|
2139
2168
|
async webAgentJson(method, path, token, payload = {}) {
|
|
2169
|
+
payload.signal?.throwIfAborted();
|
|
2140
2170
|
const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
|
|
2171
|
+
payload.signal?.throwIfAborted();
|
|
2141
2172
|
const baseUrl = await this.baseUrlFor("webagent");
|
|
2142
2173
|
return this.jsonRequest(
|
|
2143
2174
|
baseUrl,
|
|
@@ -2148,9 +2179,11 @@ var Qoni = class {
|
|
|
2148
2179
|
// Binary GET against the WebAgent API — artifact content downloads. JSON
|
|
2149
2180
|
// error payloads still map to typed QoniErrors.
|
|
2150
2181
|
async webAgentBytes(path, token, payload = {}) {
|
|
2182
|
+
payload.signal?.throwIfAborted();
|
|
2151
2183
|
const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
|
|
2184
|
+
payload.signal?.throwIfAborted();
|
|
2152
2185
|
const baseUrl = await this.baseUrlFor("webagent");
|
|
2153
|
-
|
|
2186
|
+
return this.fetchWithTimeout(
|
|
2154
2187
|
urlWithBasePath(baseUrl, this.webAgentPath(runtimeToken, path), payload.query),
|
|
2155
2188
|
{
|
|
2156
2189
|
method: "GET",
|
|
@@ -2159,20 +2192,22 @@ var Qoni = class {
|
|
|
2159
2192
|
authorization: `Bearer ${runtimeToken}`,
|
|
2160
2193
|
...payload.headers || {}
|
|
2161
2194
|
}
|
|
2162
|
-
}
|
|
2195
|
+
},
|
|
2196
|
+
async (response, signal) => {
|
|
2197
|
+
if (!response.ok) {
|
|
2198
|
+
throw errorFromPayload(
|
|
2199
|
+
response.status,
|
|
2200
|
+
await readJson(response, signal),
|
|
2201
|
+
`Qoni request failed with HTTP ${response.status}`
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
2204
|
+
return {
|
|
2205
|
+
bytes: await readBody(response, signal),
|
|
2206
|
+
mime: response.headers.get("content-type") || "application/octet-stream"
|
|
2207
|
+
};
|
|
2208
|
+
},
|
|
2209
|
+
payload.signal
|
|
2163
2210
|
);
|
|
2164
|
-
if (!response.ok) {
|
|
2165
|
-
throw errorFromPayload(
|
|
2166
|
-
response.status,
|
|
2167
|
-
await readJson(response),
|
|
2168
|
-
`Qoni request failed with HTTP ${response.status}`
|
|
2169
|
-
);
|
|
2170
|
-
}
|
|
2171
|
-
const buffer = await response.arrayBuffer();
|
|
2172
|
-
return {
|
|
2173
|
-
bytes: new Uint8Array(buffer),
|
|
2174
|
-
mime: response.headers.get("content-type") || "application/octet-stream"
|
|
2175
|
-
};
|
|
2176
2211
|
}
|
|
2177
2212
|
async *webAgentSSE(path, token, payload = {}) {
|
|
2178
2213
|
const runtimeToken = await this.productTokenFor("webagent", token, payload.requiredScopes);
|
|
@@ -2212,15 +2247,16 @@ var Qoni = class {
|
|
|
2212
2247
|
});
|
|
2213
2248
|
}
|
|
2214
2249
|
async jsonRequest(baseUrl, pathname, options) {
|
|
2215
|
-
const response = await this.fetchWithTimeout(
|
|
2250
|
+
const { response, payload } = await this.fetchWithTimeout(
|
|
2216
2251
|
urlWithBasePath(baseUrl, pathname, options.query),
|
|
2217
2252
|
{
|
|
2218
2253
|
method: options.method,
|
|
2219
2254
|
headers: requestHeaders(options),
|
|
2220
2255
|
body: requestBody(options.body)
|
|
2221
|
-
}
|
|
2256
|
+
},
|
|
2257
|
+
async (response2, signal) => ({ response: response2, payload: await readJson(response2, signal) }),
|
|
2258
|
+
options.signal
|
|
2222
2259
|
);
|
|
2223
|
-
const payload = await readJson(response);
|
|
2224
2260
|
if (!response.ok) {
|
|
2225
2261
|
throw errorFromPayload(
|
|
2226
2262
|
response.status,
|
|
@@ -2270,45 +2306,55 @@ var Qoni = class {
|
|
|
2270
2306
|
}
|
|
2271
2307
|
}
|
|
2272
2308
|
async *sseConnectOnce(baseUrl, pathname, options) {
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
accept: "text/event-stream",
|
|
2279
|
-
authorization: `Bearer ${options.token}`,
|
|
2280
|
-
...options.headers || {},
|
|
2281
|
-
...options.lastEventId ? { "last-event-id": options.lastEventId } : {}
|
|
2282
|
-
}
|
|
2283
|
-
},
|
|
2284
|
-
options.signal,
|
|
2285
|
-
{ streamBody: true }
|
|
2286
|
-
);
|
|
2287
|
-
if (!response.ok) {
|
|
2288
|
-
throw errorFromPayload(
|
|
2289
|
-
response.status,
|
|
2290
|
-
await readJson(response),
|
|
2291
|
-
`Qoni stream failed with HTTP ${response.status}`
|
|
2292
|
-
);
|
|
2293
|
-
}
|
|
2294
|
-
if (!response.body) {
|
|
2295
|
-
throw errorFromPayload(502, void 0, "Qoni stream response has no body");
|
|
2296
|
-
}
|
|
2297
|
-
const reader = response.body.getReader();
|
|
2309
|
+
let cleanup;
|
|
2310
|
+
let reader;
|
|
2311
|
+
const onAbort = () => {
|
|
2312
|
+
void reader?.cancel(options.signal?.reason).catch(() => void 0);
|
|
2313
|
+
};
|
|
2298
2314
|
const decoder = new TextDecoder();
|
|
2299
2315
|
let buffer = "";
|
|
2300
2316
|
try {
|
|
2317
|
+
const response = await this.fetchWithTimeout(
|
|
2318
|
+
urlWithBasePath(baseUrl, pathname, options.query),
|
|
2319
|
+
{
|
|
2320
|
+
method: "GET",
|
|
2321
|
+
headers: {
|
|
2322
|
+
accept: "text/event-stream",
|
|
2323
|
+
authorization: `Bearer ${options.token}`,
|
|
2324
|
+
...options.headers || {},
|
|
2325
|
+
...options.lastEventId ? { "last-event-id": options.lastEventId } : {}
|
|
2326
|
+
}
|
|
2327
|
+
},
|
|
2328
|
+
async (response2, signal) => {
|
|
2329
|
+
if (!response2.ok) {
|
|
2330
|
+
throw errorFromPayload(
|
|
2331
|
+
response2.status,
|
|
2332
|
+
await readJson(response2, signal),
|
|
2333
|
+
`Qoni stream failed with HTTP ${response2.status}`
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2336
|
+
return response2;
|
|
2337
|
+
},
|
|
2338
|
+
options.signal,
|
|
2339
|
+
{ streamCleanup: (dispose) => {
|
|
2340
|
+
cleanup = dispose;
|
|
2341
|
+
} }
|
|
2342
|
+
);
|
|
2343
|
+
if (!response.body) {
|
|
2344
|
+
throw errorFromPayload(502, void 0, "Qoni stream response has no body");
|
|
2345
|
+
}
|
|
2346
|
+
reader = response.body.getReader();
|
|
2347
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
2301
2348
|
while (true) {
|
|
2302
|
-
|
|
2303
|
-
await reader.cancel().catch(() => void 0);
|
|
2304
|
-
throw options.signal.reason || new DOMException("Aborted", "AbortError");
|
|
2305
|
-
}
|
|
2349
|
+
options.signal?.throwIfAborted();
|
|
2306
2350
|
const { done, value } = await reader.read();
|
|
2351
|
+
options.signal?.throwIfAborted();
|
|
2307
2352
|
if (done) break;
|
|
2308
2353
|
buffer += decoder.decode(value, { stream: true });
|
|
2309
2354
|
const chunks = buffer.split(/\r?\n\r?\n/);
|
|
2310
2355
|
buffer = chunks.pop() || "";
|
|
2311
2356
|
for (const chunk of chunks) {
|
|
2357
|
+
options.signal?.throwIfAborted();
|
|
2312
2358
|
const event = parseSSEEvent(chunk);
|
|
2313
2359
|
if (event) yield event;
|
|
2314
2360
|
}
|
|
@@ -2316,14 +2362,16 @@ var Qoni = class {
|
|
|
2316
2362
|
const finalEvent = parseSSEEvent(buffer);
|
|
2317
2363
|
if (finalEvent) yield finalEvent;
|
|
2318
2364
|
} finally {
|
|
2319
|
-
|
|
2365
|
+
cleanup?.();
|
|
2366
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
2367
|
+
void reader?.cancel().catch(() => void 0);
|
|
2320
2368
|
try {
|
|
2321
|
-
reader
|
|
2369
|
+
reader?.releaseLock();
|
|
2322
2370
|
} catch {
|
|
2323
2371
|
}
|
|
2324
2372
|
}
|
|
2325
2373
|
}
|
|
2326
|
-
async fetchWithTimeout(input, init, signal, opts) {
|
|
2374
|
+
async fetchWithTimeout(input, init, consume, signal, opts) {
|
|
2327
2375
|
if (signal?.aborted) throw signal.reason || new DOMException("Aborted", "AbortError");
|
|
2328
2376
|
const controller = new AbortController();
|
|
2329
2377
|
const resolved = resolveLocalGenauthRequest(input);
|
|
@@ -2338,17 +2386,39 @@ var Qoni = class {
|
|
|
2338
2386
|
didTimeout = true;
|
|
2339
2387
|
controller.abort();
|
|
2340
2388
|
}, this.timeoutMs);
|
|
2389
|
+
let response;
|
|
2341
2390
|
try {
|
|
2342
|
-
|
|
2391
|
+
response = await new Promise((resolve, reject) => {
|
|
2392
|
+
const onAbort = () => reject(controller.signal.reason);
|
|
2393
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
2394
|
+
Promise.resolve().then(() => {
|
|
2395
|
+
controller.signal.throwIfAborted();
|
|
2396
|
+
return this.fetchImpl(resolved.url, { ...requestInit, signal: controller.signal });
|
|
2397
|
+
}).then((response2) => {
|
|
2398
|
+
if (controller.signal.aborted) {
|
|
2399
|
+
void response2.body?.cancel(controller.signal.reason).catch(() => void 0);
|
|
2400
|
+
reject(controller.signal.reason);
|
|
2401
|
+
} else {
|
|
2402
|
+
resolve(response2);
|
|
2403
|
+
}
|
|
2404
|
+
}, reject).finally(() => controller.signal.removeEventListener("abort", onAbort));
|
|
2405
|
+
});
|
|
2406
|
+
const result = await consume(response, controller.signal);
|
|
2407
|
+
controller.signal.throwIfAborted();
|
|
2408
|
+
return result;
|
|
2343
2409
|
} catch (error) {
|
|
2344
|
-
|
|
2410
|
+
void response?.body?.cancel().catch(() => void 0);
|
|
2411
|
+
if (signal?.aborted) throw signal.reason;
|
|
2412
|
+
if (didTimeout || isConnectTimeoutError(error)) {
|
|
2345
2413
|
throw timeoutError(error);
|
|
2346
2414
|
}
|
|
2347
2415
|
if (isFetchNetworkError(error)) throw networkError(error);
|
|
2348
2416
|
throw error;
|
|
2349
2417
|
} finally {
|
|
2350
2418
|
clearTimeout(timeout);
|
|
2351
|
-
|
|
2419
|
+
const cleanup = () => signal?.removeEventListener("abort", abortFromSignal);
|
|
2420
|
+
if (opts?.streamCleanup) opts.streamCleanup(cleanup);
|
|
2421
|
+
else cleanup();
|
|
2352
2422
|
}
|
|
2353
2423
|
}
|
|
2354
2424
|
async baseUrlFor(service) {
|
|
@@ -2404,7 +2474,7 @@ var Qoni = class {
|
|
|
2404
2474
|
pathname,
|
|
2405
2475
|
{}
|
|
2406
2476
|
);
|
|
2407
|
-
const response = await this.fetchWithTimeout(
|
|
2477
|
+
const { response, payload } = await this.fetchWithTimeout(
|
|
2408
2478
|
urlWithBasePath(this.host, pathname),
|
|
2409
2479
|
{
|
|
2410
2480
|
method: "GET",
|
|
@@ -2412,9 +2482,9 @@ var Qoni = class {
|
|
|
2412
2482
|
accept: "application/json",
|
|
2413
2483
|
...headers
|
|
2414
2484
|
}
|
|
2415
|
-
}
|
|
2485
|
+
},
|
|
2486
|
+
async (response2, signal) => ({ response: response2, payload: await readJson(response2, signal) })
|
|
2416
2487
|
);
|
|
2417
|
-
const payload = await readJson(response);
|
|
2418
2488
|
if (!response.ok) {
|
|
2419
2489
|
throw errorFromPayload(
|
|
2420
2490
|
response.status,
|
|
@@ -2508,18 +2578,52 @@ function requestBody(body) {
|
|
|
2508
2578
|
function isBodyInit(value) {
|
|
2509
2579
|
return typeof value === "string" || value instanceof ArrayBuffer || value instanceof Blob || value instanceof FormData || value instanceof URLSearchParams;
|
|
2510
2580
|
}
|
|
2511
|
-
async function
|
|
2581
|
+
async function readBody(response, signal) {
|
|
2582
|
+
if (!response.body) {
|
|
2583
|
+
signal.throwIfAborted();
|
|
2584
|
+
return new Uint8Array();
|
|
2585
|
+
}
|
|
2586
|
+
const reader = response.body.getReader();
|
|
2587
|
+
const onAbort = () => {
|
|
2588
|
+
void reader.cancel(signal.reason).catch(() => void 0);
|
|
2589
|
+
};
|
|
2590
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2591
|
+
try {
|
|
2592
|
+
const chunks = [];
|
|
2593
|
+
let size = 0;
|
|
2594
|
+
for (; ; ) {
|
|
2595
|
+
signal.throwIfAborted();
|
|
2596
|
+
const { done, value } = await reader.read();
|
|
2597
|
+
signal.throwIfAborted();
|
|
2598
|
+
if (done) break;
|
|
2599
|
+
chunks.push(value);
|
|
2600
|
+
size += value.byteLength;
|
|
2601
|
+
}
|
|
2602
|
+
const bytes = new Uint8Array(size);
|
|
2603
|
+
let offset = 0;
|
|
2604
|
+
for (const chunk of chunks) {
|
|
2605
|
+
bytes.set(chunk, offset);
|
|
2606
|
+
offset += chunk.byteLength;
|
|
2607
|
+
}
|
|
2608
|
+
return bytes;
|
|
2609
|
+
} finally {
|
|
2610
|
+
signal.removeEventListener("abort", onAbort);
|
|
2611
|
+
void reader.cancel().catch(() => void 0);
|
|
2612
|
+
reader.releaseLock();
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
async function readJson(response, signal) {
|
|
2512
2616
|
const contentType = response.headers.get("content-type")?.toLowerCase() || "";
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
if (
|
|
2516
|
-
|
|
2617
|
+
const body = new TextDecoder().decode(await readBody(response, signal));
|
|
2618
|
+
try {
|
|
2619
|
+
if (contentType.includes("ndjson") || contentType.includes("jsonl")) {
|
|
2620
|
+
if (!body.trim()) return [];
|
|
2517
2621
|
return body.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => JSON.parse(line));
|
|
2518
|
-
} catch {
|
|
2519
|
-
return void 0;
|
|
2520
2622
|
}
|
|
2623
|
+
return JSON.parse(body);
|
|
2624
|
+
} catch {
|
|
2625
|
+
return void 0;
|
|
2521
2626
|
}
|
|
2522
|
-
return response.json().catch(() => void 0);
|
|
2523
2627
|
}
|
|
2524
2628
|
function normalizeResponse(payload, _service) {
|
|
2525
2629
|
if (isJsonObject(payload) && ("data" in payload || "meta" in payload)) {
|
|
@@ -2762,6 +2866,7 @@ function isRetryableStreamError(err) {
|
|
|
2762
2866
|
if (err && typeof err === "object") {
|
|
2763
2867
|
const e = err;
|
|
2764
2868
|
if (e.name === "AbortError") return false;
|
|
2869
|
+
if (e.status === 408) return true;
|
|
2765
2870
|
if (typeof e.retryable === "boolean") return e.retryable;
|
|
2766
2871
|
if (typeof e.status === "number") {
|
|
2767
2872
|
return e.status >= 500 || e.status === 408 || e.status === 429;
|
|
@@ -2868,9 +2973,6 @@ function normalizeDelegateAgentTokenResponse(response) {
|
|
|
2868
2973
|
}
|
|
2869
2974
|
};
|
|
2870
2975
|
}
|
|
2871
|
-
function isAbortError(error) {
|
|
2872
|
-
return error instanceof DOMException && error.name === "AbortError";
|
|
2873
|
-
}
|
|
2874
2976
|
function isConnectTimeoutError(error) {
|
|
2875
2977
|
const cause = errorCause(error);
|
|
2876
2978
|
return errorCode(cause) === "UND_ERR_CONNECT_TIMEOUT" || errorName(cause) === "ConnectTimeoutError";
|