npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sema-agent/sdk

v7.2.0

Published

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.

Downloads

12,816

Readme

@sema-agent/sdk

Typed, zero-runtime-dependency SDK for the Sema agent fleet usage plane — the single contract entry for driving agents from business systems, AIs, and humans. Ships the openapi.yaml spec it is generated against so producer and consumer anchor the same wire.

Server-side only — bearer tokens never enter the browser. Browser surfaces go through a BFF that holds the token (see docs/browser.md in the repository).

Install

npm install @sema-agent/sdk

Requires Node >= 20.

Quick start

import { AgentClient } from "@sema-agent/sdk";

const client = new AgentClient({
  baseUrl: process.env.SEMA_BASE_URL!,   // your worker / gateway endpoint
  authToken: process.env.SEMA_TOKEN!,    // service bearer token (never hardcode)
});

// Submit a task and tail its durable, resumable event stream
const task = await client.tasks.submit({ objective: "summarize the repo" });
for await (const ev of client.runs.events(task.taskId)) {
  if (ev.type === "text") console.log(ev.text);
}

Surface

  • client.tasks / client.runs — submit, durable runs, SSE event streams with resume.
  • client.sessions — session read/manage; pushSessionBundle / pullSessionBundle (top-level exports) move sessions between peers.
  • client.approvals — HITL: pending checkpoints, decide (approve/deny/answer), live stream(), per-session exemptions (listExemptions / revokeExemption).
  • client.questions — answer a live AskUserQuestion (questions.respond).
  • client.toolApprovals — the engine's permission gate on a live stream: respond (legacy tool_approval frame) and decideAsk (design/172 in-stream approval protocol — released: the endpoint shipped in server 7.3.0, this verb in SDK 6.7.0; reachable wherever capabilities.streamApproval is true).
  • client.rules — persisted permission rules ("don't ask again" as a standing answer): ccImportPrepare / ccImportRedeem import a user's CC settings allow bucket; list / revoke are the governance half (list what is live, revoke by contentDELETE /v1/rules has no :id segment). capabilities.permissionRules gates the lane; gate the revocation half on capabilities.permissionRulesRevoke instead — the first bit predates those routes, so a 7.11.0 worker answers true while they do not exist. Absent revoke bit ⇒ older worker (calling anyway yields a NotFoundError with errorCode: "not_found.route", distinguishable from every other 404 on this family and from the 501 capability.rule_store_required).
  • client.elicitations — answer an inbound MCP elicitation form.
  • client.workflows, client.fleet, client.models, client.usage, client.trace, … — see the typed exports in dist/index.d.ts and the bundled openapi.yaml.

Typed errors (APIError hierarchy: AuthError, RateLimitedError, ConflictError, ApprovalStaleError, …) let callers branch on semantics instead of strings.

6.14.0 — 2026-08-10(对表 server 7.12.0,七件的 server 半场在其树上均未发布) — rule revocation face, core 5.23.0 pickup, two capability bits (additive, no breaking change)

Seven items, every new key optional — nothing is removed, renamed or narrowed.

  • GET / DELETE /v1/rules — the rule REVOCATION face (client.rules.list / .revoke; server design/203 §2). Until now the SDK could only import rules; there was no typed way to see what a principal actually has, or to take one back — a consumer had to hand-roll HTTP against a governance surface.
    • DELETE /v1/rules has NO :id path segment. A rule's identity IS its (rule, scope) content pair — the server mints no row id — so echo both back verbatim from the listing. A client that follows REST intuition to /v1/rules/{id} 404s forever, and a 404 on this face says nothing at all.
    • Revoking is idempotent and deliberately NOT a 404: a repeat, or a rule that never existed, is 200 {status:"no-op"}. A "did this ever exist?" 404 would be an existence oracle — a cross-tenant one under the operator override — and deletion has no business answering that.
    • Read stillLive before telling anyone the rule is gone. true means the tombstone landed but add-wins kept the rule alive (a fresh approval was recorded during the call). BOTH 200 arms carry it, and the key sets are identical on purpose so one consumer branch reads both.
    • Listing is keyset-paged and the cursor is bound to (rev, principal, scope). A mismatch is a 400 request.query_invalid (body carries the current rev), not a silent restart: restarting quietly would let a client read page 2 as "continuing page 1" and hand a governance view a list that is both short and duplicated, with nobody the wiser. Pass nextCursor back byte-for-byte; absent = last page.
    • Operator override. principal reads or revokes another tenant's rules, gated on explicit OPERATOR_PRINCIPALS membership (403 auth.operator_only; an empty list means nobody, never "everybody", and identity is never inferred from the service token). This is the dual of the import lane's principal-only rule, not an exception to it: revoking is the tightening direction.
    • list is a GET and rides the retry matrix; revoke is not submit-class, so a 503 state.rule_remove_failed is handed back unretried — retry it yourself, then reconcile with list.
    • New exported types: PersistedRule, RuleAdd, RuleDot, RuleAddOrigin, RuleListParams, RuleListResult, RuleRevokeRequest, RuleRevokeResult.
  • 🔴 Event_wiring_manifest.permissionRules declares syncWired / orgGoverned (core 5.23.0 design/182 §9/§7; server projects them from 7.12.0). This one was not a missing-field paper cut. That section is additionalProperties: false, so a consumer validating a real frame against the 6.13.0 spec did not "miss two fields" — the whole section failed validation and the diagnostics surface went unreadable. Same failure family as errorCode on Event_tool_end. syncWired = this worker merges rules into the same consent trust domain as its cross-device/transport peers; orgGoverned = an org governance layer is armed, and because that layer is fail-closed, an unreadable org snapshot escalates every allow into a real human approval. Three-way reading, as always: present-and-false = "concept known, not enabled here"; the key missing entirely = "this worker never sent it". Both are honestly false on server 7.12.0.
  • RuleRefusalReason gains rule_governance_forced (server #204). The gate on this ask came from the operator governance layer, so it never entered the rule lane. Split from rule_lane_unavailable on purpose: that one is "one of four conjuncts failed" (no rule store / no candidate / unreadable command bytes / no verified owner — two of them per-CARD limits), this one is "the lane is healthy, only THIS ask is operator-governed". ⚠️ Neither is a deployment-level verdict — every member of this enum is one ask's outcome, and the only correct source for "does this worker serve rules at all" is capabilities.permissionRules. Keep offering "don't ask again" on the next ask; just not on this one.
  • TaskRequest.oneShot (core 5.23.0 TaskSpec.oneShot) — declare that this submission is one-shot: no later turn exists to receive an async background notification (the archetype is headless sema -p). It grants nothing and guarantees nothing; core consumes it as guidance to the MODEL and swaps the delegation/RunWorkflow receipt wording from "end your turn, you will be notified" to "block-wait via TaskOutput({block:true})". Omitting it on a headless call is a known way to lose those results — that is why the key exists — but sending it is best-effort, not a completion contract: the model may ignore the instruction, budget may run out, and some legs have no poll path. Keep your own wait/reconcile step if you need the results. Per-request, sibling of interactiveTools; a non-boolean is a fail-loud 400 request.field_invalid. 🔴 No capability bit advertises it, and the task body is an open set — a worker that does not yet consume the field takes it, answers 200, and behaves exactly as if you had not sent it, with nothing to probe. The caller-side wait step is therefore a requirement, not a suggestion (a bit is registered upstream).
  • settledBy declared on the turns face (TraceTurn.blocks[], GET /v1/tasks/:id/turns). The server's shared field-picker feeds the turns view, the trace SSE view and the durable tool_end from one source; the spec had only declared it on Event_tool_end. Same closed set (human / timeout / aborted), same rule: absence carries no semantics — an older worker and a posture arm that leaves it unset are indistinguishable, so absent is neither "human" nor "no approval happened". The TS types already carried it on all three faces; this closes the spec side.
  • Two capability bits: outcomeLedger (server 7.12.0 — GET /v1/outcomes, which previously had a 501 but no bit, leaving consumers to trial-by-501) and modeShellGateTranslation (server 7.12.0, design/201 §3 — hard-coded true, landed in the same commit as the permissionModespec.shellGate translation table, so "says yes ⟺ the translation is really there" is structural). 🔴 Probe modeShellGateTranslation on the server you are talking to, never on your own shell version: a shell drops its unconditional MANUAL_MODE_SHELL_GATE env injection on this bit, and keying it off the shell's version opens a widening window on all three real deployments (new shell against an old server, a replica rolled back, a mixed-version fleet). A mechanical diff of routes/capabilities.ts against the SDK's key registry shows these two were the only absentees — 62 derived keys on both sides afterwards.
  • Two more capability bits, closing two feature-negotiation gaps found by adversarial review and adopted upstream in the same window (permissionRulesRevoke, oneShot — server 7.12.0). Both were cases where an existing signal could not distinguish "off" from "this build is older":
    • permissionRulesRevokepermissionRules predates the revocation face, so a 7.11.0 worker answers true while GET/DELETE /v1/rules are not routed. The fix is a new bit, not a tightened old one: the published bit's documented meaning cannot be changed retroactively. Its PRESENCE is the signal (the deps predicate is identical); absent ⇒ older worker, and calling anyway returns not_found.route.
    • oneShot — the task body is an open set, so a worker that does not consume TaskRequest.oneShot still accepts it and answers 200; a headless caller would lose background results with nothing on the wire to show it. Probe this bit before relying on the field. Present ⇒ the key is consumed — which still is not a promise the results arrive, so keep the wait step.
  • Doc correction: client.rules's header used to state that /v1/capabilities carries no bit for this family. That is stale — capabilities.permissionRules is that bit, and its predicate is the same one behind all four endpoints' 501s (says yes ⟺ all four work, not "one of them"). Gate on it; do not trial-by-501 and do not guess from a server version number.

6.9.0 — 2026-08-08(对表 server 7.5.0) — durable approval arms + forward statements promoted to fact (additive, no breaking change)

  • AgentEvent gains three arms (#185a) — tool_approval, tool_approval_complete, approval_request. These frames were already replayed by the durable leg (runs.events() / GET /v1/runs/:id/events: the server appends them and the read boundary re-emits {type, ...data}), but the union did not declare them, so a consumer switching only on the declared arms silently dropped approval cards, and catching them needed a widening cast. Now switch (ev.type) both dispatches and narrows. The arms are intersections, not copies, so the frame types stay the single owner of their key sets and governanceForced (or any future additive key) lands for free. The spec mirrors this with Event_tool_approval / Event_tool_approval_complete / Event_approval_request in AgentEvent's oneOf + discriminator.mapping, and the reflective spec-union-arm-gate now pins all three on both sides.
    • approval_request narrows to ApprovalFrameEnvelope, NOT to ApprovalRequestFrame — deliberately. The v1 frame type pins schemaVersion: 1 and kind: "permission", but SSE parsing does not validate, so a v1-typed arm would present a future-version frame to TypeScript as v1 and invite an unchecked ev.card… read — precisely the bypass of design/172's "unknown shapes get a GENERIC card, never auto-deny" rule. Call isApprovalRequestFrameV1(ev) in the case to reach card/askId/the window keys; if it does not narrow, render the generic card.
    • approval_revoke has no arm (this batch scoped itself to the three durable-replayed frames). It does arrive on the live leg, so a stream() consumer still widens for it; registering it is an open item, weighed against the fact that AgentEvent is also the durable endpoint's response type where it never replays. Tracked in the union's own header note.
    • An arm being declared says what the payload looks like, never that the frame will arrive: tool_approval* still needs TOOL_APPROVAL_ENABLED and approval_request still needs the design/172 protocol. Runtime reachability is unchanged by this release.
    • ⚠️ One compile-time consequence, stated plainly: growing a union is additive on the wire but VISIBLE to a TypeScript consumer whose switch is exhaustive over AgentEvent — an assertNever(ev) default will now fail to compile until the three cases are handled. That is the intended signal (those frames were already arriving at runtime, and such a switch would have thrown on them), and it is why assertNever's own doc has always said to ship a tolerant default instead. No runtime behavior and no existing key changes.
  • STREAM_APPROVAL_ENABLED two-segment default is now a published fact on both segments — server 7.5.0 went to npm on 2026-08-07, so the "default ON from >= 7.5.0" half is no longer a forward statement and every PENDING/UNVERIFIED disclaimer around it is gone (README, index.ts, types.ts, resources/tool-approvals.ts, openapi.yaml). Both segments still matter: <= 7.4.0 default OFF describes every running 7.3.x/7.4.x install. The authority remains the measured capabilities.streamApproval bit — the flag is only one of its five conjuncts, so no version number implies true.
  • openapi.yaml line-anchor sweep — the ~180 file.ts:NNN forensic anchors (plus their orphaned :NNN continuations) were reduced to file-level coordinates, per the rule the file itself adopted 2026-07-29: server split src/http/server.ts into src/http/routes/*.ts and most line numbers had already rotted past end-of-file. For line-level forensics, grep the server repo by symbol name.

6.8.0 — 2026-08-07(对表 server 7.4.0) — design/172 in-stream approval protocol, SDK half (additive, no breaking change)

Published 2026-08-06 (npm latest); the server half shipped in server 7.3.0 (#151) and is gated behind STREAM_APPROVAL_ENABLED. That flag's default reads in TWO SEGMENTS: default OFF on server <= 7.4.0 (with it off the wire is byte-identical to before and none of this is reachable), and default ON from server >= 7.5.0 (on npm since 2026-08-07; the flip is written into the server CHANGELOG as BREAKING, together with the ask window default 60s -> 300s). Both segments are live deployment realities — the first still describes every 7.3.x/7.4.x install. Never infer from the version number — read capabilities.streamApproval, which is the five-way gate's actual value. The spec path is x-status: gated (shipped, off-by-flag).

  • Two new frames (client.toolApprovals type surface; on the LIVE stream they are out-of-band named frames, NOT AgentEvent arms, interleaved like tool_approval; approval_request ALSO replays on the durable runs.events() tail, where as of 6.9.0 / #185a it DOES have a declared arm — at 6.8.0 it did not, and callers on that version discriminate on the payload's type with a widening cast. approval_revoke is live-only and never becomes an arm): ApprovalRequestFrame (approval_request) and ApprovalRevokeFrame (approval_revoke), plus ApprovalCard / ApprovalRiskAxes / ApprovalCardDelegation. Parallel to, not a replacement for, tool_approval: with the flag on, one ask emits both frames (legacy first) carrying the same approvalId, so consumers dedupe on it and prefer the new frame.
    • Both frame types describe v1 only (schemaVersion: 1, and kind: "permission" on the request frame). Narrow an incoming frame with isApprovalRequestFrameV1 / isApprovalRevokeFrameV1 (both take unknown, so a JSON.parse result goes straight in); if it does not narrow, render the generic card. ApprovalFrameEnvelope documents the minimal common read shape. The predicates validate every required key and every present optional key — a risk.irreversible: "false" or argsOmitted: false must not be narrowed into trusted safety metadata. Leaving the discriminants open would let the type claim "any version may arrive" while still promising the complete v1 card.
    • expiresAtMs is cast once and never recomputed; expiresInMs is monotonically decreasing across replays — a reconnect never renews the window. The open-stream preamble is the full reconciliation baseline (drop local cards not in it); frames replayed out of the durable events tail are timeline rendering only.
    • risk.irreversible / risk.egress are three-state: true / false / absent = not annotated. Do not fold absence to false. risk.requiresRealApproval is always present and is a different fact (the coarse safety-class marker), not a substitute for the two axes.
    • Unknown schemaVersion / kind / card shape must render a generic card, never auto-deny.
  • New capability bit capabilities.streamApproval (server 7.3.0 routes/capabilities.ts:233) — the probe for this whole face. true ⇒ the two frames may arrive and decideAsk settles; false/absent ⇒ the endpoint answers 501 feature.approval_ask_disabled and the frames never appear, same predicate on both sides. Gate the UI on this bit; do not trial-by-501. The predicate is a five-way conjunction (live approval coordinator wired ∧ STREAM_APPROVAL_ENABLED ∧ a store backend ∧ backend.kind != "local" — the ask ledger must be durable ∧ the park facility, i.e. checkpoint store + DURABLE_APPROVAL). The env flag's default reads in two segments: default OFF on server <= 7.4.0 (expect false on virtually every deployment of those versions) and default ON from server >= 7.5.0 (on npm since 2026-08-07). Read the bit, not the version — the flag is only one of the five conjuncts, so true is never implied by a version alone. Orthogonal to toolApproval (live-card leg) and approvals (durable checkpoint leg).
  • New verb client.toolApprovals.decideAsk(taskId, askId, body)POST /v1/tasks/{taskId}/asks/{askId}/decision. Body {decision: "approve"|"deny", updatedInput?, note?, idempotencyKey?, actor?: {label}} — a two-word decision (allow_session is a live-bridge-only semantic and is deliberately absent here), and actor accepts only label (id/verified are always server-minted; an actor never participates in the permission decision). Not a submit verb → the SDK never auto-retries it; idempotencyKey is the only retry credential.
  • Four new typed errors for the response doctrine (design/172 §3.2): AskDecidedError (409 conflict.ask_decided, carries the first-decision echo decision/decidedAtMs/actornote is never echoed), AskIdempotencyConflictError (409 conflict.ask_idempotency), AskGoneError (410, new gone. prefix family — gone.ask_parked carries the gate coordinates {sessionId, gateBoundCallId, gateBoundInputHash} and never the checkpoint token; gone.ask carries {state, deniedReason?}), and AskParkingError (425, new parking. family — retrying fetches the final routing info, it does not retry acceptance; 425 is deliberately outside the auto-retry matrix). 410 and 425 become meaningful statuses in this SDK for the first time. not_found.ask / request.* / feature.approval_ask_disabled ride the existing prefix families. Structured detail is extracted per code, not by one catch-all extractor: the two gone. legs carry disjoint bodies, and an unknown future gone.* carries no assumed detail. A malformed actor is dropped whole (safeParse semantics), never partially salvaged. Attribution is grouped by state: DENIED accepts only the exported DENY_REASON_ROUTING_FAILURE singleton (an operator_denied there is a contract violation — an operator refusal is DECIDED(deny), never DENIED), while VOID accepts the growing void-reason group.
  • AskDecisionAck.actor is optional: when the persisted actor fails its schema check the server omits the field rather than answering 500, so never dereference actor.id without a presence check.
  • ActorAssertion (design/171 §5.1) is a value copy of @sema-agent/registry-core's ActorAssertionWire, following this repo's existing registry-core value-copy convention. ⚠️ The machine same-source anchor is wired (cleared before this release cut): @sema-agent/[email protected] really exports ActorAssertionWire, the devDependency is ^0.17.1, and the key-set + via-domain anchor test lives in test/core-514-pickup.test.ts (§4). The debt note that used to sit here described 0.17.0, whose published dist/ did not contain the export.
  • spec/openapi.yaml: the new path plus ApprovalRequestFrame / ApprovalCard / ApprovalRiskAxes / ApprovalCardDelegation / ApprovalRevokeFrame / ApprovalFrameEnvelope / ActorAssertion / ApprovalAskDecision / AskDecisionBody / AskDecisionAck schemas, and typed failure bodies (AskDecidedConflictBody / AskGoneBody / AskParkingBody) plus the 425 Retry-After header — a client generated from the contract has to be able to perform the durable-gate handoff and the parking poll, not just detect that something failed.

Blocker cleared (historical note). This entry used to carry a ⛔ release blocker: the ActorAssertion value copy had no machine anchor because @sema-agent/[email protected] did not actually export ActorAssertionWire (its CHANGELOG announced it; the published dist/ did not contain it). All three conditions were met before 6.8.0 was cut — registry-core republished as 0.17.1 with the export, the devDependency was bumped to ^0.17.1, and the key-set + via-domain anchor test was wired. Kept here as a record, NOT as a live prohibition.

6.0.0 — BREAKING: RunStatus loses timeout

One removal, mechanical (shipped 2026-08-03 alongside @sema-agent/server 6.0.0):

| Was | Now | Migration | |---|---|---| | RunStatus included "timeout" | the member is deleted | No server emits it. A switch arm on "timeout" becomes a compile error — delete it. A run that ran out of time terminalizes as failed with the reason on errorCode, so branch there instead. Consumers who never named the member are unaffected. |

Everything else in 6.x is additive: 6.1.0–6.6.x wire pickups, 6.7.0 the design/172 in-stream approval consumption surface, 6.8.0 the streamApproval capability bit + the wiring-diagnostics endpoint (see above).

5.0.0 — manual-audit BREAKING corrections (five, all lie-removals)

Every change removes a documented lie rather than adding behavior; migration is mechanical:

  1. Capabilities.excludeTools / deferTools deleted (CAPS-OPS-1). No server version ever emitted them; the real fields live on TaskRequest (unchanged). If you read them off capabilities, you were always reading undefined — delete the branch.
  2. Fabricated fallbacks → honest null (RC-2/-3): getPublishState().approvalsRequired (number | null — absent now means unknown, treat as fail-closed, not "no approvals needed"); getScopeConfigDraft()/putScopeConfigDraft() version (number | null0 was a legal-looking CAS baseline that could overwrite another writer); putMeConfig() result scope/updatedAt (string | null"" was a third state neither side recognized).
  3. pollUntilApproved abort → standard AbortError (RC-8): a caller-side cancel no longer masquerades as the server code expired_token (whose documented semantics — "restart the flow" — made cancellation restart the flow). Branch on err.name === "AbortError".
  4. WorkflowRunStatus drops phantom queued (X-20): core's closed set is running|completed|failed; the open arm still accepts unknown strings.
  5. images.list() nextCursor is string | null (wire truth: always emitted, null on the last page). r.nextCursor == null remains the correct termination test.

4.3.0 — manual-audit additive batch (no breaking change)

The ch4–ch7 additive claims from the incorporated manual (docs/manual/, blackboard [2404]②), all zero-BREAKING:

  • session-sync: PushSessionOptions.logDigest is wired (and skips the local identical short-circuit — the digest IS the content check); toNdjsonStream opt-in streaming push (stream:true, needs fetch duplex:"half"); default remains byte-identical.
  • capabilities/health: TaskRequest.retainSubagentSessions + forwardSubagentEvents (the two knobs whose capability bits existed without the field); probeAuth distinguishes principal_required (a VALID service token on a multi-tenant worker is not "unauthorized"); EngineHealth +6 previously-undeclared keys; SendfileLinkRow fully typed; workspace list/tree return types exported (and now inside the schema gate).
  • fleet/workflows: journal(limit, offset)nextOffset is finally consumable; WorkflowStreamEvent error arm carries errorCode, meta arm carries data.type; bakes.ingest checks res.ok before parsing (gateway HTML 502 → typed error, not SyntaxError); phantom FleetFrame meta ts removed (server never sent it).
  • registry/control: REGISTRY_OAUTH_ERROR_CODES + CONTROL_ROLES exported as values; registry-core anchor re-pinned to ^0.13.0 with a declared-vs-installed lockstep gate; registry spec gate upgraded to path × method (which immediately surfaced the publish get/202/?source=published gaps — all now in spec).
  • New gates: Capabilities bidirectional setdiff (fixture modernized to server 5.3.0), probeAuth suite (was zero-coverage).

Known deferred (major batch): images.list nextCursor type widening to | null (wire truth; == null is the correct termination test today).

4.2.0 — spec-audit fixes, additive (no breaking change)

[2400] 说明书审计 ch1–ch3 的修复车。全部 additive:没有类型收窄、没有删面。三处行为变化写在表里的前三行 (错误分类的 503 走向、toSseResponse 的帧形、parseSse 的空转熔断类型),其余是补面与补文档。

| Change | Why | | --- | --- | | An un-coded 503 now maps to ServiceStateError, not AuthError | A worker's own 503 always carries a machine code (state.* / draining / capability.*), so the un-coded arm is only reachable via an intermediary (ingress / mesh) — i.e. "the upstream is briefly unavailable, retry". Reading that as "you lack permission" points the operator at the wrong fix. Un-coded 401 is unchanged (fail-closed auth) | | toSseResponse now writes an event: <type> line, and strips id from the JSON payload | The server writes named frames everywhere (id:event:data:); this relay helper wrote only data:, so a downstream dispatching on the SSE event name (a browser addEventListener("tool_end", …), a name-routing relay) saw nothing named after passing through a BFF. And id is the SDK's transport cursor stamped by parseSse — writing it to the id: line and leaving it in the body handed consumers a key the wire never had | | A durable stream that keeps re-opening EMPTY now throws typed StreamEndedWithoutTerminalError (carries lastEventId) after 2 such legs, instead of a bare Error after 5 | That signature is a parked run: on suspended/needs_review the server closes a non-running tail immediately, forever. The consumer's move is runs.get(taskId) → branch on the row. The SDK does not call it for you (the parser holds no client — layering), the typed error is the seam | | Two new error families + one new class: checkpoint.* and wake.* prefix dispatch, and GateNotToolApprovalError | The checkpoint.* family is 10 core codes the server mirrors verbatim; only 3 were named, the rest collapsed into anonymous 409s. checkpoint.not_found is the one 404 in the family (→ NotFoundError). gate_not_tool_approval is the THIRD wrong-door guard next to gate_not_resumable / gate_not_plan_review — a /decide aimed at a plan_review gate | | RateLimitedError carries usedMicroUsd / limitMicroUsd on limit.cost_quota_exceeded | The server sends the cost material; it was dropped on the floor, so a UI could only say "quota exceeded" and not "used $X of $Y". Guarded extraction — a malformed field is omitted, never thrown | | AgentEvent gained 6 arms: error, question, question_complete, elicitation, elicitation_complete, workflow_complete (spec Event_* schemas + oneOf + discriminator mapping in the same batch) | All six are on the wire today. error is the 15-minute stream cap (errorCode: "STREAM_MAX_DURATION") — it means the run is still alive, reconnect with Last-Event-ID, it is not a terminal. The other five are the DURABLE half of the HITL/workflow frames: the live leg dispatches them by SSE event name, and the durable leg persists them via append(type, …), so they replay off runs.events too. The spec/SDK arm gate compares those two sides, so arms missing from BOTH were structurally invisible to it | | runs.steer returns a typed SteerReceipt (was unknown) and accepts priority?: "now" \| "next" \| "later" | Three server mint points mean three different things and only delivery tells them apart: applied (live), queued (the run is suspended — the steer parks on its checkpoint), parked_for_wake (the run ENDED — nothing happens until POST /v1/sessions/:id/wake). ⚠️ Doc correction in the same batch: a suspended run is not a 409; the old JSDoc said it was. priority is enum-validated fail-loud server-side but advisory today (core's steer is single-slot) | | TaskRequest.taskId? (caller-minted uuidv7) | The DURABLE second tier of idempotency on POST /v1/runs: Idempotency-Key is an in-memory per-pod cache, so a dispatch gateway re-routing a retry to another instance would start a second run. This replay reads the run store — it is the prerequisite for gateway failover. Non-uuidv7 ⇒ 400; another principal's taskId ⇒ 409, never a cross-tenant oracle | | AgentClientConfig.principalHeader? | The principal header's NAME is deployment config server-side. On a renamed deployment the only way out used to be hand-written fetch — exactly what the SDK constitution forbids. Renaming REPLACES the default (never sends both), and the reserved-header guard follows the configured name so caller extras still cannot forge it | | CheckpointGate.kind known set gained plan_review; PendingCheckpoint.riskDescriptor accepts null | plan_review was the one member of the server's six-kind wire set missing here — a real gate kind that read as "unknown". Both checkpoint stores send an explicit null risk descriptor (the spec already said so) | | approvals: list/stream accept opts.owner (operator scope), decide's answer is QuestionAnswer (was unknown), ApprovalStreamEvent gained the meta and error arms, a malformed data: line now throws instead of being swallowed into a silent reconnect | The stream's shape was labelled FORWARD-DRAFT and never re-checked; re-checking found two real frame types missing. ⚠️ The two answer legs differ in strictness: the durable leg REJECTS an empty answers array, the live questions.respond leg accepts it | | New export: SettingsWebSearch (was declared but unreachable from the package entry) | SemaSettings.webSearch is typed by it, and the package exports map has no deep path — so it could not be named from outside | | Doc-only corrections | The 416 eviction body DOES carry errorCode: "limit.retention_evicted" (three places said it does not, and a test fixture pinned the false shape); heartbeat cadence is 15s on the task/run/trace/workflow/fleet writers (not 30s) with GET /v1/sessions/:id/events the lone exception (bare : hb comments at a tunable ~25s); a concurrent request under the same Idempotency-Key replays the SAME promise (there is no 409); dead server.js:NNN coordinates repointed at real files; the five taskStop 409 codes and the two distinct subagent 404 codes listed in full; the /v1/assistant/* verbs documented as gated on capabilities.approvals (the checkpoint store mounts that whole lane) |

4.1.0 — wire truth, additive (no breaking change)

Contract-reconciliation batch: three shapes the server has been putting on the wire that this SDK did not declare, plus one capability face that declared things the server never sends. Every item is additive — no type narrows, no key is removed (the phantom capability keys stay until the next major).

| Change | Why | | --- | --- | | AgentEvent done.result is now TaskResult \| ActiveRunConflictDoneResult (new exported type) | On the SSE submit lane a conflict.session_active_run rejection IS the terminal done frame, and its result has no taskId/sessionId/stats (server active-run-conflict.ts toDoneFrameResult). The old declaration let ev.result.stats.turns compile and blow up at runtime. Narrow with "stats" in ev.result (or errorCode === "conflict.session_active_run") — status is on BOTH shapes and does not discriminate | | done frames gained replay?: boolean | The idempotent-replay legs (Idempotency-Key cache hit, and the caller a concurrent submit was deduplicated into) mark their terminal replay:true; absent on every live leg. A UI can now say "replayed" instead of implying the work ran twice | | TraceStreamEvent arms: data.type? = that arm's event name | Server >= 5.0.0 double-emits the frame type inside data; it is the only dispatch key left for a proxy/relay that strips the SSE event: line. Optional, because the supported floor is server 3.0.0 | | Capabilities: sessionEvents?: boolean added; modelUsage doc corrected; excludeTools/deferTools documented as phantom | sessionEvents has always been emitted (it gates GET /v1/sessions/{id}/events) and was missing on both sides. modelUsage claimed to gate GET /v1/runs/:id/model-usage, a route that does not exist — the real thing is the model_usage event arm plus the TaskStats.modelUsage echo. excludeTools/deferTools are TaskRequest fields mis-registered on the capability face and are never emitted by any server; kept (removing them is breaking) and scheduled for removal in the next major |

Also new: a route-existence contract gate — every x-status: live path in the spec must have a matching route in the pinned server artifact, so a face that is deleted server-side can no longer keep claiming to be live.

4.0.1 — contract corrections (no breaking change)

Doc/contract patch batch. Nothing here narrows a type or removes a face.

| Change | Why | | --- | --- | | approvals.get(id) now makes ONE call — GET /v1/approvals, filtered by sessionId | The by-id leg it used to try first (GET /v1/approvals/{id}) is 404 on every server 5.0.0 deployment (its legacy approval-store lane was retired), so the "fast path" was a guaranteed wasted round trip before the same list call. Not-found still throws a 404 APIError. The spec keeps the path as an x-status: retired tombstone | | AgentEvent's compaction_outcome arm: trigger widened "auto" \| "manual" \| "forced"string; the arm gained & EventIdentity and lost its index signature | The engine mints those values (the spec calls them engine-shaped passthrough), so the closed union would have become a lie on the next engine value; eventId/parentToolCallId now read as string \| undefined instead of unknown | | Scenario = KnownScenario \| (string & {}), with KnownScenario = the six the server actually serves (code, default, autonomous, code-review, scan, team) | The union still listed the retired oa and was missing three live scenarios. Kept OPEN on purpose — deployments register their own, so closing it would break every caller naming a custom one | | Corrections in the 4.0.0 table below, in spec/openapi.yaml (subagent-tail meta.taskId, memory scopes, the retired approvals path) and in the memory/draining/alias JSDoc | Each was a statement no code backed; the fixes ship with mechanical pins so they cannot rot back |

7.0.0 — BREAKING changes

Supported floor: unchanged. docs/RELEASE.md line 14 makes this batch major-only (收窄类型 = a narrowing is BREAKING); it must not ship under a minor. Of the three input narrowings, two actually break source (images.register / bakes.ingest — each has required keys); bakes.create measured out as additive. Pins for every row live in test/breaking-next-major-narrowing.test.ts (a narrowing nobody watches is one that flows back) — @ts-expect-error double-sided gates for the input narrowings, positive "the real shape now compiles" assertions for the nullable/required repayments below.

| Narrowed / changed | Migration | | --- | --- | | images.register(body): Record<string, unknown>ImageRegisterRequest (requires profile/repo/digest) | Pass a literal (or a typed variable) with the three fields the server hard-requires — it 400s request.field_invalid without them, so any call that breaks here was already failing at runtime. A dynamic Record<string, unknown> no longer type-checks: narrow it first, or satisfies ImageRegisterRequest at the construction site | | bakes.create(body): Record<string, unknown>BakeCreateRequest | Not breaking — measured, not assumed. Every field is optional, so Record<string, unknown> still satisfies TS's weak-type check and keeps compiling (a @ts-expect-error written on the assumption it would break came back as TS2578 Unused — the pin in breaking-next-major-narrowing.test.ts records that correction). ⚠️ profile XOR bands — exactly one, enforced server-side | | bakes.ingest(bakeId, secret, line): Record<string, unknown>BakeIngestLine (requires event) | The type keeps an index signature, so every build.sh passthrough field still rides. Only the statically-unknown event forces a change | | images.byProfile() / images.byDigest() return { image: ImageIndexEntry }; bakes.get() returns { bake: BakeRecordView } | Additive in practice — both types keep [k: string]: unknown, so they stay assignable to Record<string, unknown> and unknown-key reads still yield unknown. Known keys simply stop being unknown |

Same-major repayment: the "type NARROWER than the wire" family

Folded in on purpose rather than deferred again. All of these share one pathology — the server ALWAYS sends the key and the value can be null, while the SDK declared it ?: T. That is not a documentation gap, it is a judgement pointing the wrong way: ?: T invites !== undefined, which is always true when the server really sent null, and the next line calls a method on null. Splitting these across two majors would have made consumers migrate twice for one disease.

| Narrowed / changed | Migration | | --- | --- | | bakes.create() returns BakeSubmitAckstate is now required and string \| null (was state?: string), status is now the closed "queued" \| "running" \| "done" \| "failed" (was string) | Judge with r.state == null, never !== undefined — a freshly queued bake really does carry state: null (the row's SQL column before the runner's first state line). status needs no cast to branch on now | | ImageSelectResult: all seven keys required (the handler is a seven-key literal sendJson); manifestSha is string \| null; podContract / capabilities reuse the named ImagePodContract / ImageCapabilities | Drop the ! / optional chaining on the seven keys. Judge manifestSha with == null |

Not a debt after allimages.list()'s nextCursor was cited as this family's precedent, but the signature had already been nextCursor: string | null for some time; only the JSDoc still said "deferred to the next major". That stale prose is deleted: a settled account left hanging on the wall as precedent is what teaches the next person to defer this class by reflex.

4.0.0 — BREAKING changes

Supported floor: @sema-agent/server >= 3.0.0 — unchanged. Every removal below deletes a compatibility arm for pre-floor or retired wire shapes; no supported server emits the old form (the [2354] compat-purge campaign, server side shipped as @sema-agent/server 5.0.0). Nothing ships a shim. Two of the four deletions — the draining text arm and the /healthz fallback — had their tests flipped to negative assertions in the same commit (e19b4d4); the other two — the SessionSummary alias and the CONFIG_CENTER_* names — were watched by nothing until 4.0.1 added their negative pins (test/breaking-4.0.0-removals.test.ts + a compile-time pin in session-core/test/types.test.ts). A deletion nobody watches is a deletion that can flow back.

| Removed / renamed | Replacement | | --- | --- | | Draining detection via the legacy status 503 && error === "draining" text arm | errorCode is the only discriminator (the literal draining). A 503 carrying only the legacy text falls through to the 503 status tariff and yields ServiceStateError — not DrainingError, and not a bare APIError. (4.2.0 / [2400] CB-3 re-pointed that tariff: an un-coded 503 is "retry shortly", so it stopped landing on AuthError.) Only a below-floor server can still emit that shape | | Registry health probe fallback to the legacy /healthz path, and the legacy hint field on its result | /api/healthz is the only probe path; a server answering only the old path reads as ok: false, and the result object no longer carries a legacy key | | session-core transitional alias SessionSummary | The real name PersistedSessionSummary (the persistence-face type; @sema-agent/sdk's own SessionSummary is the unrelated GET /v1/sessions wire row) — the one-version transition promised at rename time is over | | CONFIG_CENTER_* env names in control-client error text and docs | SEMA_REGISTRY_* only (the server rejects the old names at boot since 5.0.0) |

Heads-up for @sema-agent/server 5.0.0 consumers (no SDK type change — the frame is open-shaped): the subagent tail stream's first meta frame now keys the durable id as taskId (was runId). Code reading data.runId off runs.subagentStream(...)'s meta frame must switch to data.taskId. Also new in 5.0.0 replays: the compaction_outcome event arm, typed in AgentEvent as of SDK 4.0.0 (20867e1 landed on main before that release; no 3.x package ever carried it). 4.0.1 opens its trigger to string — the engine mints those values, so a closed union would have been a lie the moment a new one shipped.

1.0.0 — BREAKING changes

Supported floor: @sema-agent/server >= 3.0.0 — one floor, no second number. That is the release whose HTTP error bodies and SSE error frames carry exactly one machine key (errorCode), and everything below assumes it. (Some individual removals became possible earlier — the brain_status arm, for instance, stopped being emitted at server 1.317 — but an earlier server is still unsupported, because the errorCode single-key contract is not met until 3.0.0.) Nothing below ships a compatibility shim: a shim would keep downstream tests green while the real call site is broken.

| Removed / renamed | Replacement | | --- | --- | | APIError.code (and every subclass constructor's code parameter) | APIError.errorCode — same value, same position | | Response bodies keyed on a legacy top-level code | The top-level code key is no longer read: a body that still sends it yields errorCode: undefined and no typed subclass — upgrade the server. (One narrow defensive arm survives: a body whose error field is an object with a string code still surfaces that as errorCode. It is a don't-crash-in-the-error-path guard against any object-shaped error body, not a supported wire form — no current route sends one.) | | AgentEvent arm { type: "brain_status" } | { type: "status" } (the only arm a >= 3.0.0 server emits, on both legs) | | TraceStreamEvent error frame data.code | data.errorCode | | client.memory.get / .clear / .append / .edit / .remove | client.memory.exportScope(scope) / client.memory.sync(scope, body) — the legacy MemoryStore face is gone server-side (capabilities.memory === false) and returned 404/501 on every current deployment | | Types MemoryRecord, MemoryWriteAck | Removed with the verbs above | | ApprovalDecision.checkpointToken | Send boundCallId + boundInputHash only (echoed verbatim off the PendingCheckpoint); the token is server-internal and was never surfaced | | TaskStats.costUsd (legacy float) | TaskStats.costMicroUsd (integer micro-USD). taskCostMicroUsd(stats) no longer converts a float — a float-only body now reads as undefined |

Also fixed in the same release: workspace.file()'s 413 over-cap envelope is read off errorCode (the server has always sent that key), so WorkspaceFileTooLargeError — with sizeBytes/limit — now actually fires instead of collapsing into a generic APIError.

Contract

The bundled spec is importable:

import specPath from "@sema-agent/sdk/openapi.yaml";

The supported-floor sentence above is load-bearing, not prose: the consumer contract suite reads that number back out of this file and refuses to run against a server below it (packages/sdk/test/live/_fence.ts, gated by packages/sdk/test/fence-version-policy.test.ts). Reword the sentence and the gate goes red by design — keep the Supported floor: \@sema-agent/server` >= X.Y.Z` form, or change the constant with it.

License

BUSL-1.1