@zoowork-ai/sdk 0.5.2 → 0.6.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/CHANGELOG.md CHANGED
@@ -3,6 +3,23 @@
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.6.0 — 2026-09-11
7
+
8
+ ### Changed
9
+
10
+ - **Breaking: `listAgents()` now resolves to an `AgentPage`, not an array.** Read `.data`
11
+ for the current page. The SDK preserves `page`, `page_size`, and `total`, and derives a
12
+ numeric `next_page` (`null` at the end). Existing one-page callers should replace
13
+ `const agents = await zc.listAgents(opts)` with `const { data: agents } = await zc.listAgents(opts)`.
14
+ - **Agent lists support automatic and manual pagination.** Use
15
+ `for await (const agent of zc.listAgents(opts))`, or `page.hasNextPage()` /
16
+ `page.getNextPage()`. Resolved pages are async iterable and provide `iterPages()`.
17
+ Later requests preserve the original label filters; early loop exit stops further fetches.
18
+ - Missing, invalid, or non-advancing agent pagination metadata now raises an error instead
19
+ of hiding a partial result. The API's fixed 100-item numeric pagination is unchanged.
20
+ Cross-page behavior is verified with synthetic offline HTTP tests; the live release
21
+ smoke covers one Agent/Session turn and cleanup, not a 101-agent pagination walk.
22
+
6
23
  ## 0.5.2 — 2026-09-04
7
24
 
8
25
  ### Documentation
package/README.md CHANGED
@@ -26,9 +26,14 @@ The base URL has a working default, so you do not configure an endpoint. Overrid
26
26
 
27
27
  ```ts
28
28
  // 1. Create an agent. Ownership is derived from your key, so `resource` is all you
29
- // send; the gateway also seeds the platform credentials the agent needs to call a model.
29
+ // send. Select a model returned by this deployment instead of relying on a
30
+ // remembered id or on a server default that can rotate.
31
+ const models = await zc.listModels()
32
+ const primary = models.find((model) => model.model === 'litellm/gpt-5.6-terra')?.model
33
+ if (!primary) throw new Error('Choose a model returned by listModels()')
34
+
30
35
  const agent = await zc.createAgent({
31
- resource: { name: 'research-agent', model: { primary: 'litellm/claude-sonnet-5' } },
36
+ resource: { name: 'research-agent', model: { primary } },
32
37
  })
33
38
 
34
39
  // 2. Start it. Without this, createSession() returns 409 agent_not_running.
@@ -52,15 +57,58 @@ An explicit option always beats the environment variable.
52
57
 
53
58
  > **Finding the agent you built in the app.** The first path segment of a ZooWork chat URL
54
59
  > (`/chat/<32-hex>/sessions/…`) is a *workspace* id, not an `agt_…`. Resolve it with
55
- > `zc.listAgents({ labels: { workspace_id: '<32-hex>' } })`; a bare `zc.listAgents()` lists
56
- > everything your key can see. Scope is `owner_uid AND org_id` — an agent a *colleague*
60
+ > `zc.listAgents({ labels: { workspace_id: '<32-hex>' } })`; use the pagination patterns
61
+ > below to read one page or traverse every match. Scope is `owner_uid AND org_id` — an agent a *colleague*
57
62
  > created in your org is fetchable by id but will not appear in your list.
58
63
 
59
64
  > **Wait on `status.desired_state`, never on `status.actual_state`.**
60
65
  > `actual_state` reports chat-channel connectivity. An API-only agent has no channels,
61
- > so it stays at `activating` forever and `active` is unreachable a readiness loop
62
- > that polls it never returns. `desired_state` flips to `running` in well under a second.
63
- > `await zc.waitUntilRunning(agentId)` is that loop, written correctly.
66
+ > but its projection depends on the channel-status capability: a GET can report `active`
67
+ > with zero channel counts and a `status_message` saying health was not verified when that
68
+ > capability is unsupported, while a transient health lookup failure remains `activating`.
69
+ > List and GET can therefore briefly disagree. None of these values is readiness, and
70
+ > `running` is not an `actual_state` value. Use `await zc.waitUntilRunning(agentId)`; it
71
+ > correctly polls `desired_state`.
72
+
73
+ ## Listing agents and pagination
74
+
75
+ `listAgents()` returns an awaitable, async-iterable request. Use `for await` to traverse all
76
+ matching agents; the SDK requests each next page only as you consume the results:
77
+
78
+ ```ts
79
+ for await (const agent of zc.listAgents({ labels: { project: 'research' } })) {
80
+ console.log(agent.agent_id)
81
+ // break when you have enough; later pages will not be fetched.
82
+ }
83
+ ```
84
+
85
+ For a single page, `await` the request and read `.data`. The page preserves `page`,
86
+ `page_size`, and `total`, and exposes `next_page` (`null` when there are no more results):
87
+
88
+ ```ts
89
+ const page = await zc.listAgents()
90
+ console.log(page.data, page.total, page.next_page)
91
+
92
+ if (page.hasNextPage()) {
93
+ const next = await page.getNextPage() // retains the original label filters
94
+ console.log(next.data)
95
+ }
96
+ ```
97
+
98
+ You can also use `for await (const agent of page)` to iterate from an already-fetched page,
99
+ or `for await (const batch of page.iterPages())` to process one page at a time.
100
+ `getNextPage()` rejects if there is no next page; errors fetching later pages reject iteration.
101
+
102
+ The API uses **numeric pages starting at 1**, with a fixed page size of 100. The SDK derives
103
+ `next_page` from the returned `page`, `page_size`, and `total`; it is a number, not an opaque
104
+ cursor. To resume explicitly, use `zc.listAgents({ page: nextPage, labels: originalLabels })`.
105
+ There is no configurable `limit`. Pagination is not a snapshot: concurrent additions or
106
+ deletions can shift results between pages.
107
+
108
+ **Migration from the array return:** replace `const agents = await zc.listAgents(opts)` with
109
+ `const { data: agents } = await zc.listAgents(opts)` to keep reading one page, or switch to
110
+ `for await` to read every match. Missing or invalid pagination metadata now raises an error
111
+ instead of silently returning an empty or apparently complete array.
64
112
 
65
113
  ## Streaming a turn
66
114
 
@@ -85,8 +133,14 @@ for await (const ev of zc.streamEvents(agent.agent_id, session.session_id)) {
85
133
  Three things worth knowing before you write that loop:
86
134
 
87
135
  - **The stream is session-scoped and does not close when a turn ends.** The server closes it after an idle period. Break on `isRunFinished(ev)` yourself, or you block until that timeout.
88
- - **It resumes.** Every frame carries a durable `seq`. After a dropped connection, restart with `{ after: lastSeq }` and the server replays from there nothing lost, nothing duplicated.
89
- - **REST and SSE spell the same event differently** (`event_type` vs `eventType`, and neither has a top-level `type`). The SDK normalizes both into one `SessionEvent`; you only ever read `eventType`.
136
+ - **Save the opaque cursor.** After consuming an event, retain `ev.cursor` and resume with `{ cursor }`. Do not derive it from `seq` or mix it with `after`: `after` selects the deprecated event lane, which omits user-input events. The SDK sends the cursor in the query; do not rely on a raw `Last-Event-ID` header passing through the public gateway.
137
+ - **The default unified REST and SSE wire formats use snake_case.** Older event formats differ; the SDK normalizes both into `SessionEvent`, where you read `eventType`. Keep the cursor unchanged.
138
+
139
+ For API sessions, `user.message` can carry `actor: { ref: 'customer-42' }`, including in
140
+ `initial_events`. This source-reviewed field selects per-user memory attribution. Your server
141
+ must authenticate the user and authorize the session; `actor.ref` does neither. It does not
142
+ isolate the shared agent's sandbox files or erase a session's previous context. Omit `actor`
143
+ to use the owner; IM sessions reject it. See the field's SDK comment for input constraints.
90
144
 
91
145
  ## Bring your own skill
92
146
 
@@ -106,6 +160,13 @@ and it is the first one nearly everyone gets. `scope` is `org` or `personal`; th
106
160
  control what a skill says. `uploadSkillVersion` publishes an update, and agents that installed it
107
161
  unpinned follow along without another `putAgentSkill`.
108
162
 
163
+ `uploadSkillVersion` returns a `SkillVersionRecord` with `version` and `state`, not the
164
+ `latest_version` and `status` of the `SkillRecord` returned by `uploadSkill`. This return
165
+ contract is source-reviewed, not a new live recording. On initial create, put the description
166
+ in the zip's frontmatter: the gateway drops the `description` option. Version uploads can
167
+ use that override. A successful create retried under the same name can return `409 skill_exists`;
168
+ read back first. Version uploads deduplicate identical content for the same skill, not HTTP keys.
169
+
109
170
  ## Schedules, wake and exec
110
171
 
111
172
  ```ts
@@ -125,6 +186,10 @@ const { exit_code, stdout } = await zc.exec(agent.agent_id, ['bash', '-lc', 'pwd
125
186
  `listScheduleRuns`.
126
187
  - **`updateSchedule` must omit `sessionTarget`.** It is immutable, and echoing it back from a
127
188
  `getSchedule` result — the obvious thing to do — is a 400. The types refuse it for you.
189
+ - **An interval uses `{ kind: 'every', everyMs: 60_000 }`.** Optional `anchorMs` aligns it.
190
+ The earlier `every` type was incorrect; migrate explicitly, without guessing string units.
191
+ This correction and optional `ScheduleRun.session_id` are source-reviewed. Use that session
192
+ link only when present; it is not a run-success indicator.
128
193
  - **`exec` resolves on a failed command.** A non-zero exit is still HTTP 200: check `exit_code`,
129
194
  don't wait for a rejection. It runs in `/workspace` and needs an agent-scope sandbox.
130
195
  - **A cron job can carry an outcome gate.** `payload.outcome` says what "done" looks like — a
@@ -141,8 +206,12 @@ const { exit_code, stdout } = await zc.exec(agent.agent_id, ['bash', '-lc', 'pwd
141
206
  creation time.
142
207
 
143
208
  `listApprovals` / `resolveApproval` expose the approvals resource — `decision` is one of
144
- `allow-once`, `allow-always`, `deny`. Note that human-in-the-loop is not usable end to end yet: an
145
- agent parked on an approval spends its whole turn budget waiting.
209
+ `allow-once`, `allow-always`, `deny`. End-to-end approval and turn-budget behavior need
210
+ separate verification on the deployment you use.
211
+
212
+ Approval response fields are source-reviewed, not end-to-end verified: read `requested_at`,
213
+ `allowed_decisions` and optional timeout/resolution fields defensively. `signaled: true` means
214
+ the resolution was accepted; a returned `status: 'pending'` is not completed execution.
146
215
 
147
216
  `listEnvironments`, `getEnvironment`, `createEnvironment`, `createEnvironmentVersion`,
148
217
  `getEnvironmentVersion` and `archiveEnvironment` manage prebuilt sandbox images (apt/npm/pip
@@ -151,6 +220,14 @@ start: an agent's Environment **freezes on its first sandbox creation** — afte
151
220
  is `409 environment_locked`, and stopping the agent does not clear it — and sandbox networking
152
221
  defaults to unrestricted unless the Environment declares `networking: { type: 'limited' }`.
153
222
 
223
+ Build polling must have a deadline and handle `partial_ready`: some resource classes can be
224
+ ready while others are building or failed. `getEnvironmentVersion(id, version, { resourceClass:
225
+ 'starter' })` selects one class; omitting the option keeps the aggregate read. Re-read the
226
+ aggregate before concluding the build is fully ready. These details are source-reviewed.
227
+
228
+ Channel callers must not use `allow_from` as an access-control list: the public gateway ignores
229
+ it. Use supported `dm_policy` settings.
230
+
154
231
  ## Artifacts and the system prompt
155
232
 
156
233
  ```ts
@@ -193,6 +270,40 @@ Runnable examples in [`examples/`](examples):
193
270
  - [`live-smoke.ts`](examples/live-smoke.ts) — drive one agent through one turn and verify the REST and SSE reads agree.
194
271
  - [`capability-probe.ts`](examples/capability-probe.ts) — create a throwaway agent, walk the whole lifecycle, and print a verdict per capability.
195
272
 
273
+ ## Testing and publishing (maintainers)
274
+
275
+ Testing and publishing are independent commands. After cloning this repository on a new
276
+ machine, install its locked development dependencies:
277
+
278
+ ```sh
279
+ pnpm install --frozen-lockfile
280
+ ```
281
+
282
+ Run staging E2E explicitly when you want to verify the SDK (Node 22.20+):
283
+
284
+ ```sh
285
+ pnpm test:e2e
286
+ ```
287
+
288
+ The command prepares an isolated test package, prints individual offline cases and timed live
289
+ steps, and asks for a staging API key with input hidden. Submitting the key authorizes one
290
+ temporary Agent/Session, one potentially billable model turn and cleanup. JSON reports remain
291
+ in its printed private directory. The test never publishes. Normal `pnpm test` is offline
292
+ and needs no key. See [E2E and recovery instructions](e2e/README.md) for scope and options.
293
+
294
+ To publish the checked-out version, use npm normally:
295
+
296
+ ```sh
297
+ npm login
298
+ npm publish
299
+ ```
300
+
301
+ The `prepack` hook rebuilds `dist` from source; `npm publish` then publishes that package with
302
+ your npm account. It does not run E2E, read a staging key or require an E2E result directory.
303
+ You choose when to test and publish, including on different machines. Check the version and
304
+ changelog before publishing; existing npm versions cannot be overwritten. Use
305
+ `npm publish --dry-run` to inspect the package without uploading it.
306
+
196
307
  ## License
197
308
 
198
309
  MIT
package/dist/client.d.ts CHANGED
@@ -42,6 +42,29 @@ export interface ZooworkConfig {
42
42
  /** Injected fetch for edge runtimes/tests; defaults to globalThis.fetch. */
43
43
  fetch?: (input: string, init?: RequestInit) => Promise<Response>;
44
44
  }
45
+ export interface AgentListParams {
46
+ labels?: Record<string, string>;
47
+ /** Starting page, numbered from 1. The API fixes page size at 100. */
48
+ page?: number;
49
+ }
50
+ /** A single page. Iterating this object asynchronously continues through subsequent pages. */
51
+ export interface AgentPage extends AsyncIterable<AgentRecord> {
52
+ /** Agents in this page only. */
53
+ readonly data: AgentRecord[];
54
+ readonly page: number;
55
+ readonly page_size: number;
56
+ readonly total: number;
57
+ /** Next numeric page to pass to listAgents, or null at the end. */
58
+ readonly next_page: number | null;
59
+ hasNextPage(): boolean;
60
+ /** Fetch the next page with the same filters. Rejects when there is no next page. */
61
+ getNextPage(): Promise<AgentPage>;
62
+ /** Iterate this and subsequent pages, fetching each only when needed. */
63
+ iterPages(): AsyncIterableIterator<AgentPage>;
64
+ }
65
+ /** Await one page, or use for-await directly to traverse all remaining agents. */
66
+ export interface AgentPagePromise extends Promise<AgentPage>, AsyncIterable<AgentRecord> {
67
+ }
45
68
  export declare class ZooworkError extends Error {
46
69
  status: number;
47
70
  /**
@@ -114,9 +137,12 @@ export interface ModelInfo {
114
137
  * other way to store it, so an authenticated MCP server cannot be made to work today.
115
138
  * Declare public servers only.
116
139
  *
117
- * Phase 1 is remote HTTP only: no stdio, no OAuth. A server that fails its catalog probe does
118
- * not fail the run it pins an empty catalog and emits `agent.error` with
119
- * `kind: 'mcp_connection_failed'`.
140
+ * Remote HTTP only; no stdio. Catalog failures emit `agent.error` with
141
+ * `kind: 'mcp_connection_failed'` or `'mcp_authentication_failed'`, a server name,
142
+ * an errorMessage and an optional reason. Preserve unknown reason values.
143
+ * Healthy catalogs remain tied to the configuration. Transient failed catalogs can expire:
144
+ * a later catalog resolution may probe again, not a periodic retry or recovery guarantee.
145
+ * These failure/recovery details are source-reviewed, not deployment-verified here.
120
146
  */
121
147
  export interface McpServerDeclaration {
122
148
  /** Server slug. Appears in every tool name as `mcp__<name>__<tool>`. No underscores. */
@@ -129,6 +155,12 @@ export interface McpServerDeclaration {
129
155
  credential?: string;
130
156
  /** Expose only these tool names from this server. Omit for all of them. */
131
157
  toolFilter?: string[];
158
+ /**
159
+ * How tools reach the model. `deferred` (the default when omitted) keeps them behind
160
+ * `tool_search` / `tool_describe` until loaded; `direct` declares them on the first request.
161
+ * There is no `auto` value.
162
+ */
163
+ exposure?: 'deferred' | 'direct';
132
164
  [k: string]: unknown;
133
165
  }
134
166
  /**
@@ -197,7 +229,12 @@ export interface OutcomeConfig {
197
229
  }
198
230
  export interface AgentResource {
199
231
  name: string;
200
- /** `max_tokens` caps output tokens per model request. Omit to use the platform default. */
232
+ /**
233
+ * Omit this section to pin the platform's current model defaults at create time. Those
234
+ * defaults can rotate; call `listModels()` and set `primary` explicitly for deterministic
235
+ * provisioning. `max_tokens` caps output tokens per model request; omit only that field to
236
+ * use the platform token limit.
237
+ */
201
238
  model?: {
202
239
  primary: string;
203
240
  input?: string[];
@@ -235,15 +272,18 @@ export interface AgentResource {
235
272
  environment_version?: number;
236
273
  }
237
274
  /**
238
- * Agent lifecycle state. Two fields, two very different meanings — staging-verified
239
- * 2026-08-06:
275
+ * Agent lifecycle state. Two fields, two very different meanings:
240
276
  *
241
277
  * - `desired_state` is the one that gates the API. `running` is the precondition for
242
278
  * createSession/postEvents; anything else is `409 agent_not_running`.
243
- * - `actual_state` is CHANNEL health (Mattermost/Feishu route connectivity), not API
244
- * readiness. An API-only agent has no channels to connect, so it sits at
245
- * `activating` forever and `active` is unreachable. Never gate on it, and never
246
- * poll for `running` that is not one of its values.
279
+ * - `actual_state` is a best-effort CHANNEL-health projection, not API readiness. When
280
+ * route-status is unsupported, GET can report `active` with zero channel counts and a
281
+ * `status_message` saying health was not verified; a transient unknown remains
282
+ * `activating`. `listAgents` does not make the same foreground health query, so list and
283
+ * GET can briefly differ. Never gate on this field, and never poll it for `running` —
284
+ * that is not one of its values.
285
+ *
286
+ * The channel fallback behavior is source-reviewed, not deployment-verified here.
247
287
  */
248
288
  export interface AgentStatus {
249
289
  desired_state?: 'running' | 'stopped' | 'deleted' | string;
@@ -361,7 +401,7 @@ export interface AddChannelInput {
361
401
  dm_policy?: string;
362
402
  /** Server default: `'open'`. */
363
403
  group_policy?: string;
364
- /** Write-once at create; updates cannot edit it. */
404
+ /** Retained for compatibility; the public gateway ignores this field. It is NOT an access allowlist. Use supported dm_policy settings. */
365
405
  allow_from?: string[];
366
406
  /**
367
407
  * Platform credentials for the direct (non-QR) path. Keys are platform-specific and
@@ -548,6 +588,16 @@ export interface SkillRecord {
548
588
  };
549
589
  [k: string]: unknown;
550
590
  }
591
+ /**
592
+ * The version row returned by uploadSkillVersion, not a SkillRecord.
593
+ * Source-reviewed fields; no new live recording is implied. The response stays unmodified.
594
+ */
595
+ export interface SkillVersionRecord {
596
+ skill_id: string;
597
+ version: string | number;
598
+ state: 'pending' | 'ready' | 'failed' | string;
599
+ [k: string]: unknown;
600
+ }
551
601
  /**
552
602
  * One `session_transcripts` row, as projected by `getSession(…, { history: true })`.
553
603
  *
@@ -571,7 +621,9 @@ export interface SessionRecord {
571
621
  * `getSession` and `listSessions` carry the latest run state (`running`, `succeeded`, …) here.
572
622
  * The `createSession` receipt does not include this field.
573
623
  */
574
- run_status?: string;
624
+ run_status?: string | null;
625
+ /** Pending approval count on getSession; not included in every session projection. */
626
+ pending_approvals?: number;
575
627
  /**
576
628
  * `running` on a `createSession` receipt, nullable on `getSession`, and absent from
577
629
  * `listSessions` rows. This is not the run outcome; read {@link SessionRecord.run_status}
@@ -589,6 +641,18 @@ export interface SessionRecord {
589
641
  export interface OutboundEvent {
590
642
  type: string;
591
643
  content?: unknown;
644
+ /**
645
+ * user.message on an API session only, including createSession.initial_events.
646
+ * ref is 1–200 ASCII characters from [A-Za-z0-9._:@+-]. Omit actor to use the owner.
647
+ * Map an authenticated application user to a stable opaque ref on YOUR server.
648
+ * This selects memory attribution, not authentication, session authorization or file isolation.
649
+ * IM sessions reject actor; token and other actor keys are rejected with HTTP 400.
650
+ * Source-reviewed; deployment availability must be verified separately.
651
+ */
652
+ actor?: {
653
+ ref: string;
654
+ token?: never;
655
+ };
592
656
  [k: string]: unknown;
593
657
  }
594
658
  /** One `postEvents` receipt. An accepted event carries the full event object's fields too. */
@@ -605,11 +669,10 @@ export interface SessionEventPage {
605
669
  nextCursor?: string | null;
606
670
  }
607
671
  /**
608
- * When a schedule fires. Three kinds, and only `cron` has its field names pinned by the engine
609
- * reference (`{"kind":"cron","expr":"0 9 * * *","tz":"Asia/Singapore"}`); `every` and `at` are
610
- * documented by prose only "the management plane supports cron/every/at", and an `at`
611
- * schedule "uses the supplied ISO instant". Their extra fields are therefore left open rather
612
- * than guessed at, so anything the engine accepts still type-checks.
672
+ * When a schedule fires: cron expression, everyMs interval in milliseconds (optional
673
+ * anchorMs), or an ISO instant in at. The everyMs contract is source-reviewed; it was
674
+ * previously misdeclared as every. Use everyMs explicitly: the SDK does not convert units
675
+ * or rename the obsolete every field at runtime.
613
676
  *
614
677
  * Cron is a five-field expression. Macros and a `CRON_TZ=` prefix are rejected; overlap is
615
678
  * fixed to SKIP server-side, so a fire that lands on a still-running one is dropped, not queued.
@@ -621,7 +684,8 @@ export type ScheduleSpec = {
621
684
  [k: string]: unknown;
622
685
  } | {
623
686
  kind: 'every';
624
- every: string | number;
687
+ everyMs: number;
688
+ anchorMs?: number;
625
689
  tz?: string;
626
690
  [k: string]: unknown;
627
691
  } | {
@@ -782,13 +846,16 @@ export interface ScheduleRecord {
782
846
  * This is the only row that carries `status` (e.g. `skipped` for a fire against a disabled
783
847
  * schedule).
784
848
  *
785
- * NEITHER carries `session_id`, which is the field people expect most: you cannot walk from a
786
- * fire to the session it created. To find that session, list the agent's sessions and match
787
- * `channel: 'cron'` with the `session_key` prefix `agent:{agent_id}:cron:{schedule_id}:`.
849
+ * Source-reviewed additions can include session_id when the fire has a known session.
850
+ * It is optional, not proof of success, and may be absent for command jobs or an unlinked
851
+ * fire. Older recordings above omit it. Without it, list the agent's sessions and match
852
+ * channel: 'cron' with the session_key prefix agent:{agent_id}:cron:{schedule_id}:.
788
853
  */
789
854
  export interface ScheduleRun {
790
855
  /** Which projection this row came from. Decides which of the field groups below is populated. */
791
856
  source?: 'temporal' | 'run_projection' | string;
857
+ /** Present only when a session can be associated with this fire. */
858
+ session_id?: string;
792
859
  /** Un-qualified `schedule_id`. */
793
860
  schedule_id?: string;
794
861
  fired_at?: string;
@@ -809,15 +876,25 @@ export type ApprovalDecision = 'allow-once' | 'allow-always' | 'deny';
809
876
  /**
810
877
  * A tool call parked on a human decision.
811
878
  *
812
- * Field names are unverified: the only staging observation (2026-08-07) is a 200 with an EMPTY
813
- * `approvals` array, because producing a real pending approval requires a tool policy that asks.
814
- * Read defensively.
879
+ * Fields below are source-reviewed; the existing live recordings have empty lists.
880
+ * A resolve receipt can have signaled:true while status is still pending: acceptance is not
881
+ * completion of the tool or run. Verify the resulting state separately.
815
882
  */
816
883
  export interface ApprovalRecord {
817
884
  approval_id?: string;
818
885
  session_id?: string;
819
886
  tool_name?: string;
820
887
  status?: string;
888
+ arguments_preview?: string;
889
+ stake?: unknown;
890
+ requested_at?: string;
891
+ timeout_at?: string;
892
+ allowed_decisions?: ApprovalDecision[];
893
+ resolved_by?: string;
894
+ resolved_at?: string;
895
+ signaled?: boolean;
896
+ decision?: ApprovalDecision;
897
+ /** Legacy compatibility field; current source projects requested_at instead. */
821
898
  created_at?: string;
822
899
  [k: string]: unknown;
823
900
  }
@@ -993,10 +1070,11 @@ export interface EnvironmentRecord {
993
1070
  [k: string]: unknown;
994
1071
  }
995
1072
  /**
996
- * One immutable Environment version. Builds walk
997
- * `queued → submitting → building → verifying ready`, and any phase can land in `failed`.
998
- * Poll THIS, not the Environment's top-level row: a `ready` version is the only thing an agent
999
- * can pin, and it always carries both `e2b_build_id` and a matching `template_ref`.
1073
+ * One immutable Environment version's build state. Builds can be queued, submitting, building,
1074
+ * verifying, partial_ready, ready or failed. partial_ready means some resource classes are
1075
+ * ready; others may still be building or may have failed. It is not by itself a success/failure
1076
+ * verdict for your selected class. Poll with a deadline; optionally read that resourceClass
1077
+ * and re-read the aggregate before drawing a conclusion.
1000
1078
  *
1001
1079
  * THE FIELD IS `status`, NOT `state` — staging-verified 2026-08-07. A poll loop written against
1002
1080
  * `state` compares `undefined` to `'ready'` forever and never terminates, which is precisely the
@@ -1005,8 +1083,8 @@ export interface EnvironmentRecord {
1005
1083
  export interface EnvironmentVersionRecord {
1006
1084
  environment_id?: string;
1007
1085
  version?: number;
1008
- /** `queued` `submitting` `building` `verifying` `ready`, or `failed`. */
1009
- status?: 'queued' | 'submitting' | 'building' | 'verifying' | 'ready' | 'failed' | string;
1086
+ /** Build status; partial_ready may be transient or a partial terminal result. */
1087
+ status?: 'queued' | 'submitting' | 'building' | 'verifying' | 'partial_ready' | 'ready' | 'failed' | string;
1010
1088
  /** The normalized config this version was built from — not the one you posted verbatim. */
1011
1089
  config?: EnvironmentConfig;
1012
1090
  base_environment_id?: string;
@@ -1064,11 +1142,12 @@ export interface ZooworkClient {
1064
1142
  *
1065
1143
  * Note the scope is `owner_uid AND org_id`: an agent a colleague created in your org is
1066
1144
  * fetchable by `getAgent` but will not appear here.
1145
+ *
1146
+ * `await listAgents()` returns one {@link AgentPage}; read `.data` for its agents and
1147
+ * `.next_page` / `.hasNextPage()` for continuation. `for await (const agent of listAgents())`
1148
+ * automatically fetches subsequent pages. Breaking the loop stops further requests.
1067
1149
  */
1068
- listAgents(opts?: {
1069
- labels?: Record<string, string>;
1070
- page?: number;
1071
- }): Promise<AgentRecord[]>;
1150
+ listAgents(opts?: AgentListParams): AgentPagePromise;
1072
1151
  getAgent(agentId: string): Promise<AgentRecord>;
1073
1152
  /** PUT declared sections; bumps config_version on EVERY call — gate on drift, don't blind-retry. */
1074
1153
  updateAgent(agentId: string, sections: Record<string, unknown>): Promise<AgentRecord>;
@@ -1081,13 +1160,17 @@ export interface ZooworkClient {
1081
1160
  deleteAgent(agentId: string): Promise<void>;
1082
1161
  /**
1083
1162
  * Flip `desired_state` to `running` — the precondition for every session call.
1084
- * Fast (sub-second on staging). The returned warnings are informational: an
1085
- * API-only agent reports `channel_routes_reload_failed` on every start/stop
1086
- * because it has no chat-channel routes to reload. Do not treat it as failure.
1163
+ * A successful response can contain warnings. A non-2xx response still throws ZooworkError;
1164
+ * warnings are not guaranteed on each start/stop and are not a substitute for error handling.
1087
1165
  */
1088
1166
  startAgent(agentId: string): Promise<{
1089
1167
  warnings: string[];
1090
1168
  }>;
1169
+ /**
1170
+ * Request stopped state. Dependency failures can reject after desired_state changes.
1171
+ * Read back and reconcile; desired_state alone does not prove all runtime work is cleaned up.
1172
+ * A successful response's warnings and a rejected HTTP call are different outcomes.
1173
+ */
1091
1174
  stopAgent(agentId: string): Promise<{
1092
1175
  warnings: string[];
1093
1176
  }>;
@@ -1286,10 +1369,11 @@ export interface ZooworkClient {
1286
1369
  * accepted. `SKILL.md` must be non-empty and declare both `name` and `description`.
1287
1370
  * 50 MB expanded, zip only (store/deflate), encrypted zips rejected.
1288
1371
  *
1289
- * `scope` may only be `org` or `personal`; `global` and `pack` are 403 and are published
1290
- * through an admin surface the gateway does not proxy. The gateway rewrites ownership to your
1291
- * key's tenant either way. Staging-verified 2026-08-07 — this is the ONLY path by which an
1292
- * API-key caller can add a skill the agent will actually load.
1372
+ * scope is org or personal. Other values are rejected by the public gateway with HTTP 400.
1373
+ * Ownership comes from your key. On this CREATE operation the public gateway drops the
1374
+ * description option: put the description in the zip's frontmatter instead.
1375
+ * idempotencyKey is retained as a transport option, not a replay guarantee for Skill uploads.
1376
+ * A retry after a successful create can return 409 skill_exists; read back before retrying.
1293
1377
  */
1294
1378
  uploadSkill(zip: Blob | ArrayBuffer | Uint8Array, opts: {
1295
1379
  scope: 'org' | 'personal';
@@ -1300,7 +1384,9 @@ export interface ZooworkClient {
1300
1384
  /**
1301
1385
  * Publish a new version of an existing skill from a zip. Same zip rules as
1302
1386
  * {@link ZooworkClient.uploadSkill}, plus: the frontmatter `name` must match the target
1303
- * skill's name. `description` overrides the one in the frontmatter.
1387
+ * skill's name. Unlike root create, description can override the frontmatter here.
1388
+ * Returns a version row (version/state), not a SkillRecord (latest_version/status).
1389
+ * Identical content for the same skill is deduplicated by content, not by Idempotency-Key.
1304
1390
  *
1305
1391
  * Agents that installed the skill unpinned follow the new version on their own — the registry
1306
1392
  * bumps their `config_version`; you do not re-`putAgentSkill`.
@@ -1309,7 +1395,7 @@ export interface ZooworkClient {
1309
1395
  fileName?: string;
1310
1396
  description?: string;
1311
1397
  idempotencyKey?: string;
1312
- }): Promise<SkillRecord>;
1398
+ }): Promise<SkillVersionRecord>;
1313
1399
  /**
1314
1400
  * The catalog visible to your key: global skills plus your org/personal ones. `q` matches on
1315
1401
  * name; `page` is 1-based with a fixed page size of 100. Only the `org`/`personal` rows are
@@ -1409,11 +1495,10 @@ export interface ZooworkClient {
1409
1495
  * staging-verified 2026-08-07; any other value is rejected, so there is no way to list
1410
1496
  * resolved ones.
1411
1497
  *
1412
- * Approvals are a REST resource here, NOT the `user.tool_confirmation` event loop; the two
1413
- * shapes describe the same act and do not line up. Without a Temporal signaler the route
1414
- * answers `501 not_configured`. We have never produced a real pending approval, so the
1415
- * round trip is unproven: treat human-in-the-loop as unavailable, and note that a run parked
1416
- * on an approval burns its whole turn budget waiting.
1498
+ * Approvals are a REST resource here, not an interchangeable user.tool_confirmation
1499
+ * payload. A deployment without approval support can return 501 not_configured.
1500
+ * The response contract is source-reviewed; end-to-end approval and turn-budget behavior
1501
+ * need separate verification on the deployment you use.
1417
1502
  */
1418
1503
  listApprovals(agentId: string, opts?: {
1419
1504
  status?: 'pending';
@@ -1422,7 +1507,7 @@ export interface ZooworkClient {
1422
1507
  resolveApproval(agentId: string, approvalId: string, input: {
1423
1508
  decision: ApprovalDecision;
1424
1509
  resolvedBy?: string;
1425
- }): Promise<Record<string, unknown>>;
1510
+ }): Promise<ApprovalRecord>;
1426
1511
  /**
1427
1512
  * Artifacts this agent published, one page per call — but unlike `listEvents`, the page SAYS
1428
1513
  * when it truncated: read `has_more`. `limit` defaults to 50, capped at 100; filter by
@@ -1540,9 +1625,9 @@ export interface ZooworkClient {
1540
1625
  *
1541
1626
  * `resource.config` takes exactly four keys — packages / files / build / networking — and
1542
1627
  * anything else is `400 invalid_environment_config`. Building is asynchronous: poll
1543
- * `getEnvironmentVersion` until `status === 'ready'` before pinning it on an agent, or the
1544
- * create answers `409 environment_not_ready`. The field is `status` there is no `state` on a
1545
- * version, and a loop written against one never terminates.
1628
+ * getEnvironmentVersion with a deadline before pinning it on an agent. Handle failed and
1629
+ * partial_ready explicitly; selected-class readiness is different from aggregate readiness.
1630
+ * The field is status, not state. A loop without a timeout can otherwise wait forever.
1546
1631
  */
1547
1632
  createEnvironment(input: {
1548
1633
  resource: EnvironmentResource;
@@ -1558,15 +1643,17 @@ export interface ZooworkClient {
1558
1643
  */
1559
1644
  archiveEnvironment(environmentId: string): Promise<EnvironmentRecord>;
1560
1645
  /**
1561
- * Add an immutable version to an existing Environment. Versions never mutate: a retry after a
1562
- * failed build retries THAT version and keeps its attempt log.
1646
+ * Add a new immutable configuration version to an existing Environment. This is NOT the
1647
+ * separate operation that retries an existing failed version; the SDK does not wrap retry.
1563
1648
  *
1564
1649
  * The route is reachable, but the request body was not exercised against staging on
1565
1650
  * 2026-08-07 — the SDK sends `{ resource: { config } }`, mirroring create.
1566
1651
  */
1567
1652
  createEnvironmentVersion(environmentId: string, config: EnvironmentConfig, idempotencyKey?: string): Promise<EnvironmentVersionRecord>;
1568
- /** Poll this not the Environment's top-level state to decide whether a version is usable. */
1569
- getEnvironmentVersion(environmentId: string, version: number): Promise<EnvironmentVersionRecord>;
1653
+ /** Read aggregate build state, or one resource class. Source-reviewed selector; use bounded polling. */
1654
+ getEnvironmentVersion(environmentId: string, version: number, opts?: {
1655
+ resourceClass?: 'starter' | 'pro' | 'ultra';
1656
+ }): Promise<EnvironmentVersionRecord>;
1570
1657
  }
1571
1658
  /**
1572
1659
  * Create a client.
package/dist/client.js CHANGED
@@ -308,12 +308,57 @@ export function createZooworkClient(cfg = {}) {
308
308
  ...(idempotencyKey ? { headers: { 'Idempotency-Key': idempotencyKey } } : {}),
309
309
  });
310
310
  },
311
- listAgents: async (opts = {}) => {
312
- const params = { page: opts.page };
313
- for (const [k, v] of Object.entries(opts.labels ?? {}))
314
- params[`label.${k}`] = v;
315
- const data = await json(`/agents${query(params)}`);
316
- return data.agents ?? [];
311
+ listAgents: (opts = {}) => {
312
+ // Capture the filters once so caller mutations cannot change the scope mid-walk.
313
+ const labels = { ...opts.labels };
314
+ const fetchPage = async (pageNumber) => {
315
+ if (pageNumber !== undefined && (!Number.isSafeInteger(pageNumber) || pageNumber < 1)) {
316
+ throw new RangeError('Agent list page must be a positive safe integer');
317
+ }
318
+ const params = { page: pageNumber };
319
+ for (const [k, v] of Object.entries(labels))
320
+ params[`label.${k}`] = v;
321
+ const body = await json(`/agents${query(params)}`);
322
+ // Missing metadata must not silently turn a partial result into the last page.
323
+ // Check advancement too, so a server ignoring page cannot cause an endless walk.
324
+ if (!body || !Array.isArray(body.agents) || body.page !== (pageNumber ?? 1)
325
+ || !Number.isSafeInteger(body.page_size) || body.page_size < 1
326
+ || !Number.isSafeInteger(body.total) || body.total < 0) {
327
+ throw new Error('Invalid agent list pagination: expected agents, page, page_size, and total for the requested page');
328
+ }
329
+ const nextPage = body.page < Math.ceil(body.total / body.page_size) ? body.page + 1 : null;
330
+ const result = {
331
+ data: body.agents,
332
+ page: body.page,
333
+ page_size: body.page_size,
334
+ total: body.total,
335
+ next_page: nextPage,
336
+ hasNextPage: () => nextPage !== null,
337
+ getNextPage: async () => {
338
+ if (nextPage === null)
339
+ throw new Error('No next page');
340
+ return fetchPage(nextPage);
341
+ },
342
+ async *iterPages() {
343
+ let page = result;
344
+ while (true) {
345
+ yield page;
346
+ if (!page.hasNextPage())
347
+ return;
348
+ page = await page.getNextPage();
349
+ }
350
+ },
351
+ async *[Symbol.asyncIterator]() {
352
+ for await (const page of result.iterPages())
353
+ yield* page.data;
354
+ },
355
+ };
356
+ return result;
357
+ };
358
+ const request = fetchPage(opts.page);
359
+ return Object.assign(request, {
360
+ async *[Symbol.asyncIterator]() { yield* await request; },
361
+ });
317
362
  },
318
363
  getAgent: (agentId) => json(agents(agentId)),
319
364
  updateAgent: (agentId, sections) => json(agents(agentId), { method: 'PUT', body: JSON.stringify(sections) }),
@@ -700,7 +745,7 @@ export function createZooworkClient(cfg = {}) {
700
745
  body: JSON.stringify({ resource: { config } }),
701
746
  ...(idempotencyKey ? { headers: { 'Idempotency-Key': idempotencyKey } } : {}),
702
747
  }),
703
- getEnvironmentVersion: (environmentId, version) => json(`${environments(environmentId)}/versions/${encodeURIComponent(String(version))}`),
748
+ getEnvironmentVersion: (environmentId, version, opts = {}) => json(`${environments(environmentId)}/versions/${encodeURIComponent(String(version))}${query({ resource_class: opts.resourceClass })}`),
704
749
  };
705
750
  return client;
706
751
  }
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { createZooworkClient, DEFAULT_BASE_URL, ZooworkError, type ZooworkClient, type ZooworkConfig, type ZooworkAuth, type Ownership, type ModelInfo, type AgentResource, type AgentRecord, type AgentStatus, type AgentSkill, type AgentChannel, type ChannelPlatform, type AddChannelPlatform, type GuidedSetupPlatform, type AddChannelInput, type UpdateChannelInput, type ChannelSetupInput, type ChannelSetupSession, type ChannelPollResult, type FeishuSetupInput, type FeishuSetupSession, type FeishuPollResult, type McpServerDeclaration, type SkillRecord, type SessionRecord, type SessionHistoryEntry, type SessionEvent, type SessionEventPage, type OutboundEvent, type PostEventReceipt, type ApprovalDecision, type ApprovalRecord, type ArtifactPage, type ArtifactRecord, type ArtifactStatus, type OutcomeConfig, type OutcomeEvaluator, type SystemPromptDeclaration, type SystemPromptInfo, type SystemPromptPreview, type SystemPromptPreviewInput, type SystemPromptUpgrade, type ScheduleSpec, type SchedulePayload, type ScheduleInput, type ScheduleUpdate, type ScheduleRecord, type ScheduleRun, type WakeResult, type ExecResult, type EnvironmentConfig, type EnvironmentResource, type EnvironmentRecord, type EnvironmentVersionRecord, } from './client.js';
1
+ export { createZooworkClient, DEFAULT_BASE_URL, ZooworkError, type ZooworkClient, type ZooworkConfig, type ZooworkAuth, type Ownership, type ModelInfo, type AgentResource, type AgentRecord, type AgentListParams, type AgentPage, type AgentPagePromise, type AgentStatus, type AgentSkill, type AgentChannel, type ChannelPlatform, type AddChannelPlatform, type GuidedSetupPlatform, type AddChannelInput, type UpdateChannelInput, type ChannelSetupInput, type ChannelSetupSession, type ChannelPollResult, type FeishuSetupInput, type FeishuSetupSession, type FeishuPollResult, type McpServerDeclaration, type SkillRecord, type SkillVersionRecord, type SessionRecord, type SessionHistoryEntry, type SessionEvent, type SessionEventPage, type OutboundEvent, type PostEventReceipt, type ApprovalDecision, type ApprovalRecord, type ArtifactPage, type ArtifactRecord, type ArtifactStatus, type OutcomeConfig, type OutcomeEvaluator, type SystemPromptDeclaration, type SystemPromptInfo, type SystemPromptPreview, type SystemPromptPreviewInput, type SystemPromptUpgrade, type ScheduleSpec, type SchedulePayload, type ScheduleInput, type ScheduleUpdate, type ScheduleRecord, type ScheduleRun, type WakeResult, type ExecResult, type EnvironmentConfig, type EnvironmentResource, type EnvironmentRecord, type EnvironmentVersionRecord, } from './client.js';
2
2
  export { SESSION_EVENT_TYPES, type SessionEventType, PUBLIC_INPUT_EVENT_TYPES, type PublicInputEventType, normalizeEvent, isRunFinished, runOutcome, messageText, assistantText, thinkingText, toolCall, type ToolCall, } from './events.js';
3
3
  export { parseSSE, type SSEMessage } from './sse.js';
package/dist/sse.d.ts CHANGED
@@ -1,15 +1,11 @@
1
1
  /**
2
- * SSE line parser for the `/events/stream` endpoint endpoint.
3
- *
4
- * Ported from zoowork-app-kit server/zooclaw/sse.ts that parser is correct against the
5
- * live wire and needed no changes. The `id:` field matters: The server frames each durable
6
- * event as `id: <seq>` + `data: <json>`, so dropping the id line would freeze the resume
7
- * cursor. Web Streams + TextDecoder only, so this runs in workers and browsers as well as
8
- * Node.
2
+ * SSE line parser for the /events/stream endpoint.
3
+ * Preserve id as an opaque string: unified events use it as a resume cursor, while the
4
+ * legacy lane can send numeric ids. Web Streams + TextDecoder only.
9
5
  */
10
6
  export interface SSEMessage {
11
7
  event: string;
12
- /** The SSE `id:` field the durable seq for the API event frames. */
8
+ /** The SSE id field, unchanged. Do not parse an opaque resume cursor as a number. */
13
9
  id?: string;
14
10
  data: unknown;
15
11
  }
package/dist/sse.js CHANGED
@@ -1,11 +1,7 @@
1
1
  /**
2
- * SSE line parser for the `/events/stream` endpoint endpoint.
3
- *
4
- * Ported from zoowork-app-kit server/zooclaw/sse.ts that parser is correct against the
5
- * live wire and needed no changes. The `id:` field matters: The server frames each durable
6
- * event as `id: <seq>` + `data: <json>`, so dropping the id line would freeze the resume
7
- * cursor. Web Streams + TextDecoder only, so this runs in workers and browsers as well as
8
- * Node.
2
+ * SSE line parser for the /events/stream endpoint.
3
+ * Preserve id as an opaque string: unified events use it as a resume cursor, while the
4
+ * legacy lane can send numeric ids. Web Streams + TextDecoder only.
9
5
  */
10
6
  export const isObj = (v) => !!v && typeof v === 'object';
11
7
  export async function* parseSSE(body) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoowork-ai/sdk",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "TypeScript SDK for the ZooWork Managed Agents API (Developer Preview)",
5
5
  "keywords": [
6
6
  "zoowork",
@@ -39,16 +39,22 @@
39
39
  "publishConfig": {
40
40
  "access": "public"
41
41
  },
42
+ "devDependencies": {
43
+ "@types/node": "^22.20.1",
44
+ "tsx": "^4.23.7",
45
+ "typescript": "^5.6.0",
46
+ "vitest": "^3.0.0"
47
+ },
42
48
  "scripts": {
43
49
  "build": "tsc -p tsconfig.build.json",
44
50
  "typecheck": "tsc --noEmit",
45
51
  "test": "tsc --noEmit && vitest run",
46
52
  "test:watch": "vitest",
47
- "prepublishOnly": "pnpm run typecheck && pnpm run build"
48
- },
49
- "devDependencies": {
50
- "tsx": "^4.23.7",
51
- "typescript": "^5.6.0",
52
- "vitest": "^3.0.0"
53
+ "test:e2e": "node e2e/command.ts",
54
+ "typecheck:e2e": "tsc -p tsconfig.e2e.json",
55
+ "test:e2e:offline": "node --import tsx --test --test-reporter=spec e2e/*.test.ts",
56
+ "e2e:prepare": "node e2e/runner.ts prepare",
57
+ "e2e:run": "node e2e/runner.ts run",
58
+ "e2e:verify": "node e2e/runner.ts verify"
53
59
  }
54
- }
60
+ }