@zoowork-ai/sdk 0.5.2 → 0.7.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,43 @@
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.7.0 — 2026-09-14
7
+
8
+ ### Added
9
+
10
+ - **Application-executed custom tools.** Agent resources can declare `custom_tools`; event
11
+ helpers expose requested calls; and the client can list pending calls, resolve a call, or
12
+ post typed `user.custom_tool_result` content.
13
+ - **Filtered cursor session listing.** `listSessionPage()` adds the channel, surface,
14
+ runtime-mode and archive-filtered cursor lane without changing the existing numeric-page
15
+ behavior of `listSessions()`. Session archive and delete operations are also exposed.
16
+
17
+ ### Changed
18
+
19
+ - **MCP configuration now covers runtime context and permissions.** Types include metadata
20
+ context, a server permission default, exact per-tool overrides, and trailing-prefix tool
21
+ policy selectors.
22
+ - **Channel contracts include direct DingTalk configuration and Feishu document capability
23
+ state.** Callers can inspect provider, sync, missing-scope and approval-state details from
24
+ returned capabilities.
25
+
26
+ ## 0.6.0 — 2026-09-11
27
+
28
+ ### Changed
29
+
30
+ - **Breaking: `listAgents()` now resolves to an `AgentPage`, not an array.** Read `.data`
31
+ for the current page. The SDK preserves `page`, `page_size`, and `total`, and derives a
32
+ numeric `next_page` (`null` at the end). Existing one-page callers should replace
33
+ `const agents = await zc.listAgents(opts)` with `const { data: agents } = await zc.listAgents(opts)`.
34
+ - **Agent lists support automatic and manual pagination.** Use
35
+ `for await (const agent of zc.listAgents(opts))`, or `page.hasNextPage()` /
36
+ `page.getNextPage()`. Resolved pages are async iterable and provide `iterPages()`.
37
+ Later requests preserve the original label filters; early loop exit stops further fetches.
38
+ - Missing, invalid, or non-advancing agent pagination metadata now raises an error instead
39
+ of hiding a partial result. The API's fixed 100-item numeric pagination is unchanged.
40
+ Cross-page behavior is verified with synthetic offline HTTP tests; the live release
41
+ smoke covers one Agent/Session turn and cleanup, not a 101-agent pagination walk.
42
+
6
43
  ## 0.5.2 — 2026-09-04
7
44
 
8
45
  ### 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,22 +57,65 @@ 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
 
67
115
  `run.finished` ends a turn; assistant text arrives on `agent.assistant`.
68
116
 
69
117
  ```ts
70
- import { assistantText, isRunFinished, runOutcome, toolCall } from '@zoowork-ai/sdk'
118
+ import { assistantText, customToolUse, isRunFinished, runOutcome, toolCall } from '@zoowork-ai/sdk'
71
119
 
72
120
  for await (const ev of zc.streamEvents(agent.agent_id, session.session_id)) {
73
121
  process.stdout.write(assistantText(ev)) // '' for every non-assistant event
@@ -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
@@ -136,13 +201,37 @@ const { exit_code, stdout } = await zc.exec(agent.agent_id, ['bash', '-lc', 'pwd
136
201
 
137
202
  ## Sessions, approvals, environments
138
203
 
139
- `listSessions`, `archiveSession` and `deleteSession` round out the session surface. There is no
204
+ `listSessions` keeps the legacy numeric-page contract. Use `listSessionPage` for the filtered
205
+ cursor lane: it starts with `sls1:0`, accepts channel/surface/runtime/archive filters, and returns
206
+ `next_cursor` plus a `list_cursor` on each row. Cursors are opaque and bound to the same filters.
207
+ `archiveSession` and `deleteSession` round out the session surface. There is no
140
208
  `patchSession`: the gateway does not proxy `PATCH` at all (405), so session `metadata` is fixed at
141
209
  creation time.
142
210
 
211
+ An application-executed tool is declared in `resource.custom_tools`. When
212
+ `customToolUse(ev)?.phase === 'requested'`, execute the named operation and call
213
+ `resolveCustomToolCall`; `listCustomToolCalls` recovers pending work after a restart. You may also
214
+ post a typed `user.custom_tool_result` event to the owning session. The run reports
215
+ `awaiting_approval` while paused, so use `pending_custom_tool_calls` to distinguish this wait from
216
+ a normal approval. These contracts are source-reviewed and need deployment verification.
217
+
218
+ ```ts
219
+ const call = customToolUse(ev)
220
+ if (call?.phase === 'requested') {
221
+ await zc.resolveCustomToolCall(agentId, call.callId, {
222
+ content: [{ type: 'json', value: { price: 42 } }],
223
+ resolvedBy: 'pricing-service',
224
+ })
225
+ }
226
+ ```
227
+
143
228
  `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.
229
+ `allow-once`, `allow-always`, `deny`. End-to-end approval and turn-budget behavior need
230
+ separate verification on the deployment you use.
231
+
232
+ Approval response fields are source-reviewed, not end-to-end verified: read `requested_at`,
233
+ `allowed_decisions` and optional timeout/resolution fields defensively. `signaled: true` means
234
+ the resolution was accepted; a returned `status: 'pending'` is not completed execution.
146
235
 
147
236
  `listEnvironments`, `getEnvironment`, `createEnvironment`, `createEnvironmentVersion`,
148
237
  `getEnvironmentVersion` and `archiveEnvironment` manage prebuilt sandbox images (apt/npm/pip
@@ -151,6 +240,14 @@ start: an agent's Environment **freezes on its first sandbox creation** — afte
151
240
  is `409 environment_locked`, and stopping the agent does not clear it — and sandbox networking
152
241
  defaults to unrestricted unless the Environment declares `networking: { type: 'limited' }`.
153
242
 
243
+ Build polling must have a deadline and handle `partial_ready`: some resource classes can be
244
+ ready while others are building or failed. `getEnvironmentVersion(id, version, { resourceClass:
245
+ 'starter' })` selects one class; omitting the option keeps the aggregate read. Re-read the
246
+ aggregate before concluding the build is fully ready. These details are source-reviewed.
247
+
248
+ Channel callers must not use `allow_from` as an access-control list: the public gateway ignores
249
+ it. Use supported `dm_policy` settings.
250
+
154
251
  ## Artifacts and the system prompt
155
252
 
156
253
  ```ts
@@ -193,6 +290,41 @@ Runnable examples in [`examples/`](examples):
193
290
  - [`live-smoke.ts`](examples/live-smoke.ts) — drive one agent through one turn and verify the REST and SSE reads agree.
194
291
  - [`capability-probe.ts`](examples/capability-probe.ts) — create a throwaway agent, walk the whole lifecycle, and print a verdict per capability.
195
292
 
293
+ ## Testing and publishing (maintainers)
294
+
295
+ Testing and publishing are independent commands. After cloning this repository on a new
296
+ machine, install its locked development dependencies:
297
+
298
+ ```sh
299
+ pnpm install --frozen-lockfile
300
+ ```
301
+
302
+ Run staging E2E explicitly when you want to verify the SDK (Node 22.20+):
303
+
304
+ ```sh
305
+ pnpm test:e2e
306
+ ```
307
+
308
+ The command prepares an isolated test package, prints individual offline cases and timed live
309
+ steps, and asks for a staging API key with input hidden. Submitting the key authorizes one
310
+ temporary Agent/Session, one potentially billable model turn and cleanup. JSON reports remain
311
+ in its printed private directory. The test never publishes. Normal `pnpm test` is offline
312
+ and needs no key. See [E2E and recovery instructions](e2e/README.md) for scope and options.
313
+
314
+ Publishing runs through [`.github/workflows/release.yml`](.github/workflows/release.yml). Configure
315
+ the npm package's Trusted Publisher once with organization `SerendipityOneInc`, repository
316
+ `zoowork-sdk-typescript`, workflow `release.yml`, and direct publish permission. No npm token or
317
+ repeated `npm login` is needed after that.
318
+
319
+ For each release, merge the intended version and changelog, then publish a GitHub Release whose
320
+ tag is exactly `v<package version>` — for example, `v0.7.0`. The workflow verifies that match,
321
+ runs the offline test and build gates, and publishes the public package with npm OIDC. A mismatched
322
+ tag fails before publication, and an existing npm version cannot be overwritten.
323
+
324
+ The release workflow does not run live E2E or read a staging key. Run `pnpm test:e2e` separately
325
+ before creating the GitHub Release when live verification is required. Use
326
+ `npm publish --dry-run` locally to inspect the package without uploading it.
327
+
196
328
  ## License
197
329
 
198
330
  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
  /**
@@ -100,6 +123,18 @@ export interface ModelInfo {
100
123
  api?: string;
101
124
  [k: string]: unknown;
102
125
  }
126
+ /** Approval behavior for one MCP server or one of its tools. */
127
+ export type McpToolPermission = 'always_ask' | 'always_allow';
128
+ /** Opt-in runtime coordinates sent only while a tool is executing, never during catalog discovery. */
129
+ export interface McpContextConfig {
130
+ /** Add the coordinates under `params._meta['ai.zooclaw/context']` on `tools/call`. */
131
+ meta?: boolean;
132
+ /** Add the coordinates as `x-zooclaw-*` headers on the tool call's HTTP requests. */
133
+ headers?: boolean;
134
+ }
135
+ export interface McpToolPermissionOverride {
136
+ permission: McpToolPermission;
137
+ }
103
138
  /**
104
139
  * One remote MCP server, declared as `resource.mcp[]` on create or update.
105
140
  *
@@ -114,9 +149,12 @@ export interface ModelInfo {
114
149
  * other way to store it, so an authenticated MCP server cannot be made to work today.
115
150
  * Declare public servers only.
116
151
  *
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'`.
152
+ * Remote HTTP only; no stdio. Catalog failures emit `agent.error` with
153
+ * `kind: 'mcp_connection_failed'` or `'mcp_authentication_failed'`, a server name,
154
+ * an errorMessage and an optional reason. Preserve unknown reason values.
155
+ * Healthy catalogs remain tied to the configuration. Transient failed catalogs can expire:
156
+ * a later catalog resolution may probe again, not a periodic retry or recovery guarantee.
157
+ * These failure/recovery details are source-reviewed, not deployment-verified here.
120
158
  */
121
159
  export interface McpServerDeclaration {
122
160
  /** Server slug. Appears in every tool name as `mcp__<name>__<tool>`. No underscores. */
@@ -129,8 +167,42 @@ export interface McpServerDeclaration {
129
167
  credential?: string;
130
168
  /** Expose only these tool names from this server. Omit for all of them. */
131
169
  toolFilter?: string[];
170
+ /**
171
+ * How tools reach the model. `deferred` (the default when omitted) keeps them behind
172
+ * `tool_search` / `tool_describe` until loaded; `direct` declares them on the first request.
173
+ * There is no `auto` value.
174
+ */
175
+ exposure?: 'deferred' | 'direct';
176
+ /**
177
+ * Opt in to runtime coordinates for this server. Both switches default to false. The context
178
+ * can include agent/session/computer ids and, when available, run/turn/config/actor fields.
179
+ * Catalog discovery never receives it. Header delivery may be unavailable through proxies;
180
+ * `_meta` is the portable option.
181
+ */
182
+ context?: McpContextConfig;
183
+ /** Default approval behavior for every tool on this server. Omit for the existing default-allow behavior. */
184
+ permission?: McpToolPermission;
185
+ /**
186
+ * Per-tool approval overrides keyed by the server's original tool name, not its
187
+ * `mcp__<server>__<tool>` name. Keys are exact and cannot contain `*`; at most 64 entries.
188
+ */
189
+ tools?: Record<string, McpToolPermissionOverride>;
132
190
  [k: string]: unknown;
133
191
  }
192
+ /** One application-executed tool declared under {@link AgentResource.custom_tools}. */
193
+ export interface CustomToolDeclaration {
194
+ /** Unique within the Agent; 1–64 ASCII letters, numbers, `_` or `-`. Reserved runtime names are rejected. */
195
+ name: string;
196
+ /** Non-empty tool description, at most 4 KiB UTF-8. */
197
+ description: string;
198
+ /** JSON Schema for the input. Its top-level `type` must be `object`; at most 16 KiB serialized. */
199
+ input_schema: {
200
+ type: 'object';
201
+ [k: string]: unknown;
202
+ };
203
+ /** Result wait budget in milliseconds. Defaults to 600,000; maximum 86,400,000. */
204
+ timeoutMs?: number;
205
+ }
134
206
  /**
135
207
  * The system-prompt pin (`resource.system_prompt`).
136
208
  *
@@ -197,7 +269,12 @@ export interface OutcomeConfig {
197
269
  }
198
270
  export interface AgentResource {
199
271
  name: string;
200
- /** `max_tokens` caps output tokens per model request. Omit to use the platform default. */
272
+ /**
273
+ * Omit this section to pin the platform's current model defaults at create time. Those
274
+ * defaults can rotate; call `listModels()` and set `primary` explicitly for deterministic
275
+ * provisioning. `max_tokens` caps output tokens per model request; omit only that field to
276
+ * use the platform token limit.
277
+ */
201
278
  model?: {
202
279
  primary: string;
203
280
  input?: string[];
@@ -215,9 +292,21 @@ export interface AgentResource {
215
292
  version?: number | 'latest';
216
293
  }[];
217
294
  labels?: Record<string, string>;
295
+ /**
296
+ * Tool-surface and approval policy. Name selectors in `allow`, `deny`, `rules[].match.tool`,
297
+ * `afterRules[].match.tool`, and MCP names in `deferred.pinned` accept an exact name, `*`, or
298
+ * one trailing `prefix*`. Other wildcard forms match nothing. `alsoAllow` and
299
+ * `permissions` keys remain exact. Kept open for forward-compatible policy fields.
300
+ */
218
301
  tool_policy?: Record<string, unknown>;
219
302
  /** Remote MCP servers. Only unauthenticated ones work today — see {@link McpServerDeclaration}. */
220
303
  mcp?: McpServerDeclaration[];
304
+ /**
305
+ * Tools executed by your application. At most 32. A model call emits
306
+ * `agent.custom_tool_use`; return the result with `resolveCustomToolCall` or a
307
+ * `user.custom_tool_result` event. Source-reviewed; deployment availability is unverified.
308
+ */
309
+ custom_tools?: CustomToolDeclaration[];
221
310
  /**
222
311
  * System-prompt pin. Omitted on create means "the platform version active right now", pinned
223
312
  * from then on. REPLACE-ON-WRITE on PUT, like `tool_policy` — see {@link SystemPromptDeclaration}.
@@ -235,15 +324,18 @@ export interface AgentResource {
235
324
  environment_version?: number;
236
325
  }
237
326
  /**
238
- * Agent lifecycle state. Two fields, two very different meanings — staging-verified
239
- * 2026-08-06:
327
+ * Agent lifecycle state. Two fields, two very different meanings:
240
328
  *
241
329
  * - `desired_state` is the one that gates the API. `running` is the precondition for
242
330
  * 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.
331
+ * - `actual_state` is a best-effort CHANNEL-health projection, not API readiness. When
332
+ * route-status is unsupported, GET can report `active` with zero channel counts and a
333
+ * `status_message` saying health was not verified; a transient unknown remains
334
+ * `activating`. `listAgents` does not make the same foreground health query, so list and
335
+ * GET can briefly differ. Never gate on this field, and never poll it for `running` —
336
+ * that is not one of its values.
337
+ *
338
+ * The channel fallback behavior is source-reviewed, not deployment-verified here.
247
339
  */
248
340
  export interface AgentStatus {
249
341
  desired_state?: 'running' | 'stopped' | 'deleted' | string;
@@ -272,6 +364,27 @@ export interface AgentSkill {
272
364
  }[];
273
365
  [k: string]: unknown;
274
366
  }
367
+ /** Source-reviewed asynchronous capability-configuration state for one channel feature. */
368
+ export interface AgentChannelCapabilitySync {
369
+ state: 'pending' | 'applied' | 'retry' | 'error';
370
+ [k: string]: unknown;
371
+ }
372
+ export interface FeishuChannelProviderStatus {
373
+ state: 'ready' | 'degraded';
374
+ missing_scopes: string[];
375
+ approval_state?: 'pending_admin' | null;
376
+ [k: string]: unknown;
377
+ }
378
+ export interface FeishuDocumentsCapability {
379
+ permission_admin_enabled: boolean;
380
+ sync: AgentChannelCapabilitySync;
381
+ provider: FeishuChannelProviderStatus;
382
+ [k: string]: unknown;
383
+ }
384
+ export interface AgentChannelCapabilities {
385
+ feishu_documents?: FeishuDocumentsCapability | null;
386
+ [k: string]: unknown;
387
+ }
275
388
  /**
276
389
  * One platform account bound to an agent, as the channel service reports it.
277
390
  * `dm_policy` / `group_policy` are the reachability policies (`'open'` is the
@@ -288,10 +401,13 @@ export interface AgentChannel {
288
401
  health?: string;
289
402
  status?: string;
290
403
  status_code?: string | null;
404
+ /** Source-reviewed capability state. Omitted when the platform reports none. */
405
+ capabilities?: AgentChannelCapabilities | null;
291
406
  [k: string]: unknown;
292
407
  }
293
408
  /**
294
- * The chat platforms you can bind, staging-verified 2026-08-28.
409
+ * The chat platforms you can bind. Feishu, Slack, WeCom and WeChat were staging-verified
410
+ * 2026-08-28. Direct DingTalk support is source-reviewed, not deployment-verified here.
295
411
  *
296
412
  * Three of them have a server-driven QR flow ({@link GuidedSetupPlatform}); Slack does not,
297
413
  * and structurally cannot — a Slack app is created by a person and its tokens only ever exist
@@ -300,15 +416,15 @@ export interface AgentChannel {
300
416
  *
301
417
  * WeChat is the one platform that goes the other way: `'weixin'`/`'wechat'` on
302
418
  * {@link ZooworkClient.addChannel} answers `400 channel.weixin_setup_required`, so the QR flow
303
- * is its ONLY path. See {@link AddChannelPlatform}. Any name outside this type answers
304
- * `400 channel.invalid_request`.
419
+ * is its ONLY path. DingTalk uses `'dingtalk-connector'` and currently has a direct config path
420
+ * only on the public API; its product QR flow is not exposed here. See {@link AddChannelPlatform}.
305
421
  */
306
- export type ChannelPlatform = 'feishu' | 'slack' | 'wecom' | 'weixin';
422
+ export type ChannelPlatform = 'feishu' | 'slack' | 'wecom' | 'weixin' | 'dingtalk-connector';
307
423
  /**
308
424
  * The platforms {@link ZooworkClient.addChannel} accepts — every {@link ChannelPlatform}
309
425
  * except WeChat, which refuses explicit config and takes the QR flow only.
310
426
  */
311
- export type AddChannelPlatform = 'feishu' | 'slack' | 'wecom';
427
+ export type AddChannelPlatform = 'feishu' | 'slack' | 'wecom' | 'dingtalk-connector';
312
428
  /**
313
429
  * The platforms with a server-driven QR flow: {@link ZooworkClient.startChannelSetup} →
314
430
  * render the URI → poll. Slack is absent by design, not by omission.
@@ -357,11 +473,11 @@ export interface AddChannelInput {
357
473
  */
358
474
  account?: string;
359
475
  display_name?: string;
360
- /** Server default: `'open'`. `'pairing'` is rejected with `400 channel.pairing_unsupported`. */
476
+ /** Server default: `'open'`. DingTalk accepts only `'open'`; `'pairing'` is rejected everywhere. */
361
477
  dm_policy?: string;
362
478
  /** Server default: `'open'`. */
363
479
  group_policy?: string;
364
- /** Write-once at create; updates cannot edit it. */
480
+ /** Retained for compatibility; the public gateway ignores this field. It is NOT an access allowlist. Use supported dm_policy settings. */
365
481
  allow_from?: string[];
366
482
  /**
367
483
  * Platform credentials for the direct (non-QR) path. Keys are platform-specific and
@@ -371,8 +487,11 @@ export interface AddChannelInput {
371
487
  * needs the app-level token as well as the bot token)
372
488
  * - `wecom` — `{ botId, secret }`, both required
373
489
  * - `feishu` — `{ appId, appSecret, domain }`, only when skipping the QR flow
490
+ * - `dingtalk-connector` — `{ clientId, clientSecret }`; no public guided QR route
374
491
  */
375
492
  config?: Record<string, unknown>;
493
+ /** Feishu only. Enable document-permission administration; defaults to false. */
494
+ permission_admin_enabled?: boolean;
376
495
  }
377
496
  export interface UpdateChannelInput {
378
497
  /** Which binding to touch — see {@link AddChannelInput.account}. Server default: `'default'`. */
@@ -380,6 +499,8 @@ export interface UpdateChannelInput {
380
499
  dm_policy?: string;
381
500
  group_policy?: string;
382
501
  enabled?: boolean;
502
+ /** Feishu only. Other platforms reject this field when it is present. */
503
+ permission_admin_enabled?: boolean;
383
504
  }
384
505
  /**
385
506
  * Body for {@link ZooworkClient.startChannelSetup}. Every field is optional, and each platform
@@ -422,6 +543,8 @@ export interface ChannelSetupInput {
422
543
  dm_policy?: string;
423
544
  /** Server default: `'open'`. Ignored by WeChat, which forces `'disabled'`. */
424
545
  group_policy?: string;
546
+ /** Feishu only. Enable document-permission administration; defaults to false. */
547
+ permission_admin_enabled?: boolean;
425
548
  }
426
549
  /** @deprecated Use {@link ChannelSetupInput}; this is the same shape under the old name. */
427
550
  export type FeishuSetupInput = ChannelSetupInput;
@@ -548,6 +671,16 @@ export interface SkillRecord {
548
671
  };
549
672
  [k: string]: unknown;
550
673
  }
674
+ /**
675
+ * The version row returned by uploadSkillVersion, not a SkillRecord.
676
+ * Source-reviewed fields; no new live recording is implied. The response stays unmodified.
677
+ */
678
+ export interface SkillVersionRecord {
679
+ skill_id: string;
680
+ version: string | number;
681
+ state: 'pending' | 'ready' | 'failed' | string;
682
+ [k: string]: unknown;
683
+ }
551
684
  /**
552
685
  * One `session_transcripts` row, as projected by `getSession(…, { history: true })`.
553
686
  *
@@ -571,7 +704,11 @@ export interface SessionRecord {
571
704
  * `getSession` and `listSessions` carry the latest run state (`running`, `succeeded`, …) here.
572
705
  * The `createSession` receipt does not include this field.
573
706
  */
574
- run_status?: string;
707
+ run_status?: string | null;
708
+ /** Pending approval count on getSession; not included in every session projection. */
709
+ pending_approvals?: number;
710
+ /** Pending application-executed custom-tool count on getSession. */
711
+ pending_custom_tool_calls?: number;
575
712
  /**
576
713
  * `running` on a `createSession` receipt, nullable on `getSession`, and absent from
577
714
  * `listSessions` rows. This is not the run outcome; read {@link SessionRecord.run_status}
@@ -580,17 +717,84 @@ export interface SessionRecord {
580
717
  status?: string | null;
581
718
  metadata?: Record<string, unknown>;
582
719
  archived?: boolean;
720
+ runtime_mode?: 'active' | 'preview' | 'authoring' | 'evaluation' | string;
721
+ config_version?: number;
583
722
  updated_at?: string;
723
+ /** Activity sort key used by filtered cursor listing. */
724
+ last_activity_at?: string;
725
+ /** Opaque per-row resume cursor returned only by {@link ZooworkClient.listSessionPage}. */
726
+ list_cursor?: string;
584
727
  /** Present only when the read asked for `history: true`; the most recent `limit` rows, in order. */
585
728
  history?: SessionHistoryEntry[];
586
729
  [k: string]: unknown;
587
730
  }
588
- /** Write-side events: user.message / user.interrupt / user.tool_confirmation / system.message */
731
+ /** Write-side events, including `user.custom_tool_result`; unsupported types are rejected by the API. */
589
732
  export interface OutboundEvent {
590
733
  type: string;
591
734
  content?: unknown;
735
+ /**
736
+ * user.message on an API session only, including createSession.initial_events.
737
+ * ref is 1–200 ASCII characters from [A-Za-z0-9._:@+-]. Omit actor to use the owner.
738
+ * Map an authenticated application user to a stable opaque ref on YOUR server.
739
+ * This selects memory attribution, not authentication, session authorization or file isolation.
740
+ * IM sessions reject actor; token and other actor keys are rejected with HTTP 400.
741
+ * Source-reviewed; deployment availability must be verified separately.
742
+ */
743
+ actor?: {
744
+ ref: string;
745
+ token?: never;
746
+ };
592
747
  [k: string]: unknown;
593
748
  }
749
+ export type CustomToolResultImageMimeType = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp';
750
+ /** One content block returned by an application-executed custom tool. */
751
+ export type CustomToolResultContent = {
752
+ type: 'text';
753
+ text: string;
754
+ } | {
755
+ type: 'json';
756
+ value: unknown;
757
+ } | {
758
+ type: 'image';
759
+ source: {
760
+ type: 'base64';
761
+ media_type: CustomToolResultImageMimeType;
762
+ data: string;
763
+ };
764
+ } | {
765
+ type: 'image';
766
+ data: string;
767
+ mime_type: CustomToolResultImageMimeType;
768
+ };
769
+ /** Write-side result event for {@link ZooworkClient.postEvents}. */
770
+ export type CustomToolResultEvent = OutboundEvent & {
771
+ type: 'user.custom_tool_result';
772
+ content: CustomToolResultContent[];
773
+ is_error?: boolean;
774
+ idempotency_key?: string;
775
+ } & ({
776
+ custom_tool_use_id: string;
777
+ call_id?: never;
778
+ } | {
779
+ call_id: string;
780
+ custom_tool_use_id?: never;
781
+ });
782
+ /** Options for the filtered cursor lane. Filters are part of the cursor scope. */
783
+ export interface SessionListPageOptions {
784
+ /** Opaque cursor returned by this same filter scope. Omit to start at `sls1:0`. */
785
+ cursor?: string;
786
+ /** 1–100; server default 50. */
787
+ limit?: number;
788
+ excludeChannels?: string[];
789
+ includeSurfaces?: string[];
790
+ runtimeModes?: Array<'active' | 'preview' | 'authoring' | 'evaluation'>;
791
+ includeArchived?: boolean;
792
+ }
793
+ /** One filtered session page. `next_cursor` is null at the end. */
794
+ export interface SessionListPage {
795
+ sessions: SessionRecord[];
796
+ next_cursor: string | null;
797
+ }
594
798
  /** One `postEvents` receipt. An accepted event carries the full event object's fields too. */
595
799
  export interface PostEventReceipt {
596
800
  id?: string | null;
@@ -605,11 +809,10 @@ export interface SessionEventPage {
605
809
  nextCursor?: string | null;
606
810
  }
607
811
  /**
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.
812
+ * When a schedule fires: cron expression, everyMs interval in milliseconds (optional
813
+ * anchorMs), or an ISO instant in at. The everyMs contract is source-reviewed; it was
814
+ * previously misdeclared as every. Use everyMs explicitly: the SDK does not convert units
815
+ * or rename the obsolete every field at runtime.
613
816
  *
614
817
  * Cron is a five-field expression. Macros and a `CRON_TZ=` prefix are rejected; overlap is
615
818
  * fixed to SKIP server-side, so a fire that lands on a still-running one is dropped, not queued.
@@ -621,7 +824,8 @@ export type ScheduleSpec = {
621
824
  [k: string]: unknown;
622
825
  } | {
623
826
  kind: 'every';
624
- every: string | number;
827
+ everyMs: number;
828
+ anchorMs?: number;
625
829
  tz?: string;
626
830
  [k: string]: unknown;
627
831
  } | {
@@ -782,13 +986,16 @@ export interface ScheduleRecord {
782
986
  * This is the only row that carries `status` (e.g. `skipped` for a fire against a disabled
783
987
  * schedule).
784
988
  *
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}:`.
989
+ * Source-reviewed additions can include session_id when the fire has a known session.
990
+ * It is optional, not proof of success, and may be absent for command jobs or an unlinked
991
+ * fire. Older recordings above omit it. Without it, list the agent's sessions and match
992
+ * channel: 'cron' with the session_key prefix agent:{agent_id}:cron:{schedule_id}:.
788
993
  */
789
994
  export interface ScheduleRun {
790
995
  /** Which projection this row came from. Decides which of the field groups below is populated. */
791
996
  source?: 'temporal' | 'run_projection' | string;
997
+ /** Present only when a session can be associated with this fire. */
998
+ session_id?: string;
792
999
  /** Un-qualified `schedule_id`. */
793
1000
  schedule_id?: string;
794
1001
  fired_at?: string;
@@ -809,18 +1016,46 @@ export type ApprovalDecision = 'allow-once' | 'allow-always' | 'deny';
809
1016
  /**
810
1017
  * A tool call parked on a human decision.
811
1018
  *
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.
1019
+ * Fields below are source-reviewed; the existing live recordings have empty lists.
1020
+ * A resolve receipt can have signaled:true while status is still pending: acceptance is not
1021
+ * completion of the tool or run. Verify the resulting state separately.
815
1022
  */
816
1023
  export interface ApprovalRecord {
817
1024
  approval_id?: string;
818
1025
  session_id?: string;
819
1026
  tool_name?: string;
820
1027
  status?: string;
1028
+ arguments_preview?: string;
1029
+ stake?: unknown;
1030
+ requested_at?: string;
1031
+ timeout_at?: string;
1032
+ allowed_decisions?: ApprovalDecision[];
1033
+ resolved_by?: string;
1034
+ resolved_at?: string;
1035
+ signaled?: boolean;
1036
+ decision?: ApprovalDecision;
1037
+ /** Legacy compatibility field; current source projects requested_at instead. */
821
1038
  created_at?: string;
822
1039
  [k: string]: unknown;
823
1040
  }
1041
+ export type CustomToolCallStatus = 'pending' | 'completed' | 'timeout' | 'cancelled' | string;
1042
+ /** One application-executed custom-tool call. Unknown response fields are preserved. */
1043
+ export interface CustomToolCallRecord {
1044
+ call_id: string;
1045
+ session_id: string;
1046
+ tool_call_id: string;
1047
+ name: string;
1048
+ input: Record<string, unknown>;
1049
+ status: CustomToolCallStatus;
1050
+ requested_at: string;
1051
+ timeout_at?: string;
1052
+ resolved_by?: string;
1053
+ resolved_at?: string;
1054
+ is_error?: boolean;
1055
+ /** Resolve receipt: true when a pending call was signaled, false when it was already terminal. */
1056
+ signaled?: boolean;
1057
+ [k: string]: unknown;
1058
+ }
824
1059
  /** Artifact lifecycle. Only a `ready` row carries a resolvable `url`. */
825
1060
  export type ArtifactStatus = 'pending' | 'ready' | 'failed' | 'deleted' | string;
826
1061
  /**
@@ -993,10 +1228,11 @@ export interface EnvironmentRecord {
993
1228
  [k: string]: unknown;
994
1229
  }
995
1230
  /**
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`.
1231
+ * One immutable Environment version's build state. Builds can be queued, submitting, building,
1232
+ * verifying, partial_ready, ready or failed. partial_ready means some resource classes are
1233
+ * ready; others may still be building or may have failed. It is not by itself a success/failure
1234
+ * verdict for your selected class. Poll with a deadline; optionally read that resourceClass
1235
+ * and re-read the aggregate before drawing a conclusion.
1000
1236
  *
1001
1237
  * THE FIELD IS `status`, NOT `state` — staging-verified 2026-08-07. A poll loop written against
1002
1238
  * `state` compares `undefined` to `'ready'` forever and never terminates, which is precisely the
@@ -1005,8 +1241,8 @@ export interface EnvironmentRecord {
1005
1241
  export interface EnvironmentVersionRecord {
1006
1242
  environment_id?: string;
1007
1243
  version?: number;
1008
- /** `queued` `submitting` `building` `verifying` `ready`, or `failed`. */
1009
- status?: 'queued' | 'submitting' | 'building' | 'verifying' | 'ready' | 'failed' | string;
1244
+ /** Build status; partial_ready may be transient or a partial terminal result. */
1245
+ status?: 'queued' | 'submitting' | 'building' | 'verifying' | 'partial_ready' | 'ready' | 'failed' | string;
1010
1246
  /** The normalized config this version was built from — not the one you posted verbatim. */
1011
1247
  config?: EnvironmentConfig;
1012
1248
  base_environment_id?: string;
@@ -1064,11 +1300,12 @@ export interface ZooworkClient {
1064
1300
  *
1065
1301
  * Note the scope is `owner_uid AND org_id`: an agent a colleague created in your org is
1066
1302
  * fetchable by `getAgent` but will not appear here.
1303
+ *
1304
+ * `await listAgents()` returns one {@link AgentPage}; read `.data` for its agents and
1305
+ * `.next_page` / `.hasNextPage()` for continuation. `for await (const agent of listAgents())`
1306
+ * automatically fetches subsequent pages. Breaking the loop stops further requests.
1067
1307
  */
1068
- listAgents(opts?: {
1069
- labels?: Record<string, string>;
1070
- page?: number;
1071
- }): Promise<AgentRecord[]>;
1308
+ listAgents(opts?: AgentListParams): AgentPagePromise;
1072
1309
  getAgent(agentId: string): Promise<AgentRecord>;
1073
1310
  /** PUT declared sections; bumps config_version on EVERY call — gate on drift, don't blind-retry. */
1074
1311
  updateAgent(agentId: string, sections: Record<string, unknown>): Promise<AgentRecord>;
@@ -1081,13 +1318,17 @@ export interface ZooworkClient {
1081
1318
  deleteAgent(agentId: string): Promise<void>;
1082
1319
  /**
1083
1320
  * 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.
1321
+ * A successful response can contain warnings. A non-2xx response still throws ZooworkError;
1322
+ * warnings are not guaranteed on each start/stop and are not a substitute for error handling.
1087
1323
  */
1088
1324
  startAgent(agentId: string): Promise<{
1089
1325
  warnings: string[];
1090
1326
  }>;
1327
+ /**
1328
+ * Request stopped state. Dependency failures can reject after desired_state changes.
1329
+ * Read back and reconcile; desired_state alone does not prove all runtime work is cleaned up.
1330
+ * A successful response's warnings and a rejected HTTP call are different outcomes.
1331
+ */
1091
1332
  stopAgent(agentId: string): Promise<{
1092
1333
  warnings: string[];
1093
1334
  }>;
@@ -1286,10 +1527,11 @@ export interface ZooworkClient {
1286
1527
  * accepted. `SKILL.md` must be non-empty and declare both `name` and `description`.
1287
1528
  * 50 MB expanded, zip only (store/deflate), encrypted zips rejected.
1288
1529
  *
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.
1530
+ * scope is org or personal. Other values are rejected by the public gateway with HTTP 400.
1531
+ * Ownership comes from your key. On this CREATE operation the public gateway drops the
1532
+ * description option: put the description in the zip's frontmatter instead.
1533
+ * idempotencyKey is retained as a transport option, not a replay guarantee for Skill uploads.
1534
+ * A retry after a successful create can return 409 skill_exists; read back before retrying.
1293
1535
  */
1294
1536
  uploadSkill(zip: Blob | ArrayBuffer | Uint8Array, opts: {
1295
1537
  scope: 'org' | 'personal';
@@ -1300,7 +1542,9 @@ export interface ZooworkClient {
1300
1542
  /**
1301
1543
  * Publish a new version of an existing skill from a zip. Same zip rules as
1302
1544
  * {@link ZooworkClient.uploadSkill}, plus: the frontmatter `name` must match the target
1303
- * skill's name. `description` overrides the one in the frontmatter.
1545
+ * skill's name. Unlike root create, description can override the frontmatter here.
1546
+ * Returns a version row (version/state), not a SkillRecord (latest_version/status).
1547
+ * Identical content for the same skill is deduplicated by content, not by Idempotency-Key.
1304
1548
  *
1305
1549
  * Agents that installed the skill unpinned follow the new version on their own — the registry
1306
1550
  * bumps their `config_version`; you do not re-`putAgentSkill`.
@@ -1309,7 +1553,7 @@ export interface ZooworkClient {
1309
1553
  fileName?: string;
1310
1554
  description?: string;
1311
1555
  idempotencyKey?: string;
1312
- }): Promise<SkillRecord>;
1556
+ }): Promise<SkillVersionRecord>;
1313
1557
  /**
1314
1558
  * The catalog visible to your key: global skills plus your org/personal ones. `q` matches on
1315
1559
  * name; `page` is 1-based with a fixed page size of 100. Only the `org`/`personal` rows are
@@ -1330,10 +1574,15 @@ export interface ZooworkClient {
1330
1574
  history?: boolean;
1331
1575
  limit?: number;
1332
1576
  }): Promise<SessionRecord>;
1333
- /** Newest first by `updated_at`, 50 per page, `page` is 1-based. Single page per call — there is no cursor. */
1577
+ /** Legacy newest-first numeric page lane: 50 per page, `page` is 1-based. */
1334
1578
  listSessions(agentId: string, opts?: {
1335
1579
  page?: number;
1336
1580
  }): Promise<SessionRecord[]>;
1581
+ /**
1582
+ * Filtered cursor lane. It is separate from {@link listSessions} so existing numeric-page
1583
+ * callers keep their contract. Cursors are opaque and valid only with the same filters.
1584
+ */
1585
+ listSessionPage(agentId: string, opts?: SessionListPageOptions): Promise<SessionListPage>;
1337
1586
  /**
1338
1587
  * Stamp `archived_at`. Afterwards writes are `409 session_archived` while reads keep working.
1339
1588
  * Interrupt an in-flight run first, or the archive races it.
@@ -1404,16 +1653,28 @@ export interface ZooworkClient {
1404
1653
  cursor?: string;
1405
1654
  signal?: AbortSignal;
1406
1655
  }): AsyncGenerator<SessionEvent>;
1656
+ /** Pending calls only. Any other status is rejected by the API. */
1657
+ listCustomToolCalls(agentId: string, opts?: {
1658
+ status?: 'pending';
1659
+ }): Promise<CustomToolCallRecord[]>;
1660
+ /**
1661
+ * Return one call's result. A pending call answers 202/signaled:true but stays pending until
1662
+ * the paused run consumes it; an already-terminal call answers 200/signaled:false.
1663
+ */
1664
+ resolveCustomToolCall(agentId: string, callId: string, input: {
1665
+ content: CustomToolResultContent[];
1666
+ isError?: boolean;
1667
+ resolvedBy?: string;
1668
+ }): Promise<CustomToolCallRecord>;
1407
1669
  /**
1408
1670
  * Tool calls parked on a human decision. `status` may ONLY be omitted or `'pending'` —
1409
1671
  * staging-verified 2026-08-07; any other value is rejected, so there is no way to list
1410
1672
  * resolved ones.
1411
1673
  *
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.
1674
+ * Approvals are a REST resource here, not an interchangeable user.tool_confirmation
1675
+ * payload. A deployment without approval support can return 501 not_configured.
1676
+ * The response contract is source-reviewed; end-to-end approval and turn-budget behavior
1677
+ * need separate verification on the deployment you use.
1417
1678
  */
1418
1679
  listApprovals(agentId: string, opts?: {
1419
1680
  status?: 'pending';
@@ -1422,7 +1683,7 @@ export interface ZooworkClient {
1422
1683
  resolveApproval(agentId: string, approvalId: string, input: {
1423
1684
  decision: ApprovalDecision;
1424
1685
  resolvedBy?: string;
1425
- }): Promise<Record<string, unknown>>;
1686
+ }): Promise<ApprovalRecord>;
1426
1687
  /**
1427
1688
  * Artifacts this agent published, one page per call — but unlike `listEvents`, the page SAYS
1428
1689
  * when it truncated: read `has_more`. `limit` defaults to 50, capped at 100; filter by
@@ -1540,9 +1801,9 @@ export interface ZooworkClient {
1540
1801
  *
1541
1802
  * `resource.config` takes exactly four keys — packages / files / build / networking — and
1542
1803
  * 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.
1804
+ * getEnvironmentVersion with a deadline before pinning it on an agent. Handle failed and
1805
+ * partial_ready explicitly; selected-class readiness is different from aggregate readiness.
1806
+ * The field is status, not state. A loop without a timeout can otherwise wait forever.
1546
1807
  */
1547
1808
  createEnvironment(input: {
1548
1809
  resource: EnvironmentResource;
@@ -1558,15 +1819,17 @@ export interface ZooworkClient {
1558
1819
  */
1559
1820
  archiveEnvironment(environmentId: string): Promise<EnvironmentRecord>;
1560
1821
  /**
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.
1822
+ * Add a new immutable configuration version to an existing Environment. This is NOT the
1823
+ * separate operation that retries an existing failed version; the SDK does not wrap retry.
1563
1824
  *
1564
1825
  * The route is reachable, but the request body was not exercised against staging on
1565
1826
  * 2026-08-07 — the SDK sends `{ resource: { config } }`, mirroring create.
1566
1827
  */
1567
1828
  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>;
1829
+ /** Read aggregate build state, or one resource class. Source-reviewed selector; use bounded polling. */
1830
+ getEnvironmentVersion(environmentId: string, version: number, opts?: {
1831
+ resourceClass?: 'starter' | 'pro' | 'ultra';
1832
+ }): Promise<EnvironmentVersionRecord>;
1570
1833
  }
1571
1834
  /**
1572
1835
  * 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) }),
@@ -498,6 +543,17 @@ export function createZooworkClient(cfg = {}) {
498
543
  const data = await json(`${sessions(agentId)}${query({ page: opts.page })}`);
499
544
  return data.sessions ?? [];
500
545
  },
546
+ listSessionPage: async (agentId, opts = {}) => {
547
+ const data = await json(`${sessions(agentId)}${query({
548
+ cursor: opts.cursor ?? 'sls1:0',
549
+ limit: opts.limit,
550
+ exclude_channels: opts.excludeChannels?.join(','),
551
+ include_surfaces: opts.includeSurfaces?.join(','),
552
+ runtime_modes: opts.runtimeModes?.join(','),
553
+ include_archived: opts.includeArchived === undefined ? undefined : String(opts.includeArchived),
554
+ })}`);
555
+ return { sessions: data.sessions ?? [], next_cursor: data.next_cursor ?? null };
556
+ },
501
557
  archiveSession: async (agentId, sessionId) => {
502
558
  const data = await json(`${sessions(agentId)}/${encodeURIComponent(sessionId)}/archive`, { method: 'POST' });
503
559
  return { ...data, archived: data.archived ?? false };
@@ -606,6 +662,14 @@ export function createZooworkClient(cfg = {}) {
606
662
  throw e;
607
663
  }
608
664
  },
665
+ listCustomToolCalls: async (agentId, opts = {}) => {
666
+ const data = await json(`${agents(agentId)}/custom_tool_calls${query({ status: opts.status })}`);
667
+ return data.custom_tool_calls ?? [];
668
+ },
669
+ resolveCustomToolCall: (agentId, callId, input) => json(`${agents(agentId)}/custom_tool_calls/${encodeURIComponent(callId)}/result`, {
670
+ method: 'POST',
671
+ body: JSON.stringify(input),
672
+ }),
609
673
  listApprovals: async (agentId, opts = {}) => {
610
674
  const data = await json(`${agents(agentId)}/approvals${query({ status: opts.status })}`);
611
675
  return data.approvals ?? [];
@@ -700,7 +764,7 @@ export function createZooworkClient(cfg = {}) {
700
764
  body: JSON.stringify({ resource: { config } }),
701
765
  ...(idempotencyKey ? { headers: { 'Idempotency-Key': idempotencyKey } } : {}),
702
766
  }),
703
- getEnvironmentVersion: (environmentId, version) => json(`${environments(environmentId)}/versions/${encodeURIComponent(String(version))}`),
767
+ getEnvironmentVersion: (environmentId, version, opts = {}) => json(`${environments(environmentId)}/versions/${encodeURIComponent(String(version))}${query({ resource_class: opts.resourceClass })}`),
704
768
  };
705
769
  return client;
706
770
  }
package/dist/events.d.ts CHANGED
@@ -17,10 +17,10 @@
17
17
  * and may add types within a version.
18
18
  */
19
19
  /** SESSION_EVENT_TYPES, mirrored from the API. */
20
- export declare const SESSION_EVENT_TYPES: readonly ["run.started", "run.finished", "chat.delta", "chat.final", "chat.aborted", "chat.error", "agent.lifecycle", "agent.assistant", "agent.thinking", "agent.tool", "agent.item", "agent.plan", "agent.approval", "agent.command_output", "agent.patch", "agent.compaction", "agent.error", "attachment.created", "message.outbound"];
20
+ export declare const SESSION_EVENT_TYPES: readonly ["run.started", "run.finished", "chat.delta", "chat.final", "chat.aborted", "chat.error", "agent.lifecycle", "agent.assistant", "agent.thinking", "agent.tool", "agent.item", "agent.plan", "agent.approval", "agent.custom_tool_use", "agent.command_output", "agent.patch", "agent.compaction", "agent.error", "attachment.created", "message.outbound"];
21
21
  export type SessionEventType = (typeof SESSION_EVENT_TYPES)[number];
22
22
  /** Your own inputs, echoed back in the unified event history. */
23
- export declare const PUBLIC_INPUT_EVENT_TYPES: readonly ["user.message", "user.interrupt", "user.tool_confirmation", "system.message"];
23
+ export declare const PUBLIC_INPUT_EVENT_TYPES: readonly ["user.message", "user.interrupt", "user.tool_confirmation", "user.custom_tool_result", "system.message"];
24
24
  export type PublicInputEventType = (typeof PUBLIC_INPUT_EVENT_TYPES)[number];
25
25
  /** A durable session event, normalized across the REST and SSE shapes. */
26
26
  export interface SessionEvent {
@@ -74,6 +74,20 @@ export interface ToolCall {
74
74
  isError?: boolean;
75
75
  resultPreview?: string;
76
76
  }
77
+ export interface CustomToolUse {
78
+ phase: 'requested' | 'resolved';
79
+ callId: string;
80
+ toolCallId?: string;
81
+ name?: string;
82
+ input?: Record<string, unknown>;
83
+ timeoutAt?: string;
84
+ outcome?: 'completed' | 'timeout' | 'cancelled';
85
+ isError?: boolean;
86
+ resolvedBy?: string;
87
+ resolutionChannel?: string;
88
+ }
89
+ /** Application-executed custom-tool activity; undefined for every other event type. */
90
+ export declare function customToolUse(e: SessionEvent): CustomToolUse | undefined;
77
91
  /**
78
92
  * Tool activity for an `agent.tool` event; undefined for every other type.
79
93
  *
package/dist/events.js CHANGED
@@ -31,6 +31,7 @@ export const SESSION_EVENT_TYPES = [
31
31
  'agent.item',
32
32
  'agent.plan',
33
33
  'agent.approval',
34
+ 'agent.custom_tool_use',
34
35
  'agent.command_output',
35
36
  'agent.patch',
36
37
  'agent.compaction',
@@ -43,6 +44,7 @@ export const PUBLIC_INPUT_EVENT_TYPES = [
43
44
  'user.message',
44
45
  'user.interrupt',
45
46
  'user.tool_confirmation',
47
+ 'user.custom_tool_result',
46
48
  'system.message',
47
49
  ];
48
50
  const isObj = (v) => !!v && typeof v === 'object';
@@ -124,6 +126,25 @@ export function thinkingText(e) {
124
126
  return '';
125
127
  return typeof e.payload.text === 'string' ? e.payload.text : '';
126
128
  }
129
+ /** Application-executed custom-tool activity; undefined for every other event type. */
130
+ export function customToolUse(e) {
131
+ if (e.eventType !== 'agent.custom_tool_use')
132
+ return undefined;
133
+ const p = e.payload;
134
+ const outcome = p.outcome === 'completed' || p.outcome === 'timeout' || p.outcome === 'cancelled' ? p.outcome : undefined;
135
+ return {
136
+ phase: p.phase === 'resolved' ? 'resolved' : 'requested',
137
+ callId: typeof p.callId === 'string' ? p.callId : '',
138
+ ...(typeof p.toolCallId === 'string' ? { toolCallId: p.toolCallId } : {}),
139
+ ...(typeof p.name === 'string' ? { name: p.name } : {}),
140
+ ...(isObj(p.input) ? { input: p.input } : {}),
141
+ ...(typeof p.timeoutAt === 'string' ? { timeoutAt: p.timeoutAt } : {}),
142
+ ...(outcome ? { outcome } : {}),
143
+ ...(typeof p.isError === 'boolean' ? { isError: p.isError } : {}),
144
+ ...(typeof p.resolvedBy === 'string' ? { resolvedBy: p.resolvedBy } : {}),
145
+ ...(typeof p.resolutionChannel === 'string' ? { resolutionChannel: p.resolutionChannel } : {}),
146
+ };
147
+ }
127
148
  /**
128
149
  * Tool activity for an `agent.tool` event; undefined for every other type.
129
150
  *
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';
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';
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 AgentChannelCapabilitySync, type AgentChannelCapabilities, type FeishuChannelProviderStatus, type FeishuDocumentsCapability, type ChannelPlatform, type AddChannelPlatform, type GuidedSetupPlatform, type AddChannelInput, type UpdateChannelInput, type ChannelSetupInput, type ChannelSetupSession, type ChannelPollResult, type FeishuSetupInput, type FeishuSetupSession, type FeishuPollResult, type McpContextConfig, type McpServerDeclaration, type McpToolPermission, type McpToolPermissionOverride, type CustomToolDeclaration, type CustomToolResultImageMimeType, type CustomToolResultContent, type CustomToolResultEvent, type SkillRecord, type SkillVersionRecord, type SessionRecord, type SessionListPageOptions, type SessionListPage, type SessionHistoryEntry, type SessionEvent, type SessionEventPage, type OutboundEvent, type PostEventReceipt, type ApprovalDecision, type ApprovalRecord, type CustomToolCallStatus, type CustomToolCallRecord, 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
+ export { SESSION_EVENT_TYPES, type SessionEventType, PUBLIC_INPUT_EVENT_TYPES, type PublicInputEventType, normalizeEvent, isRunFinished, runOutcome, messageText, assistantText, thinkingText, customToolUse, type CustomToolUse, toolCall, type ToolCall, } from './events.js';
3
3
  export { parseSSE, type SSEMessage } from './sse.js';
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { createZooworkClient, DEFAULT_BASE_URL, ZooworkError, } from './client.js';
2
- export { SESSION_EVENT_TYPES, PUBLIC_INPUT_EVENT_TYPES, normalizeEvent, isRunFinished, runOutcome, messageText, assistantText, thinkingText, toolCall, } from './events.js';
2
+ export { SESSION_EVENT_TYPES, PUBLIC_INPUT_EVENT_TYPES, normalizeEvent, isRunFinished, runOutcome, messageText, assistantText, thinkingText, customToolUse, toolCall, } from './events.js';
3
3
  export { parseSSE } 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.7.0",
4
4
  "description": "TypeScript SDK for the ZooWork Managed Agents API (Developer Preview)",
5
5
  "keywords": [
6
6
  "zoowork",
@@ -44,9 +44,16 @@
44
44
  "typecheck": "tsc --noEmit",
45
45
  "test": "tsc --noEmit && vitest run",
46
46
  "test:watch": "vitest",
47
- "prepublishOnly": "pnpm run typecheck && pnpm run build"
47
+ "test:e2e": "node e2e/command.ts",
48
+ "typecheck:e2e": "tsc -p tsconfig.e2e.json",
49
+ "test:e2e:offline": "node --import tsx --test --test-reporter=spec e2e/*.test.ts",
50
+ "e2e:prepare": "node e2e/runner.ts prepare",
51
+ "e2e:run": "node e2e/runner.ts run",
52
+ "e2e:verify": "node e2e/runner.ts verify",
53
+ "prepack": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && npm run build"
48
54
  },
49
55
  "devDependencies": {
56
+ "@types/node": "^22.20.1",
50
57
  "tsx": "^4.23.7",
51
58
  "typescript": "^5.6.0",
52
59
  "vitest": "^3.0.0"