chore: release package#939
Merged
Merged
Conversation
5 tasks
a24e24f to
76462f3
Compare
76462f3 to
cfd16a5
Compare
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.
This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.
Releases
@adcp/client@5.18.0
Minor Changes
af01482: Storyboard runner: add the
a2a_context_continuityvalidation checkplus cross-step A2A envelope tracking. Closes feat(testing): a2a_context_continuity validator for multi-step storyboards #962.
A2A 0.3.0 §7.1 binds follow-up
message/sendcalls to a server-sideconversation via
Message.contextId; the server MUST echo it on theresponse Task. The
@a2a-js/sdk'sDefaultRequestHandlerdoes thisautomatically —
createA2AAdapter(feat(server): A2A transport adapter (preview) peer to MCP serve() #899) passes throughrequestContext.contextId, so a passing seller built on the SDKwon't trip a single-call check. The regression class is sellers that
bypass the SDK's request handler and stamp their own
contextIdonthe response, breaking buyer-side correlation across multi-turn flows
(proposal refinement, IO signing, async approval). This kind of bug
is only surface-able on multi-step storyboards where step N+1
sends with the contextId returned by step N.
The new check runs at step N+1 and compares
a2aEnvelope.result.contextIdagainst the most recent prior step'scaptured envelope. Skip semantics:
not_applicableobservation
contextId→ skipis undefined when the call didn't reach the work layer
"validator self-skipped" from "validator passed because contexts
matched"
Failure cases:
contextId(empty/missing) on anon-first send → fail with a pointer to
/result/contextIdcontextIddiffers from prior step's →fail with both values surfaced and the prior step id named in
the diagnostic
Runner-side plumbing: per-step A2A envelopes are now tracked in
a long-lived
priorA2aEnvelopesmap onExecutionState, populatedafter each capture. The validator reads the most recent insertion-
order entry as the comparison baseline. Probe steps, MCP steps, and
capture-bypass paths don't insert, so cross-step comparisons walk
back to the most recent A2A step automatically.
Suggested by the ad-tech-protocol-expert review on feat(testing): A2A wire-shape capture + a2a_submitted_artifact validation (#904) #952. Filed as
feat(testing): a2a_context_continuity validator for multi-step storyboards #962, scoped as a separate validator since the failure mode and
fix surface are distinct from
a2a_submitted_artifact's single-callwire-shape check.
Coverage:
validateA2AContextContinuity(syntheticenvelopes covering match, divergence, missing contextId, every
skip path)
runStoryboarddriving multi-stepstoryboards: one against a conformant
createA2AAdapter(passes),one against a hand-rolled regressed adapter that stamps a fresh
contextId per send (fails)
6124deb: Storyboard runner: capture A2A wire shape on
protocol: 'a2a'runs andadd the
a2a_submitted_artifactvalidation check. Closes the regressionclass from adcp-client#904 — pre-feat(server): A2A transport adapter (preview) peer to MCP serve() #899 A2A adapters that emitted
Task.state: 'submitted'withfinal: trueandadcp_task_idinsideartifact.parts[0].datainstead ofartifact.metadatawould otherwisepass the storyboard suite despite being non-conformant per A2A 0.3.0.
The check asserts the wire-shape invariants for AdCP
submittedarmsover A2A:
Task.state === 'completed'— A2A Task.state tracks the HTTPtransport call;
'submitted'is the INITIAL state per A2A 0.3.0and forbidden as a terminal value.
Task.idandTask.contextIdnon-empty — required by A2A 0.3.0for
tasks/getaddressability and follow-up correlation.artifact.artifactIdnon-empty — required for chunked-artifactresumption and buyer-side caching.
artifact.metadata.adcp_task_idcarries the AdCP-level handle(per A2A 0.3.0 metadata-extension convention).
artifact.parts[0]is a DataPart withdata.status === 'submitted'— the AdCP payload preserves its native discriminator.
data.adcp_task_idis also present (forward-compatibility fora future AdCP tool whose response schema legitimately includes
it), it MUST equal
metadata.adcp_task_id— divergent orsolo-payload writes are the regression class.
JSON-RPC error envelopes fail the check with a distinct
error_code: 'a2a_jsonrpc_error_envelope'so dashboards can separatetransport rejections from submitted-arm shape drift.
The check self-skips with a
not_applicableobservation on non-A2Aruns (MCP, raw-probe dispatch path) so storyboards can include it
alongside MCP-shape assertions without forcing the runner to know
which transport ran.
Wires
withRawResponseCapturearound the SDK-driven A2A dispatch inthe runner so the JSON-RPC envelope is observable for validation;
captured response bodies pass through
redactSecretsbefore landingin
ValidationContext.a2aEnvelopeso AdCP-style secret-shaped fieldsin DataPart payloads (
api_key,client_secret, etc.) don't reachpersisted compliance reports.
withRawResponseCapturenow surfacespartial captures on rejection (attached as
error.captures) sostoryboard validators get a wire-shape envelope even when the SDK
threw mid-parse. Adds
A2ATaskEnvelopeto the public testing typesand exports
getCapturesFromErrorfrom the protocols module.The companion compliance scenario (feat(compliance): add create_media_buy async submitted → completed storyboard adcp#3083 — the
create_media_buy_async_submittedstoryboard) drives this check.Closes the runner-side half of adcp-client#904.
c085911: Brand
AdcpServeras nominal + lintas anyin skill examples.Two complementary defenses against the API-drift class that landed PR chore(skills+scripts): drift fixes + conformance-replay harness (v0) #945 (the creative skill teaching
server.registerTool):AdcpServeris now a branded (nominal) type. A phantom symbol-keyed property ([ADCP_SERVER_BRAND]?: never) makes(plainObject as AdcpServer)casts from structurally-similar objects fail at compile time. A realAdcpServeris only obtainable by callingcreateAdcpServer(). Closes the door on(somePlainObject as AdcpServer).registerTool(...)patterns that tried to reach for an MCP-SDK method the framework intentionally doesn't expose. Type-only change — no runtime behavior, no breaking change for any caller passing a value produced bycreateAdcpServer().scripts/typecheck-skill-examples.tsnow flagsas anyin extracted skill blocks. The pattern hides the API drift that strict types would otherwise catch — every legitimate cast has a typed alternative (typed factories likehtmlAsset(), named discriminated unions likeAssetInstance, response builders likebuildCreativeResponse()). Newas anyin a skill block fails the harness; existing uses inskills/build-seller-agent/deployment.md(Express middleware boundary code, 2 occurrences) are baselined as known. Authors who genuinely need the escape hatch can use// @ts-expect-erroragainst a specific known issue instead — greppable and self-documenting.Type-level test in
src/lib/server/adcp-server.type-checks.tslocks the brand against regression — if a future change accidentally removes the brand,tsc --noEmitfails because the negative assertions stop firing.This is dx-expert priority Rebrand to Ad Context Protocol and remove side navigation #4 from the matrix-v18 review (CI defenses AdCP Testing Framework with Authentication, Hierarchical Logging & Security Features #1–Update documentation and production configuration #3 shipped in chore(skills+scripts): drift fixes + conformance-replay harness (v0) #945, feat(scripts): typecheck-skill-examples + fix 8 skill imports #957, feat(types): strict discriminator unions — AssetInstance, sync rows, vendor pricing #961).
1158429: Add
${Parent}_${Property}Valuesconst arrays for inline anonymous string-literal unions (closes Codegen: extract inline anonymous string-literal unions to const arrays #932).Companion to the named-enum exports landed in 5.17 (PR feat(server): idempotency: 'disabled' mode + standalone enum value arrays #931). The earlier shipment covered every spec enum that has a stable named type (
MediaChannelValues,PacingValues, etc., 122 total). This release adds the inline anonymous unions that don't have stable named types in the generated TypeScript — exactly the cases where consumers were re-declaring spec literal sets in their own validation code:Naming convention. Every
z.union([z.literal(...), ...])(or itsz.array(...)-wrapped variant) inside a named object schema gets a corresponding export named${ParentSchema}_${PropertyName}Values, where the property name is PascalCased. Property paths that reference a named enum (e.g.unit: DimensionUnitSchema.optional()) are intentionally skipped — use the matching${TypeName}Valuesfromenums.generated.ts.Coverage. 104 inline-union arrays exported across 51 parent schemas. User-flagged cases all included:
ImageAssetRequirements_FormatsValues,VideoAssetRequirements_FormatsValues/_ContainersValues/_CodecsValues,AudioAssetRequirements_FormatsValues/_ChannelsValues, plus video frame-rate/scan-type/GOP-type discriminators, audio channel layouts, account scopes, payment terms, and many more.Implementation. New script
scripts/generate-inline-enum-arrays.tswalks the compiled Zod schemas via runtime introspection (Zod 4_def) rather than regex on the generated TS — cleaner and future-proofs against codegen output format changes. Output goes tosrc/lib/types/inline-enums.generated.ts. Wired into the existinggenerate-zod-schemasscript (runs after Zod codegen, since it depends on Zod schemas being current). The new testtest/lib/inline-enum-arrays.test.jscross-validates every emitted array against the parent Zod schema property — if either side drifts, the test fails fast.Behavior unchanged for existing consumers. Pure addition; no public-API rename, no breaking change to
enums.generated.ts. Adapters can drop their hand-maintainedVALID_IMAGE_FORMATS-style constants in a follow-up.df80e85: feat(testing,server): shape-drift hint + response helper for
get_plan_audit_logsThe storyboard runner's
LIST_WRAPPER_TOOLStable now coversget_plan_audit_logs, so a handler that returns a bare[{plan_id, …}]array instead of{ plans: [...] }gets the targeted hint (Use getPlanAuditLogsResponse() from @adcp/client/server) alongside the AJV error.getPlanAuditLogsResponse(data, summary?)is now exported from@adcp/client/serverand@adcp/client, mirroring the existing list-tool helpers (listPropertyListsResponse,listContentStandardsResponse, …).Note: the wrapper key is
plans, notlogsas issue feat(testing): extend shape-drift detection to get_plan_audit_logs #856's body claimed. Verified againstschemas/cache/3.0.0/governance/get-plan-audit-logs-response.jsonandtools.generated.ts:11542— audit entries are bundled under eachplans[].entries[]record. Closes feat(testing): extend shape-drift detection to get_plan_audit_logs #856.24e9569: feat(server): PostgresTaskStore.createTask accepts optional caller-supplied taskId
Compliance storyboard controller scenarios (
force_create_media_buy_arm,force_task_completion) need to inject buyer-supplied task IDs for storyboarddeterminism.
PostgresTaskStore.createTasknow accepts an optionaltaskIdfield on its first argument: when supplied, the ID is used verbatim; when
omitted, a random hex ID is generated as before. Throws if the supplied ID is
empty, longer than 128 characters, or already exists (the collision is detected
via PG uniqueness constraint, not a pre-check race).
Caveats and follow-ups:
InMemoryTaskStore(re-exported from the upstream MCP SDK) does NOT honorcaller-supplied
taskId— sellers running withoutDATABASE_URL(e.g., testpaths) get random IDs even when one is supplied. Filing an upstream MCP SDK
issue to add
taskId?: stringtoCreateTaskOptionsso both stores can honorit cleanly is the right durable fix; this PR is the Postgres-only shim until
upstream lands.
task_idnamespace onPostgresTaskStoreis process-global today (notenant scoping in the schema). Callers using caller-supplied IDs are
responsible for namespace isolation. A future migration to a composite
(tenant_id, task_id)key would close this for production use.controller tool's input schema. That wiring (runner → tool input → task
store) is a separate change tracked in the parent issue.
efb2fa6: feat(conformance): add
requires_capabilitystoryboard-level skip gateStoryboard runner now evaluates a
requires_capability: { path, equals }predicate before running any phase. When the predicate is false (agent declared the capability unsupported), the runner emits a single{ skipped: true, skip_reason: 'capability_unsupported' }storyboard result instead of a cascade of misleading per-phase failures. This fixes the idempotency universal storyboard running against agents that declareadcp.idempotency.supported: false(added in PR feat(server): idempotency: 'disabled' mode + standalone enum value arrays #931). The same mechanism applies to any future capability-gated storyboard.a085f4a: Cross-domain specialism-declaration runtime check on
createAdcpServer.When a domain handler group (
creative,signals,brandRights) is wired butcapabilities.specialismsdoesn't include any of that domain's specialisms,createAdcpServernow logs an error via the configured logger:The matrix v18 run (issue Compliance matrix maintenance loop — pickup context #785) had this drift class account for ~30% of "agent built every tool but storyboard reports no applicable tracks" cases. The conformance runner gates tracks on the
capabilities.specialismsclaim, so an agent with working tools but no claim grades as failing silently.Logged via
logger.error(matching the idempotency-disabled precedent) rather than thrown — middleware-only test harnesses legitimately wire handlers without declaring specialisms, and a hard throw would create more friction than it removes. Production agents will see the warning in boot logs and conformance failure in the matrix.mediaBuyis intentionally exempt from the check. Its specialism choices (sales-non-guaranteed vs sales-guaranteed vs sales-broadcast-tv vs sales-social etc.) are commercially significant and an agent may legitimately defer the declaration to a follow-up. Thebuild-seller-agentskill cross-cutting pitfalls section already covers the right declaration.Tests in
test/server-create-adcp-server.test.jslock the new behavior:This is dx-expert priority fix the a2a sdk client import issue #5 from the matrix-v18 review (CI defenses AdCP Testing Framework with Authentication, Hierarchical Logging & Security Features #1–Rebrand to Ad Context Protocol and remove side navigation #4 shipped in chore(skills+scripts): drift fixes + conformance-replay harness (v0) #945, feat(scripts): typecheck-skill-examples + fix 8 skill imports #957, feat(types): strict discriminator unions — AssetInstance, sync rows, vendor pricing #961, feat(server,scripts): brand AdcpServer + lint
as anyin skill blocks #970). With this, the cheap-CI-defense ladder is complete.df9d7bd: Extend
StoryboardStepHinttaxonomy:shape_drift,missing_required_field,format_mismatch,monotonic_violation(closes Hint taxonomy: extend StoryboardStepHint beyond context_value_rejected #935; supersedes feat(conformance): add ShapeDriftHint to StoryboardStepHint taxonomy #937).Issue Hint taxonomy: extend StoryboardStepHint beyond context_value_rejected #935 proposed making
StoryboardStepHintthe canonical surface for every runner-side diagnostic that has structured fields a renderer can consume. PR feat(conformance): add ShapeDriftHint to StoryboardStepHint taxonomy #937 shipped the first member (shape_drift) but left the broader vision unfinished — the structured fields were added in parallel to the existingValidationResult.warningprose, and no consumer rendered the structured fields. This release closes the loop:1. Base type + four new hint kinds. New
StoryboardStepHintBaseconstrains every hint to{ kind, message }; the union now includesShapeDriftHint(PR feat(conformance): add ShapeDriftHint to StoryboardStepHint taxonomy #937),MissingRequiredFieldHint,FormatMismatchHint, andMonotonicViolationHint. Each kind carries machine-readable fields so renderers don't regex-parse the prose:kindshape_driftbuild_creative, wrong-wrappersync_creatives/preview_creativetool,observed_variant,expected_variant,instance_pathmissing_required_fieldkeyword: "required"issues (lenient Zod accepted)tool,instance_path,schema_path,missing_fields[],schema_url?format_mismatchformat/pattern/ other non-required keyword that lenient Zod acceptedtool,instance_path,schema_path,keyword,schema_url?monotonic_violationstatus.monotonicinvariant catches an off-graph transitionresource_type,resource_id,from_status,to_status,from_step_id,legal_next_states[],enum_url2. De-duplication. Shape-drift detection moved to
shape-drift-hints.tsas the canonical surface; the legacydetectShapeDriftHint(string) invalidations.tsnow delegates to it so the two surfaces can't drift apart, and the redundant shape-drift prose was removed fromValidationResult.warning(it lives only onstep.hints[]going forward). Strict-AJVwarningprose is kept for one minor for back-compat with consumers that scrape it; new code should consumestep.hints[].3. Assertion → hint plumbing.
AssertionResultgained an optionalhint?: StoryboardStepHintthat the runner mirrors into the owning step'shints[]forscope: "step"results.status.monotonicis the first user — it now emits aMonotonicViolationHintalongside the existing proseerror. The hint surfaces under the same taxonomy regardless of which subsystem (validation, assertion, runner-internal detector) produced it.4. CLI renders structured fields.
bin/adcp-step-hints.jsbranches onhint.kindand prints per-kind detail lines under each prose hint:Renderers that don't recognize a
kindliteral still display the prosemessageverbatim (forward-compat perStoryboardStepHintBase).Wire-format compatibility. Adding union members is non-breaking — the JSDoc on
StoryboardStepHintalready said "more kinds may be added over time," and existing consumers that only rendermessagekeep working. TheValidationResult.warningprose for shape-drift is removed (its content lives onstep.hints[*].messageinstead), so consumers that scraped specificallywarningfor shape-drift recipes need to switch surfaces.Spec alignment. None required —
StoryboardStepHintis a runner-internal diagnostic surface defined by the runner-output contract. The structured fields mirror existing taxonomies the spec already uses (SchemaValidationError.instance_path/ RFC 6901,enums/*-status.jsonURLs).d059760: Strict discriminator types for creative assets, vendor pricing, and sync rows.
The codegen produces strict per-variant interfaces (
ImageAsset,CpmPricing, etc.) but doesn't emit canonical discriminated unions over them. This release adds three hand-authored unions on top of the generated bases so handler authors can opt into compile-time discriminator checking instead of runtime schema validation:AssetInstance— discriminated union of every creative asset instance (ImageAsset | VideoAsset | AudioAsset | TextAsset | HTMLAsset | URLAsset | CSSAsset | JavaScriptAsset | MarkdownAsset | VASTAsset | DAASTAsset | BriefAsset | CatalogAsset | WebhookAsset), keyed onasset_type. Use as the value type forcreative_manifest.assets[<key>]. Omittingasset_typeor returning a plain{ url, width, height }against this type fails to compile.AssetInstanceType— theasset_typediscriminator value union ('image' | 'video' | …). Useful for exhaustive switch-case helpers.SyncAccountsResponseRow— extracted named type for one row inSyncAccountsSuccess.accounts[]. Forces theactionliteral-union discriminator ('created' | 'updated' | 'unchanged' | 'failed') and thestatusenum on every row at compile time.SyncGovernanceResponseRow— same pattern forSyncGovernanceSuccess.accounts[]. Forces thestatus: 'synced' | 'failed'discriminator.PerUnitPricing,CustomPricing,VendorPricing,VendorPricingOptionare now re-exported from@adcp/client(previously onlyCpmPricing,PercentOfMediaPricing,FlatFeePricingwere).CPMPricingOption,VCPMPricingOption,CPCPricingOption,CPCVPricingOption,CPVPricingOption,CPPPricingOption,FlatRatePricingOption,TimeBasedPricingOptionre-exported (the union typePricingOptionandCPAPricingOptionwere already exported).Type tests in
src/lib/types/asset-instances.type-checks.tsuse// @ts-expect-errorto lock in the constraints — if a future codegen regression loosens any discriminator (e.g., makesasset_typeoptional),tsc --noEmitfails on a now-unexpected error. The file uses the.type-checks.tssuffix (not.test.ts) so it participates in the project's normalnpm run typecheckpass; explicitly excluded fromtsconfig.lib.jsonso it doesn't ship indist/.Drift class this catches at compile time:
This is dx-expert priority Update documentation and production configuration #3 from the matrix-v18 review (CI defenses AdCP Testing Framework with Authentication, Hierarchical Logging & Security Features #1 and Fix Fly.io deployment timeout issues #2 shipped in chore(skills+scripts): drift fixes + conformance-replay harness (v0) #945 and feat(scripts): typecheck-skill-examples + fix 8 skill imports #957).
fcc9b5b: feat(sync): pull canonical agent skills from the protocol tarball
scripts/sync-schemas.tsnow extracts protocol-managed skills (call-adcp-agent,adcp-media-buy,adcp-creative,adcp-signals,adcp-governance,adcp-si,adcp-brand) from the published/protocol/<version>.tgzbundle alongside schemas and compliance, into@adcp/client/skills/<name>/. The sync is manifest-driven and per-name — only directories enumerated inmanifest.contents.skillsare overwritten, so SDK-local skills (build-seller-agent,build-creative-agent, etc.) stay untouched.The buyer-side
call-adcp-agentskill is now sourced from the spec repo (feat(skills): hoist call-adcp-agent + bundle skills/ into protocol tarball adcp#3097) rather than maintained as a local copy — version-pinned toADCP_VERSION, Sigstore-verified via the same cosign path as schemas, no manual sync.Adds an
ADCP_BASE_URLenv override (defaults tohttps://adcontextprotocol.org) so CI / local-dev can point sync at a fake CDN for testing.Patch Changes
62beb82: Fix storyboard runner injecting
brand/accountinto tools whose request schemas declareadditionalProperties: false(5.17.0 framework dispatch: bundled storyboards fail their own request schemas (sync_plans, list_property_lists, delete_property_list) #940). The storyboard runner'sapplyBrandInvarianthelper unconditionally injectedbrand(and a syntheticaccountwhen none was present) into every outgoing request. Tools likesync_plans,list_property_lists, anddelete_property_listhave strict request schemas that do not include these fields. Before v5.17.0 this was silently tolerated (request validation defaulted to'warn'); PR MCP vs A2A validation asymmetry — SDK Zod fires only on MCP path #909 flipped the default to'strict', causing 11 storyboards to regress withVALIDATION_ERROR: must NOT have additional properties.applyBrandInvariantnow accepts an optionaltaskNameand consults the raw request schema JSON to decide which fields are safe to inject. It skips top-levelbrandinjection when the schema declaresadditionalProperties: falseand does not listbrandinproperties; similarly for the syntheticaccountconstruction. Tools that do declare these fields (e.g.get_products,create_media_buy) are unaffected. Fails open when schemas are unavailable (not synced) ortaskNameis omitted, preserving backwards compatibility.A new exported helper
schemaAllowsTopLevelField(toolName, field)is added toschema-loader.tsfor this purpose; it reads raw JSON without touching AJV internals.The runner now also leaves storyboard-authored
account: { account_id }payloads untouched.AccountReferenceisoneOfof{account_id}(closed viaadditionalProperties:false) or{brand, operator, sandbox?}. The previous code mergedbrandinto any plain-objectaccount, producing{account_id, brand}payloads that match neitheroneOfbranch under strict AJV — an issue latent for tools likelist_creativeswhose schemas use AccountReference. Brand is now merged only when the existingaccountcarriesbrandoroperator(the natural-key variant).153945b: Fix
ProtocolResponseParser.getStatusandgetTaskIdto read AdCPwork-layer fields from A2A wrapped Task responses instead of the
transport-layer fields. Closes fix(core): A2A submitted-arm responses misclassified as completed by responseParser #973.
Per feat(server): A2A transport adapter (preview) peer to MCP serve() #899's two-lifecycle contract, A2A
Task.statereflects theHTTP-call lifecycle (always
'completed'for AdCP submitted arms —the call returned with a queued AdCP task), and
Task.idis theSDK-generated transport handle (pinned to one HTTP call). The AdCP
work lifecycle and work handle live on the artifact:
artifact.parts[0].data.statusandartifact.metadata.adcp_task_idrespectively.
Pre-fix behavior:
getStatusfor an A2A submitted-arm response returned'completed'(read fromresult.status.state), preventingTaskExecutor.handleAsyncResponsefrom ever entering theSUBMITTED branch. Buyers thought async operations finished
synchronously —
result.submittedwas undefined; noSubmittedContinuationwas issued.getTaskIdreturned the A2A Task.id, which the seller's AdCPtasks/gettool would not recognize (the seller knows the AdCPtask handle, not the transport id).
Fix: when
result.kind === 'task'AND the artifact's firstDataPart carries an AdCP payload, prefer the AdCP-layer fields:
getStatus: readartifact.parts[0].data.statusif it's anADCP_STATUSenum value; fall back toresult.status.state.getTaskId: readartifact.metadata.adcp_task_idif present andpasses the session-id safety guard; fall back to
result.id.Non-AdCP A2A responses (no artifact, no DataPart, or
data.statusnot in the AdCP enum) keep the previous behavior — the transport-
layer fields are authoritative.
End-to-end consequence: combined with fix(core): pollTaskCompletion uses local UUID instead of server-assigned task ID #966 (server-task-id
plumbing) and fix(core): AdCP tasks/get polling — fix param naming, response mapping, drop wrong-lifecycle MCP-experimental primary path #967 (AdCP
tasks/getrequest/response shape), A2Asubmitted-arm polling now works end-to-end against any
createA2AAdapter-backed seller. Probe before this PR:After:
Tests:
test/lib/protocol-response-parser-a2a-submitted.test.js— 15unit tests covering AdCP-layer reads (submitted/working/failed),
fallback paths (no artifact, no DataPart, malformed status, no
metadata), interaction with MCP
structuredContent(untouched),and session-id safety guards.
test/server-a2a-submitted-end-to-end.test.js— full submitted →working → working → completed roundtrip against a real
createA2AAdapter. Asserts (1) SDK classifies as submitted,(2)
SubmittedContinuation.taskIdis the AdCP handle, (3)polling dispatches
tasks/getwith snake_casetask_id, (4)the spec-shape
tasks/getresponse resolveswaitForCompletion()withresult.media_buy_id.This is the third and final landmark of the A2A submitted-arm
polling story (fix(core): pollTaskCompletion uses local UUID instead of server-assigned task ID #966 → fix(core): AdCP tasks/get polling — fix param naming, response mapping, drop wrong-lifecycle MCP-experimental primary path #967 → fix(core): A2A submitted-arm responses misclassified as completed by responseParser #973). With it, A2A buyers can drive
guaranteed-buy / IO-signing / governance-review / batch-processing
flows end-to-end through the SDK without webhook-only fallbacks.
fbc36cb: Fix
TaskExecutor.getTaskStatusto dispatch the AdCPtasks/gettoolspec-conformantly. Closes fix(core): AdCP tasks/get polling — fix param naming, response mapping, drop wrong-lifecycle MCP-experimental primary path #967.
Pre-fix bugs:
Wrong request param: SDK passed
{ taskId }(camelCase). AdCP3.0 schema (
schemas/cache/3.0.0/bundled/core/tasks-get-request.json)requires
{ task_id }(snake_case). Conformant sellers reject asINVALID_PARAMS.
Wrong response shape mapping: SDK read
(response.task as TaskInfo)—expects a non-spec nested wrapper with camelCase fields. AdCP-spec
responses are flat snake_case (
{ task_id, task_type, status, created_at, updated_at, ... }); real spec-conformant responsesproduced
taskId: undefinedeverywhere on the polledTaskInfo.Wrong primary path: SDK tried MCP
experimental.tasks.getTaskfirst for MCP agents and fell through to the AdCP tool on
capability-missing. The MCP-experimental path tracks
transport-call lifecycle (the MCP analog of A2A
Task.state),not AdCP work lifecycle. For polling submitted-arm tasks (which
is what
pollTaskCompletiondoes) we need work status; the twointerfaces are not substitutes (per protocol-expert review on
fix(core): pollTaskCompletion uses local UUID instead of server-assigned task ID #966/fix(core): AdCP tasks/get polling — fix param naming, response mapping, drop wrong-lifecycle MCP-experimental primary path #967).
Fix:
Drop the MCP-experimental.tasks first attempt. Always dispatch the
AdCP
tasks/gettool over the agent's transport.Pass the request param as
task_id(snake_case).Map the response via a new
mapTasksGetResponseToTaskInfohelperthat walks the transport-level wrappers (MCP
structuredContent,A2A
result.artifacts[0].parts[0].data, legacy{ task: ... }nested wrapper) and the AdCP-spec flat shape, then projects to the
internal
TaskInfo.Bypass
extractResponseDatafortasks/get— the genericAdCP-error-arm detection misinterprets the spec's informational
error: { code, message }block as an error envelope and shredsthe response into
{ errors: [...] }. The new helper handlesunwrapping directly.
Pass through
result/task_datafromadditionalProperties: trueso completion data round-trips when sellers add it. (Note: AdCP
3.0 doesn't define a typed completion-payload field on
tasks/get;see adcp#3123 for the upstream clarification issue. Forward-compat
with all three possible spec resolutions.)
Behavior change: MCP sellers that supported
experimental.tasksbut did NOT register an AdCP
tasks/gettool will now see pollingfail rather than silently use the wrong-lifecycle interface. This is
deliberate — the previous behavior was incorrect (returned transport
status, not work status). Sellers should register
tasks/getas anAdCP tool to support buyer-side polling.
Adds
test/server-tasks-get-spec-shape.test.jswith six regressiontests:
Request param naming (snake_case
task_id, no camelCasetaskId)AdCP-spec flat response mapping (incl. ISO 8601 timestamps)
Result-data passthrough via additionalProperties
Error-block mapping (failed status with
error: { code, message })Legacy
{ task: ... }nested-shape backward compatNo MCP-experimental.tasks first attempt
Companion of fix(core): pollTaskCompletion uses local UUID instead of server-assigned task ID #966 (server-task-id plumbing). With both PRs landed,
MCP submitted-arm polling works end-to-end against spec-conformant
sellers. A2A submitted-arm polling still has additional bugs at the
parser layer (
getStatusreads transport state,getTaskIdextractsA2A Task.id instead of
artifact.metadata.adcp_task_id); tracked inadcp-client#973.
4b02028: Audit storyboard request-builder enrichers for placeholder-id clobber
pattern (closes audit: storyboard request-builder enrichers for placeholder-id clobber pattern #989).
Findings from the 12-site audit:
get_content_standardskeeps'unknown'—standards_idis requiredby
GetContentStandardsRequestSchema(no.optional()), so returning{}would violate the schema round-trip invariant. The'unknown'placeholder correctly triggers a clean NOT_FOUND when context lacks a
real id, surfacing the authoring gap. This differs from
get_media_buys(fixed in Storyboard request-builder injects media_buy_ids on get_media_buys, blocking broad-list pagination tests #983/fix(testing): get_media_buys / get_media_buy_delivery skip 'unknown' fallback (#983) #988) where
media_buy_idsis optional.All other
'unknown'placeholders in mutating writes (update_media_buy,calibrate_content,check_governance,update_content_standards,validate_content_delivery,acquire_rights,update_rights,creative_approval,si_send_message,si_terminate_session) arecorrect: they produce a clean NOT_FOUND, surfacing "wire context_outputs
from the create step."
Code change: Four mutating-write enrichers used
'test-creative'asthe creative/artifact-id fallback. Unlike
'unknown','test-creative'could be silently accepted by a pre-seeded test agent, masking an
authoring error. Standardised all four to
'unknown'for consistency:report_usage—creative_idcalibrate_content—artifact_idvalidate_content_delivery—artifact_idcreative_approval—creative_idTests: Added 3 unit tests for
get_content_standardstotest/lib/request-builder.test.js(unknown fallback, context injection,fixture wins).
fc70b9a: Fix
get_media_buysandget_media_buy_deliverystoryboard enrichers injectingmedia_buy_ids: ["unknown"]when no context ID is present (Storyboard request-builder injects media_buy_ids on get_media_buys, blocking broad-list pagination tests #983). Both enrichers unconditionally builtmedia_buy_ids: [context.media_buy_id ?? 'unknown']. When a storyboard tests the broad-list/pagination path (no IDs insample_request), the fixture-wins merge ({ ...enriched, ...fixture }) could not clear the injected placeholder because the fixture simply omitted the key. Agents receivedmedia_buy_ids: ["unknown"], returned 0 matches, and storyboardpagination.has_moreassertions failed.Both enrichers now omit
media_buy_idsentirely whencontext.media_buy_idis absent, matching the pattern used bylist_creativesandlist_accounts. When a real ID is present the behavior is unchanged. This unblocks theget-media-buys-pagination-integritystoryboard inadcontextprotocol/adcp#3122from upgrading to its intended multi-page seeded walk.72b3f87:
verifyIntrospection: drop theas Record<string, unknown>cast on theintrospection response stored in
AuthPrincipal.claims.JWTPayload's[propName: string]: unknownindex signature already accepts the RFC 7662response shape structurally, so the cast was hiding the real relationship
between the two types. Adds a JSDoc callout on
AuthPrincipal.claimsthatthe field carries either a decoded JWT (verifyBearer) or an RFC 7662
introspection response (verifyIntrospection), and that adapter handlers
passing claim values (
sub,username,client_id) into an LLM contextmust narrow and validate — an upstream IdP that controls those fields can
inject prompt content otherwise.
5d788bc:
TaskExecutor.pollTaskCompletion: handle every non-progressing AdCPtask status. Closes fix(core): pollTaskCompletion doesn't handle rejected | input-required | auth-required terminal/paused states #977 (both halves).
Pre-fix:
pollTaskCompletiononly exited oncompleted,failed,and
canceled. Three non-progressing statuses caused the loop to spinuntil the caller's timeout:
rejected— definitively terminal per the AdCPtask-statusenum ("Task was rejected by the agent and was not started"). Now
collapses onto the same
failed/canceledexit branch with{ success: false, status: 'failed' }.input-required— paused state. Polling alone can't advance it;the buyer must satisfy the paused condition (supply input) and
retry the original tool call. Now returns a
TaskResultIntermediatewithstatus: 'input-required',success: true(mirrors the synchronoushandleInputRequiredno-handler path).
auth-required— paused state. Same handling asinput-required. Also added toTaskResultIntermediate's statusunion and the
TaskStatustype.Error fallback: the polling path now checks
status.messagebefore the generic
Task <status>template, matching thesynchronous dispatch path.
TaskInfogains an optionalmessagefield; the
tasks/getresponse mapper preserves the top-levelmessagefield through to it.Side fixes caught by review:
mcp-tasks.mapMCPTaskToTaskInfo: thestatusMessage → errorprojection now checks against the AdCP-mapped status (post-
mapMCPTaskStatus) instead of the MCP-side raw status. The priorcheck used
['failed', 'rejected', 'canceled']against thepre-mapping string — but MCP Tasks emits
'cancelled'(British)and never
'rejected'as a standard status, so MCP-cancelledtasks weren't surfacing
statusMessageaserror.onTaskEvents:'canceled'was falling through toonTaskUpdated. Now joins'failed'and'rejected'on theonTaskFailedbranch.TaskStatusunion: adds'rejected','canceled', and'auth-required'for metadata fidelity.Tests:
test/lib/poll-task-completion-terminal-states.test.jscovers all three new exit paths plus regressions for
failed/canceled. 9 tests; mocks dispatch viaprotocol: 'a2a'so pollsroute directly through
ProtocolClient.callToolwithout the MCPTasks protocol fast path.
adcp#3126 alignment (typed
tasks/getresult field):feat(schema): add result and include_result to tasks/get adcp#3126 closed the spec ambiguity flagged in
adcp#3123 by adding a typed
resultfield ontasks/getresponses(gated by
include_result: trueon the request, populated whenstatus: 'completed'). The SDK now setsinclude_result: trueonevery polling request so spec-conformant 3.1.0+ sellers populate
the typed field; pre-3.1.0 sellers ignore the unknown request
field, and the response mapper continues to read
result(thetyped and informal paths share the same field name). Dropped the
informal
task_dataalias from the mapper —resultis thecanonical name.
fbc36cb: Fix
SubmittedContinuation.taskIdand the polling cycle to use theserver-assigned task handle instead of the SDK's runner-side
correlation UUID. Closes fix(core): pollTaskCompletion uses local UUID instead of server-assigned task ID #966.
Pre-fix bug:
setupSubmittedTaskplumbed the local UUID generated atrequest time (
TaskState.taskId, used for theactiveTasksmap andthe
{operation_id}webhook URL macro) through to theSubmittedContinuation.track()andwaitForCompletion()thenaddressed
tasks/getcalls with that local UUID — which the sellerhas never seen, so any spec-conformant seller would respond with
NOT_FOUND. Existing mock tests masked this because they ignored the
taskIdparameter when stubbing the polling response.Post-fix:
setupSubmittedTaskextracts the server-assigned handle viaresponseParser.getTaskId(response)(which already walks both theflat AdCP
response.task_idshape and the A2Aresult.kind === 'task'→
result.idshape) and uses it for both the buyer-facingSubmittedContinuation.taskIdfield and the closures' polling calls.The local UUID stays internal for
activeTasksbookkeeping and thewebhook URL macro.
When a seller violates the spec and omits the task handle entirely,
the SDK falls back to the local UUID so callers still get a non-
undefined
taskIdfield — pollers won't be able to locate the work,but this matches the historical (broken) behavior surface and avoids
introducing a hard fail at a code path that's been silently wrong.
Updates
SubmittedContinuation.taskIdJSDoc to document that itcarries the server handle and is distinct from the runner-side
correlation id.
Adds
test/server-task-id-plumbing.test.js— five regression testscovering the conformant path, polling/track invocations addressing the
right id, the spec-violation fallback, and the A2A
result.kind: 'task'branch of
responseParser.getTaskId.Companion follow-up: fix(core): AdCP tasks/get polling — fix param naming, response mapping, drop wrong-lifecycle MCP-experimental primary path #967 — fix the AdCP
tasks/getrequest paramnaming (
taskId→task_id) and the response-shape mapping. This PRplumbs the right ID; fix(core): AdCP tasks/get polling — fix param naming, response mapping, drop wrong-lifecycle MCP-experimental primary path #967 wires it into a spec-conformant request and
parses the spec-conformant response.
d62da47: Skill drift fixes (caught by
npm run typecheck:skill-examples):verifyApiKey,verifyBearer,anyOf,bridgeFromTestControllerStorefrom@adcp/client(top-level) — these symbols only exist under@adcp/client/server. Agents copy-pasting the example would getModule has no exported memberat compile time. Fixed across all affected skills (build-creative-agent,build-generative-seller-agent,build-governance-agent,build-retail-media-agent,build-seller-agent,build-si-agent,build-signals-agent,build-seller-agent/deployment.md).Plus
scripts/typecheck-skill-examples.ts— extracts every fenced TS block fromskills/**/*.md, compiles each as a standalone module against the published@adcp/clienttypes, and fails on new typecheck errors. Baseline mode (scripts/skill-examples.baseline.json) records the 142 known documentation-pattern errors (placeholder identifiers, untypedctx.store.listreturns) so the script ships green on day one and ratchets down over time. Run withnpm run typecheck:skill-examples.ea54d16: Skill drift fixes surfaced by matrix conformance harness:
server.registerTool('preview_creative', ...)with thecreative.previewCreativedomain handler that has existed sincecreateAdcpServerfirst shipped. Agents following the previous skill text wroteTypeError: server.registerTool is not a functionintoserve(), the factory threw, no tools registered, and the agent returned 401 on every request.list_creatives.creatives[].pricing_options[]uses field namemodel(notpricing_modellike products), and each model has its own required fields. Includes theflat_feeperiodrequirement that the schema enforces but earlier skill text omitted.capabilities.specialismsoncreateAdcpServeris required for storyboard track resolution. Agents that wire every tool but don't claim their specialism fail conformance with "No applicable tracks found" silently.SKILL.md(95 KB, was 136 KB) plusdeployment.mdand 6 specialism-delta files underspecialisms/. Reduces the single-file budget Claude has to process when building a sales-non-guaranteed agent.sync_accountsresponse per-rowactionfield clarified ('created' | 'updated' | 'unchanged' | 'failed'enum required by schema; previously skill examples omitted it).Plus
scripts/conformance-replay.ts— deterministic in-process schema-conformance harness covering creative-template (6/6 steps pass in ~2s). Not user-facing; ships in the published package becausescripts/**is published. v0; expansion to other specialisms in follow-ups.4d91c11: Docs (in-source): clarify why
tools/listpublishes emptyinputSchema. The framework intentionally registers tools withPASSTHROUGH_INPUT_SCHEMAso MCPtools/listreturns{ type: 'object', properties: {} }per tool — full per-tool schemas would balloon the context window for LLM consumers, who are the primary readers of MCP discovery. Tool shapes live indocs/llms.txt, the SKILL.md files, andschemas/cache/. Comment-only change atcreate-adcp-server.ts(registration +PASSTHROUGH_INPUT_SCHEMAdefinition) andSingleAgentClient.adaptRequestForServerVersion(consumer side) so future engineers don't try to "fix" the empty schemas by inlining them. Points downstream consumers atschema-loader.ts/schemaAllowsTopLevelField(5.17.0 framework dispatch: bundled storyboards fail their own request schemas (sync_plans, list_property_lists, delete_property_list) #940) as the canonical pattern when they need a tool's shape.ce4d1ce: Fix
version.tsdrift on release. Changesets bumpspackage.jsonfor the Release PR but doesn't know aboutsrc/lib/version.ts, so every release left the in-repoversion.tsstale (e.g.,package.json: 5.17.0whileversion.ts: 5.16.0). The npm tarball was always correct becausebuild:librunssync-versionon the CI runner — but the git tree drifted.Fix: chain
npm run sync-versionafterchangeset versionso the Release PR includes the syncedversion.ts. When merged, both files stay in lockstep.No runtime behavior change. The published package's
LIBRARY_VERSIONwas already correct via the build-time sync; this just keeps the git source-of-truth honest.