@sema-agent/sdk 17.2.0 → 18.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/openapi.yaml CHANGED
@@ -184,6 +184,10 @@ tags:
184
184
  description: v2 leader pipeline (gated; E2B + git push). NOT the default door-B brain.
185
185
  - name: metrics
186
186
  description: Worker health/cost (ops; uses metricsToken, not a principal).
187
+ - name: admin
188
+ description: >
189
+ Operator-only `/v1/admin/*` actions on THIS engine instance. Today: the drain endpoint (server >= 7.121.0 contract
190
+ body `DrainRequest` / receipt `DrainReceipt`, owned by `@sema-agent/settings-schema` `./fleet`).
187
191
 
188
192
  paths:
189
193
 
@@ -5595,6 +5599,65 @@ paths:
5595
5599
  '403': { description: 'Multi-tenant worker: wiring diagnostics are operator-only (`auth.operator_only`). With an EMPTY `OPERATOR_PRINCIPALS` the answer is instead 501 `capability.operator_principals_required` (server >= 7.116.0; any identity, ahead of authorization — branch on `errorCode`, not the status).' }
5596
5600
  '404': { description: 'This worker serves no wiring diagnostics (older server, or a process not assembled by the composition root).' }
5597
5601
 
5602
+ /v1/admin/drain:
5603
+ parameters:
5604
+ - $ref: '#/components/parameters/PrincipalHeader'
5605
+ post:
5606
+ tags: [admin]
5607
+ operationId: adminDrain
5608
+ x-status: live # server >= 7.121.0 contract body; 7.35.0–7.120.x took a retired `{reason}` body.
5609
+ summary: Put THIS engine instance into draining by a deadline (operator-only).
5610
+ description: >
5611
+ Server >= 7.121.0. The body is the contract `DrainRequest` (owned by `@sema-agent/settings-schema` >= 9.0.0 `./fleet`), the answer
5612
+ the contract `DrainReceipt`. Entering drain: new work is refused (billable submits 503 `draining`; HITL answers — including the
5613
+ durable `/decide` — and resumes are ADMITTED), `GET /readyz` turns 503, preemptible in-flight legs park durably at their next clean
5614
+ turn boundary, live approval cards are re-routed to durable parks; the engine stops itself when in-flight reaches zero or at
5615
+ `deadlineAt` (fixed when entered — a later hot change to the drain window does not move it). After a drain entered through this
5616
+ endpoint, the FIRST stop signal the platform sends is absorbed (deadline unchanged) and only a later one, at least the minimum gap
5617
+ apart, hard-stops — so the platform stop timeout (Docker `stop_grace_period`, systemd `TimeoutStopSec`, k8s
5618
+ `terminationGracePeriodSeconds`) must be >= the requested deadline.
5619
+ IDENTITY — the request names WHICH instance, compared field by field (no state change on a mismatch, 409 `conflict.fleet_identity`
5620
+ naming the field, never echoing the value): `worker` = the engine's worker name (`SEMA_REGISTRY_WORKER`; an engine without a center
5621
+ lane has none, so every request mismatches); `expectedGeneration` = the fleet generation (`/health.generation`), ABSENT AGAINST ABSENT
5622
+ (omitting it is the standalone form, not a wildcard — it mismatches a fleet member); `scope` = the tenant scope, both sides read as a
5623
+ value (the engine's = env `FLEET_SCOPE`; unset on the engine and omitted in the request both read as the default scope `global`);
5624
+ `instanceId` (`/health.instanceId`) is compared only when present.
5625
+ IDEMPOTENT — the same `actionId` (or any other action) while already draining answers 200 `already_draining` and echoes the FIRST
5626
+ drain's `since` / `deadlineAt`, changing nothing. Clients do not auto-retry this POST; re-send with the same `actionId` to retry.
5627
+ RETIRED (zero alias): the 7.35.0–7.120.x `{reason}` "shutdown reason declaration" body (it only recorded a sentence and never
5628
+ drained) now answers 400 `request.field_invalid`; `/health.drainReason` and the `reason` key on 503 `draining` bodies are gone too.
5629
+ Gate order (same as every operator face): identity -> authorization -> body shape -> drain entry -> four-part identity.
5630
+ requestBody:
5631
+ required: true
5632
+ content:
5633
+ application/json:
5634
+ schema: { $ref: '#/components/schemas/DrainRequest' }
5635
+ responses:
5636
+ '200':
5637
+ description: Draining (this call entered it) or already draining (the first drain's facts, unchanged).
5638
+ content:
5639
+ application/json:
5640
+ schema: { $ref: '#/components/schemas/DrainReceipt' }
5641
+ '400': { $ref: '#/components/responses/BadRequest' } # request.field_invalid — the body is not a DrainRequest (including the retired `{reason}` form); nothing was changed
5642
+ '401': { $ref: '#/components/responses/Unauthorized' } # auth.unauthorized (credential gate) / auth.principal_required (REQUIRE_PRINCIPAL deployment, no usable identity)
5643
+ '403': { description: '`auth.operator_only` — OPERATOR_PRINCIPALS is non-empty and the caller is not in it.' }
5644
+ '409':
5645
+ description: >-
5646
+ `conflict.fleet_identity` (server >= 7.121.0) — the request's four-part identity (`scope` / `worker` / `expectedGeneration` /
5647
+ `instanceId`) does not match this engine; the sentence names the mismatched fields (never the values). Nothing was changed.
5648
+ Compare with the target engine's `GET /health` (`instanceId` / `generation`), its worker name and `FLEET_SCOPE`, then re-send.
5649
+ content:
5650
+ application/json:
5651
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
5652
+ '501':
5653
+ description: >-
5654
+ `capability.operator_principals_required` — OPERATOR_PRINCIPALS is empty, so no identity is an operator on this deployment (any
5655
+ deployment shape, ahead of authorization). `capability.drain_state_required` — the host has no shutdown chain wired (an embedded
5656
+ engine), so there is no drain to enter (answered after the body check). Branch on `errorCode`.
5657
+ content:
5658
+ application/json:
5659
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
5660
+
5598
5661
  /v1/sendfile-links:
5599
5662
  parameters:
5600
5663
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -5804,7 +5867,7 @@ paths:
5804
5867
  content:
5805
5868
  application/json:
5806
5869
  schema: { $ref: '#/components/schemas/CcImportPrepareResult' }
5807
- '400': { $ref: '#/components/responses/BadRequest' } # request.body_shape — not {layers:[{layer,path,root,content}]}, or a layer `root` the server does not accept (server >= 7.120.0: not a complete absolute path — see RuleScope; sentence "scope must be 'global' or 'project:<root>' with an absolute root", no ticket minted; <= 7.119.x: an empty `root`, answered with the body-shape sentence)
5870
+ '400': { $ref: '#/components/responses/BadRequest' } # request.body_shape — not {layers:[{layer,path,root,content}]}, or a layer `root` the server does not accept (server >= 7.120.0: not a complete absolute path — see RuleScope; sentence "layers[<i>].root must be an absolute path" on >= 7.121.0, the scope sentence on 7.120.x; no ticket minted; <= 7.119.x: an empty `root`, answered with the body-shape sentence)
5808
5871
  '401': { $ref: '#/components/responses/Unauthorized' }
5809
5872
  '413': { $ref: '#/components/responses/PayloadTooLarge' } # request.payload_too_large — more than 200 candidates across all submitted layers; SHORTEN and resend (retrying the same payload cannot help)
5810
5873
  '429': { $ref: '#/components/responses/RateLimited' }
@@ -6589,7 +6652,12 @@ components:
6589
6652
  ⚠️ On the billable submit set (every POST that starts a model turn — the server's
6590
6653
  `isBillableSubmitPath`, which is wider than the three task/run endpoints) this
6591
6654
  503 slot is SHARED with producers that are not auth at all, so `errorCode` — not the status — is the
6592
- branch key: `draining` (this replica is shutting down; retry against a replacement -> DrainingError),
6655
+ branch key: `draining` (this replica is shutting down; retry against a replacement -> DrainingError; server >= 7.121.0
6656
+ refuses only NEW work while draining — HITL answers, including the durable `/decide`, and resumes are admitted — and the body
6657
+ carries no `reason` key any more),
6658
+ `state.lease_expired` (server >= 7.121.0: this fleet member's serving lease has expired and the center has been unreachable
6659
+ for the deployment's `FLEET_LEASE_OUTAGE_MAX_MS`; new work only — HITL answers and resumes pass, in-flight runs are untouched;
6660
+ it lifts when the center grants again; retry later or route elsewhere -> ServiceStateError),
6593
6661
  `state.model_roster_pending` (roster not loaded yet; retry -> ServiceStateError), and (server >=7.0.0,
6594
6662
  synchronous `POST /v1/tasks` only) `memory.admission_required` — the org-memory admission directory is
6595
6663
  transiently undecidable, refused fail-closed BEFORE any model call, mapped to MemoryAdmissionError.
@@ -6612,7 +6680,7 @@ components:
6612
6680
  schema: { type: integer }
6613
6681
  description: >
6614
6682
  Seconds to wait before retrying. Presence is per producer, not per status:
6615
- `draining` always sends it (15), `state.model_roster_pending` always sends it (5),
6683
+ `draining` always sends it (15), `state.model_roster_pending` always sends it (5), `state.lease_expired` always sends it (60),
6616
6684
  `memory.admission_required` sends it ONLY when the engine supplied a hint — a steady-state refusal
6617
6685
  (no resolver wired) honestly carries neither this header nor the body's `retryAfterSec`.
6618
6686
  The auth legs (401 bad token / 503 missing token) never send it.
@@ -7250,6 +7318,22 @@ components:
7250
7318
  Pin a SEPARATE (usually cheaper) model for compaction. Validated against the catalog on submit — an
7251
7319
  unknown ref is a 400, not a silent fallback to the main model's price.
7252
7320
  appendSystemPrompt: { type: string, description: Text appended to the assembled system prompt for this task. }
7321
+ outputStyle:
7322
+ type: object
7323
+ required: [name]
7324
+ additionalProperties: false
7325
+ description: >-
7326
+ Server >= 7.121.0 (send only when `capabilities.outputStyle` is true; an older server answers 400 `request.body_shape` for the
7327
+ top-level key): the output style's PER-TURN line, handed verbatim to the engine's seat of the same name. Each attachment cycle the
7328
+ engine states "<name> output style is active. <turnReminder>" in its own marker frame (the engine's default sentence when
7329
+ `turnReminder` is absent), echoed on the stream as `steering_injected` with `source: "output_style"`. One fact, one seat: the
7330
+ style's BODY (the guide text) still goes in `settings.outputStyle`; this key carries only the name and the per-turn line.
7331
+ The value's owner is the engine (exactly these two keys, `name` non-empty and at most 256 characters, `turnReminder` a non-empty
7332
+ string when present); the server does not judge it — a bad value is accepted and refused at prepare, the run ends `failed` with
7333
+ code `config.output_style_invalid` (never silently ignored).
7334
+ properties:
7335
+ name: { type: string, minLength: 1, maxLength: 256 }
7336
+ turnReminder: { type: string, minLength: 1 }
7253
7337
  promptProfile:
7254
7338
  type: string
7255
7339
  enum: [simple, classic]
@@ -9221,6 +9305,8 @@ components:
9221
9305
  bgNotifyFailClosed: { type: boolean }
9222
9306
  steerPriority: { type: string }
9223
9307
  hookFailureNotice: { type: boolean, description: 'server >= 7.48.0 (#281): this binary emits a SECOND `hook_notice` kind, `hook_non_blocking_failure` (the user''s own hook broke: exit != 0 and != 2 / timeout / spawn failure / bad JSON). Absent = an older worker that only emits `hook_decision_unavailable` — branch by `kind`, never crash on an unknown one.' }
9308
+ leaseClient: { type: boolean, description: 'server >= 7.121.0, a version bit (always true, rides with the `fleet` object): this binary carries the serving-lease CLIENT — when the engine is a fleet member (`FLEET_GENERATION`) and the center declares in its heartbeat reply that it answers renewals (`servingLease`), the engine renews and is governed by the lease. Deliberately NOT named `servingLease` (that is the center''s bit): this says the client exists, not that a lease is in force — read `/health.leaseState` for that. Absent = an older worker.' }
9309
+ generationEcho: { type: boolean, description: 'server >= 7.121.0, a version bit (always true, rides with the `fleet` object): this binary echoes env `FLEET_GENERATION` as `generation` on the heartbeat and on `/health` (with `leaseState` / `leaseUntil`), and the drain endpoint `POST /v1/admin/drain` compares the four-part identity including `expectedGeneration`. Absent = an older worker.' }
9224
9310
  workspace:
9225
9311
  description: >
9226
9312
  #3 (server 1.299–7.51): workspace-browse capability family — an OBJECT when the E19 snapshot-store
@@ -9257,6 +9343,13 @@ components:
9257
9343
  effortSelection: { type: boolean, description: "`reasoningEffort` accepted per request." }
9258
9344
  compactionModel: { type: boolean, description: "A separate model may be pinned for compaction." }
9259
9345
  appendSystemPrompt: { type: boolean, description: "Per-request system-prompt append is honoured." }
9346
+ outputStyle:
9347
+ type: boolean
9348
+ description: >-
9349
+ server >= 7.121.0, hardcoded true (a predicate-class bit, same family as `approvalDecidedBy` / `sessionReadRedaction`): the
9350
+ top-level request key TaskRequest.outputStyle (the output style's per-turn line) is accepted and handed verbatim to the engine.
9351
+ Send the key only when this is present; absent = an older server, which answers 400 for it. Not the same thing as
9352
+ `taskSettings.outputStyle` (that bit says `settings.outputStyle`, the style BODY, is accepted) — two seats, two facts.
9260
9353
  toolOutput: { type: boolean, description: "`tool_end` carries the model-facing output." }
9261
9354
  messageIdentity: { type: boolean, description: "Content events carry `eventId` (+ `parentToolCallId` for sub-agents)." }
9262
9355
  forwardSubagentEvents: { type: boolean, description: "Sub-agent events are forwarded onto the parent stream." }
@@ -15509,6 +15602,17 @@ components:
15509
15602
  was armed but declined/failed (check the store, check authorisation), `false` = there was no lane at all
15510
15603
  (wire one up). The two recovery verbs differ, so a consumer MUST render them apart — NEVER fold them into
15511
15604
  one "nobody approved it".
15605
+ `server.model_unreachable` (server-minted, audience `user`; server >= 7.108.0, `taskId` added in server 7.121.0) — a model call on
15606
+ this leg ended in error and its last request failed AT CONNECT (VPN switched mid-session / the proxy read at boot went stale / the
15607
+ gateway is gone) — not the provider's answer. `{ code, targetHost, proxyInUse?, remedy, sessionId, taskId? }`: `code` one of the
15608
+ connect-failure set (`ECONNREFUSED` / `ENOTFOUND` / `EAI_AGAIN` / `EHOSTUNREACH` / `ENETUNREACH` / `UND_ERR_CONNECT_TIMEOUT`),
15609
+ `targetHost` the model endpoint's `host:port` (a proxy address never rides this frame), `proxyInUse` present only when this hop
15610
+ went through a proxy and only as the variable NAME (`HTTP_PROXY` / `HTTPS_PROXY`), `remedy` a fixed sentence. `taskId`
15611
+ (server >= 7.121.0) = this leg's run resource id (streaming leg: `X-Task-Id` / the `meta` frame's `taskId`; background leg: the
15612
+ submit receipt's `taskId`; resume leg: the resumed run's id) — the same value as that run's terminal and its run-domain codes'
15613
+ `detail.taskId`; absent when the leg has no run resource (a streaming leg on a deployment without a run store) or on an older
15614
+ server. NO `runId` (the model-call layer cannot see the engine's run identity): a resume leg and its original leg share the
15615
+ `taskId`. Once per leg per target; it says "this endpoint is unreachable", not "this run failed".
15512
15616
  `session.records_skipped` (K-24; server >= 7.104.0 / S-612 form B; contract §17.4 / appendix D.3) — 🔴 SERVER-MINTED
15513
15617
  (not in core's register; owner, audience `user` and mint all live in the server). The session's saved log was
15514
15618
  OPENED and some records could not be read and were skipped (file session backend only — both SQL backends refuse a
@@ -19593,6 +19697,33 @@ components:
19593
19697
  The three user-editable CC settings layers this version reads (core `ImportedSettingsLayer`). The two
19594
19698
  layers it does NOT read are named in `CcImportPreview.uncovered` rather than being silently absent.
19595
19699
 
19700
+ DrainRequest:
19701
+ type: object
19702
+ required: [actionId, worker, deadlineAt]
19703
+ description: >-
19704
+ The drain endpoint's request (contract `DrainRequest`, `@sema-agent/settings-schema` >= 9.0.0 `./fleet`): "put this instance into
19705
+ draining by this deadline". See `POST /v1/admin/drain` for how the identity fields are compared.
19706
+ properties:
19707
+ actionId: { type: string, minLength: 1, description: 'Idempotency key: the same id again answers `already_draining` and changes nothing. Hand-issued actions pick their own (e.g. `manual:<uuid>`).' }
19708
+ worker: { type: string, minLength: 1, description: The engine's worker name (`SEMA_REGISTRY_WORKER`). }
19709
+ deadlineAt: { type: integer, minimum: 0, description: Epoch milliseconds by which the engine stops on its own. }
19710
+ scope: { type: string, minLength: 1, description: 'The tenant scope; omitted = the default scope `global` (compared against env `FLEET_SCOPE`, unset = `global`).' }
19711
+ expectedGeneration: { type: string, pattern: '^(0|[1-9][0-9]*)$', description: 'The fleet generation as a canonical decimal string (`/health.generation`); absent against absent — omit it only for a non-member.' }
19712
+ instanceId: { type: string, minLength: 1, description: The engine's per-process identity (`/health.instanceId`); compared only when present. }
19713
+ term: { type: string, minLength: 1, description: The claim term the sending executor holds for this action (center-issued, opaque); absent on a direct or hand-issued call. }
19714
+ DrainReceipt:
19715
+ type: object
19716
+ required: [actionId, since, deadlineAt, state]
19717
+ additionalProperties: false
19718
+ description: >-
19719
+ The drain endpoint's answer (contract `DrainReceipt`): the engine's drain fact plus how this call found it. `state: draining` = this
19720
+ call entered drain; `already_draining` = it already was (including a drain the engine entered on its own), and the receipt reports the
19721
+ FIRST drain's facts.
19722
+ properties:
19723
+ actionId: { type: string, minLength: 1, description: The action that entered this drain. }
19724
+ since: { type: integer, minimum: 0, description: 'Epoch ms the drain began (the same value as `/health.drainingSince`).' }
19725
+ deadlineAt: { type: integer, minimum: 0, description: Epoch ms the engine stops on its own (fixed when the drain was entered). }
19726
+ state: { type: string, enum: [draining, already_draining] }
19596
19727
  RuleScope:
19597
19728
  description: >
19598
19729
  Where a persisted rule applies (core `RuleScope`). `project.root` is the directory prefix that decides
@@ -19602,9 +19733,12 @@ components:
19602
19733
  layer `root`, and the `project:<root>` discriminant string on write / revoke / the list filter):
19603
19734
  server >= 7.120.0 accepts only a COMPLETE ABSOLUTE PATH — POSIX `/…`, a Windows drive root `X:\…` / `X:/…`,
19604
19735
  or a UNC root `\\host\…` (the engine's durable-scope predicate; every server entry calls it rather than
19605
- keeping a copy). A relative root (`proj`, `./x`, `~/x`) or a drive without a root (`C:`, `C:x`) is a 400 whose
19606
- `error` is exactly "scope must be 'global' or 'project:<root>' with an absolute root" — code
19607
- `request.body_shape` on write / revoke / cc-import prepare, `request.query_invalid` on the list filter.
19736
+ keeping a copy). A relative root (`proj`, `./x`, `~/x`) or a drive without a root (`C:`, `C:x`) is a 400 — code
19737
+ `request.body_shape` on write / revoke / cc-import prepare, `request.query_invalid` on the list filter. Branch on `errorCode`;
19738
+ the sentence is for people: on write / revoke / the list filter the `error` is exactly "scope must be 'global' or
19739
+ 'project:<root>' with an absolute root"; on cc-import prepare (whose body has no `scope` field) server >= 7.121.0 answers
19740
+ "layers[<i>].root must be an absolute path" (`<i>` = the first refused layer, from 0; same predicate), while 7.120.x
19741
+ answered with the scope sentence.
19608
19742
  Server <= 7.119.x checks only that the root is non-empty and keeps relative roots as written; such rows take
19609
19743
  no part in any check on 7.120.0 — SQL backends delete them with a one-time migration at boot, while on the
19610
19744
  file (`local`) backend they stay in the store, are absent from the listing, cannot be revoked, and each run
@@ -19699,8 +19833,8 @@ components:
19699
19833
  what is read is still only the allow bucket.
19700
19834
 
19701
19835
  🔴 `root` follows the RuleScope rule (server >= 7.120.0: a complete absolute path; otherwise 400
19702
- `request.body_shape` with "scope must be 'global' or 'project:<root>' with an absolute root" and no ticket
19703
- minted). It applies to ALL layers alike — a `userSettings` layer's root never becomes a scope, but it is the
19836
+ `request.body_shape` and no ticket minted — the `error` is "layers[<i>].root must be an absolute path" on server >= 7.121.0,
19837
+ naming the first refused layer, and the scope sentence on 7.120.x). It applies to ALL layers alike — a `userSettings` layer's root never becomes a scope, but it is the
19704
19838
  same field under the same check. On <= 7.119.x only an empty root is refused (with the body-shape sentence).
19705
19839
  Its TRUTHFULNESS is deliberately NOT validated: the server has no such tree to check against, and pretending
19706
19840
  to validate would be a lie. It is the user's own label for "which cwds should this rule be live under".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/sdk",
3
- "version": "17.2.0",
3
+ "version": "18.0.0",
4
4
  "description": "Typed, zero-runtime-dependency SDK for the Sema agent fleet usage plane. The shared substrate for all doors (CC/Codex MCP façade + web). Server-side only — tokens never enter the browser.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -52,6 +52,6 @@
52
52
  "prepublishOnly": "rm -f tsconfig.tsbuildinfo && npm run build && npm run typecheck --workspaces=false --prefix ../.. && npm test --workspaces=false --prefix ../.. && npm run prepack"
53
53
  },
54
54
  "devDependencies": {
55
- "@sema-agent/settings-schema": "^8.0.0"
55
+ "@sema-agent/settings-schema": "^9.0.0"
56
56
  }
57
57
  }