@workerkit/core 0.3.1 → 0.3.3

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 74 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.
@@ -7,21 +7,24 @@ import { CREATE, DELETE, READ_ONLY, TRIGGER, UPDATE } from "./types.js";
7
7
  // The graded-outcome vocabulary, taught wherever a score can be read or written.
8
8
  // The null-vs-zero line is the one an agent gets wrong by default: absent means
9
9
  // nobody judged the run, and reporting that as a zero would invent a failure.
10
- const SCORE_NOTE = " Success grading: selfScore (0-100) is the run's OWN assessment of how much of its task it accomplished, written by a grading pass over what it actually did — a self-report, not a measurement. ownerScore (0-100) is the human's grade and OVERRIDES selfScore wherever both exist. selfScoreReason is a one-line explainer for selfScore. All three are null when the run was never assessed (stateless workers and very short runs are never graded) — null means NOT ASSESSED, never zero: do not report an unassessed run as scoring 0, and do not average nulls into a success rate.";
10
+ const SCORE_NOTE = " Grading: selfScore (0-100) is the run's OWN assessment of how much of its task it accomplished — a self-report, not a measurement; ownerScore (0-100) is the human's grade and OVERRIDES it wherever both exist; selfScoreReason explains selfScore in one line. All three are null when the run was never assessed (stateless workers and very short runs are not graded): null means NOT ASSESSED, never zero — never report it as 0 or average nulls into a success rate.";
11
11
  const API = "/api/manage/workers";
12
12
  // ─── Shared phrasing ────────────────────────────────────────────────────────
13
- const TOKEN_ID_HINT = "The worker's numeric token ID, as returned by workers_list. A 404 not_found means the worker does not exist OR belongs to another account — the two are indistinguishable by design.";
13
+ const TOKEN_ID_HINT = "The worker's numeric tokenId, as returned by workers_list. 404 not_found = no such worker on this account (another account's is indistinguishable by design).";
14
14
  const RUN_ID_HINT = "The run's UUID, as returned by worker_run (runId), workers_list (currentRunId / lastRun.runId), or worker_runs.";
15
15
  // The whole runs family shares this degradation mode; teach it once per tool so
16
16
  // agents never retry-loop a 422.
17
- const RUNTIME_NOTE = " When hosted runs are not enabled for this environment the call returns 422 RUNTIME_NOT_ENABLED — an environment capability switch, not a transient error: do not retry, and note that every non-run tool keeps working.";
17
+ const RUNTIME_NOTE = " 422 RUNTIME_NOT_ENABLED = hosted runs are switched off in this environment — a capability, not a transient error: do not retry; every non-run tool keeps working.";
18
18
  const UTC_HINT = "ISO 8601 UTC datetime, e.g. '2026-08-01T00:00:00Z'.";
19
+ // What a decision worker's receipt carries beyond an agent run's: taught on the
20
+ // two tools that hand a receipt back.
21
+ const DECISION_BLOCK_NOTE = " A DECISION worker's receipt carries a `decision` block: outcome (one line: how many judged, routed, below the floor, acted on), confidence (share of judged items ABOVE the confidence floor, 0–100 — not a self-score), calls, and decisions[] — one row per judged item: id, answers (question → the model's label or number), confidence, route (rule:N, else, or below_floor: not sure, escalated to the owner), action (call <tool> / escalate / none) and, in live, executed/ok. findings[] are per-question distributions, openQuestions[] the items a person should look at. The block is run CONTENT: it needs readRuns; without it the block comes back withheld (contentWithheld:true) while status, timings and cost still answer.";
19
22
  // A scope added to the product AFTER a key was minted never reaches that key —
20
23
  // including a key minted with "all", which stored the bitmask of the scopes that
21
24
  // existed that day. Taught on key_info and on every tool behind a scope that is
22
25
  // younger than the surface, so a 403 there is read as "re-scope the key", not
23
26
  // "retry".
24
- const NEW_SCOPE_NOTE = " Note this scope is YOUNGER than the manager-key surface: a key minted before it existed does not carry it — including one minted with \"all\" — so a 403 here is fixed by an account admin re-scoping or re-minting the key at https://workerkit.ai, never by retrying. Read key_info's scopes list before planning work that needs it.";
27
+ const NEW_SCOPE_NOTE = " A key minted before this scope existed does not carry it (even one minted with \"all\"): a 403 here is fixed by an account admin re-scoping or re-minting the key at https://workerkit.ai, never by retrying — check key_info's scopes first.";
25
28
  // ─── Key ────────────────────────────────────────────────────────────────────
26
29
  const getKeyInfo = {
27
30
  name: "key_info",
@@ -37,17 +40,42 @@ const getKeyInfo = {
37
40
  const listWorkers = {
38
41
  name: "workers_list",
39
42
  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), live state (isRunning, inFlightRuns, currentRunId — poll run_get with currentRunId to watch it), and deployment (null = this worker is NOT on the hosted runtime and will never run; worker_deploy fixes that). 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).",
43
+ 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
44
  auth: "manager",
42
45
  method: "get",
43
- schema: {},
46
+ schema: {
47
+ 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."),
48
+ 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."),
49
+ 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."),
50
+ q: z.string().max(100).optional().describe("Case-insensitive substring of the worker's title."),
51
+ },
44
52
  path: API,
45
53
  annotations: READ_ONLY,
46
54
  };
55
+ const fleetHealth = {
56
+ name: "fleet_health",
57
+ title: "Fleet Health",
58
+ 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.",
59
+ auth: "manager",
60
+ method: "get",
61
+ schema: {},
62
+ path: `${API}/fleet/health`,
63
+ annotations: READ_ONLY,
64
+ };
65
+ const accountUsage = {
66
+ name: "account_usage",
67
+ title: "Account Usage",
68
+ 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.",
69
+ auth: "manager",
70
+ method: "get",
71
+ schema: {},
72
+ path: `${API}/account/usage`,
73
+ annotations: READ_ONLY,
74
+ };
47
75
  const getWorker = {
48
76
  name: "worker_get",
49
77
  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), 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).",
78
+ 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, modelType ('language' | 'decision' — a decision worker runs a routing table, not an instruction: instruction_get reads it, worker_run runs it, and decisionPendingSetup names any install question still blank), 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
79
  auth: "manager",
52
80
  method: "get",
53
81
  schema: {
@@ -61,17 +89,52 @@ const getWorker = {
61
89
  const runWorker = {
62
90
  name: "worker_run",
63
91
  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." +
92
+ 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. The worker's model type (worker_get → modelType) decides which fields apply: a LANGUAGE worker takes prompt and modelSlug; a DECISION worker runs its own routing table and takes neither (409 not_language_worker), taking preview / sourceArgs / maxItems instead — and sending those to a language worker is 409 not_decision_worker." +
93
+ DECISION_BLOCK_NOTE +
65
94
  RUNTIME_NOTE,
66
95
  auth: "manager",
67
96
  method: "post",
68
97
  schema: {
69
98
  tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
70
- prompt: z.string().max(8000).optional().describe("What to do on THIS run (≤8000 chars). Omit to run the worker's standing instructions unchanged."),
71
- modelSlug: z.string().max(64).optional().describe("One-off model override for this run (≤64 chars). Omit to use the worker's configured model."),
99
+ prompt: z.string().max(8000).optional().describe("Language workers: what to do on THIS run (≤8000 chars). Omit to run the worker's standing instructions unchanged."),
100
+ modelSlug: z.string().max(64).optional().describe("Language workers: one-off model override for this run (≤64 chars). Omit to use the worker's configured model."),
101
+ preview: z.boolean().optional().describe("Decision workers: run THIS run in preview whatever the deployment's mode — every row says what the worker WOULD do and nothing is done. Live is never forced the other way, and it costs the same decision-model calls as a real run."),
102
+ sourceArgs: z.record(z.string(), z.unknown()).optional().describe("Decision workers: narrow what is decided about — values merged over the spec's source arguments for THIS run only (a ticket id, a query, a status). A plain object, no placeholders; the read still runs under the worker's own permissions, so it can only see what the worker can."),
103
+ maxItems: z.number().int().min(1).max(100000).optional().describe("Decision workers: judge at most this many items this run (never above the spec's own cap). ~20 reads as a table; omit for the spec's limit."),
104
+ waitSeconds: z.number().int().min(0).max(55).optional().describe("Hold the call until the run settles and answer the settled receipt (0–55) — worth it on a decision worker, whose runs are seconds. Omit and the answer is the just-minted receipt, followed with run_get. Past the deadline the receipt comes back with a non-terminal status: poll run_get."),
72
105
  },
73
106
  path: (params) => `${API}/${params.tokenId}/run`,
74
- bodyBuilder: (params) => ({ prompt: params.prompt, modelSlug: params.modelSlug }),
107
+ bodyBuilder: (params) => ({
108
+ prompt: params.prompt,
109
+ modelSlug: params.modelSlug,
110
+ preview: params.preview,
111
+ sourceArgs: params.sourceArgs,
112
+ maxItems: params.maxItems,
113
+ waitSeconds: params.waitSeconds,
114
+ }),
115
+ annotations: TRIGGER,
116
+ };
117
+ const runWorkersBulk = {
118
+ name: "run_bulk",
119
+ title: "Run Workers in Bulk",
120
+ 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." +
121
+ RUNTIME_NOTE,
122
+ auth: "manager",
123
+ method: "post",
124
+ schema: {
125
+ workers: z
126
+ .array(z.object({
127
+ 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."),
128
+ tokenId: z.number().int().min(1).optional().describe("The worker's deprecated numeric token ID. workerId wins when both are sent."),
129
+ prompt: z.string().max(8000).optional().describe("What THIS worker should do on this run (≤8000 chars). Omit to run its standing instructions unchanged."),
130
+ modelSlug: z.string().max(64).optional().describe("One-off model override for this worker's run (≤64 chars). Omit to use its configured model."),
131
+ }))
132
+ .min(1)
133
+ .max(20)
134
+ .describe("One entry per worker to run, 1–20 of them; each needs a workerId or a tokenId."),
135
+ },
136
+ path: `${API}/runs/bulk`,
137
+ bodyBuilder: (params) => ({ workers: params.workers }),
75
138
  annotations: TRIGGER,
76
139
  };
77
140
  // ─── Fleet-wide run reads ───────────────────────────────────────────────────
@@ -81,7 +144,7 @@ const runWorker = {
81
144
  const runsFeed = {
82
145
  name: "runs_feed",
83
146
  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." +
147
+ 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
148
  RUNTIME_NOTE,
86
149
  auth: "manager",
87
150
  method: "get",
@@ -92,9 +155,9 @@ const runsFeed = {
92
155
  // two composite values.
93
156
  status: z.enum([
94
157
  "pending", "dispatched", "running", "succeeded", "failed",
95
- "timedOut", "budgetExceeded", "skipped", "canceled",
158
+ "timedOut", "budgetExceeded", "awaitingInput", "skipped", "canceled",
96
159
  "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."),
160
+ ]).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
161
  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
162
  limit: z.number().int().min(1).max(100).default(20).describe("Rows per page (1–100)."),
100
163
  },
@@ -127,8 +190,8 @@ const listRuns = {
127
190
  // read as the real subset. These are the WorkerRunStatus names verbatim.
128
191
  status: z.enum([
129
192
  "pending", "dispatched", "running", "succeeded", "failed",
130
- "timedOut", "budgetExceeded", "skipped", "canceled",
131
- ]).optional().describe("Filter by run status. Omit for all runs."),
193
+ "timedOut", "budgetExceeded", "awaitingInput", "skipped", "canceled",
194
+ ]).optional().describe("Filter by run status ('awaitingInput' = waiting on the owner's answer). Omit for all runs."),
132
195
  page: z.number().int().min(1).default(1).describe("1-based page number."),
133
196
  pageSize: z.number().int().min(1).max(100).default(20).describe("Results per page (max 100)."),
134
197
  fromUtc: z.string().optional().describe(`Only runs started at/after this time. ${UTC_HINT}`),
@@ -146,6 +209,7 @@ const getRun = {
146
209
  title: "Get Run",
147
210
  description: "One run's full receipt: status, trigger, timings, token/cost metering (including billingMode 'Platform'|'Byok' and byokFeeUsd — on a Byok run modelCostUsd is the provider's list price and was NOT charged to the wallet), finalDigest (the worker's report), runPrompt/runContext, liveness (live|stalled) and lastActivity while in flight, errorCode/errorDetail, issues, and the grades (selfScore, selfScoreReason, ownerScore — set or change ownerScore with run_score). Note: footprint, turnsTimeline, issues, and priceSnapshot arrive as raw JSON strings — parse them before reasoning over their contents. Requires readRuns." +
148
211
  SCORE_NOTE +
212
+ DECISION_BLOCK_NOTE +
149
213
  RUNTIME_NOTE,
150
214
  auth: "manager",
151
215
  method: "get",
@@ -175,6 +239,20 @@ const getRunEvents = {
175
239
  },
176
240
  annotations: READ_ONLY,
177
241
  };
242
+ const getRunTranscript = {
243
+ name: "run_transcript",
244
+ title: "Get Run Transcript",
245
+ 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." +
246
+ RUNTIME_NOTE,
247
+ auth: "manager",
248
+ method: "get",
249
+ schema: {
250
+ runId: z.string().uuid().describe(RUN_ID_HINT),
251
+ },
252
+ path: (params) => `${API}/runs/${params.runId}/transcript`,
253
+ paramFilter: () => ({}),
254
+ annotations: READ_ONLY,
255
+ };
178
256
  // ─── Two-way runs (ask / answer) ────────────────────────────────────────────
179
257
  // A run that needs something from its owner ENDS by asking (status AwaitingInput)
180
258
  // rather than blocking on a human while holding a request slot and a wallet
@@ -334,12 +412,17 @@ const SCHEDULE_TYPE_HINT = "EveryNMinutes (intervalValue 1–1440), EveryNHours
334
412
  // is in `feature`, while `error` holds the literal string "feature_gated" — the
335
413
  // one place on this surface where `error` is not the code.
336
414
  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.";
415
+ // The plan's per-worker concurrency is for runs started on demand. A schedule
416
+ // never overlaps itself, whatever the plan allows: two copies of the same
417
+ // scheduled job side by side duplicate its side effects.
418
+ 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
419
  const createSchedule = {
338
420
  name: "schedule_create",
339
421
  title: "Create Schedule",
340
422
  description: "Create a schedule for the worker. Per-type required fields: " + SCHEDULE_TYPE_HINT +
341
423
  " Omit timeZoneId to follow the worker's own time zone (the normal case). Requires the manageSchedules scope." +
342
- SCHEDULE_GATE_HINT,
424
+ SCHEDULE_GATE_HINT +
425
+ SCHEDULE_OVERLAP_NOTE,
343
426
  auth: "manager",
344
427
  method: "post",
345
428
  schema: {
@@ -368,7 +451,7 @@ const updateSchedule = {
368
451
  name: "schedule_update",
369
452
  title: "Update Schedule",
370
453
  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,
454
+ SCHEDULE_TYPE_HINT + " Requires manageSchedules." + SCHEDULE_GATE_HINT + SCHEDULE_OVERLAP_NOTE,
372
455
  auth: "manager",
373
456
  method: "patch",
374
457
  schema: {
@@ -412,7 +495,7 @@ const deleteSchedule = {
412
495
  // Where the PLATFORM sends a worker's report when a run finishes. The two facts an
413
496
  // agent gets wrong by default: availability is judged at ACCOUNT level (never from
414
497
  // the worker's own apps), and a receipt is not a guarantee.
415
- const DELIVERY_NOTE = " How delivery works: the send is PLATFORM-level and detached from the worker's own permissions — the destination row you configure IS the permission, so a worker with no email app still delivers to email. The ONE hard configuration refusal is 400 CHANNEL_NOT_CONNECTED, judged at ACCOUNT level (the operator's connected apps), so NEVER infer availability from worker_get's apps — call delivery_channels. A worker holds at most 5 destinations. Canceled runs never deliver on any channel. Skipped runs (refused at the gate) deliver to WEBHOOK destinations only, as a run.blocked event, once per worker per skip reason per UTC day. A run that ended by ASKING its owner a question (status AwaitingInput) delivers everywhere: the question is the message. Sends are AT-MOST-ONCE — never double-sent, but a crash at the wrong moment can lose one, so the run receipt's deliveries[] is a RECEIPT of what happened, not a guarantee that it did.";
498
+ const DELIVERY_NOTE = " Delivery is PLATFORM-level, detached from the worker's own permissions: the destination row you configure IS the permission (a worker with no email app still delivers to email). The one configuration refusal is 400 CHANNEL_NOT_CONNECTED, judged at ACCOUNT level — never infer availability from worker_get's apps; call delivery_channels. At most 5 destinations per worker. Canceled runs never deliver; Skipped runs (refused at the gate) reach WEBHOOK destinations only, as run.blocked, once per worker per skip reason per UTC day; a run that ended by ASKING its owner (AwaitingInput) delivers everywhere, the question being the message. Sends are AT-MOST-ONCE: never double-sent, but a crash can lose one, so the receipt's deliveries[] records what happened, not a guarantee.";
416
499
  const DELIVERY_TARGET_HINT = "The destination. ONE shape serves every channel — set only the fields YOUR channel needs; anything else is dropped. email: to[1–10 addresses] (+ optional connectionId OR sendFrom to choose the mailbox on a multi-mailbox account). slack: channelId (+ optional workspaceId). msTeams: EITHER teamId+channelId (post to a channel) OR chatId (post to a chat) — exactly one form; both or neither is a 400. telegram: chatId, a numeric id or an @username (+ optional botId). notion: boardId — the board/database that gets one item per run with the report as its content. discord: channelId (+ optional applicationId). messaging: provider (e.g. 'TWILIO'), messagingChannel ('sms' | 'whatsapp'), to[EXACTLY ONE phone number] (+ optional from; omit for the connection's default sender). webhook: url — an absolute https URL that resolves to a PUBLIC address (private, loopback and metadata ranges are a 400 at configuration and refused again at send time; redirects are never followed).";
417
500
  const DELIVERY_TARGET_SHAPE = {
418
501
  to: z.array(z.string()).optional().describe("email: 1–10 recipient addresses. messaging: exactly one phone number. Unused by every other channel."),
@@ -554,25 +637,30 @@ const deleteDelivery = {
554
637
  const getInstruction = {
555
638
  name: "instruction_get",
556
639
  title: "Get Instruction",
557
- description: "The worker's standing instruction: content, jobSentence, whenToUse, description, memoryProfile, selfFactsEnabled, isProtected, currentVersion, timestamps. A bare \"Operation completed successfully.\" response means the worker has NO instruction yet (HTTP 204) — use instruction_set to create one. For a worker installed from a protected kit, metadata is returned but the instruction text is REDACTED (it belongs to the kit's publisher) — that is not an error.",
640
+ description: "What the worker runs, by its model type (worker_get → modelType). A LANGUAGE worker: its standing instruction — content, jobSentence, whenToUse, description, memoryProfile, selfFactsEnabled, isProtected, currentVersion, timestamps; for a worker installed from a protected kit the metadata is returned and the text is REDACTED (it belongs to the kit's publisher), which is not an error. A DECISION worker: its routing table as sentences (narration: what it reads, what it asks per item, what it does with each answer, what happens when it is not sure), its install questions (setup[]) with the CURRENT answers, the ones still pending (pendingSetup[] — a pending question blocks deploy and run; fill them with instruction_set's answers) and decisionMode (live: the table acts; preview: runs report and act on nothing); the raw spec is withheld for a protected kit's worker. A bare \"Operation completed successfully.\" response means there is neither yet (HTTP 204) — use instruction_set. The version history (instruction_versions, instruction_restore) is language-only: a decision worker's table is versioned with its kit.",
558
641
  auth: "manager",
559
642
  method: "get",
560
643
  schema: {
561
644
  tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
645
+ optionsFor: z.string().max(60).optional().describe("Decision workers: one appPick install question's key (from setup[]) — answers with its LIVE options instead of the table, listed from the worker's OWN connected app through the runtime under the worker's own permissions (a folder, a label, a queue, a board). Never a 4xx for an app that could not answer: items[] comes back empty with a warning saying why (not connected, dead credential, tool refused), and you paste an id from the app into instruction_set instead. 404 when the key names no appPick question; 409 on a language-model worker."),
562
646
  },
563
647
  path: (params) => `${API}/${params.tokenId}/instruction`,
564
- paramFilter: () => ({}),
648
+ paramFilter: (params) => {
649
+ const { tokenId: _tokenId, ...query } = params;
650
+ return query;
651
+ },
565
652
  annotations: READ_ONLY,
566
653
  };
567
654
  const setInstruction = {
568
655
  name: "instruction_set",
569
656
  title: "Set Instruction",
570
- description: "Create or replace the worker's standing instruction (the prompt/workflow text handed to every run). A changed body snapshots the prior version. Omitted optional fields stay unchanged; empty string clears. Returns 403 OPERATION_NOT_ALLOWED for a protected kit's worker — that text belongs to its publisher; do not retry. Requires the manageInstructions scope.",
657
+ description: "Set what the worker runs: content for a LANGUAGE worker's standing instruction, answers for a DECISION worker's install questions. They are mutually exclusive — sending both is a 400. content replaces the whole instruction: a changed body snapshots the prior version, omitted optional fields stay unchanged, an empty string clears, and a protected kit's worker returns 403 OPERATION_NOT_ALLOWED because that text belongs to its publisher (do not retry). answers fills or changes the questions the routing table binds at every run, so a saved change reaches the next one — check it first with worker_run preview:true. A protected kit withholds its spec, never its questions, so answering them is not refused. Requires the manageInstructions scope either way.",
571
658
  auth: "manager",
572
659
  method: "put",
573
660
  schema: {
574
661
  tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
575
- content: z.string().min(1).max(100000).describe("The instruction text (1–100,000 chars). Required — this replaces the whole content."),
662
+ content: z.string().min(1).max(100000).optional().describe("Language workers: the instruction text (1–100,000 chars) — replaces the whole content."),
663
+ answers: z.record(z.string(), z.string().max(4000)).optional().describe("Decision workers: install answers by question key, from instruction_get's setup[]. A PARTIAL map changes only the keys it names; '' clears one. A scale takes a stop's label, a choice an option's value, a list one row per line, an appPick an id (instruction_get's optionsFor lists the live ones). An unknown key or an off-menu value is a 400 naming it; 409 on a language-model worker."),
576
664
  jobSentence: z.string().max(200).optional().describe("The worker's one-liner (≤200). Omit = unchanged; '' = clear."),
577
665
  whenToUse: z.string().max(500).optional().describe("Trigger text: when this worker should be used (≤500). Omit = unchanged; '' = clear."),
578
666
  description: z.string().max(2000).optional().describe("Longer display-only description (≤2000). Omit = unchanged; '' = clear."),
@@ -582,6 +670,7 @@ const setInstruction = {
582
670
  path: (params) => `${API}/${params.tokenId}/instruction`,
583
671
  bodyBuilder: (params) => ({
584
672
  content: params.content,
673
+ answers: params.answers,
585
674
  jobSentence: params.jobSentence,
586
675
  whenToUse: params.whenToUse,
587
676
  description: params.description,
@@ -639,7 +728,7 @@ const restoreInstructionVersion = {
639
728
  const setWorkerEnabled = {
640
729
  name: "worker_set_enabled",
641
730
  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.",
731
+ 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
732
  auth: "manager",
644
733
  method: "post",
645
734
  schema: {
@@ -650,6 +739,19 @@ const setWorkerEnabled = {
650
739
  bodyBuilder: (params) => ({ enabled: params.enabled }),
651
740
  annotations: UPDATE,
652
741
  };
742
+ const deleteWorker = {
743
+ name: "worker_delete",
744
+ title: "Delete Worker",
745
+ 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." +
746
+ NEW_SCOPE_NOTE,
747
+ auth: "manager",
748
+ method: "delete",
749
+ schema: {
750
+ tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
751
+ },
752
+ path: (params) => `${API}/${params.tokenId}`,
753
+ annotations: DELETE,
754
+ };
653
755
  // ─── Kit install ────────────────────────────────────────────────────────────
654
756
  // The one place on this surface an agent can CREATE a worker: install a kit from
655
757
  // the public directory. Both tools need the installKits scope (keys minted
@@ -665,7 +767,7 @@ const KIT_SLUG = z.string().min(1).max(120).regex(/^[a-z0-9][a-z0-9-]*$/, "not a
665
767
  const kitInstallPreview = {
666
768
  name: "kit_install_preview",
667
769
  title: "Preview Kit Install",
668
- description: "The kit's install form plus this account's current ability to satisfy it — call this BEFORE kit_install and gather every answer it demands. Creates nothing. Requires the installKits scope (403 names it). Returns: requiredInputs (EVERY key is mandatory at install — collect a value for each from the human, keys are exact), memorySetup (questions whose answers become the worker's first memory; only required:true entries are mandatory), categorySlots (pick ONE member per slot via categoryChoices; each member carries connection — prefer a connected one — and connectionProvider, which when set means also pass categoryChoices[].resourceId picked from operatorResources, ideally one with hasActiveConnection and a matching provider), apps + appsNeedingConnection (the connection state the new worker would START with; installing anyway is allowed — the worker starts blocked and a human finishes at connectAppsUrl), operator (which operator the install targets), limits (currentWorkers/maxWorkers — at the cap the install returns 402), kitPageUrl (the human install page). Preview is advisory: the install response's readiness block is the verdict. For the kit's full permissions manifest and instruction use kit_get on the public Directory server.",
770
+ description: "The kit's install form plus this account's current ability to satisfy it — call this BEFORE kit_install and gather every answer it demands. Creates nothing. Requires the installKits scope (403 names it). Returns: requiredInputs (EVERY key is mandatory at install — collect a value for each from the human, keys are exact), memorySetup (questions whose answers become the worker's first memory; only required:true entries are mandatory), modelType, decisionSetup, decisionNarration (a 'decision' kit runs a routing table instead of an instruction, on no model you pick: decisionNarration is that table as sentences — brief the human from it; decisionSetup are its install questions, answered by key in kit_install's decisionAnswers — a required one with no default is mandatory, except an appPick, answerable later with instruction_set; the worker starts live), categorySlots (pick ONE member per slot via categoryChoices; each member carries connection — prefer a connected one — and connectionProvider, which when set means also pass categoryChoices[].resourceId picked from operatorResources, ideally one with hasActiveConnection and a matching provider), apps + appsNeedingConnection (the connection state the new worker would START with; installing anyway is allowed — the worker starts blocked and a human finishes at connectAppsUrl), operator (which operator the install targets), limits (currentWorkers/maxWorkers — at the cap the install returns 402), kitPageUrl (the human install page). Preview is advisory: the install response's readiness block is the verdict. For the kit's full permissions manifest and instruction use kit_get on the public Directory server.",
669
771
  auth: "manager",
670
772
  method: "get",
671
773
  schema: {
@@ -682,7 +784,7 @@ const kitInstallPreview = {
682
784
  const kitInstall = {
683
785
  name: "kit_install",
684
786
  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. 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.",
787
+ description: "Install a directory kit as a NEW worker on this account. NOT idempotent: every success creates another worker — never retry a success; on a timeout check workers_list (readWorkers) first. Requires the installKits scope. Call kit_install_preview first, then supply every requiredInputs key in inputs (a missing or unknown key is a 400 naming it), required memorySetup answers in memoryAnswers, a DECISION kit's decisionSetup answers in decisionAnswers, and one categoryChoices entry per slot (a member with connectionProvider set also takes resourceId from the preview's operatorResources). CRITICAL — secrets shown ONCE: install.rawKey (the worker's pe_ API key) and install.triggers[].signingSecret can NEVER be read again; hand them to the human before discarding the response. Also returned: readiness (ready | blocked with actionable issues such as app_not_connected + candidateProviders; null means the readiness check failed AFTER the install succeeded — read it via worker_get, never re-install), workerUrl (the worker's dashboard page — hand it to the human) and connectAppsUrl (where the human connects missing apps; agents cannot). 402 {error:'limit_exceeded'} is the plan's worker cap — terminal: the human upgrades, or frees a slot with worker_delete (deleteWorkers scope; permanent, so only with their agreement). Rate: 10 installs/hour per account. Installing does NOT make the worker run: without a deployment it fires no schedule and worker_run refuses it with not_deployed. deploy:true (optionally with deployment: modelSlug from models_list and the ceilings) installs and deploys in ONE call and the response carries deployment; a deploy refused after the install still returns 201 with deploymentError naming the fix — the worker exists, so fix it and call worker_deploy, never re-install. deploy:true needs manageDeployments IN ADDITION to installKits: a key without it is refused up front (403 OPERATION_NOT_ALLOWED) and nothing is created — drop deploy, install, and have the owner re-scope the key before worker_deploy.",
686
788
  auth: "manager",
687
789
  method: "post",
688
790
  schema: {
@@ -697,6 +799,7 @@ const kitInstall = {
697
799
  })).optional().describe("One entry per categorySlots slot. Omitted slots resolve to the kit's first offered member on safe defaults."),
698
800
  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
801
  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."),
802
+ decisionAnswers: z.record(z.string(), z.string()).optional().describe("DECISION kits only: answers to the preview's decisionSetup questions by key — a choice takes an option's value, a scale a stop's label (or its value), a list one row per line (≤20 rows), an appPick an id (or leave it blank and answer it later with instruction_set); text and rows ≤200 chars. A required question with no default and no answer is a 400 naming it."),
700
803
  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."),
701
804
  deployment: z.object({
702
805
  modelSlug: z.string().max(64).optional().describe("Model to deploy on, from models_list. Omit for the kit's recommended model."),
@@ -714,6 +817,7 @@ const kitInstall = {
714
817
  categoryChoices: params.categoryChoices,
715
818
  inputs: params.inputs,
716
819
  memoryAnswers: params.memoryAnswers,
820
+ decisionAnswers: params.decisionAnswers,
717
821
  deploy: params.deploy,
718
822
  deployment: params.deployment,
719
823
  }),
@@ -726,7 +830,7 @@ const kitInstall = {
726
830
  // chat message is a different power and stays on the dashboard and the kit
727
831
  // pipeline, where a manifest gets reviewed. All three need the createWorkers
728
832
  // scope.
729
- const CLONE_NOTE = " What a clone is: a copy of the source's PERMISSIONS (so it can never hold access a human did not already approve on the source) plus, by flag, the instruction, owner-authored memory, schedules, delivery destinations and the hosted deployment. Three things are deliberately NOT straight copies — schedules arrive DISABLED so a new worker never starts firing by itself (enable them with schedule_update once you are satisfied), AGENT-AUTHORED FACTS ARE NOT COPIED (only owner-authored rules and facts travel: what a worker learned about itself stays with the worker that learned it), and a webhook delivery destination arrives DISABLED WITH NO SIGNING SECRET so revoking the clone can never break its source (mint one with delivery_secret_rotate, then enable it with delivery_update; enabling it before that is refused). The clone also inherits the SAME per-run and per-day spend ceilings, so cloning multiplies the fleet's ceiling — read fleet_budget_get before a bulk clone. Instruction history does not travel: the clone starts at version 1 with the text it was given. A source whose instruction came from a PROTECTED kit is refused while includeInstruction is true — that text belongs to the kit's publisher, so install the kit again or clone with includeInstruction:false.";
833
+ const CLONE_NOTE = " A clone copies the source's PERMISSIONS (it can never hold access a human did not already approve on the source) plus, by flag, the instruction, owner-authored memory, schedules, delivery destinations and the hosted deployment. Three things are deliberately not straight copies: schedules arrive DISABLED (enable with schedule_update once satisfied), AGENT-AUTHORED FACTS do not travel (only owner-authored rules and facts do), and a webhook destination arrives DISABLED WITH NO SIGNING SECRET so revoking the clone cannot break its source (delivery_secret_rotate, then delivery_update to enable). The clone inherits the SAME per-run and per-day ceilings, so cloning multiplies the fleet's ceiling — read fleet_budget_get before a bulk clone. Instruction history does not travel (the clone starts at version 1). A source whose instruction came from a PROTECTED kit is refused while includeInstruction is true — install the kit again or clone with includeInstruction:false.";
730
834
  const CLONE_TITLE_HINT = "The new worker's title, 1–120 chars. Required: a clone is never silently named after its source.";
731
835
  // One shape, three tools: worker_clone and worker_clone_preview take it as their
732
836
  // body, worker_clone_bulk takes an array of it. Every include flag defaults to
@@ -744,7 +848,7 @@ const CLONE_REQUEST_SHAPE = {
744
848
  const clonePreview = {
745
849
  name: "worker_clone_preview",
746
850
  title: "Preview Worker Clone",
747
- description: "What cloning this worker WOULD produce. CREATES NOTHING — this is the rail to run before worker_clone, and before worker_clone_bulk especially. It takes the same body as worker_clone (which is the only reason it is a POST) and answers with sourceWorkerId/sourceTitle, the title the clone would carry, apps[] (every app the clone would hold, each with whether the operator can actually serve it today — this is where you learn a clone would be born unable to run), wouldCopyInstruction, memoryItemCount, scheduleCount, deliveryCount, the deployment it would inherit (modelSlug, maxUsdPerRun, maxUsdPerDay), and the two lists that carry the verdict: BLOCKERS — reasons the clone WOULD BE REFUSED, so an empty list is what 'it would succeed' looks like — and NOTES, things that would succeed but are worth knowing (schedules landing disabled, only owner-authored memory travelling, a webhook destination arriving unarmed, the fleet's spend ceiling being multiplied). Read blockers before committing: it is a verdict, not advice. Requires the createWorkers scope." +
851
+ description: "What cloning this worker WOULD produce — CREATES NOTHING. Run it before worker_clone, and before worker_clone_bulk especially. Takes the same body as worker_clone (the only reason it is a POST) and answers with sourceWorkerId / sourceTitle, the title the clone would carry, apps[] (every app the clone would hold and whether the operator can serve it today — where you learn a clone would be born unable to run), wouldCopyInstruction, memoryItemCount, scheduleCount, deliveryCount, the deployment it would inherit (modelSlug, maxUsdPerRun, maxUsdPerDay), and two lists that carry the verdict: BLOCKERS — reasons the clone WOULD BE REFUSED (empty = it would succeed) — and NOTES, things that would succeed but are worth knowing (schedules landing disabled, only owner-authored memory travelling, a webhook destination arriving unarmed, the fleet's spend ceiling multiplying). blockers is a verdict, not advice. Requires the createWorkers scope." +
748
852
  NEW_SCOPE_NOTE +
749
853
  CLONE_NOTE,
750
854
  auth: "manager",
@@ -769,7 +873,7 @@ const clonePreview = {
769
873
  const cloneWorker = {
770
874
  name: "worker_clone",
771
875
  title: "Clone Worker",
772
- description: "Copy this worker into a NEW one (201 with the created worker). NOT IDEMPOTENT: every successful call creates another worker — never retry a success, and after a timeout check workers_list (needs readWorkers) before trying again. CRITICAL — the key is shown ONCE: the response's rawKey is the new worker's pe_ API key and can NEVER be read again, so hand it to the human immediately and do not discard the response before doing so. The response identifies the new worker by workerId, a UUID; every other tool on this surface addresses workers by their numeric tokenId, so look the clone up in workers_list to get one. Call worker_clone_preview first — its blockers list is the same refusal you would otherwise discover as a 400, whose message names the cause (a protected kit's instruction is the common one); 404 is an unknown or other-account source worker. The new worker starts ENABLED but with its schedules off, so nothing runs until you enable a schedule or call worker_run. Rate limit: 20 create CALLS per hour per ACCOUNT, shared with worker_clone_bulk (a bulk call of twenty workers costs one). Requires the createWorkers scope." +
876
+ description: "Copy this worker into a NEW one (201 with the created worker). NOT IDEMPOTENT: every success creates another worker — never retry a success; after a timeout check workers_list (readWorkers) first. CRITICAL — the key is shown ONCE: rawKey is the new worker's pe_ API key and can NEVER be read again; hand it to the human before discarding the response. The new worker is identified by workerId (a UUID); every other tool here addresses workers by numeric tokenId, so look the clone up in workers_list. Call worker_clone_preview first — its blockers are the same refusal you would otherwise meet as a 400 whose message names the cause (a protected kit's instruction is the common one); 404 = unknown or other-account source. The clone starts ENABLED with its schedules off: nothing runs until you enable a schedule or call worker_run. Rate: 20 create CALLS per hour per ACCOUNT, shared with worker_clone_bulk (a bulk call of twenty costs one). Requires the createWorkers scope." +
773
877
  NEW_SCOPE_NOTE +
774
878
  CLONE_NOTE,
775
879
  auth: "manager",
@@ -793,7 +897,7 @@ const cloneWorker = {
793
897
  const cloneWorkerBulk = {
794
898
  name: "worker_clone_bulk",
795
899
  title: "Clone Worker in Bulk",
796
- description: "Clone this worker several times in ONE call — how a fleet of eighteen becomes a fleet of a hundred. IT IS NOT ATOMIC AND DOES NOT ROLL BACK: PARTIAL SUCCESS IS THE NORMAL OUTCOME, because a per-item report is more useful than discarding nineteen good workers over the twentieth's bad title, and creating-then-deleting workers is worse than never creating them. So READ items[] RATHER THAN THE STATUS CODE — a 200 with created:17 of requested:20 is a success and a failure at once; each item carries index, title, success, workerId and either rawKey or error. KEEP EVERY rawKey: each is that worker's pe_ API key, shown once and never again, and a partial-success response you discard has stranded real workers whose keys nobody has. Capped at 20 workers per call (the 21st is a 400) and rate limited to 20 create calls per hour per ACCOUNT, shared with worker_clone, because a typo here creates workers. Run worker_clone_preview once for the shape you are about to repeat — the blockers that would refuse one item will refuse all of them. Requires the createWorkers scope." +
900
+ description: "Clone this worker several times in ONE call — how a fleet of eighteen becomes a fleet of a hundred. NOT ATOMIC, NO ROLLBACK: PARTIAL SUCCESS IS THE NORMAL OUTCOME (a per-item report beats discarding nineteen good workers over the twentieth's bad title), so READ items[] RATHER THAN THE STATUS CODE — a 200 with created:17 of requested:20 is a success and a failure at once; each item carries index, title, success, workerId and either rawKey or error. KEEP EVERY rawKey: each is that worker's pe_ API key, shown once and never again; a discarded partial-success response strands real workers whose keys nobody has. Capped at 20 workers per call (the 21st is a 400) and 20 create calls per hour per ACCOUNT, shared with worker_clone — a typo here creates workers. Run worker_clone_preview once for the shape you are about to repeat: a blocker that refuses one item refuses all. Requires the createWorkers scope." +
797
901
  NEW_SCOPE_NOTE +
798
902
  CLONE_NOTE,
799
903
  auth: "manager",
@@ -815,7 +919,7 @@ const BUDGET_WINDOW_NOTE = " Windows differ, deliberately: maxUsdPerDay and both
815
919
  const getWorkerBudget = {
816
920
  name: "budget_get",
817
921
  title: "Get Worker Budget",
818
- 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." +
922
+ description: "One worker's spend and rate ceilings: maxUsdPerRun, maxUsdPerDay, maxRunsPerDay, maxConcurrentRuns, modelSlug, hasDeployment (plus workerId and its deprecated alias tokenId). Four readings agents get wrong. (1) maxRunsPerDay NULL MEANS THE PLATFORM DEFAULT, NOT UNLIMITED — the gate substitutes the platform's number; never report null as 'no limit'. (2) maxUsdPerRun is RESERVED from the wallet at dispatch, so a run must be able to AFFORD it to start, and the day cap counts those RESERVATIONS, not settled cost: a worker reserving $0.50 under a $2/day cap is skipped on its fifth run even if each cost a cent (a 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; it bounds runs started on demand (worker_run, run_bulk, webhooks), while a schedule never overlaps itself whatever the plan allows. (4) hasDeployment FALSE means hosted runs are not set up, and the dollar figures then read 0 because there is no deployment to read them from — 'no deployment', NOT 'capped at zero'; check hasDeployment before quoting a 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." +
819
923
  BUDGET_WINDOW_NOTE +
820
924
  NEW_SCOPE_NOTE,
821
925
  auth: "manager",
@@ -830,7 +934,7 @@ const getWorkerBudget = {
830
934
  const setWorkerBudget = {
831
935
  name: "budget_set",
832
936
  title: "Set Worker Budget",
833
- 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." +
937
+ description: "Change one worker's ceilings and get the whole budget back. PARTIAL: an OMITTED FIELD MEANS UNCHANGED, so one ceiling can be raised without restating the rest or racing another writer. Three traps. maxUsdPerRun / maxUsdPerDay on a worker with NO HOSTED DEPLOYMENT is a 400 (nothing for a dollar cap to bind to; budget_get's hasDeployment says which those are) — maxRunsPerDay can be set on any worker. maxRunsPerDay cannot be cleared back to the platform default here, because null already means 'leave it alone': send an explicit number. LOWERING maxUsdPerRun below what a run needs does not fail loudly — the worker's next run becomes a Skipped receipt with a skipReason, which you must go and read. maxConcurrentRuns is absent by design: the account's plan sets it (Free 5, Pro 20, Team 50), so raising it is an upgrade. 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 unanswered, and a late answer still starts the run. Requires the manageBudgets scope." +
834
938
  BUDGET_WINDOW_NOTE +
835
939
  NEW_SCOPE_NOTE,
836
940
  auth: "manager",
@@ -892,11 +996,11 @@ const setFleetBudget = {
892
996
  // and completely inert: no schedule fires and worker_run answers 409
893
997
  // not_deployed. Deploying is also where the model and the spend ceilings are
894
998
  // chosen, which is why the writes sit behind their own scope.
895
- 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.";
999
+ 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 puts it on the runtime and sets its model and per-run / per-day spend ceilings.";
896
1000
  const listModels = {
897
1001
  name: "models_list",
898
1002
  title: "List Deployable Models",
899
- 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." +
1003
+ 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. Language-model workers only: a DECISION worker (worker_get modelType 'decision') runs the decision model and takes no modelSlug. Requires readWorkers." +
900
1004
  RUNTIME_NOTE,
901
1005
  auth: "manager",
902
1006
  method: "get",
@@ -934,7 +1038,7 @@ const getDeployment = {
934
1038
  const deployWorker = {
935
1039
  name: "worker_deploy",
936
1040
  title: "Deploy Worker",
937
- 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." +
1041
+ description: "Put the worker on the hosted runtime — what makes it runnable and schedulable, and the step most often forgotten after kit_install (whose deploy:true does both in one call). EVERYTHING IS OPTIONAL: send nothing and it deploys on the model its kit recommends with the platform's default ceilings; 400 model_required means the kit named none — pick one from models_list. Each refusal names its fix: 422 instruction_required (no instruction — a hosted run has nothing to execute), 422 apps_not_connected (the message names the apps: app_connect them or drop them from the worker — a half-connected worker is refused rather than left 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), 400 invalid_model / model_tier_gated (unknown, disabled or above the account's tier — choose another from models_list), 400 invalid_thinking (wrong reasoning vocabulary for the model), 400 invalid_caps (maxUsdPerDay below maxUsdPerRun). A DECISION worker needs neither a model nor an instruction — deploy it as is (a modelSlug or thinking value is refused: 400 invalid_model / invalid_thinking); it starts live, its routing table acting from the first run. Its one extra refusal is 400 decision_setup_pending, an install question still blank: instruction_get names it, instruction_get with optionsFor lists an appPick's live values, instruction_set fills it. warnings[] on success never blocks: ip_rules_enforced means the worker's IP allowlist will reject hosted runs, which arrive from platform egress. Requires manageDeployments." +
938
1042
  DEPLOYMENT_NOTE +
939
1043
  RUNTIME_NOTE +
940
1044
  NEW_SCOPE_NOTE,
@@ -961,7 +1065,7 @@ const deployWorker = {
961
1065
  const updateDeployment = {
962
1066
  name: "deployment_update",
963
1067
  title: "Update Deployment",
964
- 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." +
1068
+ 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. decisionMode (decision workers only) is the Live / Preview switch — the one field that stops a decision worker's routing table acting, and the one that puts it back; 400 invalid_decision_mode on a language-model worker, and on a decision worker modelSlug / thinking are refused instead (400 invalid_model / invalid_thinking: it has no model to change). 404 not_found = the worker is not deployed; deploy it with worker_deploy first. Requires manageDeployments." +
965
1069
  DEPLOYMENT_NOTE +
966
1070
  RUNTIME_NOTE +
967
1071
  NEW_SCOPE_NOTE,
@@ -975,6 +1079,7 @@ const updateDeployment = {
975
1079
  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."),
976
1080
  transcriptRetention: z.string().max(16).optional().describe("New transcript setting. Omit to keep."),
977
1081
  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."),
1082
+ decisionMode: z.enum(["preview", "live"]).optional().describe("Decision workers only. 'live' (the default) = the routing table's actions execute; 'preview' = runs report what they would do and act on nothing. Omit to keep. Check a changed table with worker_run preview:true before going back to live."),
978
1083
  },
979
1084
  path: (params) => `${API}/${params.tokenId}/deployment`,
980
1085
  bodyBuilder: (params) => ({
@@ -984,6 +1089,7 @@ const updateDeployment = {
984
1089
  thinking: params.thinking,
985
1090
  transcriptRetention: params.transcriptRetention,
986
1091
  action: params.action,
1092
+ decisionMode: params.decisionMode,
987
1093
  }),
988
1094
  annotations: UPDATE,
989
1095
  };
@@ -1046,7 +1152,7 @@ const LISTING_SCHEMA = {
1046
1152
  declaredModelFloor: z.string().max(80).optional().describe("Display-only minimum model (≤80)."),
1047
1153
  modelScores: z.array(z.record(z.unknown())).max(10).optional().describe("≤10 of {model, score 0-100, notes ≤300} — only for models the kit was actually run on."),
1048
1154
  };
1049
- const CONTENT_SCHEMA = z.record(z.unknown()).describe("The kit's content: instructionContent (required), whenToUse, startCommand, endCommand, endCommandDescription, skillResources, appCodes XOR apps, categorySlots, mcpServers, contactAllowAll, blockedSenderCategories, timeframePastDays, timeframeFutureDays, maxChildTokens, memoryProfile, selfFactsEnabled, memorySetup, schedules, triggers, usageWindows. The exact shape, every cap and every rule: kit_authoring_guide section 'schema'; app codes, tool keys and category slugs: kit_vocabulary. Validated strictly server-side — unknown keys are rejected, never ignored — so kit_validate first.");
1155
+ const CONTENT_SCHEMA = z.record(z.unknown()).describe("The kit's content: instructionContent (required on a LANGUAGE kit), whenToUse, startCommand, endCommand, endCommandDescription, skillResources, appCodes XOR apps, categorySlots, mcpServers, contactAllowAll, blockedSenderCategories, timeframePastDays, timeframeFutureDays, maxChildTokens, memoryProfile, selfFactsEnabled, memorySetup, schedules, triggers, usageWindows — or, for a DECISION kit, decisionSpec INSTEAD of instructionContent (a routing table the decision model runs per item; optionally modelType 'decision') and none of commands, whenToUse, skillResources, memorySetup or selfFactsEnabled; the compartment, stance and cadence fields apply to both kinds. Which kind a job is, the exact shape, every cap and every rule: kit_authoring_guide sections 'index' and 'schema'; app codes, tool keys and category slugs: kit_vocabulary. Validated strictly server-side — unknown keys are rejected, never ignored — so kit_validate first.");
1050
1156
  const getMyPublisher = {
1051
1157
  name: "publisher_get_mine",
1052
1158
  title: "Get My Publisher",
@@ -1373,7 +1479,7 @@ const getMcpServer = {
1373
1479
  const createMcpServer = {
1374
1480
  name: "mcp_server_create",
1375
1481
  title: "Register MCP Server",
1376
- description: "Register an MCP server as this account's own custom MCP app — the way to reach an app the platform does not offer — and list its tools in the same call. upstreamUrl must be the MCP endpoint itself (https), never a docs page, and must come from the user or the vendor's docs — never guessed. authType is what the server expects: None (default — discovered in this call, no credential), Bearer {token}, ApiKeyHeader {header, value}, ApiKeyQuery {param, value}, Basic {username, password}, CustomHeaders {headers} or McpOAuth (a human completes the sign-in on the dashboard). credential carries those fields: probed LIVE against the server before anything is stored, stored encrypted, never returned, and the tools are discovered with it. credentialScope: operator (default — each operator connects its own; this call stores it for operatorId or the default operator) or account (one credential serves every operator). Omit credential to register now and connect later with app_connect (provider mcp:<slug>). Returns 201 {server (the mcp_servers_list shape — id is what a kit binds, gatewayId what the next tools take), discovery {added, changed, removed, unchanged}, next (what to do now)}. The server is UNPUBLISHED until mcp_server_set_tools enables at least one tool. Errors: 400 credential_invalid = the server rejected the credential (nothing was kept — fix it and retry); 502 provider_unavailable = the URL did not answer as an MCP server (nothing was kept — check the endpoint or retry later); 422 invalid_url = not an https URL on a public host; 400 invalid_request names the valid authType / credentialScope values or the 25-server ceiling. Tell the user: the upstream URL becomes public listing data if a kit binding this server publishes publicly. Rate limit: 20 registers/hour per account. Requires the manageConnections scope." +
1482
+ description: "Register an MCP server as this account's own custom MCP app — the way to reach an app the platform does not offer — and list its tools in the same call. upstreamUrl is the MCP endpoint itself (https), from the user or the vendor's docs, never a docs page and never guessed. authType is what the server expects: None (default — discovered in this call, no credential), Bearer {token}, ApiKeyHeader {header, value}, ApiKeyQuery {param, value}, Basic {username, password}, CustomHeaders {headers} or McpOAuth (a human completes the sign-in on the dashboard); credential carries those fields — probed LIVE against the server before anything is stored, stored encrypted, never returned, and the tools are discovered with it. credentialScope: operator (default — each operator connects its own; stored for operatorId or the default operator) or account (one credential serves every operator). Omit credential to register now and connect later with app_connect (provider mcp:<slug>). Returns 201 {server (the mcp_servers_list shape — id is what a kit binds, gatewayId what the next tools take), discovery {added, changed, removed, unchanged}, next}. The server stays UNPUBLISHED until mcp_server_set_tools enables at least one tool. Errors: 400 credential_invalid (the server rejected the credential; nothing kept — fix and retry), 502 provider_unavailable (the URL did not answer as an MCP server; nothing kept — check the endpoint or retry later), 422 invalid_url (not an https URL on a public host), 400 invalid_request (names the valid authType / credentialScope values or the 25-server ceiling). Tell the user the upstream URL becomes public listing data if a kit binding this server publishes publicly. Rate: 20 registers/hour per account. Requires the manageConnections scope." +
1377
1483
  NEW_SCOPE_NOTE,
1378
1484
  auth: "manager",
1379
1485
  method: "post",
@@ -1464,11 +1570,15 @@ export const manageDescriptors = [
1464
1570
  listWorkers,
1465
1571
  getWorker,
1466
1572
  runWorker,
1573
+ runWorkersBulk,
1467
1574
  runsFeed,
1468
1575
  fleetPulse,
1576
+ fleetHealth,
1577
+ accountUsage,
1469
1578
  listRuns,
1470
1579
  getRun,
1471
1580
  getRunEvents,
1581
+ getRunTranscript,
1472
1582
  getRunQuestion,
1473
1583
  answerRunQuestion,
1474
1584
  cancelRun,
@@ -1494,6 +1604,7 @@ export const manageDescriptors = [
1494
1604
  getInstructionVersion,
1495
1605
  restoreInstructionVersion,
1496
1606
  setWorkerEnabled,
1607
+ deleteWorker,
1497
1608
  kitInstallPreview,
1498
1609
  kitInstall,
1499
1610
  clonePreview,