@zoowork-ai/sdk 0.5.0 → 0.5.2
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 +42 -0
- package/dist/client.d.ts +40 -5
- package/dist/client.js +90 -25
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,48 @@
|
|
|
3
3
|
All notable changes to `@zoowork-ai/sdk` (formerly `@zooclaw-agents/sdk`). Dates are the
|
|
4
4
|
day the behaviour was verified, not the day it was written.
|
|
5
5
|
|
|
6
|
+
## 0.5.2 — 2026-09-04
|
|
7
|
+
|
|
8
|
+
### Documentation
|
|
9
|
+
|
|
10
|
+
- **`SessionRecord` now describes the response fields the API actually returns.**
|
|
11
|
+
`createSession()` returns the legacy `status: "running"` field without `run_status`;
|
|
12
|
+
later reads expose the latest run state through `run_status`, while `status` is nullable
|
|
13
|
+
and is not the run outcome. This changes the JSDoc emitted in the published declaration
|
|
14
|
+
files; runtime code and TypeScript signatures are unchanged.
|
|
15
|
+
|
|
16
|
+
## 0.5.1 — 2026-08-31
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
|
|
20
|
+
- **`ZooworkError` keeps the evidence: `contentType`, `bodySnippet`, `cfRay`, `requestId`,
|
|
21
|
+
`retryable`.** A production `postEvents` failure surfaced as `HTTP 502, type: undefined`
|
|
22
|
+
and cost the reporter a day of black-box contrast experiments (2026-08-30,
|
|
23
|
+
`notes/probes/system-message-cold-session-probe.mts`): the edge replaces an origin 502/504
|
|
24
|
+
body wholesale with a branded `text/html` page, so no JSON envelope ever reaches the SDK —
|
|
25
|
+
and 0.5.0 then dropped the only three facts that survived. Now every transport error keeps
|
|
26
|
+
the response `Content-Type`, the first 600 characters of the raw body, and the `cf-ray`
|
|
27
|
+
header (present on JSON errors too, verified 2026-08-31) — the id to quote when reporting
|
|
28
|
+
a gateway failure. `requestId` reads `request_id` from either error envelope once the
|
|
29
|
+
server starts sending one; today it is usually absent. `retryable` is a transport-class
|
|
30
|
+
hint (`408/429/502/503/504`): it says the failure class tends to pass, not that a replay
|
|
31
|
+
is safe — pair it with `idempotency_key` before looping on it.
|
|
32
|
+
SDK-synthesized wait timeouts keep `retryable: false`: they report that the caller's own
|
|
33
|
+
polling budget expired, not that an HTTP 408 came back from the service.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **The fallback error message names what actually came back.** A non-JSON error body used
|
|
38
|
+
to read a bare `HTTP 502`; it now reads
|
|
39
|
+
`HTTP 502 (text/html; charset=UTF-8) [cf-ray a338c539…]`. Messages parsed from a server
|
|
40
|
+
envelope are unchanged — keep matching on `type`/`status`, never on message text.
|
|
41
|
+
- **`streamEvents` raises the same enriched envelope** on a non-ok response instead of the
|
|
42
|
+
bare `events stream HTTP <status>` string, so SSE failures are diagnosable the same way.
|
|
43
|
+
- **A structured `detail` object no longer stringifies into the message.** The agents-family
|
|
44
|
+
envelope may carry `detail` as an object; it previously became the literal message
|
|
45
|
+
`[object Object]`, now it falls through to the status line and stays readable in
|
|
46
|
+
`bodySnippet`.
|
|
47
|
+
|
|
6
48
|
## 0.5.0 — 2026-08-28
|
|
7
49
|
|
|
8
50
|
### Added
|
package/dist/client.d.ts
CHANGED
|
@@ -55,7 +55,39 @@ export declare class ZooworkError extends Error {
|
|
|
55
55
|
* you only need the class of failure.
|
|
56
56
|
*/
|
|
57
57
|
type?: string;
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* `Content-Type` of the error response. The field that tells an edge error page apart from an
|
|
60
|
+
* API answer: both envelopes above are `application/json`, while a gateway 502/504 arrives as
|
|
61
|
+
* `text/html` — the edge replaces the origin's body wholesale, so no JSON survives to parse
|
|
62
|
+
* (verified 2026-08-30 against both deployments;
|
|
63
|
+
* `notes/probes/system-message-cold-session-probe.mts`).
|
|
64
|
+
*/
|
|
65
|
+
contentType?: string;
|
|
66
|
+
/** First {@link BODY_SNIPPET_LIMIT} characters of the raw error body, whatever it was.
|
|
67
|
+
* Without it a non-JSON failure cannot be reconstructed from `status` alone. */
|
|
68
|
+
bodySnippet?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Cloudflare ray id (`cf-ray` response header), when the response crossed Cloudflare. It
|
|
71
|
+
* survives even the replaced-body case above — on an HTML 502 it is the only correlation id
|
|
72
|
+
* left, and the value to quote when reporting a gateway failure.
|
|
73
|
+
*/
|
|
74
|
+
cfRay?: string;
|
|
75
|
+
/** Correlation id from the error envelope (`request_id` on either vocabulary), when the
|
|
76
|
+
* server includes one. Usually absent today. */
|
|
77
|
+
requestId?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Transport-class transient hint: `true` for 408, 429, 502, 503 and 504. It says the failure
|
|
80
|
+
* CLASS tends to pass, not that a replay is safe — retrying a `postEvents` without an
|
|
81
|
+
* `idempotency_key` can still deliver twice. Pair it with idempotency keys before looping.
|
|
82
|
+
*/
|
|
83
|
+
retryable: boolean;
|
|
84
|
+
constructor(status: number, message: string, type?: string, extra?: {
|
|
85
|
+
contentType?: string;
|
|
86
|
+
bodySnippet?: string;
|
|
87
|
+
cfRay?: string;
|
|
88
|
+
requestId?: string;
|
|
89
|
+
retryable?: boolean;
|
|
90
|
+
});
|
|
59
91
|
}
|
|
60
92
|
export interface Ownership {
|
|
61
93
|
owner_uid: string;
|
|
@@ -536,12 +568,15 @@ export interface SessionRecord {
|
|
|
536
568
|
/** `api` for sessions you create, `cron` for ones a schedule fired. */
|
|
537
569
|
channel?: string;
|
|
538
570
|
/**
|
|
539
|
-
* `listSessions`
|
|
540
|
-
*
|
|
541
|
-
* of `null` for the very same session, so reading `status` off a list row gets you nothing.
|
|
571
|
+
* `getSession` and `listSessions` carry the latest run state (`running`, `succeeded`, …) here.
|
|
572
|
+
* The `createSession` receipt does not include this field.
|
|
542
573
|
*/
|
|
543
574
|
run_status?: string;
|
|
544
|
-
/**
|
|
575
|
+
/**
|
|
576
|
+
* `running` on a `createSession` receipt, nullable on `getSession`, and absent from
|
|
577
|
+
* `listSessions` rows. This is not the run outcome; read {@link SessionRecord.run_status}
|
|
578
|
+
* from a later read instead.
|
|
579
|
+
*/
|
|
545
580
|
status?: string | null;
|
|
546
581
|
metadata?: Record<string, unknown>;
|
|
547
582
|
archived?: boolean;
|
package/dist/client.js
CHANGED
|
@@ -39,6 +39,11 @@ function stripTrailingSlashes(url) {
|
|
|
39
39
|
end--;
|
|
40
40
|
return end === url.length ? url : url.slice(0, end);
|
|
41
41
|
}
|
|
42
|
+
/** Statuses whose failure class tends to pass on its own: timeout, throttle, gateway. */
|
|
43
|
+
const RETRYABLE_STATUSES = new Set([408, 429, 502, 503, 504]);
|
|
44
|
+
/** Bound on `ZooworkError.bodySnippet` — enough to keep a whole error envelope or the
|
|
45
|
+
* opening of an HTML error page, small enough to log unconditionally. */
|
|
46
|
+
const BODY_SNIPPET_LIMIT = 600;
|
|
42
47
|
export class ZooworkError extends Error {
|
|
43
48
|
status;
|
|
44
49
|
/**
|
|
@@ -52,14 +57,88 @@ export class ZooworkError extends Error {
|
|
|
52
57
|
* you only need the class of failure.
|
|
53
58
|
*/
|
|
54
59
|
type;
|
|
55
|
-
|
|
60
|
+
/**
|
|
61
|
+
* `Content-Type` of the error response. The field that tells an edge error page apart from an
|
|
62
|
+
* API answer: both envelopes above are `application/json`, while a gateway 502/504 arrives as
|
|
63
|
+
* `text/html` — the edge replaces the origin's body wholesale, so no JSON survives to parse
|
|
64
|
+
* (verified 2026-08-30 against both deployments;
|
|
65
|
+
* `notes/probes/system-message-cold-session-probe.mts`).
|
|
66
|
+
*/
|
|
67
|
+
contentType;
|
|
68
|
+
/** First {@link BODY_SNIPPET_LIMIT} characters of the raw error body, whatever it was.
|
|
69
|
+
* Without it a non-JSON failure cannot be reconstructed from `status` alone. */
|
|
70
|
+
bodySnippet;
|
|
71
|
+
/**
|
|
72
|
+
* Cloudflare ray id (`cf-ray` response header), when the response crossed Cloudflare. It
|
|
73
|
+
* survives even the replaced-body case above — on an HTML 502 it is the only correlation id
|
|
74
|
+
* left, and the value to quote when reporting a gateway failure.
|
|
75
|
+
*/
|
|
76
|
+
cfRay;
|
|
77
|
+
/** Correlation id from the error envelope (`request_id` on either vocabulary), when the
|
|
78
|
+
* server includes one. Usually absent today. */
|
|
79
|
+
requestId;
|
|
80
|
+
/**
|
|
81
|
+
* Transport-class transient hint: `true` for 408, 429, 502, 503 and 504. It says the failure
|
|
82
|
+
* CLASS tends to pass, not that a replay is safe — retrying a `postEvents` without an
|
|
83
|
+
* `idempotency_key` can still deliver twice. Pair it with idempotency keys before looping.
|
|
84
|
+
*/
|
|
85
|
+
retryable;
|
|
86
|
+
constructor(status, message, type, extra) {
|
|
56
87
|
super(message);
|
|
57
88
|
this.name = 'ZooworkError';
|
|
58
89
|
this.status = status;
|
|
59
90
|
if (type)
|
|
60
91
|
this.type = type;
|
|
92
|
+
if (extra?.contentType)
|
|
93
|
+
this.contentType = extra.contentType;
|
|
94
|
+
if (extra?.bodySnippet)
|
|
95
|
+
this.bodySnippet = extra.bodySnippet;
|
|
96
|
+
if (extra?.cfRay)
|
|
97
|
+
this.cfRay = extra.cfRay;
|
|
98
|
+
if (extra?.requestId)
|
|
99
|
+
this.requestId = extra.requestId;
|
|
100
|
+
this.retryable = extra?.retryable ?? RETRYABLE_STATUSES.has(status);
|
|
61
101
|
}
|
|
62
102
|
}
|
|
103
|
+
/** The response facts worth keeping on every transport-level ZooworkError, whatever the body. */
|
|
104
|
+
function responseForensics(res, text) {
|
|
105
|
+
return {
|
|
106
|
+
contentType: res.headers.get('content-type') ?? undefined,
|
|
107
|
+
cfRay: res.headers.get('cf-ray') ?? undefined,
|
|
108
|
+
bodySnippet: text ? text.slice(0, BODY_SNIPPET_LIMIT) : undefined,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Build the ZooworkError for a non-2xx response. Unpacks BOTH envelope vocabularies (see
|
|
113
|
+
* {@link ZooworkError.type}); when neither matches — typically an edge error page whose body
|
|
114
|
+
* replaced the origin's JSON — the message names status, content-type and ray id instead of a
|
|
115
|
+
* bare `HTTP 502`. Those three are what turn an "HTTP 502, type: undefined" report into an
|
|
116
|
+
* answerable one (2026-08-30). A structured `detail` object is deliberately NOT stringified
|
|
117
|
+
* into the message (`[object Object]`); it stays readable in `bodySnippet`.
|
|
118
|
+
*/
|
|
119
|
+
function httpError(res, text) {
|
|
120
|
+
const forensics = responseForensics(res, text);
|
|
121
|
+
let msg;
|
|
122
|
+
let type;
|
|
123
|
+
let requestId;
|
|
124
|
+
try {
|
|
125
|
+
const j = JSON.parse(text);
|
|
126
|
+
const detail = typeof j?.detail === 'string' ? j.detail : undefined;
|
|
127
|
+
msg = j?.error?.message || j?.message || detail || undefined;
|
|
128
|
+
type = j?.error?.type ?? j?.code;
|
|
129
|
+
requestId = j?.error?.request_id ?? j?.request_id;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
/* non-JSON error body — usually the edge speaking, not the API */
|
|
133
|
+
}
|
|
134
|
+
if (!msg) {
|
|
135
|
+
msg =
|
|
136
|
+
`HTTP ${res.status}` +
|
|
137
|
+
(forensics.contentType ? ` (${forensics.contentType})` : '') +
|
|
138
|
+
(forensics.cfRay ? ` [cf-ray ${forensics.cfRay}]` : '');
|
|
139
|
+
}
|
|
140
|
+
return new ZooworkError(res.status, msg, type, { ...forensics, requestId });
|
|
141
|
+
}
|
|
63
142
|
/**
|
|
64
143
|
* Create a client.
|
|
65
144
|
*
|
|
@@ -90,36 +169,22 @@ export function createZooworkClient(cfg = {}) {
|
|
|
90
169
|
}
|
|
91
170
|
const bearer = 'serviceToken' in auth ? auth.serviceToken : auth.apiKey;
|
|
92
171
|
/**
|
|
93
|
-
* TWO error envelopes, one ZooworkError shape, for every helper below
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* Reading only the first left every agent 404 with `type: undefined` and the message `HTTP 404`,
|
|
98
|
-
* so both are unpacked here. The codes stay verbatim (`not_found` vs `service_api.not_found`) —
|
|
99
|
-
* inventing a shared vocabulary would be this SDK guessing, which is what it exists not to do.
|
|
172
|
+
* TWO error envelopes, one ZooworkError shape, for every helper below — the unpacking (and
|
|
173
|
+
* why both vocabularies exist) lives in module-level {@link httpError}. The codes stay
|
|
174
|
+
* verbatim (`not_found` vs `service_api.not_found`) — inventing a shared vocabulary would be
|
|
175
|
+
* this SDK guessing, which is what it exists not to do.
|
|
100
176
|
*/
|
|
101
177
|
const readResponse = async (res, path) => {
|
|
102
178
|
const text = await res.text();
|
|
103
|
-
if (!res.ok)
|
|
104
|
-
|
|
105
|
-
let type;
|
|
106
|
-
try {
|
|
107
|
-
const j = JSON.parse(text);
|
|
108
|
-
msg = j?.error?.message || j?.message || j?.detail || msg;
|
|
109
|
-
type = j?.error?.type ?? j?.code;
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
/* non-JSON error body → keep clean status */
|
|
113
|
-
}
|
|
114
|
-
throw new ZooworkError(res.status, msg, type);
|
|
115
|
-
}
|
|
179
|
+
if (!res.ok)
|
|
180
|
+
throw httpError(res, text);
|
|
116
181
|
if (!text)
|
|
117
182
|
return {};
|
|
118
183
|
try {
|
|
119
184
|
return JSON.parse(text);
|
|
120
185
|
}
|
|
121
186
|
catch {
|
|
122
|
-
throw new ZooworkError(res.status, `non-JSON response: ${path}
|
|
187
|
+
throw new ZooworkError(res.status, `non-JSON response: ${path}`, undefined, responseForensics(res, text));
|
|
123
188
|
}
|
|
124
189
|
};
|
|
125
190
|
/**
|
|
@@ -270,7 +335,7 @@ export function createZooworkClient(cfg = {}) {
|
|
|
270
335
|
let lastSeen = 'unknown';
|
|
271
336
|
const abortedError = () => new ZooworkError(0, `waitUntilRunning(${agentId}) aborted`, 'aborted');
|
|
272
337
|
const timeoutError = () => new ZooworkError(408, `agent ${agentId} did not reach status.desired_state=running within ${timeoutMs}ms ` +
|
|
273
|
-
`(last seen: ${lastSeen})`, 'timeout');
|
|
338
|
+
`(last seen: ${lastSeen})`, 'timeout', { retryable: false });
|
|
274
339
|
for (;;) {
|
|
275
340
|
if (opts.signal?.aborted)
|
|
276
341
|
throw abortedError();
|
|
@@ -350,7 +415,7 @@ export function createZooworkClient(cfg = {}) {
|
|
|
350
415
|
let lastStatus = 'unknown';
|
|
351
416
|
const abortedError = () => new ZooworkError(0, `waitForChannelSetup(${agentId}, ${platform}, ${sessionId}) aborted`, 'aborted');
|
|
352
417
|
const timeoutError = () => new ZooworkError(408, `${platform} setup session ${sessionId} still '${lastStatus}' after ${timeoutMs}ms — ` +
|
|
353
|
-
'the QR may simply not have been scanned yet; the session itself expires server-side', 'timeout');
|
|
418
|
+
'the QR may simply not have been scanned yet; the session itself expires server-side', 'timeout', { retryable: false });
|
|
354
419
|
for (;;) {
|
|
355
420
|
if (opts.signal?.aborted)
|
|
356
421
|
throw abortedError();
|
|
@@ -517,7 +582,7 @@ export function createZooworkClient(cfg = {}) {
|
|
|
517
582
|
...(opts.signal ? { signal: opts.signal } : {}),
|
|
518
583
|
});
|
|
519
584
|
if (!res.ok)
|
|
520
|
-
throw
|
|
585
|
+
throw httpError(res, await res.text().catch(() => ''));
|
|
521
586
|
if (!res.body)
|
|
522
587
|
return;
|
|
523
588
|
for await (const msg of parseSSE(res.body)) {
|