feat(client): Agent Skills — the FDv2 delivery protocol, without the network - #82
Open
XieX wants to merge 3 commits into
Open
feat(client): Agent Skills — the FDv2 delivery protocol, without the network#82XieX wants to merge 3 commits into
XieX wants to merge 3 commits into
Conversation
…network The half of the delivery transport that has no I/O: identifying a skill object on the wire by kind inline-resource plus category skill, translating it into the raw object shape the SkillStore interface defines, holding it by (key, objectVersion), and applying a payload's events as one commit at payload-transferred. The store that puts a connection underneath this follows separately, so the three decisions that matter most can be reviewed on their own: - objectVersion is the skill's version; version is the payload's. The translation happens in one place and TestVersionTranslation asserts it in both directions, because confusing them fails silently. - Changes commit at payload-transferred, not per object. A half-applied full transfer would briefly empty the store, which with pruning on is the difference between a reconcile and deleting a customer's files. - A hashless object is held, not dropped, so verification withholds it with a reason code rather than the transport reporting it absent. Flag and segment objects share the connection and are skipped and counted, not rejected. Nothing here is exported yet; the store exports it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 0f797c0. Configure here.
The protocol reader took payloads[0]'s intentCode and applied it to the skill object set, which is what the delivery protocol requires — one payload per credential, read the first intent, tolerate the rest — but it left the assumption behind that rule undocumented and unguarded. If the one-payload guarantee ever widens, an xfer-full for another payload would start an empty pending set and the next payload-transferred would publish it: every skill reported revoked, and with pruning on, a customer's files deleted. The first payload is still the payload that is read. What is new is that the reader now knows which payload skills actually arrive on — learnt from the intent's id, or from the (p:<id>:<version>) selector, since no object or transfer event carries a payload id of its own — and declines to apply a transfer of any other, holding last known good, warning once, and counting it in diagnostics.payloads_ignored. An intent describing more than one payload warns once on its own, because that is the one case the comparison cannot catch: another payload's transfer arriving before any skill has been seen has nothing to be compared against. Behaviour under one-payload delivery is unchanged, and a full transfer of the skill payload still empties it — every skill deleted is a real state the guard must not mask. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The SDK-facing FDv2 channel now delivers skills the way streamer #4681 and gonfalon #70638 spell them: object kinds are open strings, the agent-skill payload is classified `generic`, and every generic object carries only `key`, `kind`, `version` and `object`, exactly like a flag. A skill arrives under kind `skill` with its own version folded into the key as `<key>:<version>`. There is no `category` field and no `objectVersion` field; both came from an earlier streamer draft that never shipped. Identification is now the kind alone. The wire key is split in one place, `_split_wire_key`, and both the put and the delete translation go through it. A key that will not split cleanly is held rather than dropped — version-less, or with the offending text as its version — so verification withholds it with `invalid_version` under a key the caller recognises; only a key with nothing before the delimiter is dropped, since there is no identity to hold it under. `SDK_DATA_MODEL_VERSION` goes with it: the connection's `mv` parameter only accepts flag model versions, and generic payloads ignore it. The transport stops sending it in the following change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Stacked on the
watch_skillsPR (xie/skills-watch). Second of three PRs split out of #69; the transport that puts a connection underneath this follows.The half of the delivery transport that has no I/O: identifying a skill object on the wire, translating it into the raw object shape the
SkillStoreinterface defines, holding it by(key, objectVersion), and applying a payload's events as one consistent commit. Splitting it out lets the three decisions that matter most be reviewed without a socket in the way.Three things worth reviewing closely
1. The skill's version is in the object's
key;versionis the payload's. Each version of a skill is its own object on the wire, identified as<key>:<version>(pdf-extraction:3), and that is the only place the skill's version appears. The event'sversionis the payload's, and confusing the two fails silently: the object verifies, the hash matches, and the caller gets content under a version number that means nothing. The wire key is split in exactly one place (_split_wire_key), both the put and the delete translation go through it, andTestVersionTranslationasserts it in both directions. A key that will not split cleanly is held rather than dropped — version-less, or with the offending text as its version — so verification withholds it withinvalid_versionunder a key the caller recognises; only a key with nothing before the delimiter is dropped.2. Changes commit at
payload-transferred, not per object. A payload version is the unit of consistency. A half-applied full transfer would publish a state the server never described and would briefly empty the store, which, with pruning on, is the difference between a reconcile and deleting a customer's skill files. An interrupted transfer leaves last known good intact, and listeners fire once per commit.3. A hashless object is held, not dropped. Dropping it at the transport would report
absent, indistinguishable from "no such skill", and would let a prune delete the last known-good copy on disk. Holding it means verification withholds it withmissing_content_hash, which is diagnosable: an ERROR per(key, version), deduped per reader rather than per process so two stores never quieten each other, a summary per wholly-hashless payload, and aStoreDiagnostics.hashless_objectscounter. There is deliberately no fallback that synthesises a hash from the delivered content.Also here
kind == "skill"; everything else is ignored, not rejected. Object kinds on the SDK-facing channel are open strings and the agent-skill payload is classifiedgeneric, so a skill arrives under the kind its producer registered — the bare category name — with nocategoryorobjectVersionfield (streamer #4681, gonfalon #70638). An environment's assignment carries its flag payload alongside its agent-skill payload, so flag and segment objects arrive as a matter of course. Erroring on them would turn a normal payload into a permanent reconnect loop._SkillObjectSetholds several versions of one key, with lookup semantics identical toInMemorySkillStoredown to the fall-through to a version-less entry. Its opaque snapshot keys are spelt<key>:<version>, the same as the wire, and a test pins that round trip.TestInterfaceParityasserts the two resolve identically._require_server_side_credentialrefuses a mobile key or client-side environment ID. Its tests arrive with the store constructor that calls it.Nothing here is exported yet; the store exports it. The module imports nothing from the feature but the version validator.
Tests
test_skills_fdv2.pydrives_ProtocolReaderdirectly: identification, version translation, full and change transfers, interruption, revocation, tombstones, mixed payloads, unknown kinds and events, error and goodbye, and the hashless dedupe across readers. The wire builders it introduces are shared with the transport PR's fake endpoint.🤖 Generated with Claude Code
Note
Overview
Adds
skills_fdv2.py, a stdlib-only layer belowSkillStorethat parses LaunchDarkly FDv2 events without sockets. It maps wireput-object/delete-objectinto the raw store shape, keeps skills in_SkillObjectSet, and applies updates atomically in_ProtocolReaderatpayload-transferred(not per object).The critical wire rule is
key=skillKey:objectVersionwhile the event’sversionis the payload revision and is dropped—confusing them would silently serve the wrong pinned version. Non-kind == "skill"objects (flags, segments) are ignored, not errors. Foreign payload full transfers are declined once skills’ payload id is known, so a flagxfer-fullcannot wipe held skills (and trigger prune). Hashless skills are retained for verification to withhold withmissing_content_hash, withStoreDiagnosticsand loud logging._require_server_side_credentialrejects mobile/client credentials (for the upcoming networked store).agents.mddocuments the transport contract;skills_coreclarifiesSKILL_OBJECT_KINDvs wire kind.test_skills_fdv2.py(~900 lines) exercises the reader, version translation, payload identity, and parity withInMemorySkillStore. Nothing is exported or wired from accessors yet—that’s the follow-on transport PR.Reviewed by Cursor Bugbot for commit efc4ca7. Bugbot is set up for automated code reviews on this repo. Configure here.