Skip to content

feat(client): declare ?kinds=agent-skill, and treat a 422 as "no skills yet" - #109

Merged
XieX merged 15 commits into
xie/agent-skillsfrom
xie/python-skills-kinds-param
Sep 29, 2026
Merged

XieX merged 15 commits into
xie/agent-skillsfrom
xie/python-skills-kinds-param

Conversation

@XieX

@XieX XieX commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacks on #94 -> #93 -> #87. Spec: ai-sdks-monorepo#23. TypeScript counterpart: js-ai-sdk#87. Server side: streamer#4730.

This is the SDK half of the release blocker: other SDKs retry forever when they see a skill payload. Flag Delivery's answer is ?kinds=, which narrows a connection to the payload kinds it declares, defaulting to {flagging} (so we need to opt-in to see skills).

The declaration

Every request now carries kinds=agent-skill — both endpoints, and on the first request as well as the ones after it, since it selects what the connection is served rather than describing what the store already holds. Without it the store receives the environment's flag payload and no skills at all, so this is a functional requirement, not for correctness/optimization.

It also fixes something that was already wrong. A skill-enabled environment assigns two payloads, so _ProtocolReader has been warning about the second on every connection, reading only the first intent, and never adopting a basis for the flag payload — re-downloading and discarding it on every reconnect. Declaring one kind makes the connection single-payload, which is the shape the reader is built for. (That is also why declaring flagging,agent-skill is not the safe-looking option it appears to be.)

No mv, still — but for a corrected reason. It selects the flag data model, and objectQueryForCommand overrides whatever a request asks for with the payload's own default for any non-flagging payload, so sending it would state a preference that is ignored. The old comment said the connection would be refused over it.

🤖 Generated with Claude Code, edited by @XieX


Note

Overview
FDv2 skill delivery now sends kinds=agent-skill on every poll/stream request (with basis when applicable), so connections receive only the agent-skill payload instead of flags plus skills. HTTP 422 is treated as a fatal, non-retryable error (aligned with the TypeScript SDK): delivery stops, failed/last_error are set, connection_failures is unchanged, wait_for_skills returns immediately, and operators are pointed at start() on the same store after fixing the cause (e.g. view-scoped SDK key) rather than only a process restart.

Integrity logging gains a tenth reason_code, version_mismatch, via record_version_mismatch when a pinned version does not match what the store returned (served_version on the log record; wrong_version on get_skill_result). Like key_mismatch, it emits the ld.skills.integrity_failure log only—no product integrity signal. README/agents.md and tests cover 422, kinds, give-up messaging, and the write path for version mismatches.

Reviewed by Cursor Bugbot for commit 260856b. Bugbot is set up for automated code reviews on this repo. Configure here.

…ls yet"

Delivery now narrows a connection to the payload kinds it declares and defaults
to flags (launchdarkly/streamer#4730), so the store has to ask for the
agent-skill payload or receive the environment's flags and no skills at all.
The declaration goes on every request, before any basis exists as well as
alongside one: it selects what the connection is served rather than describing
what the store already holds.

It also fixes something that was already wrong. A skill-enabled environment
assigns two payloads, so the reader has been warning about the second and
reading only the first intent, and the flag payload was re-downloaded and
discarded on every reconnect because its basis was never adopted. Declaring one
kind makes the connection single-payload, which is the shape the reader is
built for.

The 422 that comes with it is the interesting half. It is the answer when the
credential is assigned no agent-skill payload, which is every project where no
skill has ever been created -- gonfalon creates that row with the first skill
and never lazily. As an ordinary recoverable failure it would spend
max_consecutive_failures and then report "gave up after N consecutive failures:
HTTP 422" for an ordinary configuration; as a fatal one, the skill created a
minute later would never arrive without a process restart. So it is its own
class: _NoSkillPayloadError, caught ahead of _RecoverableTransportError, said
once, counted under the new payload_unavailable diagnostic, retried at
max_backoff indefinitely, and kept off connection_failures, last_error and
failed. The retry is at the cap because _failures deliberately never moves, so
the exponential schedule would otherwise sit at the initial delay forever.

Nine tests, each of which fails with the source reverted: the declaration on
both endpoints and on a from-scratch retry, the two kind constants held apart,
the 422's classification, that it never stops delivery and never counts, that
it waits the cap and not the initial delay, that a skill arriving after it is
picked up, and that it leaves the store uninitialised so a wildcard reconcile
prunes nothing. The fake endpoint gained a standing default status, since "every
request is answered 422" is not something a queue can express.

Also corrects docs that described the payload as classified `generic` and the
request as carrying `mv`.

Gate from python/: make test (1888 passed, 11 skipped), typecheck, lint,
format-check all clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"The answer for a project in which no skill has ever been created" appeared
seven times in one file: on the diagnostic, on the exception class, in
_classify_status, on the once-per-store flag, and twice in the delivery loop.

_NoSkillPayloadError now owns the explanation, since it is what the other sites
refer to, and each of those states only what is local to it: the diagnostic
names the type and keeps the "not a connection_failures" distinction, the
except block keeps why it is caught first and why it waits the cap, and
_classify_status keeps nothing -- the type it returns and the message it builds
already say it twice over.

No behaviour change, and the user-facing 422 message is untouched. 15 lines of
comment removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
XieX added a commit to launchdarkly/js-ai-sdk that referenced this pull request Sep 24, 2026
Mirrors the Python trim (launchdarkly/python-ai-sdk#109): the same sentence had
been repeated at every site that touches the 422, so NoSkillPayloadError now
owns the explanation and the others state only what is local to them — the
diagnostic links the type and keeps the "not a connectionFailures" distinction,
the loop keeps why it waits the cap, and classifyStatus keeps nothing, since
the type it returns and the message it builds already say it twice over.

No behaviour change, and the user-facing 422 message is untouched. 12 lines of
comment removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Base automatically changed from xie/python-skills-revoke-by-omission to xie/python-agent-skills-review-fixes September 24, 2026 19:33
Base automatically changed from xie/python-agent-skills-review-fixes to xie/agent-skills September 24, 2026 19:33
XieX and others added 2 commits September 28, 2026 12:15
… the 422

The declaration changed what arrives on the connection, and three places
still described the old shape:

- the README told customers the connection also carries their flags and
  that `objects_ignored` counts them, which is now the opposite of what
  happens;
- `objects_ignored`'s own docstring said the same;
- `_REQUEST_ADVICE` enumerates what the request carries, and is the text a
  user reads on the 400/405/406/414/501 family — exactly what an endpoint
  that does not understand `kinds` would answer — so leaving the parameter
  out of it pointed at the base URI instead of the likely cause.

Also, on the new text: the 422 message and the log line it lands in read as
one 70-word paragraph that stated the retry cadence twice, so the cadence is
now the log's alone; `payload_unavailable` is a public field and documented
itself with a private class name, so it names the status instead; and the
internal service name behind "created with the first skill" is out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…as missing

Two small things in the README's store section. "`connection_failures` stays
at zero" is only true of a store that has had no other trouble; what the
handler guarantees is that the 422s are kept off that counter and off
`last_error`, so say that instead. And the `StoreDiagnostics` field list
omitted `payloads_ignored`, which the type has carried all along.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
XieX and others added 2 commits September 29, 2026 12:13
Implements the §3.21/§3.24 decision for the retrieval outcome that had no
detection surface at all: a store answering a version pin with a
different version now writes the `ld.skills.integrity_failure` log record
with `reason_code: version_mismatch`, and records no product signal.

Adds `record_version_mismatch` beside `record_key_mismatch`, so the
single-emission-site rule still holds by reading one module, and widens
`IntegrityReasonCode` to ten tokens. `SkillOutcomeReason` stays a closed
five — this changes the detection surface, not the public outcome
vocabulary, and both closed sets are asserted in this change so neither
can drift. The check stays where it was, at the retrieval boundary:
`verify_raw_skill` is unary and this comparison is relational, so moving
it inside would mean an optional expected-version parameter that
silently disables the check when a caller omits it.

The signal stays out for the reason it stays out of a key mismatch: the
usual cause is a broken custom store adapter, and LaunchDarkly's own
counter must not fill with customers' adapter bugs. Pinned in both
directions, since an implementation that emitted the signal too would
look correct from every other angle.

The record names both versions. `version` keeps the meaning it has
everywhere else — the version requested — and the version the store
answered with goes in a record-only `served_version`, paralleling
`served_key`. No hash fields: verification passed, so the hashes are not
what disqualified the answer. `served_version` is recorded as an integer
and needs no redaction, being a number rather than a string off the
wire; the requested `version` beside it is deliberately not
shape-checked, since it is the caller's own pin and redacting it would
only hide a caller's mistake from them.

Neither shipped store can reach this outcome — both answer a pin with
exactly that version or `None`, so an ordinary pin miss is `absent` — so
the tests drive it through a hand-built store that answers with another
version, on the accessors and on the `write_skills` path, which resolves
each pinned reference through the same function. A version mismatch
there is per-skill and not `unavailable`, so it takes the narrow
retention: the copy on disk survives under an `error` action while a
genuinely revoked skill in the same run is still removed. Also pins that
the key check wins when a store disagrees on both key and version, since
that ordering decides a caller-visible outcome token and not only a code.

Docs updated in the same change: the README's `reason_code` table and
record field table gain rows and the alerting guidance now names both
record-only codes, and `agents.md`'s outcome table gains a detection
surface column — including the key-mismatch row it never had — plus the
four callers of the shared resolution path and the write-path retention.

Verified the emitted JSON is byte-identical to the TypeScript SDK's for
the same input, modulo `language`.

1892 tests passing, up from 1888; mypy and ruff clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements ai-sdks-monorepo #29 (§3.25, A.12), which specifies the
opposite of #23 and is expected to supersede it. 422 was classified as a
third thing -- neither broken nor terminal -- and that shape is a retry
loop with no bound and no budget: it never moved `_failures`, so it
retried at `max_backoff` for the life of the process, invisible to both
`failed` and `connection_failures`.

The platform chose the status to be terminal rather than us inferring it.
The streamer says so at both sites that produce it -- the
narrow-assignment check and the status constant -- each noting the code
exists so a misconfigured SDK stops instead of hammering the fleet.
Retrying forever is the precise behavior it was picked to prevent.

The stated cause was also wrong, which is what made the old handling look
reasonable. The gate is `payloadvers.SkillDeliveryAllowed`: delivery
enabled for the account, and a credential that is not view-scoped. Every
way of failing it is permanent. Skill existence is not among the causes
-- with the gate open and a non-view-scoped key, the assignment path
creates the agent-skill payload row lazily, so an environment holding
zero skills is assigned an empty payload that commits normally through
`payload-transferred`.

So the message is rewritten as well as the classification. It names the
two causes a reader can act on, and drops the two false claims the old
text made: that the condition is about whether any skill exists, and that
a skill created later arrives without a restart. It is what a customer
pastes into a support ticket, so it is asserted on substance.

Routing 422 through `_give_up` needed no new code and buys the right
accounting: `failed` and `last_error` are set and `connection_failures`
is untouched, because that counter measures consecutive *recoverable*
failures against the retry bound and a fatal never retries. It also makes
`wait_for_skills` return `False` immediately, through the existing
`_end_delivery` release -- the observable half of the classification, so
a boot gated on skills stops paying its whole timeout. Both are asserted
rather than implemented.

`_NoSkillPayloadError` and `diagnostics.payload_unavailable` are deleted.
Both absences are asserted by name, the way §3.22 asserts its unexported
constants, since a reintroduction is otherwise visible only in a log line
no test reads; the diagnostics field set is pinned whole alongside.
Classification is asserted to answer every status in 300..599 with
exactly one of the two classes, so the forbidden shape cannot return
under a different name.

The `?kinds=agent-skill` declaration and the kind constants are
unchanged; they already conform. README and agents.md described the
deleted field and the retry behaviour, so both are rewritten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stacked on #109 (`xie/python-skills-kinds-param`) — review that first,
or read this diff alone, which touches only the 422 classification and
the diagnostics field it fed.

Implements ai-sdks-monorepo
[#29](launchdarkly/ai-sdks-monorepo#29) (§3.25,
A.12). That spec deliberately leaves this SDK non-conforming until this
change lands.

> [!IMPORTANT]
> ai-sdks-monorepo
[#23](launchdarkly/ai-sdks-monorepo#23)
specifies the **opposite** — 422 as a third class, counted and retried
at the backoff cap — and is expected to be closed as superseded. This PR
implements #29, not #23.

## Why 422 is fatal

The platform chose the status to be terminal; this SDK is not inferring
it. The `streamer` repo says so in both places that produce it —
`internal/fdcore/narrowassign/narrowassign.go:24` and
`internal/fdcore/adapters/httpsrv/status/status.go` — each noting that
SDKs treat a non-400 4xx as terminal, so a misconfigured SDK stops
instead of hammering the fleet. Retrying forever is the precise
behaviour the status was picked to prevent.

The previous handling was a third class: neither broken nor terminal. It
never moved `_failures`, so it retried at `max_backoff` for the life of
the process, invisible to both `failed` and `connection_failures` — a
loop with no bound and no budget.

## The stated cause was also wrong

That is what made the old handling look reasonable. The gate is
`payloadvers.SkillDeliveryAllowed` in gonfalon: delivery enabled for the
account **and** a credential that is not view-scoped. Every way of
failing it is permanent.

**Skill existence is not among the causes.** With the gate open and a
non-view-scoped key, the assignment path creates the agent-skill payload
row *lazily*, so an environment holding zero skills is assigned an empty
payload that commits normally through `payload-transferred`. Nothing
here assumes gonfalon #73018 (eager row creation) lands — it is closed.

So the message is rewritten alongside the classification. It names the
two causes a reader can act on, and drops the two false claims the old
text made: that the condition is about whether any skill exists, and
that a skill created later arrives without a restart. It is what a
customer pastes into a support ticket, so it is asserted on substance
rather than prose.

The third cause the spec lists — a typo'd `kinds` value — is
deliberately **not** in the message: this SDK sends a module constant,
so it is not reachable by a customer.

## What changed

| File | Change |
| --- | --- |
| `skills_fdv2.py` | `_classify_status` returns `_FatalTransportError`
for 422, in its own branch; `_NoSkillPayloadError`, the `except` block
in `_run`, the `_warned_no_skill_payload` flag, and
`StoreDiagnostics.payload_unavailable` are deleted |
| `test_skills_fdv2.py` | Nine tests covering the new behaviour; the
three that asserted the old behaviour are inverted or deleted |
| `README.md`, `agents.md` | Both described the deleted field and the
retry behaviour |

A dedicated branch rather than folding 422 into the existing `(405, 406,
414, 501)` list: that list's `_REQUEST_ADVICE` points the reader at the
base URI, which is not where the problem is.

**Routing 422 through `_give_up` needed no new code**, and buys the
right accounting for free — `failed` and `last_error` are set and
`connection_failures` is untouched, because that counter measures
consecutive *recoverable* failures against the retry bound and a fatal
never retries. It also makes `wait_for_skills` return `False`
immediately, through the existing `_end_delivery` release. Both are
asserted rather than implemented.

## Tests

1132 pass; lint, format, and `mypy --strict` clean. Each new assertion
was verified to bite, by mutating the source three ways:

| Mutation | Result |
| --- | --- |
| 422 → recoverable | all five end-to-end 422 tests fail, plus the
classification test |
| 422 → bare `Exception` (a genuine third class) | shape test fails:
`HTTP 422 classified as Exception, which is neither exactly recoverable
nor exactly fatal` |
| Reintroduce `_NoSkillPayloadError` and `payload_unavailable` | both
absence tests fail |

The shape test sweeps `range(300, 600)` and asserts exactly one class
per status via XOR, so the forbidden shape cannot return under a
different name. The two absences are asserted by name — the way §3.22
asserts its unexported constants — since a reintroduction is otherwise
visible only in a log line no test reads; the diagnostics field set is
pinned whole alongside. The `wait_for_skills` test asserts value *and*
elapsed time against a 30s timeout.

## The one cost, from the spec

When an account's gate opens, a process whose store already gave up
needs a restart — nothing reopens delivery short of constructing a new
store, since `close` is final and a later `start` raises. Acceptable for
a beta enablement step, and deliberately preferred to retrying a
permanent rejection for the life of the process. The README and
`agents.md` both say so, so it is not rediscovered as a bug.

## Scope

The `?kinds=agent-skill` declaration and the payload/object kind
constants are unchanged — they already conform. Nothing else in the
transport is touched.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
XieX added a commit to launchdarkly/js-ai-sdk that referenced this pull request Sep 29, 2026
Stacked on #87 — base is `xie/skills-kinds-param`, not `main`.

Declaring `?kinds=agent-skill` changed what arrives on the connection,
and four places still described the pre-declaration shape. This is the
JS counterpart of launchdarkly/python-ai-sdk#109 (`91c9e20`).

### README

- **"The connection also carries your flags"** told customers a client
cannot request only the skill payload, and that `objectsIgnored` counts
the flag and segment objects arriving anyway. Both halves are now the
opposite of what happens: `FDV2_PAYLOAD_KIND` is on every request, which
is what makes the connection carry exactly one payload.
- the **`addListener`** paragraph repeated the same claim in passing, as
the reason a non-skill kind is never dispatched.

`objectsIgnored` keeps an explanation rather than losing one, because
the counter still exists and now means something narrower and worth
saying: an object kind this version does not recognise, not the
environment's flags. That wording matches Python's `objects_ignored`,
which rewrote this paragraph rather than deleting it.

### Source and agents.md

- **`objectsIgnored`'s own doc comment** said what the README paragraph
did, so the public API docs still told a reader the counter tallies
their flags.
- **`REQUEST_ADVICE`** enumerates what the request carries and left
`kinds` out. It is the text a user reads on the 400/405/406/414/501
family — exactly what an endpoint that does not understand `kinds` would
answer — so the omission pointed at the base URI instead of the likely
cause.
- **agents.md** attributed single-payload delivery to delivery itself
("Delivery provides one payload per credential"), contradicting the
paragraph twelve lines above: a skill-enabled environment assigns two,
and it is the declaration that narrows the connection to one.

### Deliberately unchanged

- `payloadUnavailable` documents itself with `{@link
NoSkillPayloadError}`, which is fine here because that class is
exported; Python's equivalent was private, which is why it was reworded
there.
- `payloadsIgnored` keeps "Zero while delivery sends one payload per
connection" — the declaration makes that condition unconditional rather
than false, and Python left its wording alone too.

### Known divergence

Python's agents.md still carries the "one payload per credential" claim
fixed here, so the two trees differ on that line until python-ai-sdk
catches up.

### Gate

`tsc --noEmit` clean, `biome check` clean, `vitest run` 1024 passed / 10
skipped in `packages/client` — the same counts #87 recorded, and the ten
are the capability-gated TOCTOU tests. Docs only; no behaviour change,
so no tests added.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
XieX and others added 2 commits September 29, 2026 14:16
The branch tip does not parse: the edit that shortened the 422 message
dropped its closing quote, leaving an unterminated string literal in
`_classify_status`. It also dropped both causes the message is asserted
to name, so `test_the_422_message_names_both_of_its_real_causes` fails
on the wording as well.

Restored at roughly the shortened length, naming the account-level
enablement, the view-scoped key and the restart that clears it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The new prose restated what the neighbouring function already explains.
`record_version_mismatch` keeps what differs from `record_key_mismatch`
-- the extra key argument, the custom adapter it exists for -- and
defers the shared rationale to it rather than arguing it a second time.
Its field comments keep the constraints a future edit could break (the
integer, the unchecked pin) and drop the restatement around them.

Same pass over the kinds declaration and the tests that cover it: the
reason a request carries `kinds` belongs on `FDV2_PAYLOAD_KIND`, and
`_url` and the test docstrings point at it instead of repeating it.

No behaviour change, and no assertion removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
XieX and others added 3 commits September 29, 2026 16:04
The 422 message and the README both sent the reader to restart their
process, and that is not what clears it. `_give_up` ends the run, not
the store: only `close` sets `_closed`, and `_closed` is the only thing
`start` refuses -- `start`'s own docstring says so, and
`test_a_restarted_store_does_not_report_the_old_failure` has covered the
general case all along. So a store that stopped on a 422 resumes in
place once the account is enabled, and the message asked for a service
restart instead. It says `start()` now, and the claim is pinned by a
test that drives the 422, opens the gate, and starts the same store.

Four more, all in the text around it:

- The README said `connection_failures` "stays at zero", which is only
  true of a run that had no other trouble -- a 503 then a 422 leaves it
  at 1, since `_give_up` does not reset it. This is the correction
  affb82c already made once, lost in the rewrite; it now says what the
  handler guarantees, which is that the 422 is kept off the counter.
- The new 422 prose in both files named `lastError`,
  `connectionFailures`, `waitForSkills`, `classifyStatus` and
  `FatalTransportError`, and had `waitForSkills` "resolve" -- the
  TypeScript spellings, against a README that lists the snake_case ones
  90 lines further down.
- `record_version_mismatch` wrote `served_version` through unchecked,
  on the reasoning `record_key_mismatch` beside it explicitly rejects:
  the value is store-controlled, and its integer-ness is a property of
  the current call order rather than of the recorder. Guarded the way
  `served_key` is, which is also what keeps the field an integer for the
  byte-comparable JSON. Reverting it logs the test's secret body
  verbatim.
- `_FakeFDv2Endpoint.default_poll_status` was scaffolding for the
  superseded design, assigned by no test, and its comment described the
  every-request-422 retry loop this branch deleted.

`agents.md`'s 422 paragraph was one 587-character line; rewrapped to the
width of the section it sits in, and it now records why the message must
not drift back to "restart the process".

1898 tests passing, up from 1896; lint, format and mypy clean. Each new
assertion verified to bite by mutating the source back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Byte-identical to the TypeScript SDK's message now, and verified so:

  LaunchDarkly will not deliver Agent Skills on this connection (HTTP
  422). The usual cause is a view-scoped SDK key. Check your SDK key or
  contact LaunchDarkly support.

The account-level condition behind the remaining 422s is out. It is not
a customer's setting, so naming it spent the reader's attention on
something they cannot change, and the phrasing implied Agent Skills is
enabled per account when what is really being described is a rollout
state -- not something an error message should narrate. Those cases are
unexpected from the customer's side, so they are treated as unexpected:
referred to support, unenumerated.

`start()` goes too, for room. The recovery is real and still documented
in the README, and `test_a_store_that_gave_up_on_a_422_resumes_on_start`
still pins it -- the message simply is not where it fits any more.

`test_the_422_message_names_both_of_its_real_causes` becomes
`..._names_its_one_actionable_cause`: the cause and the referral are
asserted, and so are the four absences, so the enumeration cannot creep
back. Reverting either half of the message fails it.

The README and `agents.md` carried the same two-cause framing; both now
say what the message says. `agents.md` explains why the message stays
short without describing the condition it leaves out -- this file is
public too, and the reasoning that keeps a rollout out of an error
string keeps it out of a contributor guide.

1898 passing, lint/format/mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The last of the "restart your process" overstatements, and the one with
the widest reach: `_give_up` writes a single line for a 401, a 403, a
404, a 422 and an exhausted retry budget alike, and it told the operator
skills would not update "until the process restarts with a working
connection". A fatal ends the run and not the store -- `close` is the
only thing that forecloses a restart -- so an operator who had just
fixed the cause was sent to bounce a service when `start()` on the same
store would do.

It now names both, cheapest first, and keeps the half that was already
true: the content already held stays readable. The prefix is unchanged,
because `test_a_restart_during_the_give_up_still_delivers` matches on it
to hold the give-up/start race open.

`test_the_give_up_line_points_at_start_not_a_process_restart` asserts
the line against the 401 path, so the claim is pinned for every fatal
that reaches it rather than for the 422 alone. Reverting the wording
fails it.

`agents.md` framed this as a 422 property; it is not, so the paragraph
now covers fatals generally and names the three surfaces that carry the
claim -- the log line, the behaviour, and the README -- with the test
pinning each.

1899 passing, up from 1898; lint, format and mypy clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@XieX
XieX merged commit 64ada1e into xie/agent-skills Sep 29, 2026
7 checks passed
@XieX
XieX deleted the xie/python-skills-kinds-param branch September 29, 2026 20:21
XieX added a commit to launchdarkly/js-ai-sdk that referenced this pull request Sep 30, 2026
Mirrors the Python trim (launchdarkly/python-ai-sdk#109): the same sentence had
been repeated at every site that touches the 422, so NoSkillPayloadError now
owns the explanation and the others state only what is local to them — the
diagnostic links the type and keeps the "not a connectionFailures" distinction,
the loop keeps why it waits the cap, and classifyStatus keeps nothing, since
the type it returns and the message it builds already say it twice over.

No behaviour change, and the user-facing 422 message is untouched. 12 lines of
comment removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants