@workerkit/core 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- // The 62 authenticated fleet-management tools, as pure descriptors. The
1
+ // The 79 authenticated fleet-management tools, as pure descriptors. The
2
2
  // descriptions ARE the product surface: they are served verbatim to MCP
3
3
  // clients (and any future CLI help), so every contract nuance an agent must
4
4
  // not get wrong is taught here.
@@ -37,17 +37,42 @@ const getKeyInfo = {
37
37
  const listWorkers = {
38
38
  name: "workers_list",
39
39
  title: "List Workers",
40
- description: "List every AI worker on this account with live status: title, avatar, status (active|paused|expired), isEnabled, expiresAt, lastRun (the newest SETTLED run — in-flight runs never appear here), schedule rollup (total/enabled/nextRunUtc), and live state (isRunning, inFlightRuns, currentRunId — poll run_get with currentRunId to watch it). Compute elapsed/next-run times against the response's serverTimeUtc, never your own clock. IMPORTANT: lastRun.resultText is the report the worker itself wrote and requires the manager key to hold the readRuns scope IN ADDITION to readWorkers — with readWorkers only it is null, which does NOT mean the run produced no report; check your key's scopes before concluding anything from a null. Scope model for every tool here: a 403 OPERATION_NOT_ALLOWED names the missing scope — the key must be re-minted with broader scopes by an account admin at https://workerkit.ai (do not retry the call).",
40
+ description: "List the AI workers on this account with live status: title, avatar, status (active|paused|expired), isEnabled, expiresAt, readiness (status ready|blocked with the actionable issues — the same block worker_get carries), deployment (null = this worker is NOT on the hosted runtime and will never run, whatever readiness says; worker_deploy fixes that), lastRun (the newest SETTLED run: runId, outcome succeeded|attention|failed|awaitingInput|skipped|canceled, skipReason, errorCode — in-flight runs never appear here), schedule rollup (total/enabled/nextRunUtc), and live state (isRunning, inFlightRuns, currentRunId — poll run_get with currentRunId to watch it). FILTERS, all optional and ANDed: status, deployed, readiness, q (title substring); an unknown status/readiness value is a 400 invalid_filter, never an empty page. totalWorkers is the count BEFORE filtering: 0 means nothing is installed on this account yet (shortlist a kit with kits_search on the Directory server, kit_install it with deploy:true), non-zero with an empty workers[] means the filter matched nothing. For 'what is wrong with this fleet' call fleet_health instead of scanning this list. Compute elapsed/next-run times against the response's serverTimeUtc, never your own clock. IMPORTANT: lastRun.resultText is the report the worker itself wrote and requires the manager key to hold the readRuns scope IN ADDITION to readWorkers — with readWorkers only it is null, which does NOT mean the run produced no report; check your key's scopes before concluding anything from a null. Scope model for every tool here: a 403 OPERATION_NOT_ALLOWED names the missing scope — the key must be re-minted with broader scopes by an account admin at https://workerkit.ai (do not retry the call).",
41
41
  auth: "manager",
42
42
  method: "get",
43
- schema: {},
43
+ schema: {
44
+ status: z.enum(["active", "paused", "expired"]).optional().describe("Only workers in this lifecycle state. paused = stopped with worker_set_enabled; expired = the key's expiry passed. Omit for all."),
45
+ deployed: z.boolean().optional().describe("true = only workers on the hosted runtime (the ones that can run); false = only workers without a deployment. Omit for both."),
46
+ readiness: z.enum(["ready", "blocked"]).optional().describe("ready = configuration lets it run hosted; blocked = something actionable is missing (an app connection, an instruction, an identity). Omit for both."),
47
+ q: z.string().max(100).optional().describe("Case-insensitive substring of the worker's title."),
48
+ },
44
49
  path: API,
45
50
  annotations: READ_ONLY,
46
51
  };
52
+ const fleetHealth = {
53
+ name: "fleet_health",
54
+ title: "Fleet Health",
55
+ description: "The fleet's rot in ONE call — the digest to brief from instead of crawling workers_list + worker_get + runs_feed: counts (workers, active, paused, expired, runsInFlight, and one per section: blocked, notDeployed, overdueSchedules, awaitingInput, lastRunAttention) plus five typed sections, each row naming the worker (workerId, tokenId, title) and what to do. blocked[] = active workers whose readiness is blocked, with issues[] (app_not_connected → app_connect on the worker's operator or the human at the dashboard; no_instruction → instruction_set; no_identity → the dashboard). notDeployed[] = active workers with NO hosted deployment: they fire no schedule and refuse worker_run with not_deployed, however ready they look — enabledSchedules > 0 there is a schedule that will never run until worker_deploy; readiness says whether deploying is enough. overdueSchedules[] = deployed workers whose earliest enabled schedule was due more than 15 minutes ago and was never claimed (nextRunUtc, overdueMinutes, deploymentStatus — Paused explains it by itself: deployment_update with resume). awaitingInput[] = runs that ended by ASKING their owner and are still waiting (questionId, runId, askedAtUtc, expiresAtUtc, chainDepth, and question — a 300-char preview that needs readRuns and is NULL without it: that null is a scope gap, not an empty question; run_question has the full text and options, run_answer takes the runId). lastRunAttention[] = active workers whose newest settled run was failed (errorCode), attention (succeeded with friction — denials, tool errors, reported issues) or skipped (skipReason says why; InsufficientCredits means the wallet, TokenDisabled/DailyRunCap/FleetSpendCap name themselves). Paused and expired workers are COUNTED, never listed: stopping a worker is a decision, not rot. Empty sections are the healthy answer. Measure every age against serverTimeUtc. Requires the readWorkers scope.",
56
+ auth: "manager",
57
+ method: "get",
58
+ schema: {},
59
+ path: `${API}/fleet/health`,
60
+ annotations: READ_ONLY,
61
+ };
62
+ const accountUsage = {
63
+ name: "account_usage",
64
+ title: "Account Usage",
65
+ description: "The account's headroom — read it BEFORE an install, a deploy or a run rather than discovering a 402 afterwards. Returns operator (the one kit_install targets by default; limits are per operator), plan (tier free|pro|team|enterprise, complimentaryUntilUtc when the tier is a time-boxed invite grant), workers {used, max, remaining} (remaining 0 → kit_install, worker_clone and re-enabling a stopped worker answer 402 limit_exceeded; worker_delete frees a slot, the plan is the other way out), hostedWorkers {used, max, remaining} (the worker_deploy cap; suspended deployments do not count), wallet {spendableBalanceUsd, promoRemainingUsd, promoExpiresAtUtc, topUpUrl} (hosted runs meter from spendableBalanceUsd — at zero worker_deploy answers 402 wallet_required and scheduled runs are refused with skipReason InsufficientCredits; AN AGENT CANNOT TOP IT UP, hand the human topUpUrl), requests {daily, hourly, monthly} each {limit, used, remaining, resetsAt} (the plan's data-plane windows — the calls workers make to apps; limit 0 means that window is DISABLED on this plan, not exhausted), and spend {todayUsd, thisMonthUsd, reservedInFlightUsd} (settled run spend plus what running runs hold; null while hosted runs are not enabled for the environment). The account's OWN ceilings live on fleet_budget_get, not here. 404 not_found means the account has no active operator. Requires the readWorkers scope.",
66
+ auth: "manager",
67
+ method: "get",
68
+ schema: {},
69
+ path: `${API}/account/usage`,
70
+ annotations: READ_ONLY,
71
+ };
47
72
  const getWorker = {
48
73
  name: "worker_get",
49
74
  title: "Get Worker",
50
- description: "One worker in full: everything workers_list shows plus timeZoneId, maxRunsPerDay, kit provenance (templateId/slug/name), hasInstruction, isProtected, jobSentence, readiness (status ready|blocked with actionable issues — worker_inactive, no_identity, app_not_connected incl. candidateProviders, no_instruction), apps[] (every app the worker has enabled with connection connected|needs_connection|unknown and the providers serving it — the per-worker view of apps_list; an app at needs_connection is fixed with app_connect on the worker's operator or on the dashboard), and 30-day activity (runs by outcome, success rate, cost; null while hosted runs are not enabled for the environment).",
75
+ description: "One worker in full: everything workers_list shows (including readiness: status ready|blocked with actionable issues — worker_inactive, no_identity, app_not_connected incl. candidateProviders, no_instruction) plus timeZoneId, maxRunsPerDay, kit provenance (templateId/slug/name), hasInstruction, isProtected, jobSentence, apps[] (every app the worker has enabled with connection connected|needs_connection|unknown and the providers serving it — the per-worker view of apps_list; an app at needs_connection is fixed with app_connect on the worker's operator or on the dashboard), deployment (status + modelSlug, or NULL WHEN THE WORKER IS NOT DEPLOYED — an undeployed worker never runs, whatever readiness says, because readiness judges configuration and deployment is what puts it on the runtime; fix with worker_deploy), and 30-day activity (runs by outcome, success rate, cost; null while hosted runs are not enabled for the environment).",
51
76
  auth: "manager",
52
77
  method: "get",
53
78
  schema: {
@@ -61,7 +86,7 @@ const getWorker = {
61
86
  const runWorker = {
62
87
  name: "worker_run",
63
88
  title: "Run Worker Now",
64
- description: "Trigger the worker to run now (recorded as an API-triggered run, attributed to the manager key's minter). The response is the minted run's RECEIPT — read status before assuming it ran: policy rejections (kill switch, usage window, daily run/spend cap, concurrency, wallet credits, readiness, or a rejected BYOK provider key) come back as HTTP 200 with status Skipped and a skipReason telling the story; that is a normal receipt, NOT an error. Most skips clear on their own (a cap resets, concurrency frees up, credits top up) — but skipReason 'ByokKeyInvalid' / errorCode 'byok_key_invalid' means the ACCOUNT's own model-provider API key was rejected by the provider, arrives with errorDetail null (no prose), and is owner-fixable ONLY: never retry it, report it to the human so they can re-add or remove the key. Structural refusals are 409 (not_deployed, deployment_paused, deployment_suspended, model_unavailable) and 404 for an unknown worker; run-endpoint errors use the {error, message} envelope where error IS the snake_case code. prompt = what to do THIS run; omit it and the worker runs on its standing instructions. Rate limit: 30 run-triggers per minute per ACCOUNT (all of this account's keys and clients share it; other accounts do not affect you). On a 429 back off for the Retry-After — never tighten a loop in response." +
89
+ description: "Trigger the worker to run now (recorded as an API-triggered run, attributed to the manager key's minter). The response is the minted run's RECEIPT — read status before assuming it ran: policy rejections (kill switch, usage window, daily run/spend cap, concurrency, wallet credits, readiness, or a rejected BYOK provider key) come back as HTTP 200 with status Skipped and a skipReason telling the story; that is a normal receipt, NOT an error. Most skips clear on their own (a cap resets, concurrency frees up, credits top up) — but skipReason 'ByokKeyInvalid' / errorCode 'byok_key_invalid' means the ACCOUNT's own model-provider API key was rejected by the provider, arrives with errorDetail null (no prose), and is owner-fixable ONLY: never retry it, report it to the human so they can re-add or remove the key. Structural refusals are 409 (not_deployed, deployment_paused, deployment_suspended, model_unavailable) and 404 for an unknown worker; run-endpoint errors use the {error, message} envelope where error IS the snake_case code. prompt = what to do THIS run; omit it and the worker runs on its standing instructions. Rate limit: 30 run-triggers per minute per ACCOUNT (all of this account's keys and clients share it; other accounts do not affect you). On a 429 back off for the Retry-After — never tighten a loop in response. To fan one prompt across many workers, run_bulk takes up to 20 in one call on a window of its own. Runs in flight per worker are bounded by the account's plan (Free 5, Pro 20, Team 50); at that limit this answers a Skipped receipt with skipReason ConcurrencyLimit." +
65
90
  RUNTIME_NOTE,
66
91
  auth: "manager",
67
92
  method: "post",
@@ -74,6 +99,29 @@ const runWorker = {
74
99
  bodyBuilder: (params) => ({ prompt: params.prompt, modelSlug: params.modelSlug }),
75
100
  annotations: TRIGGER,
76
101
  };
102
+ const runWorkersBulk = {
103
+ name: "run_bulk",
104
+ title: "Run Workers in Bulk",
105
+ description: "Run several workers now in ONE call — a prompt fanned out across a fleet without spending the single trigger's 30/min budget one worker at a time. Up to 20 workers per call, each named by workerId (preferred) or its deprecated tokenId (the GUID wins when both are sent), each with its own optional prompt and modelSlug. IT IS NOT ATOMIC AND IT DOES NOT STOP: every worker passes the mint gauntlet on its own, so an unknown id, a missing deployment or a cap already hit is reported on THAT item (success false, errorCode, error) and the loop goes on to the next — a stale id in a fan-out never discards the nineteen runs that were fine. A SKIPPED RECEIPT IS A SUCCESS HERE: a run was minted and its status/skipReason say why it did not start, so read items[] and never the status code or minted alone. Each successful item carries runId, status, skipReason and errorCode; run_events and run_get take the runId from there. Recorded as API-triggered runs, attributed to the manager key's minter. Own window: 2 calls per minute and 20 per hour per ACCOUNT, on top of the surface windows. Requires the runWorkers scope." +
106
+ RUNTIME_NOTE,
107
+ auth: "manager",
108
+ method: "post",
109
+ schema: {
110
+ workers: z
111
+ .array(z.object({
112
+ workerId: z.string().uuid().optional().describe("The worker's stable public id, as every response that names a worker returns it (preferred). Send this or tokenId."),
113
+ tokenId: z.number().int().min(1).optional().describe("The worker's deprecated numeric token ID. workerId wins when both are sent."),
114
+ prompt: z.string().max(8000).optional().describe("What THIS worker should do on this run (≤8000 chars). Omit to run its standing instructions unchanged."),
115
+ modelSlug: z.string().max(64).optional().describe("One-off model override for this worker's run (≤64 chars). Omit to use its configured model."),
116
+ }))
117
+ .min(1)
118
+ .max(20)
119
+ .describe("One entry per worker to run, 1–20 of them; each needs a workerId or a tokenId."),
120
+ },
121
+ path: `${API}/runs/bulk`,
122
+ bodyBuilder: (params) => ({ workers: params.workers }),
123
+ annotations: TRIGGER,
124
+ };
77
125
  // ─── Fleet-wide run reads ───────────────────────────────────────────────────
78
126
  // The two calls that watch a WHOLE fleet without fanning out per worker. Both are
79
127
  // account-scoped by the key itself, so neither takes a worker id, and both are
@@ -81,7 +129,7 @@ const runWorker = {
81
129
  const runsFeed = {
82
130
  name: "runs_feed",
83
131
  title: "Account Run Feed",
84
- description: "Every worker's runs in ONE account-wide feed, newest first — THE fleet-watching call. When you are minding many workers, call this instead of looping workers_list or worker_runs per worker. Each row is a scan line: runId, workerId, tokenTitle, status, outcome (running|succeeded|attention|failed|skipped|canceled — 'attention' is a SUCCEEDED run that hit friction, i.e. firewall denials, tool errors or reported issues, so report it apart from a clean success), skipReason, errorCode, triggerKind, scheduleTitle, timings, the friction counts (toolCalls, firewallDenials, toolErrors, issueCount, hasBlockerIssue), and one line of what the run did in summary + summarySource ('distilled' = a condensed record of what the run ACTUALLY did; 'report' = the worker's OWN closing account; 'error'/'skipped'/'activity' = the machine's own words — these are not equally reliable, so say which one you are quoting). Carries NO cost and NO token counts by design: read those from run_get on the one run that matters. Cursor-paged, never page-numbered — scroll by passing the previous response's nextCursor back as cursor (hasMore says whether another page exists), and re-fetch with NO cursor to refresh the head; there is deliberately no total count. A cursor we did not issue is a 400 invalid_cursor — drop it and reload the head. An unknown status is a 400 invalid_status, never a silently ignored filter. Requires the readRuns scope." +
132
+ description: "Every worker's runs in ONE account-wide feed, newest first — THE fleet-watching call. When you are minding many workers, call this instead of looping workers_list or worker_runs per worker. Each row is a scan line: runId, workerId, tokenTitle, status, outcome (running|succeeded|attention|failed|awaitingInput|skipped|canceled — 'attention' is a SUCCEEDED run that hit friction, i.e. firewall denials, tool errors or reported issues, so report it apart from a clean success; 'awaitingInput' is a run that ended by asking its owner a question — waiting on a person, not broken: run_question reads it, run_answer answers it, and fleet_health lists every open one), skipReason, errorCode, triggerKind, scheduleTitle, timings, the friction counts (toolCalls, firewallDenials, toolErrors, issueCount, hasBlockerIssue), and one line of what the run did in summary + summarySource ('distilled' = a condensed record of what the run ACTUALLY did; 'report' = the worker's OWN closing account; 'error'/'skipped'/'activity' = the machine's own words — these are not equally reliable, so say which one you are quoting). Carries NO cost and NO token counts by design: read those from run_get on the one run that matters. Cursor-paged, never page-numbered — scroll by passing the previous response's nextCursor back as cursor (hasMore says whether another page exists), and re-fetch with NO cursor to refresh the head; there is deliberately no total count. A cursor we did not issue is a 400 invalid_cursor — drop it and reload the head. An unknown status is a 400 invalid_status, never a silently ignored filter. Requires the readRuns scope." +
85
133
  RUNTIME_NOTE,
86
134
  auth: "manager",
87
135
  method: "get",
@@ -92,9 +140,9 @@ const runsFeed = {
92
140
  // two composite values.
93
141
  status: z.enum([
94
142
  "pending", "dispatched", "running", "succeeded", "failed",
95
- "timedOut", "budgetExceeded", "skipped", "canceled",
143
+ "timedOut", "budgetExceeded", "awaitingInput", "skipped", "canceled",
96
144
  "settled", "all",
97
- ]).optional().describe("Filter to one run status. 'settled' is the composite for every terminal outcome (everything not in flight) — use it for a logbook, and fleet_pulse for what is running. 'all' is an explicit no-op. Omit for all runs."),
145
+ ]).optional().describe("Filter to one run status. 'awaitingInput' = runs waiting on their owner's answer (terminal for money, open for the person). 'settled' is the composite for every terminal outcome (everything not in flight, awaitingInput included) — use it for a logbook, and fleet_pulse for what is running. 'all' is an explicit no-op. Omit for all runs."),
98
146
  cursor: z.string().optional().describe("Opaque cursor from the previous response's nextCursor — fetches the next (older) page. Omit to read the head of the feed."),
99
147
  limit: z.number().int().min(1).max(100).default(20).describe("Rows per page (1–100)."),
100
148
  },
@@ -127,8 +175,8 @@ const listRuns = {
127
175
  // read as the real subset. These are the WorkerRunStatus names verbatim.
128
176
  status: z.enum([
129
177
  "pending", "dispatched", "running", "succeeded", "failed",
130
- "timedOut", "budgetExceeded", "skipped", "canceled",
131
- ]).optional().describe("Filter by run status. Omit for all runs."),
178
+ "timedOut", "budgetExceeded", "awaitingInput", "skipped", "canceled",
179
+ ]).optional().describe("Filter by run status ('awaitingInput' = waiting on the owner's answer). Omit for all runs."),
132
180
  page: z.number().int().min(1).default(1).describe("1-based page number."),
133
181
  pageSize: z.number().int().min(1).max(100).default(20).describe("Results per page (max 100)."),
134
182
  fromUtc: z.string().optional().describe(`Only runs started at/after this time. ${UTC_HINT}`),
@@ -175,6 +223,20 @@ const getRunEvents = {
175
223
  },
176
224
  annotations: READ_ONLY,
177
225
  };
226
+ const getRunTranscript = {
227
+ name: "run_transcript",
228
+ title: "Get Run Transcript",
229
+ description: "The run's stored LLM process log — the raw record behind the digest: every model turn and tool call, as the worker made them. EXPECT 404 no_transcript MORE OFTEN THAN 200: transcripts are OFF by default and kept for 7 days, so this answers only when the worker's deployment had transcriptRetention on before the run happened and the window has not elapsed; a worker installed from a protected kit never stores one. The run receipt's transcriptAvailable (run_get) says whether this returns 200 — branch on that rather than calling this to find out. For WHAT a run did, the digest on the receipt is the intended handoff; reach for the transcript when the digest is not enough: a run that misbehaved, a tool call to inspect, a denial to trace. Up to 1 MB. Requires the readRuns scope." +
230
+ RUNTIME_NOTE,
231
+ auth: "manager",
232
+ method: "get",
233
+ schema: {
234
+ runId: z.string().uuid().describe(RUN_ID_HINT),
235
+ },
236
+ path: (params) => `${API}/runs/${params.runId}/transcript`,
237
+ paramFilter: () => ({}),
238
+ annotations: READ_ONLY,
239
+ };
178
240
  // ─── Two-way runs (ask / answer) ────────────────────────────────────────────
179
241
  // A run that needs something from its owner ENDS by asking (status AwaitingInput)
180
242
  // rather than blocking on a human while holding a request slot and a wallet
@@ -334,12 +396,17 @@ const SCHEDULE_TYPE_HINT = "EveryNMinutes (intervalValue 1–1440), EveryNHours
334
396
  // is in `feature`, while `error` holds the literal string "feature_gated" — the
335
397
  // one place on this surface where `error` is not the code.
336
398
  const SCHEDULE_GATE_HINT = " Two plan walls return 402 {error:'feature_gated', feature, upgradeTrigger, message}: feature 'schedule_cadence' means minute-based schedules are not in this plan (use EveryNHours or DailyAtTime), and 'schedule_count' means the worker is at its recurring-schedule cap (a OneShot is never counted). Both are terminal — do NOT retry; either pick an allowed cadence or tell the human to upgrade.";
399
+ // The plan's per-worker concurrency is for runs started on demand. A schedule
400
+ // never overlaps itself, whatever the plan allows: two copies of the same
401
+ // scheduled job side by side duplicate its side effects.
402
+ const SCHEDULE_OVERLAP_NOTE = " A schedule never overlaps itself: a fire that finds any run of the worker in flight lands as a Skipped receipt (skipReason ConcurrencyLimit) whatever the plan's per-worker concurrency allows — space the schedule wider than a run takes. Runs started on demand may run alongside a scheduled one, up to the plan's limit.";
337
403
  const createSchedule = {
338
404
  name: "schedule_create",
339
405
  title: "Create Schedule",
340
406
  description: "Create a schedule for the worker. Per-type required fields: " + SCHEDULE_TYPE_HINT +
341
407
  " Omit timeZoneId to follow the worker's own time zone (the normal case). Requires the manageSchedules scope." +
342
- SCHEDULE_GATE_HINT,
408
+ SCHEDULE_GATE_HINT +
409
+ SCHEDULE_OVERLAP_NOTE,
343
410
  auth: "manager",
344
411
  method: "post",
345
412
  schema: {
@@ -368,7 +435,7 @@ const updateSchedule = {
368
435
  name: "schedule_update",
369
436
  title: "Update Schedule",
370
437
  description: "Edit a schedule (partial — omitted fields stay unchanged; timeZoneId='' clears the override so the schedule follows the worker's zone again). nextRunUtc is recomputed on every edit. Per-type field rules: " +
371
- SCHEDULE_TYPE_HINT + " Requires manageSchedules." + SCHEDULE_GATE_HINT,
438
+ SCHEDULE_TYPE_HINT + " Requires manageSchedules." + SCHEDULE_GATE_HINT + SCHEDULE_OVERLAP_NOTE,
372
439
  auth: "manager",
373
440
  method: "patch",
374
441
  schema: {
@@ -639,7 +706,7 @@ const restoreInstructionVersion = {
639
706
  const setWorkerEnabled = {
640
707
  name: "worker_set_enabled",
641
708
  title: "Start or Stop Worker",
642
- description: "Start (enabled=true) or stop (enabled=false) a worker. Stopping pauses the WHOLE worker: its own API key stops working AND its schedules stop firing; re-enabling restores both. Returns {tokenId, isEnabled}. Idempotent. Requires the manageState scope.",
709
+ description: "Start (enabled=true) or stop (enabled=false) a worker. Stopping pauses the WHOLE worker: its own API key stops working AND its schedules stop firing; re-enabling restores both. THIS IS THE REVERSIBLE ONE — reach for it whenever someone says stop, pause, turn off or disable; worker_delete is permanent and nothing undoes it. Two things about stopping an ORCHESTRATOR that the response does not tell you: it CASCADES to that worker's sub-workers (children and grandchildren), and re-enabling the parent brings back ONLY the parent — each sub-worker needs its own worker_set_enabled(tokenId, true), so check workers_list afterwards rather than assuming the fleet came back. And re-enabling consumes a worker slot: at the plan's worker cap it is refused with 402 limit_exceeded, so a worker stopped before a downgrade can be stuck stopped until the owner upgrades or deletes another worker. Returns {tokenId, isEnabled}. Idempotent. Requires the manageState scope.",
643
710
  auth: "manager",
644
711
  method: "post",
645
712
  schema: {
@@ -650,6 +717,19 @@ const setWorkerEnabled = {
650
717
  bodyBuilder: (params) => ({ enabled: params.enabled }),
651
718
  annotations: UPDATE,
652
719
  };
720
+ const deleteWorker = {
721
+ name: "worker_delete",
722
+ title: "Delete Worker",
723
+ description: "Delete a worker PERMANENTLY. Not reversible by any call, on any surface: its own pe_ key stops working immediately (an agent holding it starts failing), its schedules stop firing, its instruction, memory, deployment and delivery destinations go with it, and it disappears from this API, MCP, the CLI and the dashboard alike. TO STOP A WORKER YOU MIGHT WANT BACK, USE worker_set_enabled WITH enabled:false — that pauses the key and the schedules together and is reversible (with two caveats it states: stopping cascades to sub-workers and re-enabling brings back only the worker you name, and re-enabling is refused with 402 at the plan's worker cap); THIS one cannot be undone at all, so confirm with the person before calling it, never infer it from 'get rid of', 'turn off' or 'stop'. THE SURPRISE: deleting an orchestrator DELETES ITS SUB-WORKERS TOO (children and grandchildren — a dead orchestrator must never leave live workers behind), and the response's subWorkersDeleted says how many went with it; report that number, because those workers had their own jobs. What SURVIVES: the apps stay connected for every other worker on the operator, and the run receipts stay readable through the account-wide reads (runs_feed, run_get) — a deleted worker's spending is still part of the account's history. A run in flight settles normally and is neither cancelled nor refunded; runsInFlight reports how many were running when the delete landed. Deleting FREES A WORKER SLOT, which is the fix for kit_install's 402 limit_exceeded when the owner would rather not upgrade. 404 means the worker does not exist or is not on this account — including a second delete of one already gone, so a 404 on a retry means the first call worked. Requires the deleteWorkers scope." +
724
+ NEW_SCOPE_NOTE,
725
+ auth: "manager",
726
+ method: "delete",
727
+ schema: {
728
+ tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
729
+ },
730
+ path: (params) => `${API}/${params.tokenId}`,
731
+ annotations: DELETE,
732
+ };
653
733
  // ─── Kit install ────────────────────────────────────────────────────────────
654
734
  // The one place on this surface an agent can CREATE a worker: install a kit from
655
735
  // the public directory. Both tools need the installKits scope (keys minted
@@ -682,7 +762,7 @@ const kitInstallPreview = {
682
762
  const kitInstall = {
683
763
  name: "kit_install",
684
764
  title: "Install Kit",
685
- description: "Install a directory kit as a NEW worker on this account. NOT idempotent: every successful call creates another worker — never retry a success, and on a timeout check workers_list (needs the readWorkers scope) before trying again. Requires the installKits scope. Call kit_install_preview first and supply every requiredInputs key in inputs (a missing or unknown key is a 400 naming it), answers to required memorySetup questions in memoryAnswers, and one categoryChoices entry per slot (member with connectionProvider set → also pass resourceId from the preview's operatorResources). CRITICAL — secrets shown ONCE: the response's install.rawKey (the worker's pe_ API key) and install.triggers[].signingSecret can NEVER be read again; deliver them to the human immediately and do not discard the response before doing so. The response also carries readiness (status ready|blocked with actionable issues, e.g. app_not_connected + candidateProviders; null means the readiness check itself failed AFTER the install succeeded — do not retry the install, read readiness via worker_get), workerUrl (the worker's dashboard page — hand it to the human), and connectAppsUrl (where the human connects missing apps in the browser; agents cannot connect apps). A 402 {error:'limit_exceeded'} is the plan's worker cap — terminal, do not retry; tell the human to upgrade or free a slot. Rate: 10 installs/hour per account.",
765
+ description: "Install a directory kit as a NEW worker on this account. NOT idempotent: every successful call creates another worker — never retry a success, and on a timeout check workers_list (needs the readWorkers scope) before trying again. Requires the installKits scope. Call kit_install_preview first and supply every requiredInputs key in inputs (a missing or unknown key is a 400 naming it), answers to required memorySetup questions in memoryAnswers, and one categoryChoices entry per slot (member with connectionProvider set → also pass resourceId from the preview's operatorResources). CRITICAL — secrets shown ONCE: the response's install.rawKey (the worker's pe_ API key) and install.triggers[].signingSecret can NEVER be read again; deliver them to the human immediately and do not discard the response before doing so. The response also carries readiness (status ready|blocked with actionable issues, e.g. app_not_connected + candidateProviders; null means the readiness check itself failed AFTER the install succeeded — do not retry the install, read readiness via worker_get), workerUrl (the worker's dashboard page — hand it to the human), and connectAppsUrl (where the human connects missing apps in the browser; agents cannot connect apps). A 402 {error:'limit_exceeded'} is the plan's worker cap — terminal, never retry the install. Two ways out: the human upgrades, or a worker is deleted to free the slot (worker_delete, with the deleteWorkers scope) — propose that only with the person's agreement, since deleting is permanent. Rate: 10 installs/hour per account. FINISHING THE JOB — installing does NOT make the worker run: without a deployment it fires no schedule and worker_run refuses it with not_deployed. Send deploy:true (optionally with modelSlug from models_list and the ceilings) to install and deploy in ONE call; the response then carries deployment. A deploy refused after the install still returns 201 with deploymentError naming what to fix — the worker exists either way, so never re-install; fix it and call worker_deploy on the same worker. deploy:true needs the manageDeployments scope IN ADDITION to installKits — a key without it is refused up front (403 OPERATION_NOT_ALLOWED naming the scope) and nothing is created, so drop deploy and install anyway, then ask the owner to re-scope the key before worker_deploy.",
686
766
  auth: "manager",
687
767
  method: "post",
688
768
  schema: {
@@ -697,6 +777,14 @@ const kitInstall = {
697
777
  })).optional().describe("One entry per categorySlots slot. Omitted slots resolve to the kit's first offered member on safe defaults."),
698
778
  inputs: z.record(z.string(), z.string()).optional().describe("Answers to requiredInputs, keyed EXACTLY by each entry's key. Every key is mandatory; values are single-line, 1-200 chars."),
699
779
  memoryAnswers: z.record(z.string(), z.string()).optional().describe("Answers to memorySetup questions by key. Entries with required:true are mandatory. Keep this separate from inputs — mixing the two maps is a 400."),
780
+ deploy: z.boolean().optional().describe("true = also put the new worker on the hosted runtime, so it actually runs. Without this the install creates a worker that fires no schedule and refuses worker_run with not_deployed. Needs the manageDeployments scope."),
781
+ deployment: z.object({
782
+ modelSlug: z.string().max(64).optional().describe("Model to deploy on, from models_list. Omit for the kit's recommended model."),
783
+ maxUsdPerRun: z.number().min(0.01).max(1000).optional().describe("Hard per-run ceiling in USD. Omit for the platform default."),
784
+ maxUsdPerDay: z.number().min(0.01).max(10000).optional().describe("Per-day reserved-spend ceiling, at least maxUsdPerRun. Omit for the platform default."),
785
+ thinking: z.string().max(16).optional().describe("Reasoning setting for the chosen model — see worker_deploy. Omit for 'default'."),
786
+ transcriptRetention: z.string().max(16).optional().describe("Transcript setting. Omit for the platform default."),
787
+ }).optional().describe("Deployment settings, all optional. Sending this implies deploy:true — settings for a step you did not want make no sense."),
700
788
  },
701
789
  path: (params) => `${KITS_API}/${encodeURIComponent(String(params.slug))}/install`,
702
790
  bodyBuilder: (params) => ({
@@ -706,6 +794,8 @@ const kitInstall = {
706
794
  categoryChoices: params.categoryChoices,
707
795
  inputs: params.inputs,
708
796
  memoryAnswers: params.memoryAnswers,
797
+ deploy: params.deploy,
798
+ deployment: params.deployment,
709
799
  }),
710
800
  annotations: CREATE,
711
801
  };
@@ -805,7 +895,7 @@ const BUDGET_WINDOW_NOTE = " Windows differ, deliberately: maxUsdPerDay and both
805
895
  const getWorkerBudget = {
806
896
  name: "budget_get",
807
897
  title: "Get Worker Budget",
808
- description: "One worker's spend and rate ceilings: maxUsdPerRun, maxUsdPerDay, maxRunsPerDay, maxConcurrentRuns, modelSlug, hasDeployment (plus workerId and its deprecated alias tokenId). Four readings an agent gets wrong by default. (1) maxRunsPerDay NULL MEANS THE PLATFORM DEFAULT, NOT UNLIMITED — the gate substitutes the platform's own number, so never report a null as 'no limit'. (2) maxUsdPerRun is the amount RESERVED from the wallet at dispatch, so it is also what a run must be able to AFFORD before it starts; and the day cap is counted against those RESERVATIONS rather than settled cost, so a worker reserving $0.50 under a $2/day cap is skipped on its fifth run of the day even if each one really cost a cent (the reservation is refunded when a later gate skips the run). (3) maxConcurrentRuns is enforced at mint but is CURRENTLY FIXED AT 1 rather than configurable — it is reported so the number you see is the number the gate uses, and budget_set has no parameter for it. (4) hasDeployment FALSE means hosted runs are not set up for this worker, and then the dollar figures come back as 0 because there is no deployment to read them from — that 0 means 'no deployment', NOT 'capped at zero', so check hasDeployment before quoting any ceiling. Per-worker caps do not compose; the ceiling above them is fleet_budget_get. Requires the manageBudgets scope — reading a ceiling rides the same scope as changing it." +
898
+ description: "One worker's spend and rate ceilings: maxUsdPerRun, maxUsdPerDay, maxRunsPerDay, maxConcurrentRuns, modelSlug, hasDeployment (plus workerId and its deprecated alias tokenId). Four readings an agent gets wrong by default. (1) maxRunsPerDay NULL MEANS THE PLATFORM DEFAULT, NOT UNLIMITED — the gate substitutes the platform's own number, so never report a null as 'no limit'. (2) maxUsdPerRun is the amount RESERVED from the wallet at dispatch, so it is also what a run must be able to AFFORD before it starts; and the day cap is counted against those RESERVATIONS rather than settled cost, so a worker reserving $0.50 under a $2/day cap is skipped on its fifth run of the day even if each one really cost a cent (the reservation is refunded when a later gate skips the run). (3) maxConcurrentRuns is SET BY THE ACCOUNT'S PLAN, not per worker — Free 5, Pro 20, Team 50, Enterprise uncapped — so budget_set has no parameter for it and raising it is an upgrade, not a request; it is reported so the number you see is the number the gate uses. It bounds runs started on demand (worker_run, run_bulk, webhooks); a schedule never overlaps itself whatever the plan allows. (4) hasDeployment FALSE means hosted runs are not set up for this worker, and then the dollar figures come back as 0 because there is no deployment to read them from — that 0 means 'no deployment', NOT 'capped at zero', so check hasDeployment before quoting any ceiling. Per-worker caps do not compose; the ceiling above them is fleet_budget_get. Requires the manageBudgets scope — reading a ceiling rides the same scope as changing it." +
809
899
  BUDGET_WINDOW_NOTE +
810
900
  NEW_SCOPE_NOTE,
811
901
  auth: "manager",
@@ -820,7 +910,7 @@ const getWorkerBudget = {
820
910
  const setWorkerBudget = {
821
911
  name: "budget_set",
822
912
  title: "Set Worker Budget",
823
- description: "Change one worker's ceilings, and get the whole budget back as it now stands. PARTIAL: every field is optional and an OMITTED FIELD MEANS UNCHANGED, so you can raise one ceiling without restating the rest and without racing another writer's edit to a different field. Three traps. Setting maxUsdPerRun or maxUsdPerDay on a worker with NO HOSTED DEPLOYMENT is a 400 — there is nothing for a dollar cap to bind to, and budget_get's hasDeployment says which workers those are; maxRunsPerDay, by contrast, can be set on any worker. There is NO way to clear maxRunsPerDay back to the platform default here, because null already means 'leave it alone': send an explicit number instead. And LOWERING maxUsdPerRun below what a run needs does not fail loudly — it makes that worker's next run a Skipped receipt with a skipReason, which is a receipt an agent must go and read. maxConcurrentRuns is not settable (fixed at 1) and is absent from this call by design. A ZERO per-run cap is REFUSED (400), as is a day cap below the per-run cap: a zero reserve is a run that cannot start, not a worker that spends nothing — to stop a worker, use worker_set_enabled. maxRunsPerDay is clamped to the organization policy ceiling when one is enforced. The same call sets the question timeout: awaitInputTimeoutMinutes (how long a question the worker asks stays open; clearAwaitInputTimeout removes the bound). When it passes the question is closed out, never resumed with no answer, and a late answer still starts the run. Requires the manageBudgets scope." +
913
+ description: "Change one worker's ceilings, and get the whole budget back as it now stands. PARTIAL: every field is optional and an OMITTED FIELD MEANS UNCHANGED, so you can raise one ceiling without restating the rest and without racing another writer's edit to a different field. Three traps. Setting maxUsdPerRun or maxUsdPerDay on a worker with NO HOSTED DEPLOYMENT is a 400 — there is nothing for a dollar cap to bind to, and budget_get's hasDeployment says which workers those are; maxRunsPerDay, by contrast, can be set on any worker. There is NO way to clear maxRunsPerDay back to the platform default here, because null already means 'leave it alone': send an explicit number instead. And LOWERING maxUsdPerRun below what a run needs does not fail loudly — it makes that worker's next run a Skipped receipt with a skipReason, which is a receipt an agent must go and read. maxConcurrentRuns is absent from this call by design: it is set by the account's plan (Free 5, Pro 20, Team 50), so raising it is an upgrade, not a request. A ZERO per-run cap is REFUSED (400), as is a day cap below the per-run cap: a zero reserve is a run that cannot start, not a worker that spends nothing — to stop a worker, use worker_set_enabled. maxRunsPerDay is clamped to the organization policy ceiling when one is enforced. The same call sets the question timeout: awaitInputTimeoutMinutes (how long a question the worker asks stays open; clearAwaitInputTimeout removes the bound). When it passes the question is closed out, never resumed with no answer, and a late answer still starts the run. Requires the manageBudgets scope." +
824
914
  BUDGET_WINDOW_NOTE +
825
915
  NEW_SCOPE_NOTE,
826
916
  auth: "manager",
@@ -876,6 +966,122 @@ const setFleetBudget = {
876
966
  }),
877
967
  annotations: UPDATE,
878
968
  };
969
+ // ─── Deployment (the hosted runtime) ────────────────────────────────────────
970
+ // The step between "a worker exists" and "a worker runs". Installing a kit or
971
+ // cloning without the deployment flag leaves a worker that is fully configured
972
+ // and completely inert: no schedule fires and worker_run answers 409
973
+ // not_deployed. Deploying is also where the model and the spend ceilings are
974
+ // chosen, which is why the writes sit behind their own scope.
975
+ const DEPLOYMENT_NOTE = " A worker with NO DEPLOYMENT is configuration only: its schedules never fire and worker_run refuses it with 409 not_deployed. Deploying is what puts it on the runtime, and it is also where the model and the per-run / per-day spend ceilings are set. Reading a deployment rides on readWorkers; every write here needs manageDeployments.";
976
+ const listModels = {
977
+ name: "models_list",
978
+ title: "List Deployable Models",
979
+ description: "The models this ACCOUNT may deploy a worker on, priced per million tokens — the picker for worker_deploy's modelSlug. Each row carries slug (the value deployment calls take), displayName, provider, inputUsdPerMTok / outputUsdPerMTok / cachedInputUsdPerMTok / cacheWriteInputUsdPerMTok, contextWindowK, minTier, recommended, and the reasoning vocabulary. A MODEL ABSENT FROM THIS LIST IS NOT DEPLOYABLE HERE — either the account's tier does not reach it or no live provider serves it — so never pass a slug you read somewhere else; that is a 400 one call later. reasoningStyle says what the deployment's thinking field accepts FOR THAT MODEL: 'budget' takes a per-turn token count AS A STRING ('1024', between thinkingBudgetMin and thinkingBudgetMax), 'effort' takes one of reasoningEffortOptions ('high'), 'none' takes only 'default' or 'off'. Sending the wrong kind is 400 invalid_thinking. byokProviders lists the providers this account holds its own API key for (model_keys_list) — a run on a model from one of those bills the account's key plus a platform fee instead of the wallet. Pass kitSlug to have the kit's own recommendation marked recommended:true; without it nothing is marked. Requires readWorkers." +
980
+ RUNTIME_NOTE,
981
+ auth: "manager",
982
+ method: "get",
983
+ schema: {
984
+ kitSlug: z.string().max(120).optional().describe("Kit slug whose recommended model should be marked recommended:true — typically the kit you are about to install or just installed. Omit when no kit is involved."),
985
+ },
986
+ path: `${API}/models`,
987
+ annotations: READ_ONLY,
988
+ };
989
+ const listDeployments = {
990
+ name: "deployments_list",
991
+ title: "List Deployments",
992
+ description: "Every DEPLOYED worker on the account with its model, spend ceilings and deployment status — the fleet answer to \"what is actually able to run\". A worker that appears in workers_list but NOT here is not on the runtime, however complete it looks: that is the single most common reason a newly built worker never produces a run. Account-scoped by the key, so it takes no worker id. Requires readWorkers." +
993
+ RUNTIME_NOTE,
994
+ auth: "manager",
995
+ method: "get",
996
+ schema: {},
997
+ path: `${API}/deployments`,
998
+ annotations: READ_ONLY,
999
+ };
1000
+ const getDeployment = {
1001
+ name: "deployment_get",
1002
+ title: "Get Deployment",
1003
+ description: "One worker's deployment: status, modelSlug + modelDisplayName, maxUsdPerRun / maxUsdPerDay, transcriptRetention (+ transcriptRetentionAvailable — false on a worker installed from a protected kit, where the control should not even be offered), thinking, allowPlatformEgress and warnings. 404 not_found means THE WORKER IS NOT DEPLOYED (or is not on this account — the two are indistinguishable by design); the fix is worker_deploy, not a retry. status: Provisioned / Active = it runs; Paused = schedules and run triggers are refused until deployment_update action:'resume'; Suspended = the platform stopped it and only support can lift that. Requires readWorkers." +
1004
+ RUNTIME_NOTE,
1005
+ auth: "manager",
1006
+ method: "get",
1007
+ schema: {
1008
+ tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
1009
+ },
1010
+ path: (params) => `${API}/${params.tokenId}/deployment`,
1011
+ paramFilter: () => ({}),
1012
+ annotations: READ_ONLY,
1013
+ };
1014
+ const deployWorker = {
1015
+ name: "worker_deploy",
1016
+ title: "Deploy Worker",
1017
+ description: "Put the worker on the hosted runtime — the step that makes it runnable and schedulable, and the one an agent most often forgets after kit_install (kit_install's own deploy:true does both in one call). EVERYTHING IS OPTIONAL: send nothing and the worker deploys on the model its kit recommends with the platform's default ceilings; 400 model_required means the kit named none, so pick one from models_list. The refusals are the deploy gauntlet and each names its fix: 422 instruction_required (the worker has no instruction — a hosted run has nothing to execute), 422 apps_not_connected (the message names the apps; fix with app_connect, or drop the app from the worker — a half-connected worker is refused rather than allowed to burn spend on a partial answer), 402 wallet_required (hosted runs meter from the prepaid wallet and AN AGENT CANNOT TOP IT UP — tell the human), 402 hosted_worker_limit, 409 already_deployed (use deployment_update instead), 400 model_unavailable / model_tier (choose another from models_list), 400 invalid_thinking (wrong reasoning vocabulary for the model), 400 invalid_caps (maxUsdPerDay below maxUsdPerRun). warnings[] on success is advisory and never blocks: ip_rules_enforced means the worker's IP allowlist will reject hosted runs, which come from platform egress. Requires manageDeployments." +
1018
+ DEPLOYMENT_NOTE +
1019
+ RUNTIME_NOTE +
1020
+ NEW_SCOPE_NOTE,
1021
+ auth: "manager",
1022
+ method: "post",
1023
+ schema: {
1024
+ tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
1025
+ modelSlug: z.string().max(64).optional().describe("Model to run on, from models_list (its slug field). Omit to take the kit's recommended model — 400 model_required when the kit named none."),
1026
+ maxUsdPerRun: z.number().min(0.01).max(1000).optional().describe("Hard per-run ceiling in USD, 0.01-1000 — also the amount reserved from the wallet at dispatch, so it is what a run must be able to AFFORD to start. Omit for the platform default."),
1027
+ maxUsdPerDay: z.number().min(0.01).max(10000).optional().describe("Ceiling on this worker's RESERVED spend per day, 0.01-10000. Must be at least maxUsdPerRun. Omit for the platform default."),
1028
+ thinking: z.string().max(16).optional().describe("Per-worker reasoning, validated against the CHOSEN MODEL's reasoningStyle (models_list): 'default' (the catalog decides), 'off', a token budget as a string ('1024') for budget-style models, or an effort word ('high') for effort-style models. Omit for 'default'."),
1029
+ transcriptRetention: z.string().max(16).optional().describe("Whether full run transcripts are kept. Omit for the platform default; a worker from a protected kit is clamped to 'off' silently, because that transcript would carry the publisher's own instructions."),
1030
+ },
1031
+ path: (params) => `${API}/${params.tokenId}/deployment`,
1032
+ bodyBuilder: (params) => ({
1033
+ modelSlug: params.modelSlug,
1034
+ maxUsdPerRun: params.maxUsdPerRun,
1035
+ maxUsdPerDay: params.maxUsdPerDay,
1036
+ thinking: params.thinking,
1037
+ transcriptRetention: params.transcriptRetention,
1038
+ }),
1039
+ annotations: CREATE,
1040
+ };
1041
+ const updateDeployment = {
1042
+ name: "deployment_update",
1043
+ title: "Update Deployment",
1044
+ description: "Change a live deployment, or pause and resume it. PARTIAL: an OMITTED FIELD MEANS UNCHANGED, so one ceiling can be raised without restating the rest. action:'pause' stops schedules and run triggers while KEEPING the deployment, its model and its ceilings — the reversible way to stop a worker spending, and the one to reach for before worker_undeploy; action:'resume' puts it back, and is refused on a Suspended deployment (only support lifts that). One interplay to know: changing modelSlug WITHOUT sending thinking keeps a still-valid reasoning setting and otherwise clears it to 'default', reporting a thinking_reset warning rather than refusing the model change. 404 not_found = the worker is not deployed; deploy it with worker_deploy first. Requires manageDeployments." +
1045
+ DEPLOYMENT_NOTE +
1046
+ RUNTIME_NOTE +
1047
+ NEW_SCOPE_NOTE,
1048
+ auth: "manager",
1049
+ method: "patch",
1050
+ schema: {
1051
+ tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
1052
+ modelSlug: z.string().max(64).optional().describe("New model, from models_list. Omit to keep. Read the thinking interplay in the description before changing it."),
1053
+ maxUsdPerRun: z.number().min(0.01).max(1000).optional().describe("New per-run ceiling in USD, 0.01-1000. Must stay at or below maxUsdPerDay. Omit to keep."),
1054
+ maxUsdPerDay: z.number().min(0.01).max(10000).optional().describe("New per-day reserved-spend ceiling, 0.01-10000. Must be at least maxUsdPerRun. Omit to keep."),
1055
+ thinking: z.string().max(16).optional().describe("New reasoning setting for the CURRENT model: 'default', 'off', a token budget as a string ('1024'), or an effort word ('high'). Omit to keep."),
1056
+ transcriptRetention: z.string().max(16).optional().describe("New transcript setting. Omit to keep."),
1057
+ action: z.enum(["pause", "resume"]).optional().describe("'pause' = stop schedules and run triggers, keeping the deployment; 'resume' = start again (refused while Suspended). Omit to change settings only."),
1058
+ },
1059
+ path: (params) => `${API}/${params.tokenId}/deployment`,
1060
+ bodyBuilder: (params) => ({
1061
+ modelSlug: params.modelSlug,
1062
+ maxUsdPerRun: params.maxUsdPerRun,
1063
+ maxUsdPerDay: params.maxUsdPerDay,
1064
+ thinking: params.thinking,
1065
+ transcriptRetention: params.transcriptRetention,
1066
+ action: params.action,
1067
+ }),
1068
+ annotations: UPDATE,
1069
+ };
1070
+ const undeployWorker = {
1071
+ name: "worker_undeploy",
1072
+ title: "Undeploy Worker",
1073
+ description: "Take the worker OFF the hosted runtime. The worker itself survives untouched — its permissions, instruction, memory, schedules and its own pe_ API key all remain, and it stays usable over MCP; it simply stops running and stops costing anything, and deployment_get answers 404 afterwards. Deploy it again with worker_deploy. PREFER deployment_update action:'pause' when the intent is 'stop for now': pause keeps the model and the ceilings, while this discards them. 204 on success, 404 when the worker was not deployed. Requires manageDeployments." +
1074
+ RUNTIME_NOTE +
1075
+ NEW_SCOPE_NOTE,
1076
+ auth: "manager",
1077
+ method: "delete",
1078
+ schema: {
1079
+ tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
1080
+ },
1081
+ path: (params) => `${API}/${params.tokenId}/deployment`,
1082
+ successMessage: "Worker undeployed. It still exists and keeps its instruction, memory and schedules; deploy it again with worker_deploy to make it run.",
1083
+ annotations: DELETE,
1084
+ };
879
1085
  // ─── Permissions (read-only) ────────────────────────────────────────────────
880
1086
  const getWorkerPermissions = {
881
1087
  name: "worker_permissions_get",
@@ -969,7 +1175,7 @@ const listMyKits = {
969
1175
  const validateKit = {
970
1176
  name: "kit_validate",
971
1177
  title: "Validate Kit",
972
- description: "Dry-run a publish: the SAME body kit_publish takes, judged by every publish gate at once — creates nothing. Returns canPublish (the verdict), errors[] and warnings[] each {section, message} (sections: metadata, categories, permissions, mcp, labels, scanner, scanner-llm, memory, appDescriptions, publisher, limits — the publish itself stops at the FIRST problem, this reports them all), lintWarnings[] (advisory structure nudges), requiredInputs[] (install-form fields the text declares), appLabels[], memorySetup[] (normalized) and manifestPreview (the exact permission manifest that would publish). The document is judged as a PUBLIC publish (visibility is a publish-time choice, not validated), so a private publish is strictly more permissive than its dry run. Only authored content can be dry-run: sourceWorkerId has nothing to validate (400). Pass kitRef when the body will REPLACE an existing listing (kit_replace), so the 50-kit cap and the republish rules are judged against that listing. Repeat until canPublish is true, then kit_publish the identical body." +
1178
+ description: "Dry-run a publish: the SAME body kit_publish takes, judged by every publish gate at once — creates nothing. Returns canPublish (the verdict), errors[] and warnings[] each {section, message} (sections: metadata, categories, permissions, mcp, labels, scanner, scanner-llm, memory, appDescriptions, publisher, limits — the publish itself stops at the FIRST problem, this reports them all), lintWarnings[] (advisory structure nudges), requiredInputs[] (install-form fields the text declares), appLabels[], memorySetup[] (normalized) and manifestPreview (the exact permission manifest that would publish). The document is judged as a PUBLIC publish, so a private publish is strictly more permissive than its dry run — the one exception is the public-kit cap, which follows the body visibility because private kits are unlimited. Only authored content can be dry-run: sourceWorkerId has nothing to validate (400). Pass kitRef when the body will REPLACE an existing listing (kit_replace), so the 100-public-kit cap and the republish rules are judged against that listing. Repeat until canPublish is true, then kit_publish the identical body." +
973
1179
  AUTHORING_FLOW_NOTE,
974
1180
  auth: "manager",
975
1181
  method: "post",
@@ -988,7 +1194,7 @@ const validateKit = {
988
1194
  const publishKit = {
989
1195
  name: "kit_publish",
990
1196
  title: "Publish Kit",
991
- description: "Publish a NEW kit on this account — from authored content, or from any worker on the account (sourceWorkerId: its instruction, texts, permissions, schedules and triggers become the kit; rule VALUES never travel, only that rules exist). Exactly one of content / sourceWorkerId. NOT idempotent: every successful call creates another listing — never retry a success; on a timeout read my_kits_list before trying again. visibility 'private' (recommended first) makes a kit only this account can install: kit_install its slug and you have a worker built from scratch through the reviewed manifest pipeline. 'public' (default) lists it in the directory after the fail-closed supply-chain scan — 503 kit_scan_unavailable means retry later with the SAME body, or publish private. 400 names the FIRST failing rule (run kit_validate first to see them all). 403 = the source worker was installed from a protected kit, whose text belongs to that kit's publisher. 409 = publisherName taken (first publish only). Caps: 20 publishes/hour per account, 50 published kits per account. Returns the listing (slug, status, moderationStatus, requiredInputs, lintWarnings, …); a public listing can still be flagged minutes later by the semantic scan — re-read my_kits_list, and kit_scan_get has the findings." +
1197
+ description: "Publish a NEW kit on this account — from authored content, or from any worker on the account (sourceWorkerId: its instruction, texts, permissions, schedules and triggers become the kit; rule VALUES never travel, only that rules exist). Exactly one of content / sourceWorkerId. NOT idempotent: every successful call creates another listing — never retry a success; on a timeout read my_kits_list before trying again. visibility 'private' (recommended first) makes a kit only this account can install: kit_install its slug and you have a worker built from scratch through the reviewed manifest pipeline. 'public' (default) lists it in the directory after the fail-closed supply-chain scan — 503 kit_scan_unavailable means retry later with the SAME body, or publish private. 400 names the FIRST failing rule (run kit_validate first to see them all). 403 = the source worker was installed from a protected kit, whose text belongs to that kit's publisher. 409 = publisherName taken (first publish only). Caps: 20 publishes/hour per account, 100 PUBLIC kits per account (private kits are unlimited; an unlisted or admin-removed listing still holds its slot). Returns the listing (slug, status, moderationStatus, requiredInputs, lintWarnings, …); a public listing can still be flagged minutes later by the semantic scan — re-read my_kits_list, and kit_scan_get has the findings." +
992
1198
  AUTHORING_FLOW_NOTE,
993
1199
  auth: "manager",
994
1200
  method: "post",
@@ -1039,7 +1245,7 @@ const updateKit = {
1039
1245
  const replaceKit = {
1040
1246
  name: "kit_replace",
1041
1247
  title: "Replace Kit",
1042
- description: "Replace a listing WHOLESALE from authored content — the same body as kit_publish with content (required; sourceWorkerId is not allowed here). Unlike kit_update this also replaces schedules, triggers, usage windows and trigger modes. visibility chooses the resulting state and is how a private kit is PROMOTED to public (runs the scan) or a public one demoted. Timeframes, contact policy and mcpServers the content leaves null carry over from the stored manifest — a republish never silently downgrades them. The slug never changes for an already-public kit; a born-private kit takes its clean slug on its first public release. 400 on a listing an admin removed or that is taken down. Validate first: kit_validate with the same body and this kitRef. Requires the publishKits scope." +
1248
+ description: "Replace a listing WHOLESALE from authored content — the same body as kit_publish with content (required; sourceWorkerId is not allowed here). Unlike kit_update this also replaces schedules, triggers, usage windows and trigger modes. visibility chooses the resulting state and is how a private kit is PROMOTED to public (runs the scan, and is the one republish the 100-public-kit cap can refuse — a kit already on the public lane keeps its slot) or a public one demoted. Timeframes, contact policy and mcpServers the content leaves null carry over from the stored manifest — a republish never silently downgrades them. The slug never changes for an already-public kit; a born-private kit takes its clean slug on its first public release. 400 on a listing an admin removed or that is taken down. Validate first: kit_validate with the same body and this kitRef. Requires the publishKits scope." +
1043
1249
  NEW_SCOPE_NOTE,
1044
1250
  auth: "manager",
1045
1251
  method: "put",
@@ -1338,11 +1544,15 @@ export const manageDescriptors = [
1338
1544
  listWorkers,
1339
1545
  getWorker,
1340
1546
  runWorker,
1547
+ runWorkersBulk,
1341
1548
  runsFeed,
1342
1549
  fleetPulse,
1550
+ fleetHealth,
1551
+ accountUsage,
1343
1552
  listRuns,
1344
1553
  getRun,
1345
1554
  getRunEvents,
1555
+ getRunTranscript,
1346
1556
  getRunQuestion,
1347
1557
  answerRunQuestion,
1348
1558
  cancelRun,
@@ -1368,6 +1578,7 @@ export const manageDescriptors = [
1368
1578
  getInstructionVersion,
1369
1579
  restoreInstructionVersion,
1370
1580
  setWorkerEnabled,
1581
+ deleteWorker,
1371
1582
  kitInstallPreview,
1372
1583
  kitInstall,
1373
1584
  clonePreview,
@@ -1378,6 +1589,12 @@ export const manageDescriptors = [
1378
1589
  getFleetBudget,
1379
1590
  setFleetBudget,
1380
1591
  getWorkerPermissions,
1592
+ listModels,
1593
+ listDeployments,
1594
+ getDeployment,
1595
+ deployWorker,
1596
+ updateDeployment,
1597
+ undeployWorker,
1381
1598
  getMyPublisher,
1382
1599
  setMyPublisher,
1383
1600
  listMyKits,