v5.18.0
Minor Changes
-
af01482: Storyboard runner: add the
a2a_context_continuityvalidation check
plus cross-step A2A envelope tracking. Closes #962.A2A 0.3.0 §7.1 binds follow-up
message/sendcalls to a server-side
conversation viaMessage.contextId; the server MUST echo it on the
response Task. The@a2a-js/sdk'sDefaultRequestHandlerdoes this
automatically —createA2AAdapter(#899) passes through
requestContext.contextId, so a passing seller built on the SDK
won't trip a single-call check. The regression class is sellers that
bypass the SDK's request handler and stamp their owncontextIdon
the 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's
captured envelope. Skip semantics:- Non-A2A run (no envelope captured) → skip with
not_applicable
observation - First A2A step in a run (no prior to compare) → skip
- Either envelope has no extractable
contextId→ skip - JSON-RPC error envelope (transport rejection) → skip — continuity
is undefined when the call didn't reach the work layer - Skip cases tag the observation so triage can distinguish
"validator self-skipped" from "validator passed because contexts
matched"
Failure cases:
- Current step's response has no
contextId(empty/missing) on a
non-first send → fail with a pointer to/result/contextId - Current step's response
contextIddiffers 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-livedpriorA2aEnvelopesmap onExecutionState, populated
after 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 #952. Filed as
#962, scoped as a separate validator since the failure mode and
fix surface are distinct froma2a_submitted_artifact's single-call
wire-shape check.Coverage:
- 10 unit tests against
validateA2AContextContinuity(synthetic
envelopes covering match, divergence, missing contextId, every
skip path) - 2 integration tests against
runStoryboarddriving multi-step
storyboards: one against a conformantcreateA2AAdapter(passes),
one against a hand-rolled regressed adapter that stamps a fresh
contextId per send (fails)
- Non-A2A run (no envelope captured) → skip with
-
6124deb: Storyboard runner: capture A2A wire shape on
protocol: 'a2a'runs and
add thea2a_submitted_artifactvalidation check. Closes the regression
class from adcp-client#904 — pre-#899 A2A adapters that emitted
Task.state: 'submitted'withfinal: trueandadcp_task_idinside
artifact.parts[0].datainstead ofartifact.metadatawould otherwise
pass the storyboard suite despite being non-conformant per A2A 0.3.0.The check asserts the wire-shape invariants for AdCP
submittedarms
over A2A:Task.state === 'completed'— A2A Task.state tracks the HTTP
transport call;'submitted'is the INITIAL state per A2A 0.3.0
and forbidden as a terminal value.Task.idandTask.contextIdnon-empty — required by A2A 0.3.0
fortasks/getaddressability and follow-up correlation.artifact.artifactIdnon-empty — required for chunked-artifact
resumption 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.- If
data.adcp_task_idis also present (forward-compatibility for
a future AdCP tool whose response schema legitimately includes
it), it MUST equalmetadata.adcp_task_id— divergent or
solo-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 separate
transport rejections from submitted-arm shape drift.The check self-skips with a
not_applicableobservation on non-A2A
runs (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 in
the runner so the JSON-RPC envelope is observable for validation;
captured response bodies pass throughredactSecretsbefore landing
inValidationContext.a2aEnvelopeso AdCP-style secret-shaped fields
in DataPart payloads (api_key,client_secret, etc.) don't reach
persisted compliance reports.withRawResponseCapturenow surfaces
partial captures on rejection (attached aserror.captures) so
storyboard validators get a wire-shape envelope even when the SDK
threw mid-parse. AddsA2ATaskEnvelopeto the public testing types
and exportsgetCapturesFromErrorfrom the protocols module.The companion compliance scenario (adcontextprotocol/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 #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 #4 from the matrix-v18 review (CI defenses #1–#3 shipped in #945, #957, #961).
-
1158429: Add
${Parent}_${Property}Valuesconst arrays for inline anonymous string-literal unions (closes #932).Companion to the named-enum exports landed in 5.17 (PR #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:// Before — drift bait, hand-maintained on the consumer side. const VALID_IMAGE_FORMATS = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'avif', 'tiff', 'pdf', 'eps']); const VALID_VIDEO_CONTAINERS = new Set(['mp4', 'webm', 'mov', 'avi', 'mkv']); // After — authoritative, drift-detected. import { ImageAssetRequirements_FormatsValues, VideoAssetRequirements_ContainersValues } from '@adcp/client/types';
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 #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 #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 storyboard
determinism.PostgresTaskStore.createTasknow accepts an optionaltaskId
field 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 honor
caller-suppliedtaskId— sellers running withoutDATABASE_URL(e.g., test
paths) get random IDs even when one is supplied. Filing an upstream MCP SDK
issue to addtaskId?: stringtoCreateTaskOptionsso both stores can honor
it cleanly is the right durable fix; this PR is the Postgres-only shim until
upstream lands.- The
task_idnamespace onPostgresTaskStoreis process-global today (no
tenant 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. - The storyboard runner does not yet send caller-supplied IDs through to the
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 #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:createAdcpServer: creative handlers are wired but capabilities.specialisms does not include any creative specialism. Add at least one of 'creative-ad-server', 'creative-generative', 'creative-template' to capabilities.specialisms — without it, the conformance runner reports "No applicable tracks found" and the agent grades as failing despite working tools.The matrix v18 run (issue #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:- Throws-equivalent: error logged when handlers wired without specialism
- No-error: handlers + matching specialism aligned
- No-error: no domain handlers wired
- No-error: mediaBuy without specialism (commercial-significance carve-out)
This is dx-expert priority #5 from the matrix-v18 review (CI defenses #1–#4 shipped in #945, #957, #961, #970). With this, the cheap-CI-defense ladder is complete.
-
df9d7bd: Extend
StoryboardStepHinttaxonomy:shape_drift,missing_required_field,format_mismatch,monotonic_violation(closes #935; supersedes #937).Issue #935 proposed making
StoryboardStepHintthe canonical surface for every runner-side diagnostic that has structured fields a renderer can consume. PR #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 #937),MissingRequiredFieldHint,FormatMismatchHint, andMonotonicViolationHint. Each kind carries machine-readable fields so renderers don't regex-parse the prose:kindWhen it fires Structured fields shape_driftBare-array list responses, platform-native build_creative, wrong-wrappersync_creatives/preview_creativetool,observed_variant,expected_variant,instance_pathmissing_required_fieldStrict AJV reports keyword: "required"issues (lenient Zod accepted)tool,instance_path,schema_path,missing_fields[],schema_url?format_mismatchStrict AJV rejected a format/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:💡 Hint: media_buy mb-1: active → pending_creatives (step "create" → step "regress")... media_buy mb-1: active → pending_creatives from step: create legal next states: canceled, completed, pausedRenderers 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.- Vendor-pricing exports completed —
PerUnitPricing,CustomPricing,VendorPricing,VendorPricingOptionare now re-exported from@adcp/client(previously onlyCpmPricing,PercentOfMediaPricing,FlatFeePricingwere). - Product-pricing exports completed —
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:
// Before: this slipped past TS, was caught only by runtime validator. const asset: Record<string, unknown> = { url: '...', width: 1920, height: 1080 }; return { creative_manifest: { format_id, assets: { hero: asset } } }; // After: typed as AssetInstance, missing asset_type is a compile error. const asset: AssetInstance = { url: '...', width: 1920, height: 1080 }; // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // error TS2353: Object literal may only specify known properties, and // 'url' does not exist in type 'AssetInstance'. Property 'asset_type' // is missing.
This is dx-expert priority #3 from the matrix-v18 review (CI defenses #1 and #2 shipped in #945 and #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 (adcontextprotocol/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(#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 #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 AdCP
work-layer fields from A2A wrapped Task responses instead of the
transport-layer fields. Closes #973.Per #899's two-lifecycle contract, A2A
Task.statereflects the
HTTP-call lifecycle (always'completed'for AdCP submitted arms —
the call returned with a queued AdCP task), andTask.idis the
SDK-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_id
respectively.Pre-fix behavior:
getStatusfor an A2A submitted-arm response returned
'completed'(read fromresult.status.state), preventing
TaskExecutor.handleAsyncResponsefrom ever entering the
SUBMITTED branch. Buyers thought async operations finished
synchronously —result.submittedwas undefined; no
SubmittedContinuationwas issued.getTaskIdreturned the A2A Task.id, which the seller's AdCP
tasks/gettool would not recognize (the seller knows the AdCP
task handle, not the transport id).
Fix: when
result.kind === 'task'AND the artifact's first
DataPart carries an AdCP payload, prefer the AdCP-layer fields:getStatus: readartifact.parts[0].data.statusif it's an
ADCP_STATUSenum value; fall back toresult.status.state.getTaskId: readartifact.metadata.adcp_task_idif present and
passes the session-id safety guard; fall back toresult.id.
Non-AdCP A2A responses (no artifact, no DataPart, or
data.status
not in the AdCP enum) keep the previous behavior — the transport-
layer fields are authoritative.End-to-end consequence: combined with #966 (server-task-id
plumbing) and #967 (AdCPtasks/getrequest/response shape), A2A
submitted-arm polling now works end-to-end against any
createA2AAdapter-backed seller. Probe before this PR:result.status = completed ← WRONG, treated as sync completion result.submitted = undefined result.metadata.serverTaskId = <random A2A UUID>After:
result.status = submitted result.submitted.taskId = tk_seller_handle_99 ← AdCP work handleTests:
test/lib/protocol-response-parser-a2a-submitted.test.js— 15
unit tests covering AdCP-layer reads (submitted/working/failed),
fallback paths (no artifact, no DataPart, malformed status, no
metadata), interaction with MCPstructuredContent(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 dispatchestasks/getwith snake_casetask_id, (4)
the spec-shapetasks/getresponse resolves
waitForCompletion()withresult.media_buy_id.
This is the third and final landmark of the A2A submitted-arm
polling story (#966 → #967 → #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/gettool
spec-conformantly. Closes #967.Pre-fix bugs:
-
Wrong request param: SDK passed
{ taskId }(camelCase). AdCP
3.0 schema (schemas/cache/3.0.0/bundled/core/tasks-get-request.json)
requires{ task_id }(snake_case). Conformant sellers reject as
INVALID_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 responses
producedtaskId: undefinedeverywhere on the polledTaskInfo. -
Wrong primary path: SDK tried MCP
experimental.tasks.getTask
first 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 A2ATask.state),
not AdCP work lifecycle. For polling submitted-arm tasks (which
is whatpollTaskCompletiondoes) we need work status; the two
interfaces are not substitutes (per protocol-expert review on
#966/#967).Fix:
-
Drop the MCP-experimental.tasks first attempt. Always dispatch the
AdCPtasks/gettool over the agent's transport. -
Pass the request param as
task_id(snake_case). -
Map the response via a new
mapTasksGetResponseToTaskInfohelper
that walks the transport-level wrappers (MCPstructuredContent,
A2Aresult.artifacts[0].parts[0].data, legacy{ task: ... }
nested wrapper) and the AdCP-spec flat shape, then projects to the
internalTaskInfo. -
Bypass
extractResponseDatafortasks/get— the generic
AdCP-error-arm detection misinterprets the spec's informational
error: { code, message }block as an error envelope and shreds
the response into{ errors: [...] }. The new helper handles
unwrapping directly. -
Pass through
result/task_datafromadditionalProperties: true
so completion data round-trips when sellers add it. (Note: AdCP
3.0 doesn't define a typed completion-payload field ontasks/get;
see adcp#3123 for the upstream clarification issue. Forward-compat
with all three possible spec resolutions.)Behavior change: MCP sellers that supported
experimental.tasks
but did NOT register an AdCPtasks/gettool will now see polling
fail rather than silently use the wrong-lifecycle interface. This is
deliberate — the previous behavior was incorrect (returned transport
status, not work status). Sellers should registertasks/getas an
AdCP tool to support buyer-side polling.Adds
test/server-tasks-get-spec-shape.test.jswith six regression
tests: -
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 compat -
No MCP-experimental.tasks first attempt
Companion of #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,getTaskIdextracts
A2A Task.id instead ofartifact.metadata.adcp_task_id); tracked in
adcp-client#973.
-
-
4b02028: Audit storyboard request-builder enrichers for placeholder-id clobber
pattern (closes #989).Findings from the 12-site audit:
get_content_standardskeeps'unknown'—standards_idis required
byGetContentStandardsRequestSchema(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 fromget_media_buys
(fixed in #983/#988) wheremedia_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) are
correct: they produce a clean NOT_FOUND, surfacing "wire context_outputs
from the create step."Code change: Four mutating-write enrichers used
'test-creative'as
the 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_id
Tests: Added 3 unit tests for
get_content_standardsto
test/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 (#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 the
introspection response stored inAuthPrincipal.claims.JWTPayload's
[propName: string]: unknownindex signature already accepts the RFC 7662
response shape structurally, so the cast was hiding the real relationship
between the two types. Adds a JSDoc callout onAuthPrincipal.claimsthat
the 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 context
must narrow and validate — an upstream IdP that controls those fields can
inject prompt content otherwise. -
5d788bc:
TaskExecutor.pollTaskCompletion: handle every non-progressing AdCP
task status. Closes #977 (both halves).Pre-fix:
pollTaskCompletiononly exited oncompleted,failed,
andcanceled. Three non-progressing statuses caused the loop to spin
until the caller's timeout:rejected— definitively terminal per the AdCPtask-status
enum ("Task was rejected by the agent and was not started"). Now
collapses onto the samefailed/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 synchronoushandleInputRequired
no-handler path).auth-required— paused state. Same handling as
input-required. Also added toTaskResultIntermediate's status
union and theTaskStatustype.
Error fallback: the polling path now checks
status.message
before the genericTask <status>template, matching the
synchronous dispatch path.TaskInfogains an optionalmessage
field; thetasks/getresponse mapper preserves the top-level
messagefield through to it.Side fixes caught by review:
mcp-tasks.mapMCPTaskToTaskInfo: thestatusMessage → error
projection now checks against the AdCP-mapped status (post-
mapMCPTaskStatus) instead of the MCP-side raw status. The prior
check used['failed', 'rejected', 'canceled']against the
pre-mapping string — but MCP Tasks emits'cancelled'(British)
and never'rejected'as a standard status, so MCP-cancelled
tasks weren't surfacingstatusMessageaserror.onTaskEvents:'canceled'was falling through to
onTaskUpdated. Now joins'failed'and'rejected'on the
onTaskFailedbranch.TaskStatusunion: adds'rejected','canceled', and
'auth-required'for metadata fidelity.
Tests:
test/lib/poll-task-completion-terminal-states.test.js
covers all three new exit paths plus regressions forfailed/
canceled. 9 tests; mocks dispatch viaprotocol: 'a2a'so polls
route directly throughProtocolClient.callToolwithout the MCP
Tasks protocol fast path.adcp#3126 alignment (typed
tasks/getresult field):
adcontextprotocol/adcp#3126 closed the spec ambiguity flagged in
adcp#3123 by adding a typedresultfield ontasks/getresponses
(gated byinclude_result: trueon the request, populated when
status: 'completed'). The SDK now setsinclude_result: trueon
every 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 readresult(the
typed and informal paths share the same field name). Dropped the
informaltask_dataalias from the mapper —resultis the
canonical name. -
fbc36cb: Fix
SubmittedContinuation.taskIdand the polling cycle to use the
server-assigned task handle instead of the SDK's runner-side
correlation UUID. Closes #966.Pre-fix bug:
setupSubmittedTaskplumbed the local UUID generated at
request time (TaskState.taskId, used for theactiveTasksmap and
the{operation_id}webhook URL macro) through to the
SubmittedContinuation.track()andwaitForCompletion()then
addressedtasks/getcalls with that local UUID — which the seller
has 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 via
responseParser.getTaskId(response)(which already walks both the
flat AdCPresponse.task_idshape and the A2Aresult.kind === 'task'
→result.idshape) and uses it for both the buyer-facing
SubmittedContinuation.taskIdfield and the closures' polling calls.
The local UUID stays internal foractiveTasksbookkeeping and the
webhook 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-
undefinedtaskIdfield — 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 it
carries the server handle and is distinct from the runner-side
correlation id.Adds
test/server-task-id-plumbing.test.js— five regression tests
covering the conformant path, polling/track invocations addressing the
right id, the spec-violation fallback, and the A2Aresult.kind: 'task'
branch ofresponseParser.getTaskId.Companion follow-up: #967 — fix the AdCP
tasks/getrequest param
naming (taskId→task_id) and the response-shape mapping. This PR
plumbs the right ID; #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):- 8 SKILL.md files imported
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. - 8 SKILL.md files imported
-
ea54d16: Skill drift fixes surfaced by matrix conformance harness:
- build-creative-agent: replace non-existent
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. - build-creative-agent: vendor-pricing pitfall added —
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. - All skills: cross-cutting pitfall callout —
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. - build-seller-agent: split into
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. - build-brand-rights-agent / build-generative-seller-agent / build-governance-agent / build-retail-media-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. - build-creative-agent: replace non-existent
-
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(#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.