Skip to content

Signals: deliver_to conflates signal discovery with deployment routing #1116

@bokelley

Description

@bokelley

Problem

GetSignalsRequest.deliver_to serves two unrelated purposes in a single required field:

  1. Signal discovery — "What audiences can I target on your platform?" (caller is talking to the sales agent directly)
  2. Deployment routing — "What audiences can you push to these DSPs/agents?" (caller is talking to a data provider)

These are fundamentally different questions. When calling get_signals on a platform's sales agent (Snap, LinkedIn, etc.), the caller already knows where the signals will be used — on that platform. Requiring deliver_to forces every caller to construct a Destination object pointing back at the agent they're already talking to, which is meaningless.

Impact on LLM callers

The Destination type is a discriminated union (type: 'platform' | 'agent') intersected with Record<string, unknown>. This generates deeply nested JSON Schema that LLMs struggle to parse:

{
  "anyOf": [
    { "allOf": [
      { "type": "object", "additionalProperties": { "anyOf": [{}, {"not": {}}] } },
      { "type": "object", "properties": { "type": {"const": "platform"}, "platform": {"type": "string"} } }
    ]},
    { "allOf": [
      { "type": "object", "additionalProperties": { "anyOf": [{}, {"not": {}}] } },
      { "type": "object", "properties": { "type": {"const": "agent"}, "agent_url": {"type": "string"} } }
    ]}
  ]
}

In practice, LLM callers fail on the first attempt and need multiple retries to figure out the correct format (observed in real usage with Claude calling LinkedIn AdCP).

Current workaround

Sales agent implementations either:

  • Make deliver_to optional and ignore it entirely
  • Auto-fill it with their own agent URL before passing to the handler

Both approaches indicate the field doesn't belong as a required parameter for the platform-native signal discovery case.

Proposal

Separate the two concerns. One option:

interface GetSignalsRequest {
  signal_spec?: string
  signal_ids?: SignalID[]
  
  // Optional: filter signals to those activatable on specific agents/platforms
  // When omitted, returns all signals available on the current agent
  target_agents?: Destination[]
  
  // Geo filter (still useful for both cases)
  countries?: string[]
  
  filters?: SignalFilters
  max_results?: number
  // ...
}
  • Calling get_signals on a sales agent with no target_agents → returns platform-native signals
  • Calling get_signals on a data provider with target_agents: [{type: 'agent', agent_url: '...'}] → returns signals activatable on those agents
  • countries stays as a standalone filter since it's useful in both cases

This also fixes the LLM ergonomics issue since target_agents becomes optional and only needed for the data marketplace case.

Secondary issue: Record intersection pattern

The Record<string, unknown | undefined> & { ... } pattern used for extensibility across all AdCP types produces poor JSON Schema output. Consider using additionalProperties: true on the object directly instead — same semantics, much cleaner serialization for LLM tool schemas.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions