@zoowork-ai/sdk 0.4.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 +366 -0
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/dist/client.d.ts +1424 -0
- package/dist/client.js +622 -0
- package/dist/events.d.ts +88 -0
- package/dist/events.js +151 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/sse.d.ts +17 -0
- package/dist/sse.js +69 -0
- package/package.json +54 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@zoowork-ai/sdk` (formerly `@zooclaw-agents/sdk`). Dates are the
|
|
4
|
+
day the behaviour was verified, not the day it was written.
|
|
5
|
+
|
|
6
|
+
## 0.4.0 — 2026-08-25
|
|
7
|
+
|
|
8
|
+
### Changed (breaking)
|
|
9
|
+
|
|
10
|
+
- **Renamed to `@zoowork-ai/sdk`.** The package, exports, and environment variables all
|
|
11
|
+
move from the ZooClaw name to ZooWork, with no compatibility aliases:
|
|
12
|
+
- Install `@zoowork-ai/sdk` instead of `@zooclaw-agents/sdk`.
|
|
13
|
+
- `createZooclawClient` → `createZooworkClient`; `ZooclawClient`, `ZooclawError`,
|
|
14
|
+
`ZooclawAuth`, `ZooclawConfig` → `Zoowork*`.
|
|
15
|
+
- `ZOOCLAW_API_KEY` / `ZOOCLAW_BASE_URL` → `ZOOWORK_API_KEY` / `ZOOWORK_BASE_URL`.
|
|
16
|
+
- Server-side identifiers are unchanged: API keys still start with `zct_`, and skill or
|
|
17
|
+
environment names the API returns (e.g. `zooclaw-tts`) are whatever the server says.
|
|
18
|
+
|
|
19
|
+
## 0.3.4 — 2026-08-25
|
|
20
|
+
|
|
21
|
+
### Fixed (documentation)
|
|
22
|
+
|
|
23
|
+
- **`config` keys are documented per platform**, which is what a caller actually needs:
|
|
24
|
+
`slack` takes `{ botToken, appToken }` (socket mode needs the app-level token too),
|
|
25
|
+
`wecom` takes `{ botId, secret }`, `feishu` takes `{ appId, appSecret, domain }` when you
|
|
26
|
+
skip the QR flow. They are camelCase; other keys are stored and ignored.
|
|
27
|
+
- **`ChannelPlatform` explains why only Feishu has a QR flow here**, because the two absences
|
|
28
|
+
are different. Slack structurally cannot have one — a Slack app is created by a person and
|
|
29
|
+
its tokens only exist in that person's browser, so guided setup anywhere ends in the same
|
|
30
|
+
two tokens you pass to `addChannel`. WeCom's flow exists in the product but is not exposed
|
|
31
|
+
on this API yet.
|
|
32
|
+
|
|
33
|
+
## 0.3.3 — 2026-08-25
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- **`ChannelPlatform` is `'feishu' | 'slack' | 'wecom'`.** 0.3.2 also listed `'mattermost'`,
|
|
38
|
+
which is the deployment's own internal connection rather than something an API caller binds;
|
|
39
|
+
it is filtered out of `listChannels` server-side and does not belong on this surface. The
|
|
40
|
+
type stays widened with `(string & {})`, so nothing that compiled before stops compiling.
|
|
41
|
+
|
|
42
|
+
## 0.3.2 — 2026-08-25
|
|
43
|
+
|
|
44
|
+
Probed the platform axis, which 0.3.1 had not: the routes only name Feishu, but `platform`
|
|
45
|
+
is a free string and the server knows more than one.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
- **`ChannelPlatform`** — `'feishu' | 'slack' | 'wecom' | 'mattermost'`, widened with
|
|
50
|
+
`(string & {})` so a platform that ships later needs no SDK release.
|
|
51
|
+
|
|
52
|
+
### Fixed (documentation)
|
|
53
|
+
|
|
54
|
+
- **Slack and WeCom bind through `addChannel`** — 0.3.1 read as though channels meant Feishu.
|
|
55
|
+
- **A Mattermost binding is invisible.** It binds, updates and removes normally, but the server
|
|
56
|
+
filters it out of every `listChannels` response, so an empty list is not proof nothing is bound.
|
|
57
|
+
- **WeChat cannot be bound here.** `weixin`/`wechat` answer `400 channel.weixin_setup_required`
|
|
58
|
+
naming a QR flow this API does not expose. Any other platform name answers
|
|
59
|
+
`400 channel.invalid_request`.
|
|
60
|
+
- **`addChannel` is an upsert**: the same `platform` + `account` twice answers 201 again and
|
|
61
|
+
overwrites, rather than conflicting.
|
|
62
|
+
- **`removeChannel` is idempotent, `updateChannel` is not** — removing an absent binding is
|
|
63
|
+
`200 { ok: true }`; updating one is `404 channel.not_found`.
|
|
64
|
+
- `dm_policy: 'pairing'` is rejected with `400 channel.pairing_unsupported`.
|
|
65
|
+
|
|
66
|
+
## 0.3.1 — 2026-08-25
|
|
67
|
+
|
|
68
|
+
Channels, verified. 0.3.0 shipped the surface ahead of the deployment; this replaces its
|
|
69
|
+
guesses with what staging actually answered on 2026-08-25 (11 recorded fixtures, 10 new
|
|
70
|
+
response-contract tests). No signature changed — the corrections are in the docs and the
|
|
71
|
+
tests, and two of them would have cost you a debugging session:
|
|
72
|
+
|
|
73
|
+
### Fixed (documentation and contract, not behaviour)
|
|
74
|
+
|
|
75
|
+
- **`addChannel`'s 201 means STORED, not WORKING.** Credentials are not validated at bind
|
|
76
|
+
time: bogus ones still answered 201 with `health: 'unknown'` / `status: 'configured'`, and
|
|
77
|
+
only turned `health: 'unhealthy'` / `status: 'error'` moments later. Read the verdict from a
|
|
78
|
+
follow-up `listChannels`.
|
|
79
|
+
- **`waitForFeishuSetup` does not return a terminal status for a session that stopped
|
|
80
|
+
existing.** A cancelled session answers `404 channel.feishu_session_not_found`, which the
|
|
81
|
+
helper surfaces as a thrown `ZooclawError` — 0.3.0's docs implied every ending came back as
|
|
82
|
+
a value. Whether natural expiry takes this path or reports `status: 'expired'` is still
|
|
83
|
+
unobserved; handle both.
|
|
84
|
+
- Three distinct 404 codes documented (`channel.feishu_session_not_found` /
|
|
85
|
+
`channel.not_found` / `service_api.not_found`), plus the tell for a deployment that lacks
|
|
86
|
+
the routes entirely: the engine passthrough envelope `{error:{type:'not_found'}}` instead of
|
|
87
|
+
this family's `{code, detail}`.
|
|
88
|
+
- Observed defaults recorded: `expires_in: 600`, `poll_interval: 5`; `enabled: false` moves
|
|
89
|
+
`status` to `'disabled'` and resets `health`; `brand: 'lark'` really does switch the URI host
|
|
90
|
+
to `open.larksuite.com`.
|
|
91
|
+
|
|
92
|
+
## 0.3.0 — 2026-08-25
|
|
93
|
+
|
|
94
|
+
The surface, published the day the gateway release reached staging. Verified in 0.3.1.
|
|
95
|
+
|
|
96
|
+
### Added
|
|
97
|
+
|
|
98
|
+
- **Channels.** Bind chat platforms to an API-created agent: `listChannels`, `addChannel`
|
|
99
|
+
(explicit platform config), `updateChannel`, `removeChannel`, and the Feishu/Lark QR device
|
|
100
|
+
flow — `startFeishuSetup` / `pollFeishuSetup` / `cancelFeishuSetup` plus `waitForFeishuSetup`,
|
|
101
|
+
which drives the poll loop at the server's suggested interval, returns every terminal
|
|
102
|
+
outcome (`success` / `expired` / `denied` / `error`) instead of throwing on the human ones,
|
|
103
|
+
and bounds in-flight polls the way `waitUntilRunning` does. New types: `AgentChannel`,
|
|
104
|
+
`AddChannelInput`, `UpdateChannelInput`, `FeishuSetupInput`, `FeishuSetupSession`,
|
|
105
|
+
`FeishuPollResult`. On gateway deployments without the channels release every route here
|
|
106
|
+
answers 404.
|
|
107
|
+
- `deleteAgent` doc: it is a soft delete, and on channel-capable gateways a successful delete
|
|
108
|
+
best-effort disables the agent's bound channels (cleanup failures never gate the delete).
|
|
109
|
+
|
|
110
|
+
## 0.2.1 — 2026-08-25
|
|
111
|
+
|
|
112
|
+
### Changed
|
|
113
|
+
|
|
114
|
+
- **`DEFAULT_BASE_URL` now points at the production API** (`https://clawapi.ecap.gsmo.ai/service/v1`).
|
|
115
|
+
A client with no `baseUrl` and no `ZOOCLAW_BASE_URL` — the recommended setup — now reaches
|
|
116
|
+
production, which is where API keys are issued. Verified end to end on 2026-08-25: create →
|
|
117
|
+
start → session → a real model turn → replay, all against production with a production key.
|
|
118
|
+
If you were relying on the previous default while pointing at another deployment, set
|
|
119
|
+
`ZOOCLAW_BASE_URL` (or pass `baseUrl`) explicitly.
|
|
120
|
+
|
|
121
|
+
## 0.2.0 — 2026-08-19
|
|
122
|
+
|
|
123
|
+
Everything below was verified against staging on 2026-08-19: input echo with the
|
|
124
|
+
`processedAt` lifecycle, cursor pagination, `pse1:` stream resume, idempotent retry dedup,
|
|
125
|
+
full-object receipts, and `max_tokens` visibly capping a reply.
|
|
126
|
+
|
|
127
|
+
### Added
|
|
128
|
+
|
|
129
|
+
- **Unified event history.** The events read surface now carries your own inputs
|
|
130
|
+
(`user.message`, `user.interrupt`, `user.tool_confirmation`, `system.message`) alongside
|
|
131
|
+
engine events — the log alone renders the whole conversation: `listAllEvents` follows the
|
|
132
|
+
server's `next_cursor`/`has_more` pagination (and still walks `after` against servers
|
|
133
|
+
without it), `listEvents`/`streamEvents` accept `cursor`, `listEventsPage` returns one page
|
|
134
|
+
with its pagination fields for hand-paging, streamed events carry a `cursor` resume token,
|
|
135
|
+
and events expose `id` and `processedAt`. `PUBLIC_INPUT_EVENT_TYPES` is exported next to
|
|
136
|
+
`SESSION_EVENT_TYPES`. Passing `after` anywhere selects the deprecated engine-only lane.
|
|
137
|
+
- **Event-level idempotency on `postEvents`** — give each event an `idempotency_key` and
|
|
138
|
+
timeout retries stop double-delivering; accepted events come back as full event objects
|
|
139
|
+
(`PostEventReceipt`).
|
|
140
|
+
- **`resource.model.max_tokens`** — output-token cap per model request, passed through on
|
|
141
|
+
create and config PUT and enforced by the platform.
|
|
142
|
+
|
|
143
|
+
## 0.1.0 — 2026-08-17
|
|
144
|
+
|
|
145
|
+
**Breaking.** A trim, not a feature release: four pieces of `createAgent`'s surface either
|
|
146
|
+
raced the platform or answered 404, so they are gone rather than documented. The minor bump is
|
|
147
|
+
the surface change; nothing new was added.
|
|
148
|
+
|
|
149
|
+
### Removed
|
|
150
|
+
|
|
151
|
+
- **`resource.warm`** — pre-warming the agent-scope sandbox at create races the platform's
|
|
152
|
+
credential injection (verified 2026-08-16, `zooclaw-engine#791`): the sandbox can come up
|
|
153
|
+
before the built-in-skill credentials land, and the env snapshot never refreshes, leaving
|
|
154
|
+
those skills permanently broken in that sandbox. Removing the parameter makes the race
|
|
155
|
+
unreachable instead of documenting it. `createAgent` also strips `warm` at runtime, so a JS
|
|
156
|
+
caller bypassing the types cannot resurrect it.
|
|
157
|
+
- **`resource.onboarding`** — the interactive onboarding interview is never what an API caller
|
|
158
|
+
wants. `createAgent` now always sends `onboarding: false`, and strips a caller-supplied value
|
|
159
|
+
at runtime alongside `warm`.
|
|
160
|
+
- **`putCredential()` / `listCredentials()`** — both answer 404 through the gateway. The
|
|
161
|
+
platform seeds model credentials itself at create; there is no supported way to store your
|
|
162
|
+
own or your end users' third-party credentials, so the methods no longer imply one.
|
|
163
|
+
|
|
164
|
+
### Changed
|
|
165
|
+
|
|
166
|
+
- **`createAgent(input)` takes `ownership` as optional.** The gateway derives the tenant
|
|
167
|
+
anchors from your API key, so `{ resource }` is the whole input. The field is kept for
|
|
168
|
+
callers that reach the engine without the gateway.
|
|
169
|
+
- **`AgentResource` no longer carries an `[k: string]: unknown` index signature.** Unknown
|
|
170
|
+
fields are a type error now instead of passing silently — which is how `warm` and
|
|
171
|
+
`onboarding` would otherwise have kept compiling after removal.
|
|
172
|
+
|
|
173
|
+
## 0.0.6 — 2026-08-14
|
|
174
|
+
|
|
175
|
+
Three engine surfaces that landed this week — the system-prompt pin, the artifacts control
|
|
176
|
+
plane, and outcome-gated cron — plus one new wire field. Everything below was driven live
|
|
177
|
+
through the `/service/v1` gateway on 2026-08-14 and is pinned by re-recorded fixtures.
|
|
178
|
+
|
|
179
|
+
### Added
|
|
180
|
+
|
|
181
|
+
- **`getSystemPrompt(agentId)` / `previewSystemPrompt(agentId, input)` /
|
|
182
|
+
`upgradeSystemPrompt(agentId, input)`** — the pin as declared and the rendered template in
|
|
183
|
+
effect; deterministic assembly of the exact prompt for given runtime facts, without touching
|
|
184
|
+
a session (13 `slot_hashes`, `transcript` always `[]`); and the one write that moves the pin.
|
|
185
|
+
`resource.system_prompt` is typed on `AgentResource` (`{source:'platform',version}` |
|
|
186
|
+
`{source:'custom',base_version,template}`): a fresh create pins the active platform version
|
|
187
|
+
on its own, the pin never follows later activations on its own, and on PUT the section is
|
|
188
|
+
REPLACE-ON-WRITE like `tool_policy`. `upgradeSystemPrompt` is a real CAS —
|
|
189
|
+
`expected_config_version` must be current or the answer is `409 config_version_changed`
|
|
190
|
+
(both directions recorded as fixtures). The route uses the `{id}:verb` grammar, which the
|
|
191
|
+
gateway blocked until fix #3387 landed the same day this shipped — on older gateway
|
|
192
|
+
deployments this one method answers a gateway 404.
|
|
193
|
+
- **`listArtifacts` / `getArtifact` / `downloadArtifact` / `deleteArtifact`** — the control
|
|
194
|
+
plane over what the agent's own in-loop `artifact_publish` tool produced (publishing from
|
|
195
|
+
outside the loop still does not exist). These routes demand `owner_uid`+`org_id` selectors
|
|
196
|
+
and the gateway does not inject them, so the SDK derives both from the agent's own projection
|
|
197
|
+
and caches them per agent — the first artifact call costs one extra GET. `listArtifacts`
|
|
198
|
+
returns the page VERBATIM (`{artifacts, page, has_more}`): unlike `listEvents`, this list
|
|
199
|
+
says when it truncated, and flattening it away would have re-created that bug. The colon in
|
|
200
|
+
`:download` goes RAW on the wire — this family matches the literal colon, the opposite of
|
|
201
|
+
the environments family's `%3A`.
|
|
202
|
+
- **`OutcomeConfig`** on `SchedulePayload.outcome` and `AgentResource.outcome` — the
|
|
203
|
+
evaluate-revise-finalize gate for unattended cron fires (`command` or `rubric` evaluator,
|
|
204
|
+
`maxIterations` 1–5, `publish: after_satisfied | always | never`). Stored verbatim, no
|
|
205
|
+
defaults injected; a job-level value overrides the agent default and an explicit `null` opts
|
|
206
|
+
the job out. Cron fires only.
|
|
207
|
+
- **`EnvironmentVersionRecord.base_template_ref`** — new on the wire this week: the exact
|
|
208
|
+
base-image build a version layers on.
|
|
209
|
+
|
|
210
|
+
### Changed
|
|
211
|
+
|
|
212
|
+
- Response fixtures re-recorded against staging 2026-08-14. Count-pinned assertions
|
|
213
|
+
(global skill catalog, org environment list) now assert against the recording instead of a
|
|
214
|
+
number that drifts.
|
|
215
|
+
|
|
216
|
+
## 0.0.5 — 2026-08-10
|
|
217
|
+
|
|
218
|
+
### Added
|
|
219
|
+
|
|
220
|
+
- **`listAgents(opts?)`** — `GET /agents` with `label.*` filters and `page`, unwrapping `{agents}`.
|
|
221
|
+
`{ labels: { workspace_id: '…' } }` resolves a ZooClaw chat-URL workspace id to its agent — the
|
|
222
|
+
missing "get your agent_id with nothing but your key" step.
|
|
223
|
+
Scope is the engine's `owner_uid AND org_id`, so an agent a colleague created in your org is
|
|
224
|
+
fetchable by id but absent from your list. Page size is fixed at 100 by the engine.
|
|
225
|
+
Verified against staging on 2026-08-10, the day the gateway opened collection-level `GET /agents`
|
|
226
|
+
(it had answered `404 service_api.not_found` until then — FEEDBACK #16).
|
|
227
|
+
|
|
228
|
+
## 0.0.4 — 2026-08-07
|
|
229
|
+
|
|
230
|
+
The Developer Preview surface goes from "agents and sessions" to the whole management plane, and
|
|
231
|
+
**eight response types are corrected against real recorded staging responses**. If you read any of
|
|
232
|
+
the fields in the table below, read that section before upgrading — the corrections are the point of
|
|
233
|
+
this release, and one of them was an infinite loop.
|
|
234
|
+
|
|
235
|
+
Still a patch bump: the version stays in 0.0.x until the first formal release, so the number does
|
|
236
|
+
not signal stability. Treat the response-shape section as breaking regardless of what the number says.
|
|
237
|
+
|
|
238
|
+
### Response shapes corrected
|
|
239
|
+
|
|
240
|
+
Every one of these was found by driving the real API, not by reading a spec. Each row is what the
|
|
241
|
+
SDK's type promised, what the server actually sends, and what it cost the caller.
|
|
242
|
+
|
|
243
|
+
**1. `ScheduleRecord` had `schedule` and `sessionTarget`. The server sends neither.**
|
|
244
|
+
The read shape and the write shape are different documents. The cadence you sent as
|
|
245
|
+
`schedule: { kind: 'cron', expr }` reads back at `scheduleSpec.cronExpressions[0]`; the target you
|
|
246
|
+
sent as `sessionTarget: 'isolated'` reads back at `execution.kind`; and `scheduleId` is the
|
|
247
|
+
fully-qualified `cron/{computer_id}/{agent_id}/{schedule_id}` — the id you chose is `name`.
|
|
248
|
+
_Caller impact:_ `record.schedule.expr` was `undefined`, and passing `record.scheduleId` back to
|
|
249
|
+
`getSchedule` built a path that could not resolve.
|
|
250
|
+
|
|
251
|
+
**2. `updateSchedule` promised a `getSchedule` → `PUT` round trip and stripped only one field.**
|
|
252
|
+
The real body is refused for **six**: `execution`, `originMetadata`, `creatorPrincipalRef`,
|
|
253
|
+
`contextSnapshot` and `sessionTarget` are `400`, and `scheduleSpec` is worse (see 3). All six are now
|
|
254
|
+
`never` in `ScheduleUpdate` and stripped at runtime, so the obvious JavaScript round trip works.
|
|
255
|
+
_Caller impact:_ read-modify-write on a schedule was a `400 invalid_request`.
|
|
256
|
+
|
|
257
|
+
**3. `scheduleSpec` on a `PUT` is a SILENT NO-OP.**
|
|
258
|
+
`{ scheduleSpec, enabled: false }` answers **HTTP 200**, applies `enabled`, and leaves the cadence
|
|
259
|
+
exactly as it was. To change the cadence send `schedule: { kind: 'cron', expr, tz }` — the input
|
|
260
|
+
vocabulary. _Caller impact:_ a successful-looking update that quietly did not change the schedule.
|
|
261
|
+
|
|
262
|
+
**4. `ScheduleRun`'s fields were invented.**
|
|
263
|
+
`runs[]` is ONE array with TWO row shapes, discriminated by `source`: `run_projection` rows carry
|
|
264
|
+
`fired_at` / `status` / `consecutive_errors` (the outcome), `temporal` rows carry `scheduled_at` /
|
|
265
|
+
`taken_at` / `workflow_id` / `temporal_run_id` (the dispatch). Rows are grouped by source, not sorted
|
|
266
|
+
by time. **Neither shape carries `session_id`** — there is no walk from a fire to the session it
|
|
267
|
+
created; match `channel: 'cron'` and the `session_key` prefix instead.
|
|
268
|
+
_Caller impact:_ `run.status` was `undefined` on half the rows and `runs[0]` was not the latest fire.
|
|
269
|
+
|
|
270
|
+
**5. `EnvironmentRecord.state` and `.ownership` do not exist.**
|
|
271
|
+
Lifecycle is `status` (`active` / `archived`) and the tenant is flat: `scope` + `org_id`. The record
|
|
272
|
+
also gained `latest_ready_version`, and the two version numbers are not the same number:
|
|
273
|
+
`latest_version` is `1` the instant you create an Environment while version 1 is still `queued`;
|
|
274
|
+
`latest_ready_version` is `null` until a build lands. **Pin `latest_ready_version`.**
|
|
275
|
+
_Caller impact:_ `env.state` and `env.ownership.org_id` were `undefined`, and pinning
|
|
276
|
+
`latest_version` on an agent answered `409 environment_not_ready`.
|
|
277
|
+
|
|
278
|
+
**6. `EnvironmentVersionRecord.state` does not exist — the field is `status`.**
|
|
279
|
+
The JSDoc told callers to poll `getEnvironmentVersion` until `state === 'ready'`. `state` is
|
|
280
|
+
permanently `undefined`, so that loop compares `undefined` to `'ready'` for as long as the process
|
|
281
|
+
lives. _Caller impact:_ **an infinite loop** — the same class of bug as the `actual_state` trap this
|
|
282
|
+
SDK was written to prevent. Also note `e2b_build_id` is populated while a version is still
|
|
283
|
+
`building`, so it is not a readiness signal either. `status === 'ready'` is.
|
|
284
|
+
|
|
285
|
+
**7. `SessionRecord` had no `run_status`, and `status` is not the outcome.**
|
|
286
|
+
`listSessions` rows carry `run_status` (`succeeded`, `running`, …) and have **no `status` key at
|
|
287
|
+
all**; `getSession` returns `status: null` for the very same session, alongside its own `run_status`.
|
|
288
|
+
`status` is now typed `string | null`. _Caller impact:_ **this is the one correction that affects
|
|
289
|
+
published 0.0.3 consumers** — `session.status` was typed `string | undefined`, and reading it got
|
|
290
|
+
`null` from a read and `undefined` from a list row, never an outcome.
|
|
291
|
+
|
|
292
|
+
**8. `SkillRecord.ownership` required both fields as strings.**
|
|
293
|
+
An `org`-scope skill answers `owner_uid: null` (it belongs to the org, not a person), and a `global`
|
|
294
|
+
catalog row answers **both** fields as `null`. `SkillRecord.ownership` is now deliberately looser
|
|
295
|
+
than `Ownership`. In the same response, `latest_version` comes back as the **string `"1"`** from the
|
|
296
|
+
multipart create while other surfaces spell it as a number — compare loosely, or `Number()` it.
|
|
297
|
+
_Caller impact:_ `ownership.owner_uid` was typed non-null and was `null`, and `latest_version === 1`
|
|
298
|
+
was `false`.
|
|
299
|
+
|
|
300
|
+
### Platform behaviour you have to know
|
|
301
|
+
|
|
302
|
+
These are not SDK bugs and cannot be typed away. They are how the platform behaves.
|
|
303
|
+
|
|
304
|
+
- **`scheduleSpec` on a `PUT` is accepted, answers 200, and is ignored** (see 3). The response of a
|
|
305
|
+
no-op update is byte-identical to the response of a real one; only a follow-up `getSchedule` can
|
|
306
|
+
tell you which you got.
|
|
307
|
+
- **`triggerSchedule` on a DISABLED schedule answers `triggered: true`** while the run projection
|
|
308
|
+
records `status: "skipped"`. `triggered` means the fire was dispatched, never that the turn ran.
|
|
309
|
+
The outcome is only in `listScheduleRuns`, on the `run_projection` row.
|
|
310
|
+
|
|
311
|
+
### Added
|
|
312
|
+
|
|
313
|
+
- **Schedules** — `listSchedules`, `createSchedule`, `getSchedule`, `updateSchedule`,
|
|
314
|
+
`deleteSchedule`, `triggerSchedule`, `listScheduleRuns`. Schedules outlive their agent: delete them
|
|
315
|
+
yourself before deleting the agent.
|
|
316
|
+
- **Environments** — `listEnvironments`, `getEnvironment`, `createEnvironment`,
|
|
317
|
+
`createEnvironmentVersion`, `getEnvironmentVersion`, `archiveEnvironment`. `archiveEnvironment`
|
|
318
|
+
percent-encodes the colon in `{id}:archive`; a raw `:` is a 404, which is the whole reason the
|
|
319
|
+
method exists.
|
|
320
|
+
- **Skill registry** — `uploadSkill`, `uploadSkillVersion`, `listSkills`, `deleteSkill`. The zip's
|
|
321
|
+
single top-level directory name must equal the `name` in `SKILL.md`'s frontmatter; `scope` may only
|
|
322
|
+
be `org` or `personal`.
|
|
323
|
+
- **Sessions** — `listSessions`, `archiveSession`, `deleteSession`, and `listAllEvents`, which pages
|
|
324
|
+
`listEvents` to the end. `listEvents` truncates at 500 with nothing in the response to say so.
|
|
325
|
+
- **Readiness** — `waitUntilRunning`, which polls `status.desired_state` and never `actual_state`.
|
|
326
|
+
`actual_state` is chat-channel health; an API-only agent parks at `activating` forever.
|
|
327
|
+
- **Approvals** — `listApprovals`, `resolveApproval`. The route answers, but no real pending approval
|
|
328
|
+
has ever been produced, so `ApprovalRecord`'s field names are unverified. Read defensively.
|
|
329
|
+
- **Automation and operations** — `wake`, `exec`. A non-zero `exec` exit is still HTTP 200: the
|
|
330
|
+
promise resolves, check `exit_code`.
|
|
331
|
+
- **MCP** — `McpServerDeclaration` on `AgentResource.mcp`. Unauthenticated remote servers only;
|
|
332
|
+
`credential` is accepted and stored but unusable through the gateway.
|
|
333
|
+
|
|
334
|
+
### Fixed
|
|
335
|
+
|
|
336
|
+
- **`ZooclawError.type` was `undefined` for every agent-family error.** There are two error
|
|
337
|
+
envelopes: most families answer `{ error: { type, message } }`, the agents family answers
|
|
338
|
+
`{ code, detail }`. Only the first was parsed, so an agent `404` reached callers as
|
|
339
|
+
`type: undefined` with the message `HTTP 404`. Both are parsed now, and the codes are surfaced
|
|
340
|
+
verbatim — note the vocabularies differ (`not_found` vs `service_api.not_found`).
|
|
341
|
+
- `createEnvironment`'s doc comment told callers to poll for `state: 'ready'`. See correction 6.
|
|
342
|
+
|
|
343
|
+
### Tests
|
|
344
|
+
|
|
345
|
+
`pnpm test` is now the whole gate, and it runs the type checker before the suite — about half of what
|
|
346
|
+
this SDK guarantees is type-level, and reintroducing a wrong response type fails `tsc` rather than an
|
|
347
|
+
assertion.
|
|
348
|
+
|
|
349
|
+
- `src/__fixtures__/` — 55 REAL recorded staging responses, scrubbed of ids and credentials but
|
|
350
|
+
otherwise byte-faithful, down to `latest_version: "1"` being a string. Never hand-authored: a
|
|
351
|
+
hand-written fixture encodes the same guess the type does, so it can only agree with a wrong type.
|
|
352
|
+
- `src/responses.test.ts` — every fixture replayed through the method that returns it. Asserts
|
|
353
|
+
declared fields, absent fields, `null` versus missing, and — for each record type — that the SDK
|
|
354
|
+
declares nothing no recorded response has ever carried. All eight corrections above are red under
|
|
355
|
+
that check if reintroduced.
|
|
356
|
+
- Tests now import through `src/index.ts`, the published entry point, and `src/index.test.ts` pins
|
|
357
|
+
the export set, so a symbol missing from the entry point is a failing test rather than a broken
|
|
358
|
+
consumer.
|
|
359
|
+
- `src/sse.test.ts` — the frame parser split into its own file.
|
|
360
|
+
- CI on push and pull request: Node 24, `pnpm install --frozen-lockfile`, `pnpm test`, `pnpm build`.
|
|
361
|
+
The staging probes in `examples/` are excluded from the test glob and never run in CI — they need a
|
|
362
|
+
real API key and mutate a live tenant.
|
|
363
|
+
|
|
364
|
+
## 0.0.3 — 2026-08-05
|
|
365
|
+
|
|
366
|
+
Initial public release: agents, sessions, durable events and the SSE stream.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Serendipity One Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# @zoowork-ai/sdk
|
|
2
|
+
|
|
3
|
+
TypeScript SDK for the [ZooWork Managed Agents](https://github.com/SerendipityOneInc/zoowork-agents-docs) API. Developer Preview.
|
|
4
|
+
|
|
5
|
+
Zero runtime dependencies — it uses the platform `fetch`, which you can override for edge runtimes and tests. ESM only, Node 20+.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @zoowork-ai/sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
You need an API key (`zct_...`) issued for your organization — create one in the ZooWork App under **Settings → API Keys** (any personal org; enterprise orgs need the admin role), or ask your org admin for one. The secret is shown exactly once at creation. Keep it server-side: it authenticates as your whole organization, not as one end user.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { createZooworkClient } from '@zoowork-ai/sdk'
|
|
17
|
+
|
|
18
|
+
const zc = createZooworkClient({ apiKey: process.env.ZOOWORK_API_KEY })
|
|
19
|
+
|
|
20
|
+
// Or set ZOOWORK_API_KEY and pass nothing at all:
|
|
21
|
+
// const zc = createZooworkClient()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
The base URL has a working default, so you do not configure an endpoint. Override it with
|
|
25
|
+
`ZOOWORK_BASE_URL`, or with `baseUrl` on the call, to point at a different deployment.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
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.
|
|
30
|
+
const agent = await zc.createAgent({
|
|
31
|
+
resource: { name: 'research-agent', model: { primary: 'litellm/claude-sonnet-5' } },
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// 2. Start it. Without this, createSession() returns 409 agent_not_running.
|
|
35
|
+
await zc.startAgent(agent.agent_id)
|
|
36
|
+
|
|
37
|
+
// 3. Open a session with the first message already in it.
|
|
38
|
+
const session = await zc.createSession(agent.agent_id, {
|
|
39
|
+
initial_events: [{ type: 'user.message', content: 'What can you do?' }],
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
| Option | Environment variable | Default |
|
|
46
|
+
|---|---|---|
|
|
47
|
+
| `apiKey` | `ZOOWORK_API_KEY` | none - construction throws without one |
|
|
48
|
+
| `baseUrl` | `ZOOWORK_BASE_URL` | the public gateway (`DEFAULT_BASE_URL`) |
|
|
49
|
+
| `fetch` | - | `globalThis.fetch` |
|
|
50
|
+
|
|
51
|
+
An explicit option always beats the environment variable.
|
|
52
|
+
|
|
53
|
+
> **Finding the agent you built in the app.** The first path segment of a ZooWork chat URL
|
|
54
|
+
> (`/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*
|
|
57
|
+
> created in your org is fetchable by id but will not appear in your list.
|
|
58
|
+
|
|
59
|
+
> **Wait on `status.desired_state`, never on `status.actual_state`.**
|
|
60
|
+
> `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.
|
|
64
|
+
|
|
65
|
+
## Streaming a turn
|
|
66
|
+
|
|
67
|
+
`run.finished` ends a turn; assistant text arrives on `agent.assistant`.
|
|
68
|
+
|
|
69
|
+
```ts
|
|
70
|
+
import { assistantText, isRunFinished, runOutcome, toolCall } from '@zoowork-ai/sdk'
|
|
71
|
+
|
|
72
|
+
for await (const ev of zc.streamEvents(agent.agent_id, session.session_id)) {
|
|
73
|
+
process.stdout.write(assistantText(ev)) // '' for every non-assistant event
|
|
74
|
+
|
|
75
|
+
const call = toolCall(ev) // present only on agent.tool; pair start/end by toolCallId
|
|
76
|
+
if (call?.phase === 'start') console.log(`\n[tool] ${call.toolName}`)
|
|
77
|
+
|
|
78
|
+
if (isRunFinished(ev)) {
|
|
79
|
+
console.log(`\n-> ${runOutcome(ev)}`) // succeeded | failed | aborted
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Three things worth knowing before you write that loop:
|
|
86
|
+
|
|
87
|
+
- **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`.
|
|
90
|
+
|
|
91
|
+
## Bring your own skill
|
|
92
|
+
|
|
93
|
+
A skill is a zip. One upload creates the skill *and* its first version; `putAgentSkill` attaches it.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { readFile } from 'node:fs/promises'
|
|
97
|
+
|
|
98
|
+
const skill = await zc.uploadSkill(await readFile('market-research.zip'), { scope: 'org' })
|
|
99
|
+
await zc.putAgentSkill(agent.agent_id, skill.skill_id)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The zip's single top-level directory must be named exactly like the `name` in its `SKILL.md`
|
|
103
|
+
frontmatter — `market-research/SKILL.md` declaring `name: market-research`. A mismatch is a 400,
|
|
104
|
+
and it is the first one nearly everyone gets. `scope` is `org` or `personal`; the preinstalled
|
|
105
|
+
`global` skills are listable but not installable with an API key, so this is the only way to
|
|
106
|
+
control what a skill says. `uploadSkillVersion` publishes an update, and agents that installed it
|
|
107
|
+
unpinned follow along without another `putAgentSkill`.
|
|
108
|
+
|
|
109
|
+
## Schedules, wake and exec
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
await zc.createSchedule(agent.agent_id, {
|
|
113
|
+
schedule_id: 'daily-report',
|
|
114
|
+
schedule: { kind: 'cron', expr: '0 9 * * *', tz: 'Asia/Singapore' },
|
|
115
|
+
payload: { kind: 'agentTurn', message: 'Generate the daily report.' },
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
await zc.wake(agent.agent_id, { text: 'Review the pending deployment.' }) // at the next heartbeat
|
|
119
|
+
|
|
120
|
+
const { exit_code, stdout } = await zc.exec(agent.agent_id, ['bash', '-lc', 'pwd'])
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- **Schedules outlive their agent.** `stopAgent` and `deleteAgent` leave them running; list and
|
|
124
|
+
delete them yourself. Also available: `getSchedule`, `updateSchedule`, `triggerSchedule`,
|
|
125
|
+
`listScheduleRuns`.
|
|
126
|
+
- **`updateSchedule` must omit `sessionTarget`.** It is immutable, and echoing it back from a
|
|
127
|
+
`getSchedule` result — the obvious thing to do — is a 400. The types refuse it for you.
|
|
128
|
+
- **`exec` resolves on a failed command.** A non-zero exit is still HTTP 200: check `exit_code`,
|
|
129
|
+
don't wait for a rejection. It runs in `/workspace` and needs an agent-scope sandbox.
|
|
130
|
+
- **A cron job can carry an outcome gate.** `payload.outcome` says what "done" looks like — a
|
|
131
|
+
sandbox `command` whose exit 0 means satisfied, or an LLM `rubric` graded in a fresh context.
|
|
132
|
+
The run evaluates and revises itself up to `maxIterations` (1–5), and under the default
|
|
133
|
+
`publish: 'after_satisfied'` a result that failed evaluation is not announced. An agent-level
|
|
134
|
+
default lives at `resource.outcome`; a job's own `outcome` overrides it, and an explicit
|
|
135
|
+
`null` opts the job out. Cron fires only — heartbeats and interactive sessions never evaluate.
|
|
136
|
+
|
|
137
|
+
## Sessions, approvals, environments
|
|
138
|
+
|
|
139
|
+
`listSessions`, `archiveSession` and `deleteSession` round out the session surface. There is no
|
|
140
|
+
`patchSession`: the gateway does not proxy `PATCH` at all (405), so session `metadata` is fixed at
|
|
141
|
+
creation time.
|
|
142
|
+
|
|
143
|
+
`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.
|
|
146
|
+
|
|
147
|
+
`listEnvironments`, `getEnvironment`, `createEnvironment`, `createEnvironmentVersion`,
|
|
148
|
+
`getEnvironmentVersion` and `archiveEnvironment` manage prebuilt sandbox images (apt/npm/pip
|
|
149
|
+
packages, files, a build script, and an outbound allowlist). Two facts worth having before you
|
|
150
|
+
start: an agent's Environment **freezes on its first sandbox creation** — after that every change
|
|
151
|
+
is `409 environment_locked`, and stopping the agent does not clear it — and sandbox networking
|
|
152
|
+
defaults to unrestricted unless the Environment declares `networking: { type: 'limited' }`.
|
|
153
|
+
|
|
154
|
+
## Artifacts and the system prompt
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
const { artifacts, has_more } = await zc.listArtifacts(agent.agent_id)
|
|
158
|
+
const { url } = await zc.downloadArtifact(agent.agent_id, artifacts[0].artifact_id)
|
|
159
|
+
|
|
160
|
+
const { declaration, effective } = await zc.getSystemPrompt(agent.agent_id)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Artifacts are published by the agent's own `artifact_publish` tool during a turn — there is no
|
|
164
|
+
API for publishing from outside the loop. This surface lists what the agent published,
|
|
165
|
+
re-resolves an access URL (`downloadArtifact` mints a fresh one; the URL is a revocable bearer
|
|
166
|
+
capability, so treat it like a secret), and deletes. These routes demand `owner_uid`/`org_id`
|
|
167
|
+
selectors; the SDK derives both from the agent's own projection and caches them, at the cost of
|
|
168
|
+
one extra GET on first use.
|
|
169
|
+
|
|
170
|
+
`getSystemPrompt` answers the pinned template version and the rendered result;
|
|
171
|
+
`previewSystemPrompt` assembles the exact prompt for a given set of runtime facts without
|
|
172
|
+
touching any session. The pin is set at create time and never follows a later platform
|
|
173
|
+
activation on its own — moving it is one explicit call, `upgradeSystemPrompt`, which takes
|
|
174
|
+
the agent's current `config_version` as a CAS (`409 config_version_changed` on a stale one)
|
|
175
|
+
and answers the new pin plus the version bump it cost.
|
|
176
|
+
|
|
177
|
+
## Two helpers
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
const agent = await zc.waitUntilRunning(agentId) // polls desired_state, not actual_state
|
|
181
|
+
const events = await zc.listAllEvents(agentId, sessionId) // pages past the silent 500 cap
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Each wraps a trap that is invisible from the outside: readiness lives in `status.desired_state`,
|
|
185
|
+
and `listEvents` truncates at 500 events with nothing in the response to say it did.
|
|
186
|
+
|
|
187
|
+
## Documentation
|
|
188
|
+
|
|
189
|
+
Full guides and the capability matrix: **[zoowork-agents-docs](https://github.com/SerendipityOneInc/zoowork-agents-docs)**.
|
|
190
|
+
|
|
191
|
+
Runnable examples in [`examples/`](examples):
|
|
192
|
+
|
|
193
|
+
- [`live-smoke.ts`](examples/live-smoke.ts) — drive one agent through one turn and verify the REST and SSE reads agree.
|
|
194
|
+
- [`capability-probe.ts`](examples/capability-probe.ts) — create a throwaway agent, walk the whole lifecycle, and print a verdict per capability.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|