Class: A2A::Protocol::AgentCardServer

Inherits:
Object
  • Object
show all
Defined in:
lib/a2a/protocol/agent_card_server.rb

Overview

Serves agent cards with caching and automatic generation

The agent card server provides HTTP endpoints for agent card discovery, supports multiple output formats, and includes configurable caching.

Examples:

Basic usage

server = A2A::Protocol::AgentCardServer.new
server.configure do |config|
  config.cache_ttl = 300 # 5 minutes
  config.enable_signatures = true
end

# Register capabilities
server.capability_registry.register(capability)

# Generate agent card
card = server.generate_card(
  name: "My Agent",
  description: "A helpful agent"
)

Defined Under Namespace

Classes: Config

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(capability_registry: nil) ⇒ AgentCardServer

Initialize a new agent card server

Parameters:



56
57
58
59
60
61
# File 'lib/a2a/protocol/agent_card_server.rb', line 56

def initialize(capability_registry: nil)
  @capability_registry = capability_registry || CapabilityRegistry.new
  @config = Config.new
  @cache = {}
  @cache_timestamps = {}
end

Instance Attribute Details

#capability_registryObject (readonly)

Returns the value of attribute capability_registry.



31
32
33
# File 'lib/a2a/protocol/agent_card_server.rb', line 31

def capability_registry
  @capability_registry
end

#configObject (readonly)

Returns the value of attribute config.



31
32
33
# File 'lib/a2a/protocol/agent_card_server.rb', line 31

def config
  @config
end

Instance Method Details

#cache_statsHash

Get cache statistics

Returns:

  • (Hash)

    Cache statistics



232
233
234
235
236
237
238
239
# File 'lib/a2a/protocol/agent_card_server.rb', line 232

def cache_stats
  {
    entries: @cache.size,
    keys: @cache.keys,
    oldest_entry: @cache_timestamps.values.min,
    newest_entry: @cache_timestamps.values.max
  }
end

#capability_to_skill(capability) ⇒ A2A::Types::AgentSkill (private)

Convert a capability to an agent skill

Parameters:

  • capability (Capability)

    The capability to convert

Returns:



248
249
250
251
252
253
254
255
256
257
258
259
# File 'lib/a2a/protocol/agent_card_server.rb', line 248

def capability_to_skill(capability)
  A2A::Types::AgentSkill.new(
    id: capability.name,
    name: capability.name.split("_").map(&:capitalize).join(" "),
    description: capability.description,
    tags: capability.tags,
    examples: capability.examples,
    input_modes: determine_capability_input_modes(capability),
    output_modes: determine_capability_output_modes(capability),
    security: capability.security_requirements
  )
end

#clear_cache(cache_key = nil) ⇒ Object

Clear cache

Parameters:

  • cache_key (String, nil) (defaults to: nil)

    Specific key to clear, or nil for all



218
219
220
221
222
223
224
225
226
# File 'lib/a2a/protocol/agent_card_server.rb', line 218

def clear_cache(cache_key = nil)
  if cache_key
    @cache.delete(cache_key)
    @cache_timestamps.delete(cache_key)
  else
    @cache.clear
    @cache_timestamps.clear
  end
end

#configure {|config| ... } ⇒ Object

Configure the server

Yields:

  • (config)

    Configuration block

Yield Parameters:

  • config (Config)

    The configuration object



68
69
70
# File 'lib/a2a/protocol/agent_card_server.rb', line 68

def configure
  yield(@config) if block_given?
end

#determine_capabilitiesA2A::Types::AgentCapabilities (private)

Determine agent capabilities from registry

Returns:



265
266
267
268
269
270
271
272
273
274
275
276
277
# File 'lib/a2a/protocol/agent_card_server.rb', line 265

def determine_capabilities
  streaming = @capability_registry.all.any?(&:streaming?)
  push_notifications = false # TODO: Determine from server config
  state_history = false # TODO: Determine from server config
  extensions = [] # TODO: Collect from capabilities

  A2A::Types::AgentCapabilities.new(
    streaming: streaming,
    push_notifications: push_notifications,
    state_transition_history: state_history,
    extensions: extensions
  )
end

#determine_capability_input_modes(capability) ⇒ Array<String> (private)

Determine input modes for a capability

Parameters:

Returns:

  • (Array<String>)

    Input modes



304
305
306
307
308
309
310
311
312
313
314
315
316
317
# File 'lib/a2a/protocol/agent_card_server.rb', line 304

def determine_capability_input_modes(capability)
  # Analyze input schema to determine modes
  return ["text"] unless capability.input_schema

  modes = ["text"] # Default to text

  # Check if schema accepts file inputs
  modes << "file" if schema_accepts_files?(capability.input_schema)

  # Check if schema accepts structured data
  modes << "data" if schema_accepts_data?(capability.input_schema)

  modes.uniq
end

#determine_capability_output_modes(capability) ⇒ Array<String> (private)

Determine output modes for a capability

Parameters:

Returns:

  • (Array<String>)

    Output modes



324
325
326
327
328
329
330
331
332
333
334
335
336
337
# File 'lib/a2a/protocol/agent_card_server.rb', line 324

def determine_capability_output_modes(capability)
  # Analyze output schema to determine modes
  return ["text"] unless capability.output_schema

  modes = ["text"] # Default to text

  # Check if schema produces files
  modes << "file" if schema_produces_files?(capability.output_schema)

  # Check if schema produces structured data
  modes << "data" if schema_produces_data?(capability.output_schema)

  modes.uniq
end

#determine_input_modes(skills) ⇒ Array<String> (private)

Determine input modes from skills

Parameters:

Returns:

  • (Array<String>)

    Input modes



284
285
286
287
# File 'lib/a2a/protocol/agent_card_server.rb', line 284

def determine_input_modes(skills)
  modes = skills.flat_map { |skill| skill.input_modes || ["text"] }.uniq
  modes.empty? ? ["text"] : modes
end

#determine_output_modes(skills) ⇒ Array<String> (private)

Determine output modes from skills

Parameters:

Returns:

  • (Array<String>)

    Output modes



294
295
296
297
# File 'lib/a2a/protocol/agent_card_server.rb', line 294

def determine_output_modes(skills)
  modes = skills.flat_map { |skill| skill.output_modes || ["text"] }.uniq
  modes.empty? ? ["text"] : modes
end

#generate_card(name:, description:, url:, version: "1.0.0", preferred_transport: "JSONRPC", **additional_options) ⇒ A2A::Types::AgentCard

Generate an agent card from registered capabilities

Parameters:

  • name (String)

    Agent name

  • description (String)

    Agent description

  • version (String) (defaults to: "1.0.0")

    Agent version

  • url (String)

    Primary agent URL

  • preferred_transport (String) (defaults to: "JSONRPC")

    Preferred transport

  • additional_options (Hash)

    Additional card options

Returns:



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/a2a/protocol/agent_card_server.rb', line 82

def generate_card(name:, description:, url:, version: "1.0.0",
                  preferred_transport: "JSONRPC", **additional_options)
  # Convert capabilities to skills
  skills = @capability_registry.all.map { |cap| capability_to_skill(cap) }

  # Determine capabilities from registry
  capabilities = determine_capabilities

  # Build default input/output modes from skills
  input_modes = determine_input_modes(skills)
  output_modes = determine_output_modes(skills)

  # Create the agent card
  card_options = {
    name: name,
    description: description,
    version: version,
    url: url,
    preferred_transport: preferred_transport,
    skills: skills,
    capabilities: capabilities,
    default_input_modes: input_modes,
    default_output_modes: output_modes,
    protocol_version: @config.default_protocol_version,
    supports_authenticated_extended_card: @config.enable_authenticated_extended_cards
  }.merge(additional_options)

  card = A2A::Types::AgentCard.new(**card_options)

  # Add signatures if enabled
  if @config.enable_signatures && @config.signing_key
    signatures = [generate_signature(card)]
    card = A2A::Types::AgentCard.new(**card_options, signatures: signatures)
  end

  card
end

#generate_jws_token(card) ⇒ String (private)

Generate a complete JWS token for the agent card

Parameters:

Returns:

  • (String)

    The JWS token



448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# File 'lib/a2a/protocol/agent_card_server.rb', line 448

def generate_jws_token(card)
  # This is a placeholder implementation
  # In a real implementation, you would use a proper JWT library

  header = {
    alg: @config.signing_algorithm,
    typ: "JWT"
  }

  payload = card.to_h

  require "base64"
  header_b64 = Base64.urlsafe_encode64(header.to_json).delete("=")
  payload_b64 = Base64.urlsafe_encode64(payload.to_json).delete("=")

  # Placeholder signature (in real implementation, sign with private key)
  signature_b64 = Base64.urlsafe_encode64("signature_#{Time.now.to_i}").delete("=")

  "#{header_b64}.#{payload_b64}.#{signature_b64}"
end

#generate_signature(_card) ⇒ A2A::Types::AgentCardSignature (private)

Generate a JWS signature for the agent card

Parameters:

Returns:



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
# File 'lib/a2a/protocol/agent_card_server.rb', line 422

def generate_signature(_card)
  # This is a placeholder implementation
  # In a real implementation, you would use a proper JWT library

  header = {
    alg: @config.signing_algorithm,
    typ: "JWT"
  }

  require "base64"
  header_b64 = Base64.urlsafe_encode64(header.to_json).delete("=")

  # Placeholder signature (in real implementation, sign with private key)
  signature_b64 = Base64.urlsafe_encode64("signature_#{Time.now.to_i}").delete("=")

  A2A::Types::AgentCardSignature.new(
    signature: signature_b64,
    protected_header: header_b64
  )
end

#get_authenticated_extended_card(auth_context: {}, **base_card_params) ⇒ A2A::Types::AgentCard

Get authenticated extended agent card

This method allows for dynamic modification of the agent card based on the authentication context.

Parameters:

  • auth_context (Hash) (defaults to: {})

    Authentication context

  • base_card_params (Hash)

    Base card parameters

Returns:



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/a2a/protocol/agent_card_server.rb', line 150

def get_authenticated_extended_card(auth_context: {}, **base_card_params)
  unless @config.enable_authenticated_extended_cards
    raise A2A::Errors::A2AError.new(
      "Authenticated extended cards are not enabled",
      code: -32_001
    )
  end

  # Generate base card
  card = generate_card(**base_card_params)

  # Apply modifications based on auth context
  card = @config.card_modification_callback.call(card, auth_context) if @config.card_modification_callback

  card
end

#get_card(cache_key: "default", **card_params) ⇒ A2A::Types::AgentCard

Get agent card with caching

Parameters:

  • cache_key (String) (defaults to: "default")

    Cache key for the card

  • card_params (Hash)

    Parameters for card generation

Returns:



126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/a2a/protocol/agent_card_server.rb', line 126

def get_card(cache_key: "default", **card_params)
  # Check cache
  if (cached_card = get_from_cache(cache_key))
    return cached_card
  end

  # Generate new card
  card = generate_card(**card_params)

  # Cache the card
  store_in_cache(cache_key, card)

  card
end

#get_from_cache(cache_key) ⇒ A2A::Types::AgentCard? (private)

Get card from cache if valid

Parameters:

  • cache_key (String)

    The cache key

Returns:



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
# File 'lib/a2a/protocol/agent_card_server.rb', line 390

def get_from_cache(cache_key)
  return nil unless @cache.key?(cache_key)

  timestamp = @cache_timestamps[cache_key]
  return nil unless timestamp

  # Check if cache entry is still valid
  if Time.now - timestamp < @config.cache_ttl
    @cache[cache_key]
  else
    # Remove expired entry
    @cache.delete(cache_key)
    @cache_timestamps.delete(cache_key)
    nil
  end
end

#schema_accepts_data?(schema) ⇒ Boolean (private)

Check if schema accepts structured data

Parameters:

  • schema (Hash)

    The JSON schema

Returns:

  • (Boolean)

    True if structured data is accepted



360
361
362
363
364
365
# File 'lib/a2a/protocol/agent_card_server.rb', line 360

def schema_accepts_data?(schema)
  return false unless schema.is_a?(Hash)

  type = schema[:type] || schema["type"]
  %w[object array].include?(type)
end

#schema_accepts_files?(schema) ⇒ Boolean (private)

Check if schema accepts file inputs

Parameters:

  • schema (Hash)

    The JSON schema

Returns:

  • (Boolean)

    True if files are accepted



344
345
346
347
348
349
350
351
352
353
# File 'lib/a2a/protocol/agent_card_server.rb', line 344

def schema_accepts_files?(schema)
  # Simple heuristic: look for file-related properties
  return false unless schema.is_a?(Hash)

  properties = schema[:properties] || schema["properties"] || {}
  properties.any? do |name, prop_schema|
    name.to_s.include?("file") ||
      (prop_schema.is_a?(Hash) && prop_schema[:format] == "binary")
  end
end

#schema_produces_data?(schema) ⇒ Boolean (private)

Check if schema produces structured data

Parameters:

  • schema (Hash)

    The JSON schema

Returns:

  • (Boolean)

    True if structured data is produced



381
382
383
# File 'lib/a2a/protocol/agent_card_server.rb', line 381

def schema_produces_data?(schema)
  schema_accepts_data?(schema) # Same logic for now
end

#schema_produces_files?(schema) ⇒ Boolean (private)

Check if schema produces files

Parameters:

  • schema (Hash)

    The JSON schema

Returns:

  • (Boolean)

    True if files are produced



372
373
374
# File 'lib/a2a/protocol/agent_card_server.rb', line 372

def schema_produces_files?(schema)
  schema_accepts_files?(schema) # Same logic for now
end

#serve_card(format: "json", cache_key: "default", **card_params) ⇒ Hash

Serve agent card as HTTP response data

Parameters:

  • format (String) (defaults to: "json")

    Output format ('json' or 'jws')

  • cache_key (String) (defaults to: "default")

    Cache key

  • card_params (Hash)

    Card generation parameters

Returns:

  • (Hash)

    HTTP response data with headers and body



174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/a2a/protocol/agent_card_server.rb', line 174

def serve_card(format: "json", cache_key: "default", **card_params)
  card = get_card(cache_key: cache_key, **card_params)

  case format.downcase
  when "json"
    {
      status: 200,
      headers: {
        "Content-Type" => "application/json",
        "Cache-Control" => "max-age=#{@config.cache_ttl}"
      },
      body: card.to_json
    }
  when "jws"
    if @config.enable_signatures && @config.signing_key
      jws_token = generate_jws_token(card)
      {
        status: 200,
        headers: {
          "Content-Type" => "application/jose+json",
          "Cache-Control" => "max-age=#{@config.cache_ttl}"
        },
        body: jws_token
      }
    else
      {
        status: 400,
        headers: { "Content-Type" => "application/json" },
        body: { error: "JWS signing not configured" }.to_json
      }
    end
  else
    {
      status: 400,
      headers: { "Content-Type" => "application/json" },
      body: { error: "Unsupported format: #{format}" }.to_json
    }
  end
end

#store_in_cache(cache_key, card) ⇒ Object (private)

Store card in cache

Parameters:



412
413
414
415
# File 'lib/a2a/protocol/agent_card_server.rb', line 412

def store_in_cache(cache_key, card)
  @cache[cache_key] = card
  @cache_timestamps[cache_key] = Time.now
end