@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.
@@ -0,0 +1,1424 @@
1
+ /**
2
+ * ZooWork Managed Agents SDK — core client. Developer Preview.
3
+ *
4
+ * Authenticate with your organization API key (`zct_…`). It carries full tenant
5
+ * authority, so it is SERVER-SIDE ONLY: never ship it in a browser or mobile bundle.
6
+ *
7
+ * IDs are opaque strings and unknown response fields must be ignored — both are
8
+ * forward-compatibility rules, not suggestions. Errors surface an `error.type`; match on
9
+ * that, never on the message text.
10
+ */
11
+ import { type SessionEvent } from './events.js';
12
+ /**
13
+ * The production API base URL — the default. You should not need to set this:
14
+ * `ZOOWORK_BASE_URL` overrides it, and so does the `baseUrl` option, only when you
15
+ * need to point at a different deployment.
16
+ */
17
+ export declare const DEFAULT_BASE_URL = "https://clawapi.ecap.gsmo.ai/service/v1";
18
+ export type ZooworkAuth = {
19
+ serviceToken: string;
20
+ } | {
21
+ apiKey: string;
22
+ };
23
+ export interface ZooworkConfig {
24
+ /**
25
+ * Your API key (`zct_...`). Defaults to `ZOOWORK_API_KEY`.
26
+ *
27
+ * Server-side only: it authenticates as your whole organization, not as one end user.
28
+ */
29
+ apiKey?: string;
30
+ /**
31
+ * API base including the version prefix. Defaults to `ZOOWORK_BASE_URL`, then to
32
+ * {@link DEFAULT_BASE_URL} (production). Set it only to point at a deployment other
33
+ * than the production one.
34
+ */
35
+ baseUrl?: string;
36
+ /**
37
+ * Advanced. `{ apiKey }` is equivalent to the top-level `apiKey` field. `{ serviceToken }`
38
+ * selects a privileged deployment-internal credential and is not available to API-key
39
+ * callers.
40
+ */
41
+ auth?: ZooworkAuth;
42
+ /** Injected fetch for edge runtimes/tests; defaults to globalThis.fetch. */
43
+ fetch?: (input: string, init?: RequestInit) => Promise<Response>;
44
+ }
45
+ export declare class ZooworkError extends Error {
46
+ status: number;
47
+ /**
48
+ * The machine-readable error code. Match on this, never on the message.
49
+ *
50
+ * TWO VOCABULARIES, because there are two error envelopes — staging-verified 2026-08-07. The
51
+ * sessions/schedules/environments family answers `{ error: { type, message } }` with a bare code
52
+ * (`agent_not_running`, `session_archived`, `environment_not_ready`); the agents family answers
53
+ * `{ code, detail }` with a DOTTED one (`service_api.not_found`). Both land here, so a caller
54
+ * always gets something — but do not assume one spelling covers both, and prefer `status` when
55
+ * you only need the class of failure.
56
+ */
57
+ type?: string;
58
+ constructor(status: number, message: string, type?: string);
59
+ }
60
+ export interface Ownership {
61
+ owner_uid: string;
62
+ org_id: string;
63
+ }
64
+ export interface ModelInfo {
65
+ model: string;
66
+ display_name?: string;
67
+ family?: string;
68
+ api?: string;
69
+ [k: string]: unknown;
70
+ }
71
+ /**
72
+ * One remote MCP server, declared as `resource.mcp[]` on create or update.
73
+ *
74
+ * Staging-verified 2026-08-07, end to end, for UNAUTHENTICATED public servers: the tools appear
75
+ * in the model's manifest as `mcp__<server>__<tool>` and really execute. Two traps:
76
+ *
77
+ * - `name` must not contain an underscore. The tool-name grammar is `mcp__<server>__<tool>`,
78
+ * so an underscore in the server name makes the split ambiguous and the server is rejected.
79
+ * - `credential` is a dead end for API-key callers. The slug is accepted and stored on the
80
+ * agent, but the endpoint that would hold the secret it points at
81
+ * (`PUT /agents/{id}/credentials/{app}`) is 404 through the gateway BY DESIGN. There is no
82
+ * other way to store it, so an authenticated MCP server cannot be made to work today.
83
+ * Declare public servers only.
84
+ *
85
+ * Phase 1 is remote HTTP only: no stdio, no OAuth. A server that fails its catalog probe does
86
+ * not fail the run — it pins an empty catalog and emits `agent.error` with
87
+ * `kind: 'mcp_connection_failed'`.
88
+ */
89
+ export interface McpServerDeclaration {
90
+ /** Server slug. Appears in every tool name as `mcp__<name>__<tool>`. No underscores. */
91
+ name: string;
92
+ /** Absolute URL of the MCP endpoint. Loopback, private ranges, cloud metadata and redirects are refused. */
93
+ url: string;
94
+ /** Defaults to `streamable-http`. */
95
+ transport?: 'streamable-http' | 'sse';
96
+ /** Credential slug for a static bearer. Declarable, but unusable through the gateway — see above. */
97
+ credential?: string;
98
+ /** Expose only these tool names from this server. Omit for all of them. */
99
+ toolFilter?: string[];
100
+ [k: string]: unknown;
101
+ }
102
+ /**
103
+ * The system-prompt pin (`resource.system_prompt`).
104
+ *
105
+ * Pin an immutable PLATFORM template version, or override the whole template with a CUSTOM one
106
+ * (`base_version` records the platform version it derives from; the template must fill all 13
107
+ * functional slots exactly once and stay under 64 KiB UTF-8). Create pins the platform version
108
+ * active at that moment on its own — a plain create answered `{source:'platform',version:1}`,
109
+ * staging-verified 2026-08-14 — and the pin NEVER follows a later activation on its own:
110
+ * ordinary PUTs, skill changes and rerenders keep it. Moving it is an explicit call,
111
+ * {@link ZooworkClient.upgradeSystemPrompt}.
112
+ *
113
+ * On PUT this section is REPLACE-ON-WRITE, like `tool_policy` — not merged.
114
+ */
115
+ export type SystemPromptDeclaration = {
116
+ source: 'platform';
117
+ version: number;
118
+ } | {
119
+ source: 'custom';
120
+ base_version: number;
121
+ template: string;
122
+ };
123
+ /**
124
+ * How an unattended cron fire is judged (`command`: a sandbox command whose exit 0 means
125
+ * satisfied — ≤8 KiB, `timeoutSec` 1–600 (default 120); `rubric`: an LLM grader in a fresh
126
+ * context — `rubric.type` must be `'text'`, ≤32 KiB). `subagent` is a reserved slot the API
127
+ * still rejects. Validation is write-strict at EVERY level: an unknown key anywhere in the
128
+ * outcome object is a 400 naming the field, so a typo cannot silently drop a limit.
129
+ */
130
+ export type OutcomeEvaluator = {
131
+ type: 'command';
132
+ command: string;
133
+ timeoutSec?: number;
134
+ cwd?: string;
135
+ skipIfUnchanged?: boolean;
136
+ } | {
137
+ type: 'rubric';
138
+ rubric: {
139
+ type: 'text';
140
+ text: string;
141
+ };
142
+ model?: string;
143
+ };
144
+ /**
145
+ * What "done" looks like for an unattended cron fire — evaluated INSIDE the run.
146
+ *
147
+ * Attach it to a cron schedule (`payload.outcome`) or as an agent-level default (top-level
148
+ * `outcome` on {@link AgentResource}); the job-level value overrides the default, and an
149
+ * explicit `null` on the job disables the default for that job. It governs cron fires ONLY —
150
+ * heartbeats and interactive sessions never evaluate. The runtime loops
151
+ * evaluate → revise → finalize inside the same run, up to `maxIterations` (1–5, default 3),
152
+ * and under the default `publish: 'after_satisfied'` nothing that failed evaluation is
153
+ * announced or published. `description` is what the evaluator judges against, ≤4096 chars.
154
+ *
155
+ * Staging-verified 2026-08-14: a schedule carrying this was accepted (201) and read back
156
+ * verbatim under `payload.outcome`, and an agent-level PUT landed in `declared.outcome`.
157
+ */
158
+ export interface OutcomeConfig {
159
+ description: string;
160
+ evaluator: OutcomeEvaluator;
161
+ /** 1–5; the engine defaults an omitted value to 3 at run time (nothing is stored for you). */
162
+ maxIterations?: number;
163
+ /** Defaults to `after_satisfied` at run time: an unsatisfied result stays unpublished. */
164
+ publish?: 'after_satisfied' | 'always' | 'never';
165
+ }
166
+ export interface AgentResource {
167
+ name: string;
168
+ /** `max_tokens` caps output tokens per model request. Omit to use the platform default. */
169
+ model?: {
170
+ primary: string;
171
+ input?: string[];
172
+ max_tokens?: number;
173
+ };
174
+ persona?: {
175
+ docs: {
176
+ name: string;
177
+ content: string;
178
+ seed_policy?: string;
179
+ }[];
180
+ };
181
+ skills?: {
182
+ skill_id: string;
183
+ version?: number | 'latest';
184
+ }[];
185
+ labels?: Record<string, string>;
186
+ tool_policy?: Record<string, unknown>;
187
+ /** Remote MCP servers. Only unauthenticated ones work today — see {@link McpServerDeclaration}. */
188
+ mcp?: McpServerDeclaration[];
189
+ /**
190
+ * System-prompt pin. Omitted on create means "the platform version active right now", pinned
191
+ * from then on. REPLACE-ON-WRITE on PUT, like `tool_policy` — see {@link SystemPromptDeclaration}.
192
+ */
193
+ system_prompt?: SystemPromptDeclaration;
194
+ /**
195
+ * Agent-level default outcome for unattended cron fires. A schedule's `payload.outcome`
196
+ * overrides it per job; an explicit `null` there opts that job out. See {@link OutcomeConfig}.
197
+ */
198
+ outcome?: OutcomeConfig | null;
199
+ sandbox?: {
200
+ scope: 'agent' | 'session';
201
+ };
202
+ environment_id?: string;
203
+ environment_version?: number;
204
+ }
205
+ /**
206
+ * Agent lifecycle state. Two fields, two very different meanings — staging-verified
207
+ * 2026-08-06:
208
+ *
209
+ * - `desired_state` is the one that gates the API. `running` is the precondition for
210
+ * createSession/postEvents; anything else is `409 agent_not_running`.
211
+ * - `actual_state` is CHANNEL health (Mattermost/Feishu route connectivity), not API
212
+ * readiness. An API-only agent has no channels to connect, so it sits at
213
+ * `activating` forever and `active` is unreachable. Never gate on it, and never
214
+ * poll for `running` — that is not one of its values.
215
+ */
216
+ export interface AgentStatus {
217
+ desired_state?: 'running' | 'stopped' | 'deleted' | string;
218
+ actual_state?: 'activating' | 'active' | 'degraded' | 'error' | 'stopped' | 'deleting' | string;
219
+ /** Authoritative config version on the READ path (GET/PUT). */
220
+ config_version?: number;
221
+ render_state?: string;
222
+ status_message?: string | null;
223
+ channels?: {
224
+ expected?: number;
225
+ connected?: number;
226
+ degraded_since?: string | null;
227
+ };
228
+ [k: string]: unknown;
229
+ }
230
+ export interface AgentSkill {
231
+ skill_id?: string;
232
+ name?: string;
233
+ version?: number | string;
234
+ scope?: 'global' | 'org' | 'personal' | 'pack' | string;
235
+ eligible?: boolean;
236
+ files?: {
237
+ path: string;
238
+ size?: number;
239
+ sha256?: string;
240
+ }[];
241
+ [k: string]: unknown;
242
+ }
243
+ /**
244
+ * One platform account bound to an agent, as the channel service reports it.
245
+ * `dm_policy` / `group_policy` are the reachability policies (`'open'` is the
246
+ * server default); `health`/`status`/`status_code` are the connection's state
247
+ * as the platform adapter sees it.
248
+ */
249
+ export interface AgentChannel {
250
+ platform: string;
251
+ account: string;
252
+ display_name?: string | null;
253
+ dm_policy?: string;
254
+ group_policy?: string;
255
+ enabled?: boolean;
256
+ health?: string;
257
+ status?: string;
258
+ status_code?: string | null;
259
+ [k: string]: unknown;
260
+ }
261
+ /**
262
+ * The chat platforms you can bind, staging-verified 2026-08-25.
263
+ *
264
+ * Only `'feishu'` has a server-driven QR flow here, and the two reasons the others lack one
265
+ * are different. Slack structurally cannot have one — a Slack app is created by a person, and
266
+ * its tokens only ever exist in that person's browser — so `addChannel` with `botToken` +
267
+ * `appToken` is its permanent path. WeCom's flow exists in the product but is not exposed on
268
+ * this API yet, so today it also binds through `addChannel`.
269
+ *
270
+ * WeChat (`'weixin'`/`'wechat'`) is absent because it cannot be bound here at all: it answers
271
+ * `400 channel.weixin_setup_required`, naming a QR flow this API does not expose. Any other
272
+ * name answers `400 channel.invalid_request`.
273
+ */
274
+ export type ChannelPlatform = 'feishu' | 'slack' | 'wecom';
275
+ export interface AddChannelInput {
276
+ /** See {@link ChannelPlatform}. Typed loosely so a newly supported platform needs no SDK release. */
277
+ platform: ChannelPlatform | (string & {});
278
+ /** Server default: `'default'`. */
279
+ account?: string;
280
+ display_name?: string;
281
+ /** Server default: `'open'`. `'pairing'` is rejected with `400 channel.pairing_unsupported`. */
282
+ dm_policy?: string;
283
+ /** Server default: `'open'`. */
284
+ group_policy?: string;
285
+ /** Write-once at create; updates cannot edit it. */
286
+ allow_from?: string[];
287
+ /**
288
+ * Platform credentials for the direct (non-QR) path. Keys are platform-specific and
289
+ * **camelCase**; anything else is stored and ignored:
290
+ *
291
+ * - `slack` — `{ botToken: 'xoxb-…', appToken: 'xapp-…' }`, both required (socket mode
292
+ * needs the app-level token as well as the bot token)
293
+ * - `wecom` — `{ botId, secret }`, both required
294
+ * - `feishu` — `{ appId, appSecret, domain }`, only when skipping the QR flow
295
+ */
296
+ config?: Record<string, unknown>;
297
+ }
298
+ export interface UpdateChannelInput {
299
+ /** Which platform account to touch. Server default: `'default'`. */
300
+ account?: string;
301
+ dm_policy?: string;
302
+ group_policy?: string;
303
+ enabled?: boolean;
304
+ }
305
+ export interface FeishuSetupInput {
306
+ /** `'feishu'` (default) or `'lark'` — the international brand of the same platform. */
307
+ brand?: 'feishu' | 'lark';
308
+ /** Server default: `'default'`. */
309
+ account?: string;
310
+ /** Server default: `'open'`. */
311
+ dm_policy?: string;
312
+ /** Server default: `'open'`. */
313
+ group_policy?: string;
314
+ }
315
+ /**
316
+ * A running Feishu QR registration. Render `verification_uri_complete` to the person
317
+ * doing the binding (typically as a QR code), then poll with `pollFeishuSetup` /
318
+ * `waitForFeishuSetup` until it leaves `pending`. The session expires after
319
+ * `expires_in` seconds.
320
+ */
321
+ export interface FeishuSetupSession {
322
+ session_id: string;
323
+ verification_uri_complete: string;
324
+ expires_in: number;
325
+ /** Suggested seconds between polls; the server may omit it. */
326
+ poll_interval?: number | null;
327
+ [k: string]: unknown;
328
+ }
329
+ /**
330
+ * One poll of a Feishu setup session. The gateway's own vocabulary for `status` is
331
+ * `pending | success | expired | denied | error`; treat anything unknown as
332
+ * still-in-flight rather than throwing.
333
+ */
334
+ export interface FeishuPollResult {
335
+ status: string;
336
+ channel_configured?: boolean;
337
+ message?: string | null;
338
+ poll_interval?: number | null;
339
+ [k: string]: unknown;
340
+ }
341
+ export interface AgentRecord {
342
+ agent_id: string;
343
+ computer_id?: string;
344
+ /**
345
+ * CREATE ONLY. `POST /agents` answers with a flat create receipt carrying this
346
+ * field; `GET`/`PUT` answer with the projection instead, where the version lives at
347
+ * `status.config_version`. Read it as `agent.status?.config_version ?? agent.config_version`.
348
+ */
349
+ config_version?: number;
350
+ /** The agent's configuration (name/model/persona/labels/mcp/...). Absent from the create receipt. */
351
+ declared?: Record<string, unknown>;
352
+ resolved_skills?: {
353
+ skill_id: string;
354
+ name?: string;
355
+ version?: number | string;
356
+ eligible?: boolean;
357
+ }[];
358
+ /**
359
+ * The Environment version this agent is actually pinned to. Audit-grade, but the
360
+ * `environment_id` in here is NOT queryable: `getEnvironment()` on the platform default
361
+ * answers 404, because the gateway forces an org selector and the default belongs to no org.
362
+ * That is a selector mismatch, not a permission problem.
363
+ */
364
+ resolved_environment?: {
365
+ environment_id?: string;
366
+ version?: number;
367
+ provider?: string;
368
+ template_ref?: string;
369
+ build_id?: string;
370
+ /** Defaults to `{ type: 'unrestricted' }` when the Environment declares no networking. */
371
+ networking?: {
372
+ type?: 'unrestricted' | 'limited' | string;
373
+ allowed_hosts?: string[];
374
+ };
375
+ [k: string]: unknown;
376
+ };
377
+ /**
378
+ * `true` once the first sandbox has been created — from then on the Environment pin is
379
+ * FROZEN and every attempt to change it is `409 environment_locked`. Stopping the agent does
380
+ * not clear it, and there is no escape hatch through the gateway
381
+ * (`POST /agents/{id}:replace-environment` is 404 there). Choose the Environment at create
382
+ * time or live with it. Staging-verified 2026-08-07.
383
+ */
384
+ environment_locked?: boolean;
385
+ /** ISO 8601 instant the lock was written; `null` while still unlocked. */
386
+ environment_locked_at?: string | null;
387
+ status?: AgentStatus;
388
+ ownership?: Ownership;
389
+ [k: string]: unknown;
390
+ }
391
+ /**
392
+ * A skill registry row, as returned by `uploadSkill` / `listSkills`.
393
+ *
394
+ * `latest_version` came back as the STRING `"1"` from the multipart create on staging
395
+ * (2026-08-07) while other surfaces spell it as a number — compare loosely, or `Number()` it.
396
+ */
397
+ export interface SkillRecord {
398
+ skill_id: string;
399
+ scope?: 'org' | 'personal' | 'global' | 'pack' | string;
400
+ name?: string;
401
+ description?: string;
402
+ latest_version?: number | string | null;
403
+ /** `active`, … */
404
+ status?: string;
405
+ pack_id?: string | null;
406
+ created_by?: string;
407
+ created_at?: string;
408
+ updated_at?: string;
409
+ /**
410
+ * The tenant the gateway rewrote your upload to. `owner_uid` comes back `null` on an
411
+ * `org`-scope skill — it belongs to the org, not to a person — so this is deliberately looser
412
+ * than {@link Ownership}, which requires both.
413
+ */
414
+ ownership?: {
415
+ owner_uid?: string | null;
416
+ org_id?: string | null;
417
+ [k: string]: unknown;
418
+ };
419
+ [k: string]: unknown;
420
+ }
421
+ /**
422
+ * One `session_transcripts` row, as projected by `getSession(…, { history: true })`.
423
+ *
424
+ * This is the AT-REST transcript, not the event log: conversation text lives under
425
+ * `entry.message` (`{ role, content }`) for `entry_type: 'message'`. Use it to recover an
426
+ * answer whose events you missed; use `listEvents` when you want the event stream.
427
+ */
428
+ export interface SessionHistoryEntry {
429
+ seq: number;
430
+ entry_type: string;
431
+ entry: Record<string, unknown>;
432
+ created_at?: string;
433
+ }
434
+ export interface SessionRecord {
435
+ session_id: string;
436
+ /** `api:{session_id}` for a session you created; `agent:{agent_id}:cron:{schedule_id}:…` for a scheduled fire. */
437
+ session_key?: string;
438
+ /** `api` for sessions you create, `cron` for ones a schedule fired. */
439
+ channel?: string;
440
+ /**
441
+ * `listSessions` is the surface that carries the run outcome (`succeeded`, …) — and it spells
442
+ * it `run_status`, not `status`. Staging-verified 2026-08-07: `getSession` returns a `status`
443
+ * of `null` for the very same session, so reading `status` off a list row gets you nothing.
444
+ */
445
+ run_status?: string;
446
+ /** Observed `null` on `getSession`. Prefer {@link SessionRecord.run_status} from `listSessions`. */
447
+ status?: string | null;
448
+ metadata?: Record<string, unknown>;
449
+ archived?: boolean;
450
+ updated_at?: string;
451
+ /** Present only when the read asked for `history: true`; the most recent `limit` rows, in order. */
452
+ history?: SessionHistoryEntry[];
453
+ [k: string]: unknown;
454
+ }
455
+ /** Write-side events: user.message / user.interrupt / user.tool_confirmation / system.message */
456
+ export interface OutboundEvent {
457
+ type: string;
458
+ content?: unknown;
459
+ [k: string]: unknown;
460
+ }
461
+ /** One `postEvents` receipt. An accepted event carries the full event object's fields too. */
462
+ export interface PostEventReceipt {
463
+ id?: string | null;
464
+ type?: string;
465
+ accepted?: boolean;
466
+ [k: string]: unknown;
467
+ }
468
+ /** One page of the unified event history, with its pagination fields when the server sends them. */
469
+ export interface SessionEventPage {
470
+ events: SessionEvent[];
471
+ hasMore?: boolean;
472
+ nextCursor?: string | null;
473
+ }
474
+ /**
475
+ * When a schedule fires. Three kinds, and only `cron` has its field names pinned by the engine
476
+ * reference (`{"kind":"cron","expr":"0 9 * * *","tz":"Asia/Singapore"}`); `every` and `at` are
477
+ * documented by prose only — "the management plane supports cron/every/at", and an `at`
478
+ * schedule "uses the supplied ISO instant". Their extra fields are therefore left open rather
479
+ * than guessed at, so anything the engine accepts still type-checks.
480
+ *
481
+ * Cron is a five-field expression. Macros and a `CRON_TZ=` prefix are rejected; overlap is
482
+ * fixed to SKIP server-side, so a fire that lands on a still-running one is dropped, not queued.
483
+ */
484
+ export type ScheduleSpec = {
485
+ kind: 'cron';
486
+ expr: string;
487
+ tz?: string;
488
+ [k: string]: unknown;
489
+ } | {
490
+ kind: 'every';
491
+ every: string | number;
492
+ tz?: string;
493
+ [k: string]: unknown;
494
+ } | {
495
+ kind: 'at';
496
+ at: string;
497
+ tz?: string;
498
+ [k: string]: unknown;
499
+ };
500
+ /** What a schedule does when it fires. `agentTurn` is the one the management plane accepts. */
501
+ export interface SchedulePayload {
502
+ kind: 'agentTurn' | string;
503
+ message?: string;
504
+ /**
505
+ * Evaluate-revise-finalize gate for THIS job — overrides the agent-level default, and an
506
+ * explicit `null` opts this job out of that default. Cron fires only. Staging-verified
507
+ * 2026-08-14 (201, read back verbatim). See {@link OutcomeConfig}.
508
+ */
509
+ outcome?: OutcomeConfig | null;
510
+ [k: string]: unknown;
511
+ }
512
+ export interface ScheduleInput {
513
+ /** Caller-chosen id, `^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`. Re-creating it with a DIFFERENT definition is 409. */
514
+ schedule_id: string;
515
+ schedule: ScheduleSpec;
516
+ payload: SchedulePayload;
517
+ jobKind?: string;
518
+ /**
519
+ * Where the turn runs. Omit for `isolated` (a fresh session per fire); `session:<id>` targets
520
+ * an existing session of this agent. `current` is agent-only and rejected here. IMMUTABLE
521
+ * after create — see {@link ScheduleUpdate}.
522
+ */
523
+ sessionTarget?: 'isolated' | string;
524
+ /** `{ mode: 'none' }` or a typed `announce`. Webhook delivery is rejected. */
525
+ delivery?: {
526
+ mode: 'none' | 'announce' | string;
527
+ [k: string]: unknown;
528
+ };
529
+ enabled?: boolean;
530
+ deleteAfterRun?: boolean;
531
+ [k: string]: unknown;
532
+ }
533
+ /**
534
+ * The PUT body. Same vocabulary as {@link ScheduleInput}, minus the fields the server owns.
535
+ *
536
+ * The six `never` fields below are the six ways a caller loses time here, and every one of
537
+ * them is something a `getSchedule()` result contains — which is why they are compile errors
538
+ * rather than runtime surprises. `updateSchedule` also strips them at runtime, so a JavaScript
539
+ * caller round-tripping a read still succeeds. Staging-verified 2026-08-07.
540
+ */
541
+ export interface ScheduleUpdate {
542
+ /** The cadence, in the INPUT vocabulary. `{ kind: 'cron', expr, tz }` — verified to apply. */
543
+ schedule?: ScheduleSpec;
544
+ payload?: SchedulePayload;
545
+ jobKind?: string;
546
+ /** Immutable. Sending it back — even unchanged — is `400 sessionTarget is immutable`. */
547
+ sessionTarget?: never;
548
+ /**
549
+ * The READ shape of the cadence. Accepted with a 200 and then SILENTLY IGNORED — a PUT
550
+ * carrying `scheduleSpec: { cronExpressions: ['45 9 * * *'] }` leaves the old expression in
551
+ * place while any sibling field in the same body applies. Send `schedule` instead.
552
+ */
553
+ scheduleSpec?: never;
554
+ /** Server-derived: `400 execution, originMetadata, creatorPrincipalRef, and contextSnapshot are server-derived`. */
555
+ execution?: never;
556
+ /** Server-derived — same 400 as `execution`. */
557
+ originMetadata?: never;
558
+ /** Server-derived — same 400 as `execution`. */
559
+ contextSnapshot?: never;
560
+ /** Server-derived — same 400 as `execution`. */
561
+ creatorPrincipalRef?: never;
562
+ delivery?: {
563
+ mode: 'none' | 'announce' | string;
564
+ [k: string]: unknown;
565
+ };
566
+ enabled?: boolean;
567
+ deleteAfterRun?: boolean;
568
+ [k: string]: unknown;
569
+ }
570
+ /**
571
+ * A schedule as the API spells it — and it spells it three ways, staging-verified 2026-08-07.
572
+ *
573
+ * NOTHING YOU SENT COMES BACK UNDER THE NAME YOU SENT IT. The write shape ({@link ScheduleInput})
574
+ * and the read shape are different documents:
575
+ *
576
+ * - `schedule: { kind: 'cron', expr, tz }` is stored and read back as
577
+ * {@link ScheduleRecord.scheduleSpec} — `{ timezoneName, catchupWindowMs, cronExpressions[] }`.
578
+ * There is no `schedule` key on any read. Reach for `scheduleSpec.cronExpressions[0]`.
579
+ * - `sessionTarget: 'isolated'` is read back as `execution: { kind: 'isolated' }`. There is no
580
+ * `sessionTarget` key on any read either.
581
+ * - `scheduleId` is the FULLY-QUALIFIED name `cron/{computer_id}/{agent_id}/{schedule_id}`, not
582
+ * the id you chose. Your id is `name`.
583
+ *
584
+ * The three responses also disagree with each other: `POST` answers with only snake_case
585
+ * `schedule_name`; `GET /schedules/{id}` answers the camelCase projection below; `GET /schedules`
586
+ * answers the raw Temporal describe (`spec` / `state` / `memo` / `next_action_times`) with the
587
+ * camelCase projection merged on top. Read defensively and match on what you find.
588
+ */
589
+ export interface ScheduleRecord {
590
+ /** FULLY-QUALIFIED: `cron/{computer_id}/{agent_id}/{schedule_id}`. Not the id you passed in. */
591
+ scheduleId?: string;
592
+ /** The `schedule_id` you chose. This is the one you pass back to get/update/delete. */
593
+ name?: string;
594
+ computerId?: string;
595
+ agentId?: string;
596
+ apiAgentId?: string;
597
+ /** snake_case, from the create/trigger receipts: `cron/{computer_id}/{agent_id}/{schedule_id}`. */
598
+ schedule_name?: string;
599
+ /**
600
+ * The NORMALIZED cadence — what `ScheduleInput.schedule` became. Read-only: sending it back on
601
+ * a PUT is silently ignored (see {@link ScheduleUpdate.scheduleSpec}).
602
+ */
603
+ scheduleSpec?: {
604
+ timezoneName?: string;
605
+ catchupWindowMs?: number;
606
+ cronExpressions?: string[];
607
+ [k: string]: unknown;
608
+ };
609
+ /** Where the turn runs — what `ScheduleInput.sessionTarget` became. `{ kind: 'isolated' }`, etc. */
610
+ execution?: {
611
+ kind?: string;
612
+ sessionId?: string;
613
+ [k: string]: unknown;
614
+ };
615
+ payload?: SchedulePayload;
616
+ jobKind?: string;
617
+ delivery?: Record<string, unknown>;
618
+ enabled?: boolean;
619
+ deleteAfterRun?: boolean;
620
+ /** Server-derived, and rejected on the way back in — see {@link ZooworkClient.updateSchedule}. */
621
+ originMetadata?: Record<string, unknown>;
622
+ /** Server-derived, and rejected on the way back in. */
623
+ contextSnapshot?: unknown[];
624
+ origin?: string;
625
+ consecutiveErrors?: number;
626
+ createdAt?: string;
627
+ updatedAt?: string;
628
+ /** `GET /schedules` (list) only: the raw Temporal describe, in a different vocabulary again. */
629
+ spec?: Record<string, unknown>;
630
+ /** List only. `{ paused, note }` — NOT the lifecycle state, and unrelated to `enabled`. */
631
+ state?: {
632
+ paused?: boolean;
633
+ note?: string;
634
+ [k: string]: unknown;
635
+ };
636
+ /** List only. Carries the un-qualified `schedule_id` at `memo.schedule_id`. */
637
+ memo?: Record<string, unknown>;
638
+ /** List only. The next few fire instants, ISO 8601, in UTC. */
639
+ next_action_times?: string[];
640
+ [k: string]: unknown;
641
+ }
642
+ /**
643
+ * One past fire. `runs[]` mixes TWO ROW SHAPES and you have to switch on `source` to read one —
644
+ * staging-verified 2026-08-07, where a single `listScheduleRuns` returned both:
645
+ *
646
+ * - `source: 'temporal'` — a DISPATCH record: `scheduled_at` / `taken_at` / `workflow_id` /
647
+ * `temporal_run_id`. It says a workflow was handed the fire, and nothing about the outcome.
648
+ * - `source: 'run_projection'` — an OUTCOME record: `fired_at` / `status` / `consecutive_errors`.
649
+ * This is the only row that carries `status` (e.g. `skipped` for a fire against a disabled
650
+ * schedule).
651
+ *
652
+ * NEITHER carries `session_id`, which is the field people expect most: you cannot walk from a
653
+ * fire to the session it created. To find that session, list the agent's sessions and match
654
+ * `channel: 'cron'` with the `session_key` prefix `agent:{agent_id}:cron:{schedule_id}:`.
655
+ */
656
+ export interface ScheduleRun {
657
+ /** Which projection this row came from. Decides which of the field groups below is populated. */
658
+ source?: 'temporal' | 'run_projection' | string;
659
+ /** Un-qualified `schedule_id`. */
660
+ schedule_id?: string;
661
+ fired_at?: string;
662
+ /** e.g. `skipped`. Present on `run_projection` rows only — a `temporal` row has no status. */
663
+ status?: string;
664
+ consecutive_errors?: number;
665
+ /** When the fire was due. */
666
+ scheduled_at?: string;
667
+ /** When the worker picked it up. */
668
+ taken_at?: string;
669
+ /** `{schedule_name}-workflow-{ISO instant}`. */
670
+ workflow_id?: string;
671
+ temporal_run_id?: string;
672
+ [k: string]: unknown;
673
+ }
674
+ /** The exact resolution vocabulary. Anything else is a 400. */
675
+ export type ApprovalDecision = 'allow-once' | 'allow-always' | 'deny';
676
+ /**
677
+ * A tool call parked on a human decision.
678
+ *
679
+ * Field names are unverified: the only staging observation (2026-08-07) is a 200 with an EMPTY
680
+ * `approvals` array, because producing a real pending approval requires a tool policy that asks.
681
+ * Read defensively.
682
+ */
683
+ export interface ApprovalRecord {
684
+ approval_id?: string;
685
+ session_id?: string;
686
+ tool_name?: string;
687
+ status?: string;
688
+ created_at?: string;
689
+ [k: string]: unknown;
690
+ }
691
+ /** Artifact lifecycle. Only a `ready` row carries a resolvable `url`. */
692
+ export type ArtifactStatus = 'pending' | 'ready' | 'failed' | 'deleted' | string;
693
+ /**
694
+ * One published artifact — an immutable snapshot of a `/workspace` file, created by the
695
+ * agent's OWN in-loop `artifact_publish` tool during a turn. There is no API for publishing
696
+ * from outside the loop; this surface lists, reads, re-resolves and deletes what the agent
697
+ * published. Publishing is a COPY: the source file stays in the workspace.
698
+ *
699
+ * `url` is a revocable bearer capability — treat it like a secret, and expect its lifetime to
700
+ * be deployment policy (staging hands out 24-hour presigned GETs).
701
+ */
702
+ export interface ArtifactRecord {
703
+ artifact_id: string;
704
+ agent_id?: string;
705
+ session_id?: string;
706
+ run_id?: string;
707
+ turn?: number;
708
+ /** Where in `/workspace` it was published from. */
709
+ source_path?: string;
710
+ file_name?: string;
711
+ content_type?: string;
712
+ size?: number;
713
+ sha256?: string;
714
+ status?: ArtifactStatus;
715
+ /** Absent until the upload finalizes — a row without it answers `409 artifact_not_ready` to download. */
716
+ url?: string;
717
+ created_at?: string;
718
+ finalized_at?: string | null;
719
+ deleted_at?: string | null;
720
+ [k: string]: unknown;
721
+ }
722
+ /** One page of artifacts. `has_more` is real — this list DOES tell you when it truncated. */
723
+ export interface ArtifactPage {
724
+ artifacts: ArtifactRecord[];
725
+ page?: number;
726
+ has_more?: boolean;
727
+ [k: string]: unknown;
728
+ }
729
+ /**
730
+ * `getSystemPrompt` result: the pin as declared, and what is actually in effect.
731
+ *
732
+ * `declaration: null` together with `effective.source: 'legacy'` marks a pre-templates agent
733
+ * still on virtual legacy behavior — only an explicit engine-side upgrade migrates those.
734
+ * Agents created after templates landed carry a real declaration from birth.
735
+ */
736
+ export interface SystemPromptInfo {
737
+ agent_id?: string;
738
+ config_version?: number;
739
+ declaration?: SystemPromptDeclaration | null;
740
+ /** Rendered-template metadata (source/profile/version/hashes…), or the virtual-legacy marker. */
741
+ effective?: Record<string, unknown>;
742
+ [k: string]: unknown;
743
+ }
744
+ /**
745
+ * The runtime facts `previewSystemPrompt` assembles from. The required ones are required by
746
+ * the engine — omitting any is a 400 naming it.
747
+ */
748
+ export interface SystemPromptPreviewInput {
749
+ /** Must equal the agent's CURRENT `status.config_version`, or the answer is `409 config_version_changed`. */
750
+ config_version: number;
751
+ now_ms: number;
752
+ session_id: string;
753
+ model_display: string;
754
+ workspace_dir: string;
755
+ tool_names: string[];
756
+ channel?: string;
757
+ chat_type?: string;
758
+ session_key?: string;
759
+ subagent?: {
760
+ role?: string;
761
+ taskName?: string;
762
+ };
763
+ [k: string]: unknown;
764
+ }
765
+ export interface SystemPromptPreview {
766
+ agent_id?: string;
767
+ config_version?: number;
768
+ /** The assembled prompt, verbatim. */
769
+ system_prompt?: string;
770
+ char_count?: number;
771
+ /** One hash per functional slot on a templated agent; `null` when legacy assembly produced no slots. */
772
+ slot_hashes?: Record<string, string> | null;
773
+ /** Always `[]` — the preview never reads the transcript. */
774
+ transcript?: unknown[];
775
+ [k: string]: unknown;
776
+ }
777
+ /** `upgradeSystemPrompt` receipt: the new pin and the version bump it cost. */
778
+ export interface SystemPromptUpgrade {
779
+ agent_id?: string;
780
+ /** The NEW config version — the upgrade writes a config change like any other. */
781
+ config_version?: number;
782
+ declaration?: SystemPromptDeclaration;
783
+ template_hash?: string;
784
+ [k: string]: unknown;
785
+ }
786
+ /**
787
+ * An Environment build spec. The top level accepts EXACTLY these four keys — any other key is
788
+ * `400 invalid_environment_config`, which is why this type has no index signature.
789
+ *
790
+ * Package install order is fixed apt → npm → pip. Files land under
791
+ * `/opt/zooclaw/environment/`, and a top-level `bin/*` marked executable is linked into
792
+ * `/usr/local/bin`. No user-defined secrets, env vars, or start hooks — the platform injects
793
+ * its own runtime credentials for built-in skills, but that layer is internal and not
794
+ * extensible.
795
+ */
796
+ export interface EnvironmentConfig {
797
+ packages?: {
798
+ apt?: string[];
799
+ npm?: string[];
800
+ pip?: string[];
801
+ };
802
+ files?: {
803
+ path?: string;
804
+ contentBase64?: string;
805
+ upload_id?: string;
806
+ executable?: boolean;
807
+ }[];
808
+ build?: {
809
+ script?: string;
810
+ verify_script?: string;
811
+ };
812
+ /**
813
+ * Omitted means `{ type: 'unrestricted' }` — the sandbox reaches the whole internet by
814
+ * default. `allowed_hosts` is accepted only with `type: 'limited'`.
815
+ */
816
+ networking?: {
817
+ type: 'unrestricted' | 'limited';
818
+ allowed_hosts?: string[];
819
+ };
820
+ }
821
+ export interface EnvironmentResource {
822
+ name: string;
823
+ description?: string;
824
+ /** Supply to create a new version lineage under an existing id; omit to mint one. */
825
+ environment_id?: string;
826
+ config: EnvironmentConfig;
827
+ [k: string]: unknown;
828
+ }
829
+ /**
830
+ * An Environment row. Staging-verified 2026-08-07.
831
+ *
832
+ * THE TWO VERSION NUMBERS ARE NOT THE SAME NUMBER, and picking the wrong one pins an agent to a
833
+ * build that does not exist yet: `latest_version` is the newest version that has been CREATED
834
+ * (it is `1` the instant you create an Environment, while that version is still `queued`), and
835
+ * `latest_ready_version` is the newest one that finished building — `null` until a build lands.
836
+ * Pin `latest_ready_version`, or `createAgent` answers `409 environment_not_ready`.
837
+ *
838
+ * There is no `state` here and no nested `ownership`: lifecycle is `status`
839
+ * (`active` / `archived`) and the tenant is flat, as `scope` + `org_id`.
840
+ */
841
+ export interface EnvironmentRecord {
842
+ environment_id: string;
843
+ name?: string;
844
+ description?: string;
845
+ scope?: 'org' | string;
846
+ org_id?: string;
847
+ /** `active` | `archived`. */
848
+ status?: string;
849
+ /** Newest version CREATED — may still be building. Not safe to pin. */
850
+ latest_version?: number | null;
851
+ /** Newest version that reached `ready`. `null` until a build finishes. Pin THIS. */
852
+ latest_ready_version?: number | null;
853
+ created_by?: string;
854
+ created_at?: string;
855
+ updated_at?: string;
856
+ /** Set once archived; `null` while active. */
857
+ archived_at?: string | null;
858
+ /** CREATE ONLY: the first version, inline — no follow-up `getEnvironmentVersion` needed to see it. */
859
+ version?: EnvironmentVersionRecord;
860
+ [k: string]: unknown;
861
+ }
862
+ /**
863
+ * One immutable Environment version. Builds walk
864
+ * `queued → submitting → building → verifying → ready`, and any phase can land in `failed`.
865
+ * Poll THIS, not the Environment's top-level row: a `ready` version is the only thing an agent
866
+ * can pin, and it always carries both `e2b_build_id` and a matching `template_ref`.
867
+ *
868
+ * THE FIELD IS `status`, NOT `state` — staging-verified 2026-08-07. A poll loop written against
869
+ * `state` compares `undefined` to `'ready'` forever and never terminates, which is precisely the
870
+ * hang this note exists to prevent.
871
+ */
872
+ export interface EnvironmentVersionRecord {
873
+ environment_id?: string;
874
+ version?: number;
875
+ /** `queued` → `submitting` → `building` → `verifying` → `ready`, or `failed`. */
876
+ status?: 'queued' | 'submitting' | 'building' | 'verifying' | 'ready' | 'failed' | string;
877
+ /** The normalized config this version was built from — not the one you posted verbatim. */
878
+ config?: EnvironmentConfig;
879
+ base_environment_id?: string;
880
+ base_version?: number;
881
+ source_hash?: string;
882
+ spec_hash?: string;
883
+ e2b_template_name?: string | null;
884
+ e2b_template_id?: string | null;
885
+ /** `null` until the build reaches `ready`. */
886
+ e2b_build_id?: string | null;
887
+ /** `null` until the build reaches `ready`. */
888
+ template_ref?: string | null;
889
+ /** The exact base-image build this version layers on — recorded 2026-08-14, new on the wire. */
890
+ base_template_ref?: string | null;
891
+ /** Which phase failed, on `status: 'failed'`. */
892
+ failure_stage?: string | null;
893
+ failure_message?: string | null;
894
+ created_by?: string;
895
+ created_at?: string;
896
+ ready_at?: string | null;
897
+ [k: string]: unknown;
898
+ }
899
+ /** `POST /agents/{id}/exec` result. A failed command still arrives here, not as a rejection. */
900
+ export interface ExecResult {
901
+ /** Non-zero means the COMMAND failed. The HTTP call succeeded regardless. */
902
+ exit_code: number;
903
+ stdout: string;
904
+ stderr: string;
905
+ }
906
+ export interface WakeResult {
907
+ mode: 'now' | 'next-heartbeat' | string;
908
+ /** The reminder was written to the pending queue. */
909
+ queued: boolean;
910
+ /** `mode: 'now'` only — whether the heartbeat schedule was actually kicked. */
911
+ triggered: boolean;
912
+ }
913
+ export type { SessionEvent } from './events.js';
914
+ export interface ZooworkClient {
915
+ listModels(): Promise<ModelInfo[]>;
916
+ /**
917
+ * Create an agent. The gateway derives ownership from your API key and always skips the
918
+ * interactive onboarding interview — the agent answers your first message directly.
919
+ * `ownership` is accepted for callers that talk to the engine without the gateway;
920
+ * through the gateway it is overwritten and can be omitted.
921
+ */
922
+ createAgent(input: {
923
+ resource: AgentResource;
924
+ ownership?: Ownership;
925
+ }, idempotencyKey?: string): Promise<AgentRecord>;
926
+ /**
927
+ * List the agents owned by your key's bound user (engine query: `owner_uid AND org_id`,
928
+ * both injected by the gateway). `labels` filters on declared labels — e.g.
929
+ * `{ labels: { workspace_id: '…' } }` resolves an app workspace id (the first path
930
+ * segment of a ZooWork chat URL) to its agent. Page size is fixed at 100 by the engine.
931
+ *
932
+ * Note the scope is `owner_uid AND org_id`: an agent a colleague created in your org is
933
+ * fetchable by `getAgent` but will not appear here.
934
+ */
935
+ listAgents(opts?: {
936
+ labels?: Record<string, string>;
937
+ page?: number;
938
+ }): Promise<AgentRecord[]>;
939
+ getAgent(agentId: string): Promise<AgentRecord>;
940
+ /** PUT declared sections; bumps config_version on EVERY call — gate on drift, don't blind-retry. */
941
+ updateAgent(agentId: string, sections: Record<string, unknown>): Promise<AgentRecord>;
942
+ /**
943
+ * Soft delete: the agent stops resolving on the API, but this is not a resource purge.
944
+ * On gateway releases that carry the channels surface, a successful delete also
945
+ * best-effort disables the agent's bound channels — a failure there never turns the
946
+ * delete into an error, so a chat binding can in rare cases outlive its agent.
947
+ */
948
+ deleteAgent(agentId: string): Promise<void>;
949
+ /**
950
+ * Flip `desired_state` to `running` — the precondition for every session call.
951
+ * Fast (sub-second on staging). The returned warnings are informational: an
952
+ * API-only agent reports `channel_routes_reload_failed` on every start/stop
953
+ * because it has no chat-channel routes to reload. Do not treat it as failure.
954
+ */
955
+ startAgent(agentId: string): Promise<{
956
+ warnings: string[];
957
+ }>;
958
+ stopAgent(agentId: string): Promise<{
959
+ warnings: string[];
960
+ }>;
961
+ /**
962
+ * Poll until `status.desired_state === 'running'`, then hand back that projection.
963
+ *
964
+ * READ THIS BEFORE WRITING YOUR OWN LOOP. `desired_state` is the only readiness signal.
965
+ * Polling `status.actual_state` for `'running'` is the documented way to hang forever:
966
+ * `actual_state` reports CHAT-CHANNEL health, `running` is not one of its values, and an
967
+ * API-only agent parks at `activating` for the rest of its life. Every hand-rolled readiness
968
+ * loop we have seen gets this wrong.
969
+ *
970
+ * Defaults: 30s budget, 500ms between polls — start is sub-second on staging, so the budget
971
+ * is for a bad day, not the normal one. On timeout it throws a {@link ZooworkError} with
972
+ * `status: 408` / `type: 'timeout'`; on abort, `status: 0` / `type: 'aborted'`. Both are
973
+ * synthesized locally — the server never sends either.
974
+ *
975
+ * Both bounds cover an IN-FLIGHT poll, not just the gap between polls: each request carries a
976
+ * signal that fires on the caller's `signal` or on whatever is left of the budget. A gateway
977
+ * that accepts the connection and then never answers therefore ends this wait on schedule
978
+ * instead of hanging it (`fetch` imposes no timeout of its own).
979
+ */
980
+ waitUntilRunning(agentId: string, opts?: {
981
+ timeoutMs?: number;
982
+ intervalMs?: number;
983
+ signal?: AbortSignal;
984
+ }): Promise<AgentRecord>;
985
+ /** Skills already attached to the agent, resolved and merged. `verbose` includes ineligible/excluded entries. */
986
+ listAgentSkills(agentId: string, opts?: {
987
+ verbose?: boolean;
988
+ }): Promise<AgentSkill[]>;
989
+ /**
990
+ * Attach a skill by id. Only skills the caller's tenant owns are installable
991
+ * through the `/service/v1` gateway (`org` / `personal` scope); `global`
992
+ * catalog entries are listable but answer 404 here.
993
+ */
994
+ putAgentSkill(agentId: string, skillId: string, opts?: {
995
+ enabled?: boolean;
996
+ versionPin?: number | null;
997
+ }): Promise<{
998
+ config_version?: number;
999
+ warnings?: string[];
1000
+ }>;
1001
+ deleteAgentSkill(agentId: string, skillId: string): Promise<void>;
1002
+ /**
1003
+ * Channels currently bound to the agent. Empty for a pure API agent.
1004
+ *
1005
+ * Lists the platforms in {@link ChannelPlatform}. The deployment's own internal channels are
1006
+ * filtered out server-side and never appear here.
1007
+ */
1008
+ listChannels(agentId: string): Promise<AgentChannel[]>;
1009
+ /**
1010
+ * Bind a channel from explicit platform config (the non-QR path) — `config` carries the
1011
+ * platform's own credential keys. Answers the created channel (HTTP 201). This is the ONLY
1012
+ * path for Slack and WeCom; Feishu also has the QR flow. See {@link ChannelPlatform} for what
1013
+ * binds and what does not.
1014
+ *
1015
+ * **It is an upsert, not a create.** Binding the same `platform` + `account` twice answers
1016
+ * `201` again and overwrites the first binding rather than conflicting.
1017
+ *
1018
+ * ⚠️ **201 means STORED, not WORKING.** Credentials are not validated at bind time: a channel
1019
+ * created from deliberately bogus credentials still answered 201 with `health: 'unknown'`,
1020
+ * `status: 'configured'`, and only turned `health: 'unhealthy'` / `status: 'error'` moments
1021
+ * later (staging-verified 2026-08-25). Read the verdict from `health`/`status` on a follow-up
1022
+ * {@link listChannels}; treating the 201 as success ships a silently broken binding.
1023
+ */
1024
+ addChannel(agentId: string, input: AddChannelInput): Promise<AgentChannel>;
1025
+ /**
1026
+ * Change `dm_policy` / `group_policy` / `enabled` on one bound platform account, and get the
1027
+ * channel back in its NEW state. `allow_from` is deliberately not editable — write-once at
1028
+ * create.
1029
+ *
1030
+ * `enabled: false` is not just a flag: it was observed moving `status` to `'disabled'` and
1031
+ * resetting `health` to `'unknown'`. Updating a platform with no binding answers
1032
+ * `404 channel.not_found`.
1033
+ */
1034
+ updateChannel(agentId: string, platform: string, input?: UpdateChannelInput): Promise<AgentChannel>;
1035
+ /**
1036
+ * Unbind one platform account (server default account: `'default'`).
1037
+ *
1038
+ * Idempotent, unlike {@link updateChannel}: removing a binding that is not there answers
1039
+ * `200 { ok: true }` rather than `404 channel.not_found`.
1040
+ */
1041
+ removeChannel(agentId: string, platform: string, opts?: {
1042
+ account?: string;
1043
+ }): Promise<void>;
1044
+ /**
1045
+ * Start the Feishu/Lark QR registration. YOU own the UI: render
1046
+ * `verification_uri_complete` (usually as a QR code) and drive the poll loop —
1047
+ * `waitForFeishuSetup` does the loop part for you.
1048
+ *
1049
+ * Observed defaults: `expires_in: 600`, `poll_interval: 5`. `brand` picks the real host —
1050
+ * `'feishu'` answers an `open.feishu.cn` URI, `'lark'` an `open.larksuite.com` one, so the
1051
+ * brand has to match the workspace the person will approve it in.
1052
+ */
1053
+ startFeishuSetup(agentId: string, input?: FeishuSetupInput): Promise<FeishuSetupSession>;
1054
+ /**
1055
+ * One poll of a setup session. `status: 'pending'` means keep going; a cancelled or expired
1056
+ * session answers `404 channel.feishu_session_not_found` rather than a terminal status, so
1057
+ * a hand-rolled loop must treat that 404 as an end condition, not as a transport error.
1058
+ */
1059
+ pollFeishuSetup(agentId: string, sessionId: string): Promise<FeishuPollResult>;
1060
+ /** Abandon a setup session. Afterwards polling it answers `404 channel.feishu_session_not_found`. */
1061
+ cancelFeishuSetup(agentId: string, sessionId: string): Promise<void>;
1062
+ /**
1063
+ * Poll a Feishu setup session until it leaves `pending`, then hand back that terminal poll.
1064
+ * A status the server reports in the body — `success` / `expired` / `denied` / `error` — is
1065
+ * RETURNED, not thrown: "the person rejected it" is an outcome, not an exception.
1066
+ *
1067
+ * But a session can also stop existing, and then polling answers
1068
+ * `404 channel.feishu_session_not_found`, which surfaces here as a thrown
1069
+ * {@link ZooworkError} carrying that `type`. Confirmed for a cancelled session
1070
+ * (staging 2026-08-25); whether a session that simply runs past `expires_in` reports
1071
+ * `status: 'expired'` in a 200 or disappears into this 404 was NOT observed — handle both.
1072
+ *
1073
+ * Pacing follows the server's `poll_interval` when present (observed default 5s; the local
1074
+ * fallback matches). The default budget is 600s, which is also the observed `expires_in` —
1075
+ * pass the session's own value when you have it. On timeout it throws `status: 408` /
1076
+ * `type: 'timeout'`; on abort, `status: 0` / `type: 'aborted'` — both synthesized locally,
1077
+ * and every in-flight poll is bounded the way {@link waitUntilRunning} bounds its polls.
1078
+ * `onPoll` fires after every poll, terminal one included, for progress UI.
1079
+ */
1080
+ waitForFeishuSetup(agentId: string, sessionId: string, opts?: {
1081
+ timeoutMs?: number;
1082
+ signal?: AbortSignal;
1083
+ onPoll?: (poll: FeishuPollResult) => void;
1084
+ }): Promise<FeishuPollResult>;
1085
+ /**
1086
+ * The agent's system-prompt pin and the rendered template in effect. Staging-verified
1087
+ * 2026-08-14 — a fresh agent answers a real `declaration` (`{source:'platform',version:1}`),
1088
+ * a pre-templates agent answers `declaration: null` with a virtual-legacy `effective`.
1089
+ */
1090
+ getSystemPrompt(agentId: string): Promise<SystemPromptInfo>;
1091
+ /**
1092
+ * Assemble the EXACT prompt for a given set of runtime facts, without touching any session.
1093
+ * Deterministic for fixed inputs (`transcript` is always `[]`), and `slot_hashes` names each
1094
+ * template slot for diffing. `config_version` must be the agent's CURRENT one.
1095
+ * Staging-verified 2026-08-14; the raw `:` in `system-prompt:preview` passes the gateway.
1096
+ */
1097
+ previewSystemPrompt(agentId: string, input: SystemPromptPreviewInput): Promise<SystemPromptPreview>;
1098
+ /**
1099
+ * Move the system-prompt pin — the ONE write that does, since nothing else ever follows a
1100
+ * later platform activation. Omit `template_version` to upgrade to the currently ACTIVE
1101
+ * platform version; pass one to pin a specific immutable version.
1102
+ *
1103
+ * `expected_config_version` is REQUIRED and it is a real CAS: it must equal the agent's
1104
+ * current `status.config_version` or the answer is `409 config_version_changed` — read
1105
+ * fresh, then upgrade. The 200 receipt carries the NEW `config_version` (the upgrade is a
1106
+ * config write like any other) plus the pinned `declaration` and `template_hash`.
1107
+ *
1108
+ * Staging-verified 2026-08-14, the day gateway fix #3387 opened the `{id}:verb` route
1109
+ * grammar (this route was 404 through the gateway until then — do not expect it on older
1110
+ * gateway deployments).
1111
+ */
1112
+ upgradeSystemPrompt(agentId: string, input: {
1113
+ expected_config_version: number;
1114
+ template_version?: number;
1115
+ }): Promise<SystemPromptUpgrade>;
1116
+ /**
1117
+ * Upload a skill package as a zip. One call creates the skill row AND version 1.
1118
+ *
1119
+ * THE RULE THAT COSTS EVERYONE THEIR FIRST ATTEMPT: the zip's single top-level directory name
1120
+ * must equal the `name` in `SKILL.md`'s frontmatter (compared case- and underscore-
1121
+ * insensitively), or the whole thing is a 400 —
1122
+ * `top-level directory 'my-test-skill' must match SKILL.md name 'fulong-probe-skill'`.
1123
+ * So `zip -r skill.zip market-research/` where `market-research/SKILL.md` declares
1124
+ * `name: market-research`. A zip whose ROOT is the skill (SKILL.md at the top) is also
1125
+ * accepted. `SKILL.md` must be non-empty and declare both `name` and `description`.
1126
+ * 50 MB expanded, zip only (store/deflate), encrypted zips rejected.
1127
+ *
1128
+ * `scope` may only be `org` or `personal`; `global` and `pack` are 403 and are published
1129
+ * through an admin surface the gateway does not proxy. The gateway rewrites ownership to your
1130
+ * key's tenant either way. Staging-verified 2026-08-07 — this is the ONLY path by which an
1131
+ * API-key caller can add a skill the agent will actually load.
1132
+ */
1133
+ uploadSkill(zip: Blob | ArrayBuffer | Uint8Array, opts: {
1134
+ scope: 'org' | 'personal';
1135
+ fileName?: string;
1136
+ description?: string;
1137
+ idempotencyKey?: string;
1138
+ }): Promise<SkillRecord>;
1139
+ /**
1140
+ * Publish a new version of an existing skill from a zip. Same zip rules as
1141
+ * {@link ZooworkClient.uploadSkill}, plus: the frontmatter `name` must match the target
1142
+ * skill's name. `description` overrides the one in the frontmatter.
1143
+ *
1144
+ * Agents that installed the skill unpinned follow the new version on their own — the registry
1145
+ * bumps their `config_version`; you do not re-`putAgentSkill`.
1146
+ */
1147
+ uploadSkillVersion(skillId: string, zip: Blob | ArrayBuffer | Uint8Array, opts?: {
1148
+ fileName?: string;
1149
+ description?: string;
1150
+ idempotencyKey?: string;
1151
+ }): Promise<SkillRecord>;
1152
+ /**
1153
+ * The catalog visible to your key: global skills plus your org/personal ones. `q` matches on
1154
+ * name; `page` is 1-based with a fixed page size of 100. Only the `org`/`personal` rows are
1155
+ * installable — `global` entries list but answer 404 from `putAgentSkill`.
1156
+ */
1157
+ listSkills(opts?: {
1158
+ scope?: 'org' | 'personal' | 'global' | string;
1159
+ q?: string;
1160
+ page?: number;
1161
+ }): Promise<SkillRecord[]>;
1162
+ /** 204. No in-use guard for org/personal skills: agents holding it just lose it. */
1163
+ deleteSkill(skillId: string): Promise<void>;
1164
+ createSession(agentId: string, input: {
1165
+ initial_events?: OutboundEvent[];
1166
+ metadata?: Record<string, unknown>;
1167
+ }, idempotencyKey?: string): Promise<SessionRecord>;
1168
+ getSession(agentId: string, sessionId: string, opts?: {
1169
+ history?: boolean;
1170
+ limit?: number;
1171
+ }): Promise<SessionRecord>;
1172
+ /** Newest first by `updated_at`, 50 per page, `page` is 1-based. Single page per call — there is no cursor. */
1173
+ listSessions(agentId: string, opts?: {
1174
+ page?: number;
1175
+ }): Promise<SessionRecord[]>;
1176
+ /**
1177
+ * Stamp `archived_at`. Afterwards writes are `409 session_archived` while reads keep working.
1178
+ * Interrupt an in-flight run first, or the archive races it.
1179
+ */
1180
+ archiveSession(agentId: string, sessionId: string): Promise<{
1181
+ session_id?: string;
1182
+ archived: boolean;
1183
+ }>;
1184
+ /** Soft delete (204). An in-flight run is cancelled first; transcripts and events survive for audit. */
1185
+ deleteSession(agentId: string, sessionId: string): Promise<void>;
1186
+ /**
1187
+ * 202; `user.interrupt` with no in-flight run returns `accepted:false` — not an error.
1188
+ * Accepted events come back as full event objects (with `seq`) where the server supports the
1189
+ * unified history; give each event an `idempotency_key` to make timeout retries safe.
1190
+ */
1191
+ postEvents(agentId: string, sessionId: string, events: OutboundEvent[]): Promise<{
1192
+ events: PostEventReceipt[];
1193
+ }>;
1194
+ /**
1195
+ * ONE page of durable events — the unified history, which includes your own inputs
1196
+ * (`user.message`, …) alongside engine events. `limit` defaults to 100 and is capped at 500;
1197
+ * the page's pagination fields are dropped, so use {@link ZooworkClient.listAllEvents}, or
1198
+ * {@link ZooworkClient.listEventsPage} to page by hand.
1199
+ * Passing `after` selects the deprecated engine-only lane — old cursors only.
1200
+ */
1201
+ listEvents(agentId: string, sessionId: string, opts?: {
1202
+ after?: number;
1203
+ cursor?: string;
1204
+ types?: string[];
1205
+ limit?: number;
1206
+ }): Promise<SessionEvent[]>;
1207
+ /**
1208
+ * The page-returning primitive under `listEvents`: same options, plus the page's `hasMore`
1209
+ * and `nextCursor` (absent on servers without cursor pagination). Feed `nextCursor` back as
1210
+ * `cursor` to page by hand.
1211
+ */
1212
+ listEventsPage(agentId: string, sessionId: string, opts?: {
1213
+ after?: number;
1214
+ cursor?: string;
1215
+ types?: string[];
1216
+ limit?: number;
1217
+ }): Promise<SessionEventPage>;
1218
+ /**
1219
+ * Every durable event: follows the server's `next_cursor` until `has_more` is false, and
1220
+ * falls back to walking `after` against servers without cursor pagination (that walk stops
1221
+ * when a page fails to advance, so a server that ignored `after` cannot spin it forever).
1222
+ *
1223
+ * `pageSize` is the per-request `limit` (default and maximum 500). Events come back in
1224
+ * ascending `seq`, deduplicated across page boundaries. Passing `after` forces the
1225
+ * deprecated engine-only lane.
1226
+ */
1227
+ listAllEvents(agentId: string, sessionId: string, opts?: {
1228
+ after?: number;
1229
+ types?: string[];
1230
+ pageSize?: number;
1231
+ }): Promise<SessionEvent[]>;
1232
+ /**
1233
+ * Durable event stream with server-side resume.
1234
+ *
1235
+ * The stream is SESSION-scoped and unbounded: it does NOT close when a turn ends, and the
1236
+ * server closes it on idle. Detect turn end with `isRunFinished`, and resume by passing the
1237
+ * last event's `cursor` (its `after` fallback selects the deprecated engine-only lane).
1238
+ * `chat.delta` preview frames are skipped — they are snapshot-replace frames on a separate
1239
+ * Redis-only lane, not durable events.
1240
+ */
1241
+ streamEvents(agentId: string, sessionId: string, opts?: {
1242
+ after?: number;
1243
+ cursor?: string;
1244
+ signal?: AbortSignal;
1245
+ }): AsyncGenerator<SessionEvent>;
1246
+ /**
1247
+ * Tool calls parked on a human decision. `status` may ONLY be omitted or `'pending'` —
1248
+ * staging-verified 2026-08-07; any other value is rejected, so there is no way to list
1249
+ * resolved ones.
1250
+ *
1251
+ * Approvals are a REST resource here, NOT the `user.tool_confirmation` event loop; the two
1252
+ * shapes describe the same act and do not line up. Without a Temporal signaler the route
1253
+ * answers `501 not_configured`. We have never produced a real pending approval, so the
1254
+ * round trip is unproven: treat human-in-the-loop as unavailable, and note that a run parked
1255
+ * on an approval burns its whole turn budget waiting.
1256
+ */
1257
+ listApprovals(agentId: string, opts?: {
1258
+ status?: 'pending';
1259
+ }): Promise<ApprovalRecord[]>;
1260
+ /** Resolve one approval. `decision` is exactly one of allow-once / allow-always / deny. */
1261
+ resolveApproval(agentId: string, approvalId: string, input: {
1262
+ decision: ApprovalDecision;
1263
+ resolvedBy?: string;
1264
+ }): Promise<Record<string, unknown>>;
1265
+ /**
1266
+ * Artifacts this agent published, one page per call — but unlike `listEvents`, the page SAYS
1267
+ * when it truncated: read `has_more`. `limit` defaults to 50, capped at 100; filter by
1268
+ * `sessionId`, `sourcePath`, or `createdBefore` (ISO timestamp). Empty list staging-verified
1269
+ * 2026-08-14; a populated one needs a turn that called `artifact_publish`.
1270
+ */
1271
+ listArtifacts(agentId: string, opts?: {
1272
+ page?: number;
1273
+ limit?: number;
1274
+ sessionId?: string;
1275
+ sourcePath?: string;
1276
+ createdBefore?: string;
1277
+ }): Promise<ArtifactPage>;
1278
+ /** One artifact row. A foreign or unknown id is 404 (hidden, not 403). */
1279
+ getArtifact(agentId: string, artifactId: string): Promise<ArtifactRecord>;
1280
+ /**
1281
+ * Mint a fresh access URL for a `ready` artifact: `{artifact_id, url}`. A row that never
1282
+ * finalized is `409 artifact_not_ready`. The colon in `:download` goes RAW on the wire —
1283
+ * this family matches the literal colon, unlike the environments family's `%3A`.
1284
+ */
1285
+ downloadArtifact(agentId: string, artifactId: string): Promise<{
1286
+ artifact_id?: string;
1287
+ url?: string;
1288
+ }>;
1289
+ /**
1290
+ * Delete one artifact; the 200 body is the row as the engine leaves it. Not yet exercised
1291
+ * against staging — doing so needs an artifact a real turn published first.
1292
+ */
1293
+ deleteArtifact(agentId: string, artifactId: string): Promise<ArtifactRecord>;
1294
+ listSchedules(agentId: string): Promise<ScheduleRecord[]>;
1295
+ /**
1296
+ * 201 with a create receipt carrying only `schedule_name`
1297
+ * (`cron/{computer_id}/{agent_id}/{schedule_id}`) — not the definition. Read it back with
1298
+ * `getSchedule` if you need the stored shape.
1299
+ *
1300
+ * Schedules outlive their agent: `stopAgent`/`deleteAgent` do not remove them. List and
1301
+ * delete them yourself before deleting an agent. Re-creating an existing `schedule_id` with a
1302
+ * different definition is a 409; an identical retry is accepted.
1303
+ */
1304
+ createSchedule(agentId: string, input: ScheduleInput, idempotencyKey?: string): Promise<ScheduleRecord>;
1305
+ /** Note the camelCase body (`scheduleId`/`computerId`/`agentId`) — create answered in snake_case. */
1306
+ getSchedule(agentId: string, scheduleId: string): Promise<ScheduleRecord>;
1307
+ /**
1308
+ * Update the definition.
1309
+ *
1310
+ * A `getSchedule()` result is NOT a legal PUT body, and this is the method where that costs
1311
+ * you. Six of its fields are refused or ignored on the way back in:
1312
+ * `execution` / `originMetadata` / `contextSnapshot` / `creatorPrincipalRef` are
1313
+ * `400 execution, originMetadata, creatorPrincipalRef, and contextSnapshot are server-derived`,
1314
+ * `sessionTarget` is `400 sessionTarget is immutable`, and `scheduleSpec` — the only place a
1315
+ * read puts the cadence — is accepted and then SILENTLY IGNORED. The type refuses all six at
1316
+ * compile time and the SDK strips them before sending, so the obvious JavaScript round trip
1317
+ * (read, tweak, write) both succeeds and preserves the schedule. Staging-verified 2026-08-07.
1318
+ *
1319
+ * TO CHANGE THE CADENCE, send `schedule: { kind: 'cron', expr, tz }` — the INPUT vocabulary.
1320
+ * Echoing back the `scheduleSpec` you just read leaves the old expression in place while every
1321
+ * other field in the same body applies. That is the nastiest failure on this route, because it
1322
+ * answers 200.
1323
+ *
1324
+ * PUT and DELETE carry no cross-timeout idempotency guarantee; after a timeout, reconcile by
1325
+ * listing and reading runs rather than blind-retrying.
1326
+ */
1327
+ updateSchedule(agentId: string, scheduleId: string, update: ScheduleUpdate): Promise<ScheduleRecord>;
1328
+ deleteSchedule(agentId: string, scheduleId: string): Promise<void>;
1329
+ /** Fire it once, now, out of band. Does not disturb the cadence. */
1330
+ triggerSchedule(agentId: string, scheduleId: string): Promise<{
1331
+ schedule_name?: string;
1332
+ triggered: boolean;
1333
+ }>;
1334
+ /** Past fires, newest first. `limit` defaults to 20 and is capped at 100. */
1335
+ listScheduleRuns(agentId: string, scheduleId: string, opts?: {
1336
+ limit?: number;
1337
+ }): Promise<ScheduleRun[]>;
1338
+ /**
1339
+ * Push a reminder into the agent's heartbeat queue.
1340
+ *
1341
+ * `next-heartbeat` (the default) only writes the pending row — it needs no Temporal client,
1342
+ * but nothing consumes it unless the agent has a heartbeat configured. `now` writes the row
1343
+ * AND kicks the heartbeat schedule, and is `409` when no heartbeat is enabled; if the
1344
+ * heartbeat is already busy the kick is skipped (overlap SKIP) and the row waits for the next
1345
+ * one. `deliverToUser: false` keeps the reminder internal to the agent's own reasoning.
1346
+ */
1347
+ wake(agentId: string, input: {
1348
+ text: string;
1349
+ mode?: 'now' | 'next-heartbeat';
1350
+ deliverToUser?: boolean;
1351
+ }): Promise<WakeResult>;
1352
+ /**
1353
+ * Run a command in the agent's sandbox. `args` is argv, not a shell string — use
1354
+ * `['bash', '-lc', 'pwd']` for shell semantics.
1355
+ *
1356
+ * A NON-ZERO EXIT IS STILL HTTP 200. This promise resolves; check `exit_code`. It does not
1357
+ * reject on a failed command, only on a failed call.
1358
+ *
1359
+ * cwd is fixed to `/workspace`. Requires an agent-scope sandbox and a rendered config:
1360
+ * a session-scope agent is `409 exec_requires_agent_scope`, an unrendered one is
1361
+ * `409 exec_config_not_ready`, and a deployment with no sandbox backend is
1362
+ * `501 not_configured`. Default timeout 300s; stdout and stderr are each capped at 200,000
1363
+ * characters. This is an operations side door — it bypasses nothing, because it is not the
1364
+ * agent's tool path.
1365
+ */
1366
+ exec(agentId: string, args: string[]): Promise<ExecResult>;
1367
+ /**
1368
+ * Environments visible to your org, 1-based `page`. Note that the platform DEFAULT
1369
+ * Environment — the one a fresh agent is pinned to — is not in here and is not fetchable: the
1370
+ * gateway forces an org selector and the default belongs to no org.
1371
+ */
1372
+ listEnvironments(opts?: {
1373
+ page?: number;
1374
+ }): Promise<EnvironmentRecord[]>;
1375
+ /** 404 for any Environment outside your org, including the platform default. */
1376
+ getEnvironment(environmentId: string): Promise<EnvironmentRecord>;
1377
+ /**
1378
+ * Create an Environment (and its first version). Give it a stable `idempotencyKey`.
1379
+ *
1380
+ * `resource.config` takes exactly four keys — packages / files / build / networking — and
1381
+ * anything else is `400 invalid_environment_config`. Building is asynchronous: poll
1382
+ * `getEnvironmentVersion` until `status === 'ready'` before pinning it on an agent, or the
1383
+ * create answers `409 environment_not_ready`. The field is `status` — there is no `state` on a
1384
+ * version, and a loop written against one never terminates.
1385
+ */
1386
+ createEnvironment(input: {
1387
+ resource: EnvironmentResource;
1388
+ ownership: Ownership;
1389
+ }, idempotencyKey?: string): Promise<EnvironmentRecord>;
1390
+ /**
1391
+ * Archive an Environment.
1392
+ *
1393
+ * THE COLON MUST BE PERCENT-ENCODED. The route is `POST /environments/{id}:archive`, and a
1394
+ * raw `:` makes the engine miss the route and answer 404 — verified twice on 2026-08-07. The
1395
+ * SDK sends `%3A` for you; this is the whole reason this method exists rather than you
1396
+ * building the path.
1397
+ */
1398
+ archiveEnvironment(environmentId: string): Promise<EnvironmentRecord>;
1399
+ /**
1400
+ * Add an immutable version to an existing Environment. Versions never mutate: a retry after a
1401
+ * failed build retries THAT version and keeps its attempt log.
1402
+ *
1403
+ * The route is reachable, but the request body was not exercised against staging on
1404
+ * 2026-08-07 — the SDK sends `{ resource: { config } }`, mirroring create.
1405
+ */
1406
+ createEnvironmentVersion(environmentId: string, config: EnvironmentConfig, idempotencyKey?: string): Promise<EnvironmentVersionRecord>;
1407
+ /** Poll this — not the Environment's top-level state — to decide whether a version is usable. */
1408
+ getEnvironmentVersion(environmentId: string, version: number): Promise<EnvironmentVersionRecord>;
1409
+ }
1410
+ /**
1411
+ * Create a client.
1412
+ *
1413
+ * ```ts
1414
+ * const zc = createZooworkClient({ apiKey: 'zct_...' })
1415
+ * const zc = createZooworkClient() // reads ZOOWORK_API_KEY
1416
+ * ```
1417
+ *
1418
+ * Resolution order for both settings is the same: explicit argument, then environment
1419
+ * variable, then (for `baseUrl` only) the built-in default.
1420
+ *
1421
+ * @throws if no API key can be resolved — a missing key is a setup mistake worth failing
1422
+ * loudly at construction rather than as a 401 on the first call.
1423
+ */
1424
+ export declare function createZooworkClient(cfg?: ZooworkConfig): ZooworkClient;