From 4255e3984999faaeec8cc84987c5b74e065c9676 Mon Sep 17 00:00:00 2001 From: a2a-bot Date: Thu, 16 Oct 2025 19:47:51 +0000 Subject: [PATCH 01/14] Update to specification from 0a9f629e801d4ae89f94991fc28afe9429c91cbc --- src/a2a/grpc/a2a_pb2.py | 60 ++++++++++-------- src/a2a/grpc/a2a_pb2.pyi | 28 +++++++++ src/a2a/grpc/a2a_pb2_grpc.py | 44 +++++++++++++ src/a2a/types.py | 118 +++++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 27 deletions(-) diff --git a/src/a2a/grpc/a2a_pb2.py b/src/a2a/grpc/a2a_pb2.py index 9b4b73013..bbb2429cd 100644 --- a/src/a2a/grpc/a2a_pb2.py +++ b/src/a2a/grpc/a2a_pb2.py @@ -30,7 +30,7 @@ from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ta2a.proto\x12\x06\x61\x32\x61.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xde\x01\n\x18SendMessageConfiguration\x12\x32\n\x15\x61\x63\x63\x65pted_output_modes\x18\x01 \x03(\tR\x13\x61\x63\x63\x65ptedOutputModes\x12K\n\x11push_notification\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.PushNotificationConfigR\x10pushNotification\x12%\n\x0ehistory_length\x18\x03 \x01(\x05R\rhistoryLength\x12\x1a\n\x08\x62locking\x18\x04 \x01(\x08R\x08\x62locking\"\xf1\x01\n\x04Task\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12*\n\x06status\x18\x03 \x01(\x0b\x32\x12.a2a.v1.TaskStatusR\x06status\x12.\n\tartifacts\x18\x04 \x03(\x0b\x32\x10.a2a.v1.ArtifactR\tartifacts\x12)\n\x07history\x18\x05 \x03(\x0b\x32\x0f.a2a.v1.MessageR\x07history\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\x99\x01\n\nTaskStatus\x12\'\n\x05state\x18\x01 \x01(\x0e\x32\x11.a2a.v1.TaskStateR\x05state\x12(\n\x06update\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageR\x07message\x12\x38\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\ttimestamp\"\xa9\x01\n\x04Part\x12\x14\n\x04text\x18\x01 \x01(\tH\x00R\x04text\x12&\n\x04\x66ile\x18\x02 \x01(\x0b\x32\x10.a2a.v1.FilePartH\x00R\x04\x66ile\x12&\n\x04\x64\x61ta\x18\x03 \x01(\x0b\x32\x10.a2a.v1.DataPartH\x00R\x04\x64\x61ta\x12\x33\n\x08metadata\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadataB\x06\n\x04part\"\x93\x01\n\x08\x46ilePart\x12$\n\rfile_with_uri\x18\x01 \x01(\tH\x00R\x0b\x66ileWithUri\x12(\n\x0f\x66ile_with_bytes\x18\x02 \x01(\x0cH\x00R\rfileWithBytes\x12\x1b\n\tmime_type\x18\x03 \x01(\tR\x08mimeType\x12\x12\n\x04name\x18\x04 \x01(\tR\x04nameB\x06\n\x04\x66ile\"7\n\x08\x44\x61taPart\x12+\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructR\x04\x64\x61ta\"\xff\x01\n\x07Message\x12\x1d\n\nmessage_id\x18\x01 \x01(\tR\tmessageId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12\x17\n\x07task_id\x18\x03 \x01(\tR\x06taskId\x12 \n\x04role\x18\x04 \x01(\x0e\x32\x0c.a2a.v1.RoleR\x04role\x12&\n\x07\x63ontent\x18\x05 \x03(\x0b\x32\x0c.a2a.v1.PartR\x07\x63ontent\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\x12\x1e\n\nextensions\x18\x07 \x03(\tR\nextensions\"\xda\x01\n\x08\x41rtifact\x12\x1f\n\x0b\x61rtifact_id\x18\x01 \x01(\tR\nartifactId\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x04 \x01(\tR\x0b\x64\x65scription\x12\"\n\x05parts\x18\x05 \x03(\x0b\x32\x0c.a2a.v1.PartR\x05parts\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\x12\x1e\n\nextensions\x18\x07 \x03(\tR\nextensions\"\xc6\x01\n\x15TaskStatusUpdateEvent\x12\x17\n\x07task_id\x18\x01 \x01(\tR\x06taskId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12*\n\x06status\x18\x03 \x01(\x0b\x32\x12.a2a.v1.TaskStatusR\x06status\x12\x14\n\x05\x66inal\x18\x04 \x01(\x08R\x05\x66inal\x12\x33\n\x08metadata\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\xeb\x01\n\x17TaskArtifactUpdateEvent\x12\x17\n\x07task_id\x18\x01 \x01(\tR\x06taskId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12,\n\x08\x61rtifact\x18\x03 \x01(\x0b\x32\x10.a2a.v1.ArtifactR\x08\x61rtifact\x12\x16\n\x06\x61ppend\x18\x04 \x01(\x08R\x06\x61ppend\x12\x1d\n\nlast_chunk\x18\x05 \x01(\x08R\tlastChunk\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\x94\x01\n\x16PushNotificationConfig\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n\x03url\x18\x02 \x01(\tR\x03url\x12\x14\n\x05token\x18\x03 \x01(\tR\x05token\x12\x42\n\x0e\x61uthentication\x18\x04 \x01(\x0b\x32\x1a.a2a.v1.AuthenticationInfoR\x0e\x61uthentication\"P\n\x12\x41uthenticationInfo\x12\x18\n\x07schemes\x18\x01 \x03(\tR\x07schemes\x12 \n\x0b\x63redentials\x18\x02 \x01(\tR\x0b\x63redentials\"@\n\x0e\x41gentInterface\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1c\n\ttransport\x18\x02 \x01(\tR\ttransport\"\xc8\x07\n\tAgentCard\x12)\n\x10protocol_version\x18\x10 \x01(\tR\x0fprotocolVersion\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x10\n\x03url\x18\x03 \x01(\tR\x03url\x12/\n\x13preferred_transport\x18\x0e \x01(\tR\x12preferredTransport\x12K\n\x15\x61\x64\x64itional_interfaces\x18\x0f \x03(\x0b\x32\x16.a2a.v1.AgentInterfaceR\x14\x61\x64\x64itionalInterfaces\x12\x31\n\x08provider\x18\x04 \x01(\x0b\x32\x15.a2a.v1.AgentProviderR\x08provider\x12\x18\n\x07version\x18\x05 \x01(\tR\x07version\x12+\n\x11\x64ocumentation_url\x18\x06 \x01(\tR\x10\x64ocumentationUrl\x12=\n\x0c\x63\x61pabilities\x18\x07 \x01(\x0b\x32\x19.a2a.v1.AgentCapabilitiesR\x0c\x63\x61pabilities\x12Q\n\x10security_schemes\x18\x08 \x03(\x0b\x32&.a2a.v1.AgentCard.SecuritySchemesEntryR\x0fsecuritySchemes\x12,\n\x08security\x18\t \x03(\x0b\x32\x10.a2a.v1.SecurityR\x08security\x12.\n\x13\x64\x65\x66\x61ult_input_modes\x18\n \x03(\tR\x11\x64\x65\x66\x61ultInputModes\x12\x30\n\x14\x64\x65\x66\x61ult_output_modes\x18\x0b \x03(\tR\x12\x64\x65\x66\x61ultOutputModes\x12*\n\x06skills\x18\x0c \x03(\x0b\x32\x12.a2a.v1.AgentSkillR\x06skills\x12O\n$supports_authenticated_extended_card\x18\r \x01(\x08R!supportsAuthenticatedExtendedCard\x12:\n\nsignatures\x18\x11 \x03(\x0b\x32\x1a.a2a.v1.AgentCardSignatureR\nsignatures\x12\x19\n\x08icon_url\x18\x12 \x01(\tR\x07iconUrl\x1aZ\n\x14SecuritySchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x16.a2a.v1.SecuritySchemeR\x05value:\x02\x38\x01\"E\n\rAgentProvider\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\"\n\x0corganization\x18\x02 \x01(\tR\x0corganization\"\x98\x01\n\x11\x41gentCapabilities\x12\x1c\n\tstreaming\x18\x01 \x01(\x08R\tstreaming\x12-\n\x12push_notifications\x18\x02 \x01(\x08R\x11pushNotifications\x12\x36\n\nextensions\x18\x03 \x03(\x0b\x32\x16.a2a.v1.AgentExtensionR\nextensions\"\x91\x01\n\x0e\x41gentExtension\x12\x10\n\x03uri\x18\x01 \x01(\tR\x03uri\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08required\x18\x03 \x01(\x08R\x08required\x12/\n\x06params\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructR\x06params\"\xf4\x01\n\nAgentSkill\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x03 \x01(\tR\x0b\x64\x65scription\x12\x12\n\x04tags\x18\x04 \x03(\tR\x04tags\x12\x1a\n\x08\x65xamples\x18\x05 \x03(\tR\x08\x65xamples\x12\x1f\n\x0binput_modes\x18\x06 \x03(\tR\ninputModes\x12!\n\x0coutput_modes\x18\x07 \x03(\tR\x0boutputModes\x12,\n\x08security\x18\x08 \x03(\x0b\x32\x10.a2a.v1.SecurityR\x08security\"\x8b\x01\n\x12\x41gentCardSignature\x12!\n\tprotected\x18\x01 \x01(\tB\x03\xe0\x41\x02R\tprotected\x12!\n\tsignature\x18\x02 \x01(\tB\x03\xe0\x41\x02R\tsignature\x12/\n\x06header\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructR\x06header\"\x8a\x01\n\x1aTaskPushNotificationConfig\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12X\n\x18push_notification_config\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.PushNotificationConfigR\x16pushNotificationConfig\" \n\nStringList\x12\x12\n\x04list\x18\x01 \x03(\tR\x04list\"\x93\x01\n\x08Security\x12\x37\n\x07schemes\x18\x01 \x03(\x0b\x32\x1d.a2a.v1.Security.SchemesEntryR\x07schemes\x1aN\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12(\n\x05value\x18\x02 \x01(\x0b\x32\x12.a2a.v1.StringListR\x05value:\x02\x38\x01\"\xe6\x03\n\x0eSecurityScheme\x12U\n\x17\x61pi_key_security_scheme\x18\x01 \x01(\x0b\x32\x1c.a2a.v1.APIKeySecuritySchemeH\x00R\x14\x61piKeySecurityScheme\x12[\n\x19http_auth_security_scheme\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.HTTPAuthSecuritySchemeH\x00R\x16httpAuthSecurityScheme\x12T\n\x16oauth2_security_scheme\x18\x03 \x01(\x0b\x32\x1c.a2a.v1.OAuth2SecuritySchemeH\x00R\x14oauth2SecurityScheme\x12k\n\x1fopen_id_connect_security_scheme\x18\x04 \x01(\x0b\x32#.a2a.v1.OpenIdConnectSecuritySchemeH\x00R\x1bopenIdConnectSecurityScheme\x12S\n\x14mtls_security_scheme\x18\x05 \x01(\x0b\x32\x1f.a2a.v1.MutualTlsSecuritySchemeH\x00R\x12mtlsSecuritySchemeB\x08\n\x06scheme\"h\n\x14\x41PIKeySecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08location\x18\x02 \x01(\tR\x08location\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\"w\n\x16HTTPAuthSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06scheme\x18\x02 \x01(\tR\x06scheme\x12#\n\rbearer_format\x18\x03 \x01(\tR\x0c\x62\x65\x61rerFormat\"\x92\x01\n\x14OAuth2SecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12(\n\x05\x66lows\x18\x02 \x01(\x0b\x32\x12.a2a.v1.OAuthFlowsR\x05\x66lows\x12.\n\x13oauth2_metadata_url\x18\x03 \x01(\tR\x11oauth2MetadataUrl\"n\n\x1bOpenIdConnectSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12-\n\x13open_id_connect_url\x18\x02 \x01(\tR\x10openIdConnectUrl\";\n\x17MutualTlsSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\"\xb0\x02\n\nOAuthFlows\x12S\n\x12\x61uthorization_code\x18\x01 \x01(\x0b\x32\".a2a.v1.AuthorizationCodeOAuthFlowH\x00R\x11\x61uthorizationCode\x12S\n\x12\x63lient_credentials\x18\x02 \x01(\x0b\x32\".a2a.v1.ClientCredentialsOAuthFlowH\x00R\x11\x63lientCredentials\x12\x37\n\x08implicit\x18\x03 \x01(\x0b\x32\x19.a2a.v1.ImplicitOAuthFlowH\x00R\x08implicit\x12\x37\n\x08password\x18\x04 \x01(\x0b\x32\x19.a2a.v1.PasswordOAuthFlowH\x00R\x08passwordB\x06\n\x04\x66low\"\x8a\x02\n\x1a\x41uthorizationCodeOAuthFlow\x12+\n\x11\x61uthorization_url\x18\x01 \x01(\tR\x10\x61uthorizationUrl\x12\x1b\n\ttoken_url\x18\x02 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x03 \x01(\tR\nrefreshUrl\x12\x46\n\x06scopes\x18\x04 \x03(\x0b\x32..a2a.v1.AuthorizationCodeOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdd\x01\n\x1a\x43lientCredentialsOAuthFlow\x12\x1b\n\ttoken_url\x18\x01 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12\x46\n\x06scopes\x18\x03 \x03(\x0b\x32..a2a.v1.ClientCredentialsOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdb\x01\n\x11ImplicitOAuthFlow\x12+\n\x11\x61uthorization_url\x18\x01 \x01(\tR\x10\x61uthorizationUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12=\n\x06scopes\x18\x03 \x03(\x0b\x32%.a2a.v1.ImplicitOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xcb\x01\n\x11PasswordOAuthFlow\x12\x1b\n\ttoken_url\x18\x01 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12=\n\x06scopes\x18\x03 \x03(\x0b\x32%.a2a.v1.PasswordOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xc1\x01\n\x12SendMessageRequest\x12.\n\x07request\x18\x01 \x01(\x0b\x32\x0f.a2a.v1.MessageB\x03\xe0\x41\x02R\x07message\x12\x46\n\rconfiguration\x18\x02 \x01(\x0b\x32 .a2a.v1.SendMessageConfigurationR\rconfiguration\x12\x33\n\x08metadata\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"P\n\x0eGetTaskRequest\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x04name\x12%\n\x0ehistory_length\x18\x02 \x01(\x05R\rhistoryLength\"\'\n\x11\x43\x61ncelTaskRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\":\n$GetTaskPushNotificationConfigRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"=\n\'DeleteTaskPushNotificationConfigRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xa9\x01\n\'CreateTaskPushNotificationConfigRequest\x12\x1b\n\x06parent\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06parent\x12 \n\tconfig_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x08\x63onfigId\x12?\n\x06\x63onfig\x18\x03 \x01(\x0b\x32\".a2a.v1.TaskPushNotificationConfigB\x03\xe0\x41\x02R\x06\x63onfig\"-\n\x17TaskSubscriptionRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"{\n%ListTaskPushNotificationConfigRequest\x12\x16\n\x06parent\x18\x01 \x01(\tR\x06parent\x12\x1b\n\tpage_size\x18\x02 \x01(\x05R\x08pageSize\x12\x1d\n\npage_token\x18\x03 \x01(\tR\tpageToken\"\x15\n\x13GetAgentCardRequest\"m\n\x13SendMessageResponse\x12\"\n\x04task\x18\x01 \x01(\x0b\x32\x0c.a2a.v1.TaskH\x00R\x04task\x12\'\n\x03msg\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageH\x00R\x07messageB\t\n\x07payload\"\xfa\x01\n\x0eStreamResponse\x12\"\n\x04task\x18\x01 \x01(\x0b\x32\x0c.a2a.v1.TaskH\x00R\x04task\x12\'\n\x03msg\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageH\x00R\x07message\x12\x44\n\rstatus_update\x18\x03 \x01(\x0b\x32\x1d.a2a.v1.TaskStatusUpdateEventH\x00R\x0cstatusUpdate\x12J\n\x0f\x61rtifact_update\x18\x04 \x01(\x0b\x32\x1f.a2a.v1.TaskArtifactUpdateEventH\x00R\x0e\x61rtifactUpdateB\t\n\x07payload\"\x8e\x01\n&ListTaskPushNotificationConfigResponse\x12<\n\x07\x63onfigs\x18\x01 \x03(\x0b\x32\".a2a.v1.TaskPushNotificationConfigR\x07\x63onfigs\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xfa\x01\n\tTaskState\x12\x1a\n\x16TASK_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14TASK_STATE_SUBMITTED\x10\x01\x12\x16\n\x12TASK_STATE_WORKING\x10\x02\x12\x18\n\x14TASK_STATE_COMPLETED\x10\x03\x12\x15\n\x11TASK_STATE_FAILED\x10\x04\x12\x18\n\x14TASK_STATE_CANCELLED\x10\x05\x12\x1d\n\x19TASK_STATE_INPUT_REQUIRED\x10\x06\x12\x17\n\x13TASK_STATE_REJECTED\x10\x07\x12\x1c\n\x18TASK_STATE_AUTH_REQUIRED\x10\x08*;\n\x04Role\x12\x14\n\x10ROLE_UNSPECIFIED\x10\x00\x12\r\n\tROLE_USER\x10\x01\x12\x0e\n\nROLE_AGENT\x10\x02\x32\xbb\n\n\nA2AService\x12\x63\n\x0bSendMessage\x12\x1a.a2a.v1.SendMessageRequest\x1a\x1b.a2a.v1.SendMessageResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/message:send:\x01*\x12k\n\x14SendStreamingMessage\x12\x1a.a2a.v1.SendMessageRequest\x1a\x16.a2a.v1.StreamResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x12/v1/message:stream:\x01*0\x01\x12R\n\x07GetTask\x12\x16.a2a.v1.GetTaskRequest\x1a\x0c.a2a.v1.Task\"!\xda\x41\x04name\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/{name=tasks/*}\x12[\n\nCancelTask\x12\x19.a2a.v1.CancelTaskRequest\x1a\x0c.a2a.v1.Task\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/{name=tasks/*}:cancel:\x01*\x12s\n\x10TaskSubscription\x12\x1f.a2a.v1.TaskSubscriptionRequest\x1a\x16.a2a.v1.StreamResponse\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/{name=tasks/*}:subscribe0\x01\x12\xc5\x01\n CreateTaskPushNotificationConfig\x12/.a2a.v1.CreateTaskPushNotificationConfigRequest\x1a\".a2a.v1.TaskPushNotificationConfig\"L\xda\x41\rparent,config\x82\xd3\xe4\x93\x02\x36\",/v1/{parent=tasks/*/pushNotificationConfigs}:\x06\x63onfig\x12\xae\x01\n\x1dGetTaskPushNotificationConfig\x12,.a2a.v1.GetTaskPushNotificationConfigRequest\x1a\".a2a.v1.TaskPushNotificationConfig\";\xda\x41\x04name\x82\xd3\xe4\x93\x02.\x12,/v1/{name=tasks/*/pushNotificationConfigs/*}\x12\xbe\x01\n\x1eListTaskPushNotificationConfig\x12-.a2a.v1.ListTaskPushNotificationConfigRequest\x1a..a2a.v1.ListTaskPushNotificationConfigResponse\"=\xda\x41\x06parent\x82\xd3\xe4\x93\x02.\x12,/v1/{parent=tasks/*}/pushNotificationConfigs\x12P\n\x0cGetAgentCard\x12\x1b.a2a.v1.GetAgentCardRequest\x1a\x11.a2a.v1.AgentCard\"\x10\x82\xd3\xe4\x93\x02\n\x12\x08/v1/card\x12\xa8\x01\n DeleteTaskPushNotificationConfig\x12/.a2a.v1.DeleteTaskPushNotificationConfigRequest\x1a\x16.google.protobuf.Empty\";\xda\x41\x04name\x82\xd3\xe4\x93\x02.*,/v1/{name=tasks/*/pushNotificationConfigs/*}Bi\n\ncom.a2a.v1B\x08\x41\x32\x61ProtoP\x01Z\x18google.golang.org/a2a/v1\xa2\x02\x03\x41XX\xaa\x02\x06\x41\x32\x61.V1\xca\x02\x06\x41\x32\x61\\V1\xe2\x02\x12\x41\x32\x61\\V1\\GPBMetadata\xea\x02\x07\x41\x32\x61::V1b\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\ta2a.proto\x12\x06\x61\x32\x61.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1cgoogle/protobuf/struct.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xde\x01\n\x18SendMessageConfiguration\x12\x32\n\x15\x61\x63\x63\x65pted_output_modes\x18\x01 \x03(\tR\x13\x61\x63\x63\x65ptedOutputModes\x12K\n\x11push_notification\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.PushNotificationConfigR\x10pushNotification\x12%\n\x0ehistory_length\x18\x03 \x01(\x05R\rhistoryLength\x12\x1a\n\x08\x62locking\x18\x04 \x01(\x08R\x08\x62locking\"\xf1\x01\n\x04Task\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12*\n\x06status\x18\x03 \x01(\x0b\x32\x12.a2a.v1.TaskStatusR\x06status\x12.\n\tartifacts\x18\x04 \x03(\x0b\x32\x10.a2a.v1.ArtifactR\tartifacts\x12)\n\x07history\x18\x05 \x03(\x0b\x32\x0f.a2a.v1.MessageR\x07history\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\x99\x01\n\nTaskStatus\x12\'\n\x05state\x18\x01 \x01(\x0e\x32\x11.a2a.v1.TaskStateR\x05state\x12(\n\x06update\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageR\x07message\x12\x38\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\ttimestamp\"\xa9\x01\n\x04Part\x12\x14\n\x04text\x18\x01 \x01(\tH\x00R\x04text\x12&\n\x04\x66ile\x18\x02 \x01(\x0b\x32\x10.a2a.v1.FilePartH\x00R\x04\x66ile\x12&\n\x04\x64\x61ta\x18\x03 \x01(\x0b\x32\x10.a2a.v1.DataPartH\x00R\x04\x64\x61ta\x12\x33\n\x08metadata\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadataB\x06\n\x04part\"\x93\x01\n\x08\x46ilePart\x12$\n\rfile_with_uri\x18\x01 \x01(\tH\x00R\x0b\x66ileWithUri\x12(\n\x0f\x66ile_with_bytes\x18\x02 \x01(\x0cH\x00R\rfileWithBytes\x12\x1b\n\tmime_type\x18\x03 \x01(\tR\x08mimeType\x12\x12\n\x04name\x18\x04 \x01(\tR\x04nameB\x06\n\x04\x66ile\"7\n\x08\x44\x61taPart\x12+\n\x04\x64\x61ta\x18\x01 \x01(\x0b\x32\x17.google.protobuf.StructR\x04\x64\x61ta\"\xff\x01\n\x07Message\x12\x1d\n\nmessage_id\x18\x01 \x01(\tR\tmessageId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12\x17\n\x07task_id\x18\x03 \x01(\tR\x06taskId\x12 \n\x04role\x18\x04 \x01(\x0e\x32\x0c.a2a.v1.RoleR\x04role\x12&\n\x07\x63ontent\x18\x05 \x03(\x0b\x32\x0c.a2a.v1.PartR\x07\x63ontent\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\x12\x1e\n\nextensions\x18\x07 \x03(\tR\nextensions\"\xda\x01\n\x08\x41rtifact\x12\x1f\n\x0b\x61rtifact_id\x18\x01 \x01(\tR\nartifactId\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x04 \x01(\tR\x0b\x64\x65scription\x12\"\n\x05parts\x18\x05 \x03(\x0b\x32\x0c.a2a.v1.PartR\x05parts\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\x12\x1e\n\nextensions\x18\x07 \x03(\tR\nextensions\"\xc6\x01\n\x15TaskStatusUpdateEvent\x12\x17\n\x07task_id\x18\x01 \x01(\tR\x06taskId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12*\n\x06status\x18\x03 \x01(\x0b\x32\x12.a2a.v1.TaskStatusR\x06status\x12\x14\n\x05\x66inal\x18\x04 \x01(\x08R\x05\x66inal\x12\x33\n\x08metadata\x18\x05 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\xeb\x01\n\x17TaskArtifactUpdateEvent\x12\x17\n\x07task_id\x18\x01 \x01(\tR\x06taskId\x12\x1d\n\ncontext_id\x18\x02 \x01(\tR\tcontextId\x12,\n\x08\x61rtifact\x18\x03 \x01(\x0b\x32\x10.a2a.v1.ArtifactR\x08\x61rtifact\x12\x16\n\x06\x61ppend\x18\x04 \x01(\x08R\x06\x61ppend\x12\x1d\n\nlast_chunk\x18\x05 \x01(\x08R\tlastChunk\x12\x33\n\x08metadata\x18\x06 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"\x94\x01\n\x16PushNotificationConfig\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n\x03url\x18\x02 \x01(\tR\x03url\x12\x14\n\x05token\x18\x03 \x01(\tR\x05token\x12\x42\n\x0e\x61uthentication\x18\x04 \x01(\x0b\x32\x1a.a2a.v1.AuthenticationInfoR\x0e\x61uthentication\"P\n\x12\x41uthenticationInfo\x12\x18\n\x07schemes\x18\x01 \x03(\tR\x07schemes\x12 \n\x0b\x63redentials\x18\x02 \x01(\tR\x0b\x63redentials\"@\n\x0e\x41gentInterface\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\x1c\n\ttransport\x18\x02 \x01(\tR\ttransport\"\xc8\x07\n\tAgentCard\x12)\n\x10protocol_version\x18\x10 \x01(\tR\x0fprotocolVersion\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x10\n\x03url\x18\x03 \x01(\tR\x03url\x12/\n\x13preferred_transport\x18\x0e \x01(\tR\x12preferredTransport\x12K\n\x15\x61\x64\x64itional_interfaces\x18\x0f \x03(\x0b\x32\x16.a2a.v1.AgentInterfaceR\x14\x61\x64\x64itionalInterfaces\x12\x31\n\x08provider\x18\x04 \x01(\x0b\x32\x15.a2a.v1.AgentProviderR\x08provider\x12\x18\n\x07version\x18\x05 \x01(\tR\x07version\x12+\n\x11\x64ocumentation_url\x18\x06 \x01(\tR\x10\x64ocumentationUrl\x12=\n\x0c\x63\x61pabilities\x18\x07 \x01(\x0b\x32\x19.a2a.v1.AgentCapabilitiesR\x0c\x63\x61pabilities\x12Q\n\x10security_schemes\x18\x08 \x03(\x0b\x32&.a2a.v1.AgentCard.SecuritySchemesEntryR\x0fsecuritySchemes\x12,\n\x08security\x18\t \x03(\x0b\x32\x10.a2a.v1.SecurityR\x08security\x12.\n\x13\x64\x65\x66\x61ult_input_modes\x18\n \x03(\tR\x11\x64\x65\x66\x61ultInputModes\x12\x30\n\x14\x64\x65\x66\x61ult_output_modes\x18\x0b \x03(\tR\x12\x64\x65\x66\x61ultOutputModes\x12*\n\x06skills\x18\x0c \x03(\x0b\x32\x12.a2a.v1.AgentSkillR\x06skills\x12O\n$supports_authenticated_extended_card\x18\r \x01(\x08R!supportsAuthenticatedExtendedCard\x12:\n\nsignatures\x18\x11 \x03(\x0b\x32\x1a.a2a.v1.AgentCardSignatureR\nsignatures\x12\x19\n\x08icon_url\x18\x12 \x01(\tR\x07iconUrl\x1aZ\n\x14SecuritySchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12,\n\x05value\x18\x02 \x01(\x0b\x32\x16.a2a.v1.SecuritySchemeR\x05value:\x02\x38\x01\"E\n\rAgentProvider\x12\x10\n\x03url\x18\x01 \x01(\tR\x03url\x12\"\n\x0corganization\x18\x02 \x01(\tR\x0corganization\"\x98\x01\n\x11\x41gentCapabilities\x12\x1c\n\tstreaming\x18\x01 \x01(\x08R\tstreaming\x12-\n\x12push_notifications\x18\x02 \x01(\x08R\x11pushNotifications\x12\x36\n\nextensions\x18\x03 \x03(\x0b\x32\x16.a2a.v1.AgentExtensionR\nextensions\"\x91\x01\n\x0e\x41gentExtension\x12\x10\n\x03uri\x18\x01 \x01(\tR\x03uri\x12 \n\x0b\x64\x65scription\x18\x02 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08required\x18\x03 \x01(\x08R\x08required\x12/\n\x06params\x18\x04 \x01(\x0b\x32\x17.google.protobuf.StructR\x06params\"\xf4\x01\n\nAgentSkill\x12\x0e\n\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n\x04name\x18\x02 \x01(\tR\x04name\x12 \n\x0b\x64\x65scription\x18\x03 \x01(\tR\x0b\x64\x65scription\x12\x12\n\x04tags\x18\x04 \x03(\tR\x04tags\x12\x1a\n\x08\x65xamples\x18\x05 \x03(\tR\x08\x65xamples\x12\x1f\n\x0binput_modes\x18\x06 \x03(\tR\ninputModes\x12!\n\x0coutput_modes\x18\x07 \x03(\tR\x0boutputModes\x12,\n\x08security\x18\x08 \x03(\x0b\x32\x10.a2a.v1.SecurityR\x08security\"\x8b\x01\n\x12\x41gentCardSignature\x12!\n\tprotected\x18\x01 \x01(\tB\x03\xe0\x41\x02R\tprotected\x12!\n\tsignature\x18\x02 \x01(\tB\x03\xe0\x41\x02R\tsignature\x12/\n\x06header\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructR\x06header\"\x8a\x01\n\x1aTaskPushNotificationConfig\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\x12X\n\x18push_notification_config\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.PushNotificationConfigR\x16pushNotificationConfig\" \n\nStringList\x12\x12\n\x04list\x18\x01 \x03(\tR\x04list\"\x93\x01\n\x08Security\x12\x37\n\x07schemes\x18\x01 \x03(\x0b\x32\x1d.a2a.v1.Security.SchemesEntryR\x07schemes\x1aN\n\x0cSchemesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12(\n\x05value\x18\x02 \x01(\x0b\x32\x12.a2a.v1.StringListR\x05value:\x02\x38\x01\"\xe6\x03\n\x0eSecurityScheme\x12U\n\x17\x61pi_key_security_scheme\x18\x01 \x01(\x0b\x32\x1c.a2a.v1.APIKeySecuritySchemeH\x00R\x14\x61piKeySecurityScheme\x12[\n\x19http_auth_security_scheme\x18\x02 \x01(\x0b\x32\x1e.a2a.v1.HTTPAuthSecuritySchemeH\x00R\x16httpAuthSecurityScheme\x12T\n\x16oauth2_security_scheme\x18\x03 \x01(\x0b\x32\x1c.a2a.v1.OAuth2SecuritySchemeH\x00R\x14oauth2SecurityScheme\x12k\n\x1fopen_id_connect_security_scheme\x18\x04 \x01(\x0b\x32#.a2a.v1.OpenIdConnectSecuritySchemeH\x00R\x1bopenIdConnectSecurityScheme\x12S\n\x14mtls_security_scheme\x18\x05 \x01(\x0b\x32\x1f.a2a.v1.MutualTlsSecuritySchemeH\x00R\x12mtlsSecuritySchemeB\x08\n\x06scheme\"h\n\x14\x41PIKeySecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x1a\n\x08location\x18\x02 \x01(\tR\x08location\x12\x12\n\x04name\x18\x03 \x01(\tR\x04name\"w\n\x16HTTPAuthSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12\x16\n\x06scheme\x18\x02 \x01(\tR\x06scheme\x12#\n\rbearer_format\x18\x03 \x01(\tR\x0c\x62\x65\x61rerFormat\"\x92\x01\n\x14OAuth2SecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12(\n\x05\x66lows\x18\x02 \x01(\x0b\x32\x12.a2a.v1.OAuthFlowsR\x05\x66lows\x12.\n\x13oauth2_metadata_url\x18\x03 \x01(\tR\x11oauth2MetadataUrl\"n\n\x1bOpenIdConnectSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\x12-\n\x13open_id_connect_url\x18\x02 \x01(\tR\x10openIdConnectUrl\";\n\x17MutualTlsSecurityScheme\x12 \n\x0b\x64\x65scription\x18\x01 \x01(\tR\x0b\x64\x65scription\"\xb0\x02\n\nOAuthFlows\x12S\n\x12\x61uthorization_code\x18\x01 \x01(\x0b\x32\".a2a.v1.AuthorizationCodeOAuthFlowH\x00R\x11\x61uthorizationCode\x12S\n\x12\x63lient_credentials\x18\x02 \x01(\x0b\x32\".a2a.v1.ClientCredentialsOAuthFlowH\x00R\x11\x63lientCredentials\x12\x37\n\x08implicit\x18\x03 \x01(\x0b\x32\x19.a2a.v1.ImplicitOAuthFlowH\x00R\x08implicit\x12\x37\n\x08password\x18\x04 \x01(\x0b\x32\x19.a2a.v1.PasswordOAuthFlowH\x00R\x08passwordB\x06\n\x04\x66low\"\x8a\x02\n\x1a\x41uthorizationCodeOAuthFlow\x12+\n\x11\x61uthorization_url\x18\x01 \x01(\tR\x10\x61uthorizationUrl\x12\x1b\n\ttoken_url\x18\x02 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x03 \x01(\tR\nrefreshUrl\x12\x46\n\x06scopes\x18\x04 \x03(\x0b\x32..a2a.v1.AuthorizationCodeOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdd\x01\n\x1a\x43lientCredentialsOAuthFlow\x12\x1b\n\ttoken_url\x18\x01 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12\x46\n\x06scopes\x18\x03 \x03(\x0b\x32..a2a.v1.ClientCredentialsOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xdb\x01\n\x11ImplicitOAuthFlow\x12+\n\x11\x61uthorization_url\x18\x01 \x01(\tR\x10\x61uthorizationUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12=\n\x06scopes\x18\x03 \x03(\x0b\x32%.a2a.v1.ImplicitOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xcb\x01\n\x11PasswordOAuthFlow\x12\x1b\n\ttoken_url\x18\x01 \x01(\tR\x08tokenUrl\x12\x1f\n\x0brefresh_url\x18\x02 \x01(\tR\nrefreshUrl\x12=\n\x06scopes\x18\x03 \x03(\x0b\x32%.a2a.v1.PasswordOAuthFlow.ScopesEntryR\x06scopes\x1a\x39\n\x0bScopesEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\xc1\x01\n\x12SendMessageRequest\x12.\n\x07request\x18\x01 \x01(\x0b\x32\x0f.a2a.v1.MessageB\x03\xe0\x41\x02R\x07message\x12\x46\n\rconfiguration\x18\x02 \x01(\x0b\x32 .a2a.v1.SendMessageConfigurationR\rconfiguration\x12\x33\n\x08metadata\x18\x03 \x01(\x0b\x32\x17.google.protobuf.StructR\x08metadata\"P\n\x0eGetTaskRequest\x12\x17\n\x04name\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x04name\x12%\n\x0ehistory_length\x18\x02 \x01(\x05R\rhistoryLength\"\xb4\x02\n\x10ListTasksRequest\x12\x1d\n\ncontext_id\x18\x01 \x01(\tR\tcontextId\x12)\n\x06status\x18\x02 \x01(\x0e\x32\x11.a2a.v1.TaskStateR\x06status\x12\x1b\n\tpage_size\x18\x03 \x01(\x05R\x08pageSize\x12\x1d\n\npage_token\x18\x04 \x01(\tR\tpageToken\x12%\n\x0ehistory_length\x18\x05 \x01(\x05R\rhistoryLength\x12\x46\n\x11last_updated_time\x18\x06 \x01(\x0b\x32\x1a.google.protobuf.TimestampR\x0flastUpdatedTime\x12+\n\x11include_artifacts\x18\x07 \x01(\x08R\x10includeArtifacts\"~\n\x11ListTasksResponse\x12\"\n\x05tasks\x18\x01 \x03(\x0b\x32\x0c.a2a.v1.TaskR\x05tasks\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n\ntotal_size\x18\x03 \x01(\x05R\ttotalSize\"\'\n\x11\x43\x61ncelTaskRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\":\n$GetTaskPushNotificationConfigRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"=\n\'DeleteTaskPushNotificationConfigRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"\xa9\x01\n\'CreateTaskPushNotificationConfigRequest\x12\x1b\n\x06parent\x18\x01 \x01(\tB\x03\xe0\x41\x02R\x06parent\x12 \n\tconfig_id\x18\x02 \x01(\tB\x03\xe0\x41\x02R\x08\x63onfigId\x12?\n\x06\x63onfig\x18\x03 \x01(\x0b\x32\".a2a.v1.TaskPushNotificationConfigB\x03\xe0\x41\x02R\x06\x63onfig\"-\n\x17TaskSubscriptionRequest\x12\x12\n\x04name\x18\x01 \x01(\tR\x04name\"{\n%ListTaskPushNotificationConfigRequest\x12\x16\n\x06parent\x18\x01 \x01(\tR\x06parent\x12\x1b\n\tpage_size\x18\x02 \x01(\x05R\x08pageSize\x12\x1d\n\npage_token\x18\x03 \x01(\tR\tpageToken\"\x15\n\x13GetAgentCardRequest\"m\n\x13SendMessageResponse\x12\"\n\x04task\x18\x01 \x01(\x0b\x32\x0c.a2a.v1.TaskH\x00R\x04task\x12\'\n\x03msg\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageH\x00R\x07messageB\t\n\x07payload\"\xfa\x01\n\x0eStreamResponse\x12\"\n\x04task\x18\x01 \x01(\x0b\x32\x0c.a2a.v1.TaskH\x00R\x04task\x12\'\n\x03msg\x18\x02 \x01(\x0b\x32\x0f.a2a.v1.MessageH\x00R\x07message\x12\x44\n\rstatus_update\x18\x03 \x01(\x0b\x32\x1d.a2a.v1.TaskStatusUpdateEventH\x00R\x0cstatusUpdate\x12J\n\x0f\x61rtifact_update\x18\x04 \x01(\x0b\x32\x1f.a2a.v1.TaskArtifactUpdateEventH\x00R\x0e\x61rtifactUpdateB\t\n\x07payload\"\x8e\x01\n&ListTaskPushNotificationConfigResponse\x12<\n\x07\x63onfigs\x18\x01 \x03(\x0b\x32\".a2a.v1.TaskPushNotificationConfigR\x07\x63onfigs\x12&\n\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken*\xfa\x01\n\tTaskState\x12\x1a\n\x16TASK_STATE_UNSPECIFIED\x10\x00\x12\x18\n\x14TASK_STATE_SUBMITTED\x10\x01\x12\x16\n\x12TASK_STATE_WORKING\x10\x02\x12\x18\n\x14TASK_STATE_COMPLETED\x10\x03\x12\x15\n\x11TASK_STATE_FAILED\x10\x04\x12\x18\n\x14TASK_STATE_CANCELLED\x10\x05\x12\x1d\n\x19TASK_STATE_INPUT_REQUIRED\x10\x06\x12\x17\n\x13TASK_STATE_REJECTED\x10\x07\x12\x1c\n\x18TASK_STATE_AUTH_REQUIRED\x10\x08*;\n\x04Role\x12\x14\n\x10ROLE_UNSPECIFIED\x10\x00\x12\r\n\tROLE_USER\x10\x01\x12\x0e\n\nROLE_AGENT\x10\x02\x32\x90\x0b\n\nA2AService\x12\x63\n\x0bSendMessage\x12\x1a.a2a.v1.SendMessageRequest\x1a\x1b.a2a.v1.SendMessageResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\"\x10/v1/message:send:\x01*\x12k\n\x14SendStreamingMessage\x12\x1a.a2a.v1.SendMessageRequest\x1a\x16.a2a.v1.StreamResponse\"\x1d\x82\xd3\xe4\x93\x02\x17\"\x12/v1/message:stream:\x01*0\x01\x12R\n\x07GetTask\x12\x16.a2a.v1.GetTaskRequest\x1a\x0c.a2a.v1.Task\"!\xda\x41\x04name\x82\xd3\xe4\x93\x02\x14\x12\x12/v1/{name=tasks/*}\x12S\n\tListTasks\x12\x18.a2a.v1.ListTasksRequest\x1a\x19.a2a.v1.ListTasksResponse\"\x11\x82\xd3\xe4\x93\x02\x0b\x12\t/v1/tasks\x12[\n\nCancelTask\x12\x19.a2a.v1.CancelTaskRequest\x1a\x0c.a2a.v1.Task\"$\x82\xd3\xe4\x93\x02\x1e\"\x19/v1/{name=tasks/*}:cancel:\x01*\x12s\n\x10TaskSubscription\x12\x1f.a2a.v1.TaskSubscriptionRequest\x1a\x16.a2a.v1.StreamResponse\"$\x82\xd3\xe4\x93\x02\x1e\x12\x1c/v1/{name=tasks/*}:subscribe0\x01\x12\xc5\x01\n CreateTaskPushNotificationConfig\x12/.a2a.v1.CreateTaskPushNotificationConfigRequest\x1a\".a2a.v1.TaskPushNotificationConfig\"L\xda\x41\rparent,config\x82\xd3\xe4\x93\x02\x36\",/v1/{parent=tasks/*/pushNotificationConfigs}:\x06\x63onfig\x12\xae\x01\n\x1dGetTaskPushNotificationConfig\x12,.a2a.v1.GetTaskPushNotificationConfigRequest\x1a\".a2a.v1.TaskPushNotificationConfig\";\xda\x41\x04name\x82\xd3\xe4\x93\x02.\x12,/v1/{name=tasks/*/pushNotificationConfigs/*}\x12\xbe\x01\n\x1eListTaskPushNotificationConfig\x12-.a2a.v1.ListTaskPushNotificationConfigRequest\x1a..a2a.v1.ListTaskPushNotificationConfigResponse\"=\xda\x41\x06parent\x82\xd3\xe4\x93\x02.\x12,/v1/{parent=tasks/*}/pushNotificationConfigs\x12P\n\x0cGetAgentCard\x12\x1b.a2a.v1.GetAgentCardRequest\x1a\x11.a2a.v1.AgentCard\"\x10\x82\xd3\xe4\x93\x02\n\x12\x08/v1/card\x12\xa8\x01\n DeleteTaskPushNotificationConfig\x12/.a2a.v1.DeleteTaskPushNotificationConfigRequest\x1a\x16.google.protobuf.Empty\";\xda\x41\x04name\x82\xd3\xe4\x93\x02.*,/v1/{name=tasks/*/pushNotificationConfigs/*}Bi\n\ncom.a2a.v1B\x08\x41\x32\x61ProtoP\x01Z\x18google.golang.org/a2a/v1\xa2\x02\x03\x41XX\xaa\x02\x06\x41\x32\x61.V1\xca\x02\x06\x41\x32\x61\\V1\xe2\x02\x12\x41\x32\x61\\V1\\GPBMetadata\xea\x02\x07\x41\x32\x61::V1b\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -70,6 +70,8 @@ _globals['_A2ASERVICE'].methods_by_name['SendStreamingMessage']._serialized_options = b'\202\323\344\223\002\027\"\022/v1/message:stream:\001*' _globals['_A2ASERVICE'].methods_by_name['GetTask']._loaded_options = None _globals['_A2ASERVICE'].methods_by_name['GetTask']._serialized_options = b'\332A\004name\202\323\344\223\002\024\022\022/v1/{name=tasks/*}' + _globals['_A2ASERVICE'].methods_by_name['ListTasks']._loaded_options = None + _globals['_A2ASERVICE'].methods_by_name['ListTasks']._serialized_options = b'\202\323\344\223\002\013\022\t/v1/tasks' _globals['_A2ASERVICE'].methods_by_name['CancelTask']._loaded_options = None _globals['_A2ASERVICE'].methods_by_name['CancelTask']._serialized_options = b'\202\323\344\223\002\036\"\031/v1/{name=tasks/*}:cancel:\001*' _globals['_A2ASERVICE'].methods_by_name['TaskSubscription']._loaded_options = None @@ -84,10 +86,10 @@ _globals['_A2ASERVICE'].methods_by_name['GetAgentCard']._serialized_options = b'\202\323\344\223\002\n\022\010/v1/card' _globals['_A2ASERVICE'].methods_by_name['DeleteTaskPushNotificationConfig']._loaded_options = None _globals['_A2ASERVICE'].methods_by_name['DeleteTaskPushNotificationConfig']._serialized_options = b'\332A\004name\202\323\344\223\002.*,/v1/{name=tasks/*/pushNotificationConfigs/*}' - _globals['_TASKSTATE']._serialized_start=8066 - _globals['_TASKSTATE']._serialized_end=8316 - _globals['_ROLE']._serialized_start=8318 - _globals['_ROLE']._serialized_end=8377 + _globals['_TASKSTATE']._serialized_start=8505 + _globals['_TASKSTATE']._serialized_end=8755 + _globals['_ROLE']._serialized_start=8757 + _globals['_ROLE']._serialized_end=8816 _globals['_SENDMESSAGECONFIGURATION']._serialized_start=202 _globals['_SENDMESSAGECONFIGURATION']._serialized_end=424 _globals['_TASK']._serialized_start=427 @@ -170,26 +172,30 @@ _globals['_SENDMESSAGEREQUEST']._serialized_end=6941 _globals['_GETTASKREQUEST']._serialized_start=6943 _globals['_GETTASKREQUEST']._serialized_end=7023 - _globals['_CANCELTASKREQUEST']._serialized_start=7025 - _globals['_CANCELTASKREQUEST']._serialized_end=7064 - _globals['_GETTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7066 - _globals['_GETTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7124 - _globals['_DELETETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7126 - _globals['_DELETETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7187 - _globals['_CREATETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7190 - _globals['_CREATETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7359 - _globals['_TASKSUBSCRIPTIONREQUEST']._serialized_start=7361 - _globals['_TASKSUBSCRIPTIONREQUEST']._serialized_end=7406 - _globals['_LISTTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7408 - _globals['_LISTTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7531 - _globals['_GETAGENTCARDREQUEST']._serialized_start=7533 - _globals['_GETAGENTCARDREQUEST']._serialized_end=7554 - _globals['_SENDMESSAGERESPONSE']._serialized_start=7556 - _globals['_SENDMESSAGERESPONSE']._serialized_end=7665 - _globals['_STREAMRESPONSE']._serialized_start=7668 - _globals['_STREAMRESPONSE']._serialized_end=7918 - _globals['_LISTTASKPUSHNOTIFICATIONCONFIGRESPONSE']._serialized_start=7921 - _globals['_LISTTASKPUSHNOTIFICATIONCONFIGRESPONSE']._serialized_end=8063 - _globals['_A2ASERVICE']._serialized_start=8380 - _globals['_A2ASERVICE']._serialized_end=9719 + _globals['_LISTTASKSREQUEST']._serialized_start=7026 + _globals['_LISTTASKSREQUEST']._serialized_end=7334 + _globals['_LISTTASKSRESPONSE']._serialized_start=7336 + _globals['_LISTTASKSRESPONSE']._serialized_end=7462 + _globals['_CANCELTASKREQUEST']._serialized_start=7464 + _globals['_CANCELTASKREQUEST']._serialized_end=7503 + _globals['_GETTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7505 + _globals['_GETTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7563 + _globals['_DELETETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7565 + _globals['_DELETETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7626 + _globals['_CREATETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7629 + _globals['_CREATETASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7798 + _globals['_TASKSUBSCRIPTIONREQUEST']._serialized_start=7800 + _globals['_TASKSUBSCRIPTIONREQUEST']._serialized_end=7845 + _globals['_LISTTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_start=7847 + _globals['_LISTTASKPUSHNOTIFICATIONCONFIGREQUEST']._serialized_end=7970 + _globals['_GETAGENTCARDREQUEST']._serialized_start=7972 + _globals['_GETAGENTCARDREQUEST']._serialized_end=7993 + _globals['_SENDMESSAGERESPONSE']._serialized_start=7995 + _globals['_SENDMESSAGERESPONSE']._serialized_end=8104 + _globals['_STREAMRESPONSE']._serialized_start=8107 + _globals['_STREAMRESPONSE']._serialized_end=8357 + _globals['_LISTTASKPUSHNOTIFICATIONCONFIGRESPONSE']._serialized_start=8360 + _globals['_LISTTASKPUSHNOTIFICATIONCONFIGRESPONSE']._serialized_end=8502 + _globals['_A2ASERVICE']._serialized_start=8819 + _globals['_A2ASERVICE']._serialized_end=10243 # @@protoc_insertion_point(module_scope) diff --git a/src/a2a/grpc/a2a_pb2.pyi b/src/a2a/grpc/a2a_pb2.pyi index 06005e850..d3f606df7 100644 --- a/src/a2a/grpc/a2a_pb2.pyi +++ b/src/a2a/grpc/a2a_pb2.pyi @@ -497,6 +497,34 @@ class GetTaskRequest(_message.Message): history_length: int def __init__(self, name: _Optional[str] = ..., history_length: _Optional[int] = ...) -> None: ... +class ListTasksRequest(_message.Message): + __slots__ = ("context_id", "status", "page_size", "page_token", "history_length", "last_updated_time", "include_artifacts") + CONTEXT_ID_FIELD_NUMBER: _ClassVar[int] + STATUS_FIELD_NUMBER: _ClassVar[int] + PAGE_SIZE_FIELD_NUMBER: _ClassVar[int] + PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int] + HISTORY_LENGTH_FIELD_NUMBER: _ClassVar[int] + LAST_UPDATED_TIME_FIELD_NUMBER: _ClassVar[int] + INCLUDE_ARTIFACTS_FIELD_NUMBER: _ClassVar[int] + context_id: str + status: TaskState + page_size: int + page_token: str + history_length: int + last_updated_time: _timestamp_pb2.Timestamp + include_artifacts: bool + def __init__(self, context_id: _Optional[str] = ..., status: _Optional[_Union[TaskState, str]] = ..., page_size: _Optional[int] = ..., page_token: _Optional[str] = ..., history_length: _Optional[int] = ..., last_updated_time: _Optional[_Union[datetime.datetime, _timestamp_pb2.Timestamp, _Mapping]] = ..., include_artifacts: _Optional[bool] = ...) -> None: ... + +class ListTasksResponse(_message.Message): + __slots__ = ("tasks", "next_page_token", "total_size") + TASKS_FIELD_NUMBER: _ClassVar[int] + NEXT_PAGE_TOKEN_FIELD_NUMBER: _ClassVar[int] + TOTAL_SIZE_FIELD_NUMBER: _ClassVar[int] + tasks: _containers.RepeatedCompositeFieldContainer[Task] + next_page_token: str + total_size: int + def __init__(self, tasks: _Optional[_Iterable[_Union[Task, _Mapping]]] = ..., next_page_token: _Optional[str] = ..., total_size: _Optional[int] = ...) -> None: ... + class CancelTaskRequest(_message.Message): __slots__ = ("name",) NAME_FIELD_NUMBER: _ClassVar[int] diff --git a/src/a2a/grpc/a2a_pb2_grpc.py b/src/a2a/grpc/a2a_pb2_grpc.py index 9b0ad41bc..4a6d90915 100644 --- a/src/a2a/grpc/a2a_pb2_grpc.py +++ b/src/a2a/grpc/a2a_pb2_grpc.py @@ -40,6 +40,11 @@ def __init__(self, channel): request_serializer=a2a__pb2.GetTaskRequest.SerializeToString, response_deserializer=a2a__pb2.Task.FromString, _registered_method=True) + self.ListTasks = channel.unary_unary( + '/a2a.v1.A2AService/ListTasks', + request_serializer=a2a__pb2.ListTasksRequest.SerializeToString, + response_deserializer=a2a__pb2.ListTasksResponse.FromString, + _registered_method=True) self.CancelTask = channel.unary_unary( '/a2a.v1.A2AService/CancelTask', request_serializer=a2a__pb2.CancelTaskRequest.SerializeToString, @@ -113,6 +118,13 @@ def GetTask(self, request, context): context.set_details('Method not implemented!') raise NotImplementedError('Method not implemented!') + def ListTasks(self, request, context): + """List tasks with optional filtering and pagination. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + def CancelTask(self, request, context): """Cancel a task from the agent. If supported one should expect no more task updates for the task. @@ -184,6 +196,11 @@ def add_A2AServiceServicer_to_server(servicer, server): request_deserializer=a2a__pb2.GetTaskRequest.FromString, response_serializer=a2a__pb2.Task.SerializeToString, ), + 'ListTasks': grpc.unary_unary_rpc_method_handler( + servicer.ListTasks, + request_deserializer=a2a__pb2.ListTasksRequest.FromString, + response_serializer=a2a__pb2.ListTasksResponse.SerializeToString, + ), 'CancelTask': grpc.unary_unary_rpc_method_handler( servicer.CancelTask, request_deserializer=a2a__pb2.CancelTaskRequest.FromString, @@ -321,6 +338,33 @@ def GetTask(request, metadata, _registered_method=True) + @staticmethod + def ListTasks(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/a2a.v1.A2AService/ListTasks', + a2a__pb2.ListTasksRequest.SerializeToString, + a2a__pb2.ListTasksResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + @staticmethod def CancelTask(request, target, diff --git a/src/a2a/types.py b/src/a2a/types.py index 918a06b5e..67b940d93 100644 --- a/src/a2a/types.py +++ b/src/a2a/types.py @@ -1271,6 +1271,69 @@ class ListTaskPushNotificationConfigSuccessResponse(A2ABaseModel): """ +class ListTasksParams(A2ABaseModel): + """ + Parameters for listing tasks with optional filtering criteria. + """ + + context_id: str | None = None + """ + Filter tasks by context ID to get tasks from a specific conversation or session. + """ + history_length: int | None = None + """ + Number of recent messages to include in each task's history. Must be non-negative. Defaults to 0 if not specified. + """ + include_artifacts: bool | None = None + """ + Whether to include artifacts in the returned tasks. Defaults to false to reduce payload size. + """ + last_updated_after: int | None = None + """ + Filter tasks updated after this timestamp (milliseconds since epoch). Only tasks with a last updated time greater than or equal to this value will be returned. + """ + metadata: dict[str, Any] | None = None + """ + Request-specific metadata. + """ + page_size: int | None = None + """ + Maximum number of tasks to return. Must be between 1 and 100. Defaults to 50 if not specified. + """ + page_token: str | None = None + """ + Token for pagination. Use the nextPageToken from a previous ListTasksResult response. + """ + status: TaskState | None = None + """ + Filter tasks by their current status state. + """ + + +class ListTasksRequest(A2ABaseModel): + """ + JSON-RPC request model for the 'tasks/list' method. + """ + + id: str | int + """ + A unique identifier established by the client. It must be a String, a Number, or null. + The server must reply with the same value in the response. This property is omitted for notifications. + """ + jsonrpc: Literal['2.0'] = '2.0' + """ + The version of the JSON-RPC protocol. MUST be exactly "2.0". + """ + method: Literal['tasks/list'] = 'tasks/list' + """ + A String containing the name of the method to be invoked. + """ + params: ListTasksParams | None = None + """ + A Structured value that holds the parameter values to be used during the invocation of the method. + """ + + class MessageSendConfiguration(A2ABaseModel): """ Defines configuration options for a `message/send` or `message/stream` request. @@ -1694,6 +1757,7 @@ class A2ARequest( SendMessageRequest | SendStreamingMessageRequest | GetTaskRequest + | ListTasksRequest | CancelTaskRequest | SetTaskPushNotificationConfigRequest | GetTaskPushNotificationConfigRequest @@ -1707,6 +1771,7 @@ class A2ARequest( SendMessageRequest | SendStreamingMessageRequest | GetTaskRequest + | ListTasksRequest | CancelTaskRequest | SetTaskPushNotificationConfigRequest | GetTaskPushNotificationConfigRequest @@ -1936,6 +2001,48 @@ class GetTaskSuccessResponse(A2ABaseModel): """ +class ListTasksResult(A2ABaseModel): + """ + Result object for tasks/list method containing an array of tasks and pagination information. + """ + + next_page_token: str + """ + Token for retrieving the next page. Empty string if no more results. + """ + page_size: int + """ + Maximum number of tasks returned in this response. + """ + tasks: list[Task] + """ + Array of tasks matching the specified criteria. + """ + total_size: int + """ + Total number of tasks available (before pagination). + """ + + +class ListTasksSuccessResponse(A2ABaseModel): + """ + JSON-RPC success response model for the 'tasks/list' method. + """ + + id: str | int | None = None + """ + The identifier established by the client. + """ + jsonrpc: Literal['2.0'] = '2.0' + """ + The version of the JSON-RPC protocol. MUST be exactly "2.0". + """ + result: ListTasksResult + """ + The result object on success. + """ + + class SendMessageSuccessResponse(A2ABaseModel): """ Represents a successful JSON-RPC response for the `message/send` method. @@ -1998,6 +2105,7 @@ class JSONRPCResponse( | SendStreamingMessageSuccessResponse | GetTaskSuccessResponse | CancelTaskSuccessResponse + | ListTasksSuccessResponse | SetTaskPushNotificationConfigSuccessResponse | GetTaskPushNotificationConfigSuccessResponse | ListTaskPushNotificationConfigSuccessResponse @@ -2011,6 +2119,7 @@ class JSONRPCResponse( | SendStreamingMessageSuccessResponse | GetTaskSuccessResponse | CancelTaskSuccessResponse + | ListTasksSuccessResponse | SetTaskPushNotificationConfigSuccessResponse | GetTaskPushNotificationConfigSuccessResponse | ListTaskPushNotificationConfigSuccessResponse @@ -2023,6 +2132,15 @@ class JSONRPCResponse( """ +class ListTasksResponse( + RootModel[JSONRPCErrorResponse | ListTasksSuccessResponse] +): + root: JSONRPCErrorResponse | ListTasksSuccessResponse + """ + JSON-RPC response for the 'tasks/list' method. + """ + + class SendMessageResponse( RootModel[JSONRPCErrorResponse | SendMessageSuccessResponse] ): From e5853a6c97564d51407a1991c1ca05e23472af41 Mon Sep 17 00:00:00 2001 From: lkawka Date: Mon, 20 Oct 2025 08:40:33 +0000 Subject: [PATCH 02/14] Add empty list_tasks methods --- src/a2a/client/base_client.py | 11 +++++++++++ src/a2a/client/client.py | 11 +++++++++++ src/a2a/client/transports/base.py | 11 +++++++++++ src/a2a/client/transports/grpc.py | 12 ++++++++++++ src/a2a/client/transports/jsonrpc.py | 12 ++++++++++++ src/a2a/client/transports/rest.py | 12 ++++++++++++ src/a2a/server/apps/jsonrpc/jsonrpc_app.py | 6 ++++++ .../default_request_handler.py | 11 +++++++++++ .../server/request_handlers/grpc_handler.py | 17 +++++++++++++++++ .../request_handlers/jsonrpc_handler.py | 19 +++++++++++++++++++ .../request_handlers/request_handler.py | 19 +++++++++++++++++++ .../server/request_handlers/rest_handler.py | 1 + 12 files changed, 142 insertions(+) diff --git a/src/a2a/client/base_client.py b/src/a2a/client/base_client.py index f4a8d03de..a28a53e72 100644 --- a/src/a2a/client/base_client.py +++ b/src/a2a/client/base_client.py @@ -14,6 +14,8 @@ from a2a.types import ( AgentCard, GetTaskPushNotificationConfigParams, + ListTasksParams, + ListTasksResult, Message, MessageSendConfiguration, MessageSendParams, @@ -133,6 +135,15 @@ async def get_task( """ return await self._transport.get_task(request, context=context) + async def list_tasks( + self, + request: ListTasksParams, + *, + context: ClientCallContext | None = None, + ) -> ListTasksResult: + """Retrieves tasks for an agent.""" + return await self._transport.list_tasks(request, context=context) + async def cancel_task( self, request: TaskIdParams, diff --git a/src/a2a/client/client.py b/src/a2a/client/client.py index 7cc10423d..529c1c134 100644 --- a/src/a2a/client/client.py +++ b/src/a2a/client/client.py @@ -12,6 +12,8 @@ from a2a.types import ( AgentCard, GetTaskPushNotificationConfigParams, + ListTasksParams, + ListTasksResult, Message, PushNotificationConfig, Task, @@ -131,6 +133,15 @@ async def get_task( ) -> Task: """Retrieves the current state and history of a specific task.""" + @abstractmethod + async def list_tasks( + self, + request: ListTasksParams, + *, + context: ClientCallContext | None = None, + ) -> ListTasksResult: + """Retrieves tasks for an agent.""" + @abstractmethod async def cancel_task( self, diff --git a/src/a2a/client/transports/base.py b/src/a2a/client/transports/base.py index 3573cb7ca..29e6928ff 100644 --- a/src/a2a/client/transports/base.py +++ b/src/a2a/client/transports/base.py @@ -5,6 +5,8 @@ from a2a.types import ( AgentCard, GetTaskPushNotificationConfigParams, + ListTasksParams, + ListTasksResult, Message, MessageSendParams, Task, @@ -50,6 +52,15 @@ async def get_task( ) -> Task: """Retrieves the current state and history of a specific task.""" + @abstractmethod + async def list_tasks( + self, + request: ListTasksParams, + *, + context: ClientCallContext | None = None, + ) -> ListTasksResult: + """Retrieves tasks for an agent.""" + @abstractmethod async def cancel_task( self, diff --git a/src/a2a/client/transports/grpc.py b/src/a2a/client/transports/grpc.py index e50b0ea81..368ccb30a 100644 --- a/src/a2a/client/transports/grpc.py +++ b/src/a2a/client/transports/grpc.py @@ -20,6 +20,8 @@ from a2a.types import ( AgentCard, GetTaskPushNotificationConfigParams, + ListTasksParams, + ListTasksResult, Message, MessageSendParams, Task, @@ -145,6 +147,16 @@ async def get_task( ) return proto_utils.FromProto.task(task) + async def list_tasks( + self, + request: ListTasksParams, + *, + context: ClientCallContext | None = None, + ) -> ListTasksResult: + """Retrieves tasks for an agent.""" + # TODO: #515 - Implement method + raise NotImplementedError('tasks/list not implemented') + async def cancel_task( self, request: TaskIdParams, diff --git a/src/a2a/client/transports/jsonrpc.py b/src/a2a/client/transports/jsonrpc.py index bfba09d71..3166180c6 100644 --- a/src/a2a/client/transports/jsonrpc.py +++ b/src/a2a/client/transports/jsonrpc.py @@ -30,6 +30,8 @@ GetTaskRequest, GetTaskResponse, JSONRPCErrorResponse, + ListTasksParams, + ListTasksResult, Message, MessageSendParams, SendMessageRequest, @@ -222,6 +224,16 @@ async def get_task( raise A2AClientJSONRPCError(response.root) return response.root.result + async def list_tasks( + self, + request: ListTasksParams, + *, + context: ClientCallContext | None = None, + ) -> ListTasksResult: + """Retrieves tasks for an agent.""" + # TODO: #515 - Implement method + raise NotImplementedError('tasks/list not implemented') + async def cancel_task( self, request: TaskIdParams, diff --git a/src/a2a/client/transports/rest.py b/src/a2a/client/transports/rest.py index eef7b0f2e..1f8834b28 100644 --- a/src/a2a/client/transports/rest.py +++ b/src/a2a/client/transports/rest.py @@ -17,6 +17,8 @@ from a2a.types import ( AgentCard, GetTaskPushNotificationConfigParams, + ListTasksParams, + ListTasksResult, Message, MessageSendParams, Task, @@ -222,6 +224,16 @@ async def get_task( ParseDict(response_data, task) return proto_utils.FromProto.task(task) + async def list_tasks( + self, + request: ListTasksParams, + *, + context: ClientCallContext | None = None, + ) -> ListTasksResult: + """Retrieves tasks for an agent.""" + # TODO: #515 - Implement method + raise NotImplementedError('tasks/list not implemented') + async def cancel_task( self, request: TaskIdParams, diff --git a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py index ea73ff592..bbbb49ec6 100644 --- a/src/a2a/server/apps/jsonrpc/jsonrpc_app.py +++ b/src/a2a/server/apps/jsonrpc/jsonrpc_app.py @@ -36,6 +36,7 @@ JSONRPCRequest, JSONRPCResponse, ListTaskPushNotificationConfigRequest, + ListTasksRequest, MethodNotFoundError, SendMessageRequest, SendStreamingMessageRequest, @@ -160,6 +161,7 @@ class JSONRPCApplication(ABC): SendMessageRequest | SendStreamingMessageRequest | GetTaskRequest + | ListTasksRequest | CancelTaskRequest | SetTaskPushNotificationConfigRequest | GetTaskPushNotificationConfigRequest @@ -442,6 +444,10 @@ async def _process_non_streaming_request( handler_result = await self.handler.on_get_task( request_obj, context ) + case ListTasksRequest(): + handler_result = await self.handler.list_tasks( + request_obj, context + ) case SetTaskPushNotificationConfigRequest(): handler_result = ( await self.handler.set_push_notification_config( diff --git a/src/a2a/server/request_handlers/default_request_handler.py b/src/a2a/server/request_handlers/default_request_handler.py index 30d1ee891..6ea9b463e 100644 --- a/src/a2a/server/request_handlers/default_request_handler.py +++ b/src/a2a/server/request_handlers/default_request_handler.py @@ -32,6 +32,8 @@ InternalError, InvalidParamsError, ListTaskPushNotificationConfigParams, + ListTasksParams, + ListTasksResult, Message, MessageSendParams, Task, @@ -121,6 +123,15 @@ async def on_get_task( # Apply historyLength parameter if specified return apply_history_length(task, params.history_length) + async def on_list_tasks( + self, + params: ListTasksParams, + context: ServerCallContext | None = None, + ) -> ListTasksResult: + """Default handler for 'tasks/list'.""" + # TODO: #515 - Implement method + raise NotImplementedError('tasks/list not implemented') + async def on_cancel_task( self, params: TaskIdParams, context: ServerCallContext | None = None ) -> Task | None: diff --git a/src/a2a/server/request_handlers/grpc_handler.py b/src/a2a/server/request_handlers/grpc_handler.py index e2ec69a15..ff6fecca9 100644 --- a/src/a2a/server/request_handlers/grpc_handler.py +++ b/src/a2a/server/request_handlers/grpc_handler.py @@ -331,6 +331,23 @@ async def GetTask( await self.abort_context(e, context) return a2a_pb2.Task() + async def ListTasks( + self, + request: a2a_pb2.ListTasksRequest, + context: grpc.aio.ServicerContext, + ) -> a2a_pb2.ListTasksResponse: + """Handles the 'ListTasks' gRPC method. + + Args: + request: The incoming `ListTasksRequest` object. + context: Context provided by the server. + + Returns: + A `ListTasksResponse` object. + """ + # TODO: #515 - Implement method + raise NotImplementedError('tasks/list not implemented') + async def GetAgentCard( self, request: a2a_pb2.GetAgentCardRequest, diff --git a/src/a2a/server/request_handlers/jsonrpc_handler.py b/src/a2a/server/request_handlers/jsonrpc_handler.py index 567c61484..61a46201b 100644 --- a/src/a2a/server/request_handlers/jsonrpc_handler.py +++ b/src/a2a/server/request_handlers/jsonrpc_handler.py @@ -28,6 +28,8 @@ ListTaskPushNotificationConfigRequest, ListTaskPushNotificationConfigResponse, ListTaskPushNotificationConfigSuccessResponse, + ListTasksRequest, + ListTasksResponse, Message, SendMessageRequest, SendMessageResponse, @@ -359,6 +361,23 @@ async def on_get_task( root=JSONRPCErrorResponse(id=request.id, error=TaskNotFoundError()) ) + async def list_tasks( + self, + request: ListTasksRequest, + context: ServerCallContext | None = None, + ) -> ListTasksResponse: + """Handles the 'tasks/list' JSON-RPC method. + + Args: + request: The incoming `ListTasksRequest` object. + context: Context provided by the server. + + Returns: + A `ListTasksResponse` object containing the Task or a JSON-RPC error. + """ + # TODO: #515 - Implement method + raise NotImplementedError('tasks/list not implemented') + async def list_push_notification_config( self, request: ListTaskPushNotificationConfigRequest, diff --git a/src/a2a/server/request_handlers/request_handler.py b/src/a2a/server/request_handlers/request_handler.py index 7ce76cc90..dc2d308a5 100644 --- a/src/a2a/server/request_handlers/request_handler.py +++ b/src/a2a/server/request_handlers/request_handler.py @@ -7,6 +7,8 @@ DeleteTaskPushNotificationConfigParams, GetTaskPushNotificationConfigParams, ListTaskPushNotificationConfigParams, + ListTasksParams, + ListTasksResult, Message, MessageSendParams, Task, @@ -43,6 +45,23 @@ async def on_get_task( The `Task` object if found, otherwise `None`. """ + @abstractmethod + async def on_list_tasks( + self, params: ListTasksParams, context: ServerCallContext | None = None + ) -> ListTasksResult: + """Handles the tasks/list method. + + Retrieves all task for an agent. Supports filtering, pagination, + ordering, limiting the history length, excluding artifacts, etc. + + Args: + params: Parameters with filtering criteria. + context: Context provided by the server. + + Returns: + The `ListTasksResult` containing the tasks. + """ + @abstractmethod async def on_cancel_task( self, diff --git a/src/a2a/server/request_handlers/rest_handler.py b/src/a2a/server/request_handlers/rest_handler.py index 59057487c..cd7ffb412 100644 --- a/src/a2a/server/request_handlers/rest_handler.py +++ b/src/a2a/server/request_handlers/rest_handler.py @@ -304,4 +304,5 @@ async def list_tasks( Raises: NotImplementedError: This method is not yet implemented. """ + # TODO: #515 - Implement method raise NotImplementedError('list tasks not implemented') From 643a2e0763f565e1659d0a4a989ff26092f48083 Mon Sep 17 00:00:00 2001 From: lkawka Date: Tue, 21 Oct 2025 08:41:32 +0000 Subject: [PATCH 03/14] Add client support for task/list method --- src/a2a/client/transports/grpc.py | 9 ++- src/a2a/client/transports/jsonrpc.py | 16 ++++- src/a2a/client/transports/rest.py | 36 ++++++++++- src/a2a/utils/constants.py | 2 + src/a2a/utils/proto_utils.py | 41 +++++++++++++ tests/client/test_grpc_client.py | 38 ++++++++++++ tests/client/test_jsonrpc_client.py | 38 ++++++++++++ tests/utils/test_proto_utils.py | 91 ++++++++++++++++++++++++++++ 8 files changed, 265 insertions(+), 6 deletions(-) diff --git a/src/a2a/client/transports/grpc.py b/src/a2a/client/transports/grpc.py index 368ccb30a..aff33c1e8 100644 --- a/src/a2a/client/transports/grpc.py +++ b/src/a2a/client/transports/grpc.py @@ -2,6 +2,8 @@ from collections.abc import AsyncGenerator +from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE + try: import grpc @@ -154,8 +156,11 @@ async def list_tasks( context: ClientCallContext | None = None, ) -> ListTasksResult: """Retrieves tasks for an agent.""" - # TODO: #515 - Implement method - raise NotImplementedError('tasks/list not implemented') + response = await self.stub.ListTasks( + proto_utils.ToProto.list_tasks_request(request) + ) + page_size = request.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE + return proto_utils.FromProto.list_tasks_result(response, page_size) async def cancel_task( self, diff --git a/src/a2a/client/transports/jsonrpc.py b/src/a2a/client/transports/jsonrpc.py index 3166180c6..7e1e6fab7 100644 --- a/src/a2a/client/transports/jsonrpc.py +++ b/src/a2a/client/transports/jsonrpc.py @@ -31,6 +31,8 @@ GetTaskResponse, JSONRPCErrorResponse, ListTasksParams, + ListTasksRequest, + ListTasksResponse, ListTasksResult, Message, MessageSendParams, @@ -231,8 +233,18 @@ async def list_tasks( context: ClientCallContext | None = None, ) -> ListTasksResult: """Retrieves tasks for an agent.""" - # TODO: #515 - Implement method - raise NotImplementedError('tasks/list not implemented') + rpc_request = ListTasksRequest(params=request, id=str(uuid4())) + payload, modified_kwargs = await self._apply_interceptors( + 'tasks/list', + rpc_request.model_dump(mode='json', exclude_none=True), + self._get_http_args(context), + context, + ) + response_data = await self._send_request(payload, modified_kwargs) + response = ListTasksResponse.model_validate(response_data) + if isinstance(response.root, JSONRPCErrorResponse): + raise A2AClientJSONRPCError(response.root) + return response.root.result async def cancel_task( self, diff --git a/src/a2a/client/transports/rest.py b/src/a2a/client/transports/rest.py index 1f8834b28..94dcd2798 100644 --- a/src/a2a/client/transports/rest.py +++ b/src/a2a/client/transports/rest.py @@ -8,6 +8,7 @@ from google.protobuf.json_format import MessageToDict, Parse, ParseDict from httpx_sse import SSEError, aconnect_sse +from pydantic import BaseModel from a2a.client.card_resolver import A2ACardResolver from a2a.client.errors import A2AClientHTTPError, A2AClientJSONError @@ -29,6 +30,7 @@ TaskStatusUpdateEvent, ) from a2a.utils import proto_utils +from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE from a2a.utils.telemetry import SpanKind, trace_class @@ -231,8 +233,20 @@ async def list_tasks( context: ClientCallContext | None = None, ) -> ListTasksResult: """Retrieves tasks for an agent.""" - # TODO: #515 - Implement method - raise NotImplementedError('tasks/list not implemented') + _, modified_kwargs = await self._apply_interceptors( + request.model_dump(mode='json', exclude_none=True), + self._get_http_args(context), + context, + ) + response_data = await self._send_get_request( + '/v1/tasks', + _model_to_query_params(request), + modified_kwargs, + ) + response = a2a_pb2.ListTasksResponse() + ParseDict(response_data, response) + page_size = request.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE + return proto_utils.FromProto.list_tasks_result(response, page_size) async def cancel_task( self, @@ -375,3 +389,21 @@ async def get_card( async def close(self) -> None: """Closes the httpx client.""" await self.httpx_client.aclose() + + +def _model_to_query_params(instance: BaseModel) -> dict[str, str]: + data = instance.model_dump(mode='json', exclude_none=True) + return _json_to_query_params(data) + + +def _json_to_query_params(data: dict[str, Any]) -> dict[str, str]: + query_dict = {} + for key, value in data.items(): + if isinstance(value, list): + query_dict[key] = ','.join(map(str, value)) + elif isinstance(value, bool): + query_dict[key] = str(value).lower() + else: + query_dict[key] = str(value) + + return query_dict diff --git a/src/a2a/utils/constants.py b/src/a2a/utils/constants.py index 2935251a5..464b07c99 100644 --- a/src/a2a/utils/constants.py +++ b/src/a2a/utils/constants.py @@ -4,3 +4,5 @@ PREV_AGENT_CARD_WELL_KNOWN_PATH = '/.well-known/agent.json' EXTENDED_AGENT_CARD_PATH = '/agent/authenticatedExtendedCard' DEFAULT_RPC_URL = '/' +DEFAULT_LIST_TASKS_PAGE_SIZE = 50 +"""Default page size for the `tasks/list` method.""" diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index e619cd72c..acfc2395a 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -8,6 +8,7 @@ from typing import Any from google.protobuf import json_format, struct_pb2 +from google.protobuf.timestamp_pb2 import Timestamp from a2a import types from a2a.grpc import a2a_pb2 @@ -566,6 +567,24 @@ def role(cls, role: types.Role) -> a2a_pb2.Role: case _: return a2a_pb2.Role.ROLE_UNSPECIFIED + @classmethod + def list_tasks_request( + cls, params: types.ListTasksParams + ) -> a2a_pb2.ListTasksRequest: + last_updated_time = None + if params.last_updated_after is not None: + last_updated_time = Timestamp() + last_updated_time.FromMilliseconds(params.last_updated_after) + return a2a_pb2.ListTasksRequest( + context_id=params.context_id, + status=cls.task_state(params.status) if params.status else None, + page_size=params.page_size, + page_token=params.page_token, + history_length=params.history_length, + last_updated_time=last_updated_time, + include_artifacts=params.include_artifacts, + ) + class FromProto: """Converts proto types to Python types.""" @@ -796,6 +815,28 @@ def task_id_params( ) return types.TaskIdParams(id=m.group(1)) + @classmethod + def list_tasks_result( + cls, + response: a2a_pb2.ListTasksResponse, + page_size: int, + ) -> types.ListTasksResult: + """Converts a ListTasksResponse to a ListTasksResult. + + Args: + response: The ListTasksResponse to convert. + page_size: The maximum number of tasks returned in this response. + + Returns: + A `ListTasksResult` object. + """ + return types.ListTasksResult( + next_page_token=response.next_page_token, + page_size=page_size, + tasks=[cls.task(t) for t in response.tasks], + total_size=response.total_size, + ) + @classmethod def task_push_notification_config_request( cls, diff --git a/tests/client/test_grpc_client.py b/tests/client/test_grpc_client.py index 19f5abc16..31f7aabda 100644 --- a/tests/client/test_grpc_client.py +++ b/tests/client/test_grpc_client.py @@ -25,6 +25,7 @@ TaskStatus, TaskStatusUpdateEvent, TextPart, + ListTasksParams, ) from a2a.utils import get_text_parts, proto_utils from a2a.utils.errors import ServerError @@ -37,6 +38,7 @@ def mock_grpc_stub() -> AsyncMock: stub.SendMessage = AsyncMock() stub.SendStreamingMessage = MagicMock() stub.GetTask = AsyncMock() + stub.ListTasks = AsyncMock() stub.CancelTask = AsyncMock() stub.CreateTaskPushNotificationConfig = AsyncMock() stub.GetTaskPushNotificationConfig = AsyncMock() @@ -91,6 +93,16 @@ def sample_task() -> Task: ) +@pytest.fixture +def sample_task_2() -> Task: + """Provides a sample Task object.""" + return Task( + id='task-2', + context_id='ctx-2', + status=TaskStatus(state=TaskState.failed), + ) + + @pytest.fixture def sample_message() -> Message: """Provides a sample Message object.""" @@ -283,6 +295,32 @@ async def test_get_task( assert response.id == sample_task.id +@pytest.mark.asyncio +async def test_list_tasks( + grpc_transport: GrpcTransport, + mock_grpc_stub: AsyncMock, + sample_task: Task, + sample_task_2: Task, +): + """Test listing tasks.""" + mock_grpc_stub.ListTasks.return_value = a2a_pb2.ListTasksResponse( + tasks=[ + proto_utils.ToProto.task(t) for t in [sample_task, sample_task_2] + ], + total_size=2, + ) + params = ListTasksParams() + + result = await grpc_transport.list_tasks(params) + + mock_grpc_stub.ListTasks.assert_awaited_once_with( + proto_utils.ToProto.list_tasks_request(params) + ) + assert result.total_size == 2 + assert not result.next_page_token + assert [t.id for t in result.tasks] == [sample_task.id, sample_task_2.id] + + @pytest.mark.asyncio async def test_get_task_with_history( grpc_transport: GrpcTransport, mock_grpc_stub: AsyncMock, sample_task: Task diff --git a/tests/client/test_jsonrpc_client.py b/tests/client/test_jsonrpc_client.py index 58feec25d..f4b8a94af 100644 --- a/tests/client/test_jsonrpc_client.py +++ b/tests/client/test_jsonrpc_client.py @@ -31,6 +31,8 @@ TaskIdParams, TaskPushNotificationConfig, TaskQueryParams, + ListTasksParams, + ListTasksResult, ) from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH @@ -560,6 +562,42 @@ async def test_get_task_success( sent_payload = mock_send_request.call_args.args[0] assert sent_payload['method'] == 'tasks/get' + @pytest.mark.asyncio + async def test_list_tasks_success( + self, mock_httpx_client: AsyncMock, mock_agent_card: MagicMock + ): + client = JsonRpcTransport( + httpx_client=mock_httpx_client, agent_card=mock_agent_card + ) + params = ListTasksParams() + mock_rpc_response = { + 'id': '123', + 'jsonrpc': '2.0', + 'result': { + 'nextPageToken': '', + 'tasks': [MINIMAL_TASK], + 'pageSize': 10, + 'totalSize': 1, + }, + } + + with patch.object( + client, '_send_request', new_callable=AsyncMock + ) as mock_send_request: + mock_send_request.return_value = mock_rpc_response + response = await client.list_tasks(request=params) + + assert isinstance(response, ListTasksResult) + assert ( + response.model_dump() + == ListTasksResult( + next_page_token='', + page_size=10, + tasks=[Task.model_validate(MINIMAL_TASK)], + total_size=1, + ).model_dump() + ) + @pytest.mark.asyncio async def test_cancel_task_success( self, mock_httpx_client: AsyncMock, mock_agent_card: MagicMock diff --git a/tests/utils/test_proto_utils.py b/tests/utils/test_proto_utils.py index da54f833f..cccf1d498 100644 --- a/tests/utils/test_proto_utils.py +++ b/tests/utils/test_proto_utils.py @@ -6,6 +6,7 @@ from a2a.grpc import a2a_pb2 from a2a.utils import proto_utils from a2a.utils.errors import ServerError +from google.protobuf.timestamp_pb2 import Timestamp # --- Test Data --- @@ -55,6 +56,43 @@ def sample_task(sample_message: types.Message) -> types.Task: ) +@pytest.fixture +def sample_proto_task() -> a2a_pb2.Task: + sample_message = a2a_pb2.Message( + message_id='msg-1', + context_id='ctx-1', + task_id='task-1', + role=a2a_pb2.ROLE_USER, + content=[ + a2a_pb2.Part(text='Hello'), + a2a_pb2.Part( + file=a2a_pb2.FilePart( + file_with_uri='file:///test.txt', + mime_type='text/plain', + name='test.txt', + ) + ), + a2a_pb2.Part(data=a2a_pb2.DataPart(data={'key': 'value'})), + ], + metadata={'source': 'test'}, + ) + return a2a_pb2.Task( + id='task-1', + context_id='ctx-1', + status=a2a_pb2.TaskStatus( + state=a2a_pb2.TASK_STATE_WORKING, + update=sample_message, + ), + artifacts=[ + a2a_pb2.Artifact( + artifact_id='art-1', + parts=[a2a_pb2.Part(text='Artifact content')], + ) + ], + history=[sample_message], + ) + + @pytest.fixture def sample_agent_card() -> types.AgentCard: return types.AgentCard( @@ -127,6 +165,45 @@ class FakePartType: with pytest.raises(ValueError, match='Unsupported part type'): proto_utils.ToProto.part(mock_part) + @pytest.mark.parametrize( + 'params,expected', + [ + pytest.param( + types.ListTasksParams(), + a2a_pb2.ListTasksRequest(), + id='empty', + ), + pytest.param( + types.ListTasksParams( + context_id='ctx-1', + history_length=256, + include_artifacts=True, + last_updated_after=1761042977029, + metadata={'meta': 'data'}, + page_size=16, + page_token='1', + status=types.TaskState.working, + ), + a2a_pb2.ListTasksRequest( + context_id='ctx-1', + history_length=256, + include_artifacts=True, + last_updated_time=Timestamp( + seconds=1761042977, nanos=29000000 + ), + page_size=16, + page_token='1', + status=a2a_pb2.TaskState.TASK_STATE_WORKING, + ), + id='full', + ), + ], + ) + def test_list_tasks_request(self, params, expected): + request = proto_utils.ToProto.list_tasks_request(params) + + assert request == expected + class TestFromProto: def test_part_unsupported_type(self): @@ -143,6 +220,20 @@ def test_task_query_params_invalid_name(self): proto_utils.FromProto.task_query_params(request) assert isinstance(exc_info.value.error, types.InvalidParamsError) + def test_list_tasks_result(self, sample_proto_task): + response = a2a_pb2.ListTasksResponse( + next_page_token='1', + tasks=[sample_proto_task], + total_size=1, + ) + + result = proto_utils.FromProto.list_tasks_result(response, 10) + + assert result.next_page_token == '1' + assert result.page_size == 10 + assert len(result.tasks) == 1 + assert result.total_size == 1 + class TestProtoUtils: def test_roundtrip_message(self, sample_message: types.Message): From c1d3392279a375cfe8ef3c6f92b2ea62947c99de Mon Sep 17 00:00:00 2001 From: lkawka Date: Tue, 4 Nov 2025 18:36:49 +0000 Subject: [PATCH 04/14] Add server support --- .../default_request_handler.py | 16 ++- .../server/request_handlers/grpc_handler.py | 11 +- .../request_handlers/jsonrpc_handler.py | 22 +++- .../request_handlers/response_helpers.py | 6 + .../server/request_handlers/rest_handler.py | 20 +-- src/a2a/server/tasks/database_task_store.py | 55 +++++++- src/a2a/server/tasks/inmemory_task_store.py | 58 ++++++++- src/a2a/server/tasks/task_store.py | 20 ++- src/a2a/utils/proto_utils.py | 26 ++++ .../test_default_request_handler.py | 30 +++++ .../request_handlers/test_grpc_handler.py | 36 ++++++ .../request_handlers/test_jsonrpc_handler.py | 35 +++++ .../server/tasks/test_database_task_store.py | 118 +++++++++++++++++ .../server/tasks/test_inmemory_task_store.py | 120 +++++++++++++++++- 14 files changed, 551 insertions(+), 22 deletions(-) diff --git a/src/a2a/server/request_handlers/default_request_handler.py b/src/a2a/server/request_handlers/default_request_handler.py index 6ea9b463e..68dc88001 100644 --- a/src/a2a/server/request_handlers/default_request_handler.py +++ b/src/a2a/server/request_handlers/default_request_handler.py @@ -45,6 +45,7 @@ TaskState, UnsupportedOperationError, ) +from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE from a2a.utils.errors import ServerError from a2a.utils.task import apply_history_length from a2a.utils.telemetry import SpanKind, trace_class @@ -129,8 +130,13 @@ async def on_list_tasks( context: ServerCallContext | None = None, ) -> ListTasksResult: """Default handler for 'tasks/list'.""" - # TODO: #515 - Implement method - raise NotImplementedError('tasks/list not implemented') + page = await self.task_store.list(params, context) + return ListTasksResult( + next_page_token=page.next_page_token, + page_size=params.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE, + tasks=page.tasks, + total_size=page.total_size, + ) async def on_cancel_task( self, params: TaskIdParams, context: ServerCallContext | None = None @@ -590,3 +596,9 @@ async def on_delete_task_push_notification_config( await self._push_config_store.delete_info( params.id, params.push_notification_config_id ) + + +def _next_page_token(current_page_token: str) -> str: + if not current_page_token: + return '1' + return str(int(current_page_token) + 1) diff --git a/src/a2a/server/request_handlers/grpc_handler.py b/src/a2a/server/request_handlers/grpc_handler.py index ff6fecca9..7dedf675b 100644 --- a/src/a2a/server/request_handlers/grpc_handler.py +++ b/src/a2a/server/request_handlers/grpc_handler.py @@ -345,8 +345,15 @@ async def ListTasks( Returns: A `ListTasksResponse` object. """ - # TODO: #515 - Implement method - raise NotImplementedError('tasks/list not implemented') + try: + server_context = self.context_builder.build(context) + result = await self.request_handler.on_list_tasks( + proto_utils.FromProto.list_tasks_params(request), server_context + ) + return proto_utils.ToProto.list_tasks_response(result) + except ServerError as e: + await self.abort_context(e, context) + return a2a_pb2.ListTasksResponse() async def GetAgentCard( self, diff --git a/src/a2a/server/request_handlers/jsonrpc_handler.py b/src/a2a/server/request_handlers/jsonrpc_handler.py index 61a46201b..3b4687915 100644 --- a/src/a2a/server/request_handlers/jsonrpc_handler.py +++ b/src/a2a/server/request_handlers/jsonrpc_handler.py @@ -28,8 +28,11 @@ ListTaskPushNotificationConfigRequest, ListTaskPushNotificationConfigResponse, ListTaskPushNotificationConfigSuccessResponse, + ListTasksParams, ListTasksRequest, ListTasksResponse, + ListTasksResult, + ListTasksSuccessResponse, Message, SendMessageRequest, SendMessageResponse, @@ -375,8 +378,23 @@ async def list_tasks( Returns: A `ListTasksResponse` object containing the Task or a JSON-RPC error. """ - # TODO: #515 - Implement method - raise NotImplementedError('tasks/list not implemented') + try: + result = await self.request_handler.on_list_tasks( + request.params or ListTasksParams(), context + ) + except ServerError as e: + return ListTasksResponse( + root=JSONRPCErrorResponse( + id=request.id, error=e.error if e.error else InternalError() + ) + ) + return prepare_response_object( + request.id, + result, + (ListTasksResult,), + ListTasksSuccessResponse, + ListTasksResponse, + ) async def list_push_notification_config( self, diff --git a/src/a2a/server/request_handlers/response_helpers.py b/src/a2a/server/request_handlers/response_helpers.py index 4c55c4197..0e39b17f3 100644 --- a/src/a2a/server/request_handlers/response_helpers.py +++ b/src/a2a/server/request_handlers/response_helpers.py @@ -18,6 +18,9 @@ JSONRPCErrorResponse, ListTaskPushNotificationConfigResponse, ListTaskPushNotificationConfigSuccessResponse, + ListTasksResponse, + ListTasksResult, + ListTasksSuccessResponse, Message, SendMessageResponse, SendMessageSuccessResponse, @@ -42,6 +45,7 @@ SendStreamingMessageResponse, ListTaskPushNotificationConfigResponse, DeleteTaskPushNotificationConfigResponse, + ListTasksResponse, ) """Type variable for RootModel response types.""" @@ -56,6 +60,7 @@ SendStreamingMessageSuccessResponse, ListTaskPushNotificationConfigSuccessResponse, DeleteTaskPushNotificationConfigSuccessResponse, + ListTasksSuccessResponse, ) """Type variable for SuccessResponse types.""" @@ -69,6 +74,7 @@ | A2AError | JSONRPCError | list[TaskPushNotificationConfig] + | ListTasksResult ) """Type alias for possible event types produced by handlers.""" diff --git a/src/a2a/server/request_handlers/rest_handler.py b/src/a2a/server/request_handlers/rest_handler.py index cd7ffb412..1cbaf7689 100644 --- a/src/a2a/server/request_handlers/rest_handler.py +++ b/src/a2a/server/request_handlers/rest_handler.py @@ -21,6 +21,7 @@ from a2a.types import ( AgentCard, GetTaskPushNotificationConfigParams, + ListTasksParams, TaskIdParams, TaskNotFoundError, TaskQueryParams, @@ -264,12 +265,12 @@ async def on_get_task( return MessageToDict(proto_utils.ToProto.task(task)) raise ServerError(error=TaskNotFoundError()) - async def list_push_notifications( + async def list_tasks( self, request: Request, context: ServerCallContext, ) -> dict[str, Any]: - """Handles the 'tasks/pushNotificationConfig/list' REST method. + """Handles the 'tasks/list' REST method. This method is currently not implemented. @@ -278,19 +279,21 @@ async def list_push_notifications( context: Context provided by the server. Returns: - A list of `dict` representing the `TaskPushNotificationConfig` objects. + A list of dict representing the`Task` objects. Raises: NotImplementedError: This method is not yet implemented. """ - raise NotImplementedError('list notifications not implemented') + params = ListTasksParams.model_validate(request.query_params) + result = await self.request_handler.on_list_tasks(params, context) + return MessageToDict(proto_utils.ToProto.list_tasks_response(result)) - async def list_tasks( + async def list_push_notifications( self, request: Request, context: ServerCallContext, ) -> dict[str, Any]: - """Handles the 'tasks/list' REST method. + """Handles the 'tasks/pushNotificationConfig/list' REST method. This method is currently not implemented. @@ -299,10 +302,9 @@ async def list_tasks( context: Context provided by the server. Returns: - A list of dict representing the`Task` objects. + A list of `dict` representing the `TaskPushNotificationConfig` objects. Raises: NotImplementedError: This method is not yet implemented. """ - # TODO: #515 - Implement method - raise NotImplementedError('list tasks not implemented') + raise NotImplementedError('list notifications not implemented') diff --git a/src/a2a/server/tasks/database_task_store.py b/src/a2a/server/tasks/database_task_store.py index 07ba7e970..dc762ed1c 100644 --- a/src/a2a/server/tasks/database_task_store.py +++ b/src/a2a/server/tasks/database_task_store.py @@ -2,7 +2,7 @@ try: - from sqlalchemy import Table, delete, select + from sqlalchemy import Table, delete, func, select from sqlalchemy.ext.asyncio import ( AsyncEngine, AsyncSession, @@ -21,8 +21,9 @@ from a2a.server.context import ServerCallContext from a2a.server.models import Base, TaskModel, create_task_model -from a2a.server.tasks.task_store import TaskStore -from a2a.types import Task # Task is the Pydantic model +from a2a.server.tasks.task_store import TaskStore, TasksPage +from a2a.types import ListTasksParams, Task +from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE logger = logging.getLogger(__name__) @@ -147,6 +148,54 @@ async def get( logger.debug('Task %s not found in store.', task_id) return None + async def list( + self, params: ListTasksParams, context: ServerCallContext | None = None + ) -> TasksPage: + """Retrieves all tasks from the database.""" + await self._ensure_initialized() + async with self.async_session_maker() as session: + page_number = int(params.page_token) if params.page_token else 0 + page_size = params.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE + offset = page_number * page_size + + # Base query for filtering + base_stmt = select(self.task_model) + if params.context_id: + base_stmt = base_stmt.where( + self.task_model.context_id == params.context_id + ) + if params.status is not None: + base_stmt = base_stmt.where( + self.task_model.status['state'].as_string() + == params.status.value + ) + + # Get total count + count_stmt = select(func.count()).select_from(base_stmt.alias()) + total_count = (await session.execute(count_stmt)).scalar_one() + + # Get paginated results + stmt = ( + base_stmt.order_by(self.task_model.id.desc()) + .limit(page_size) + .offset(offset) + ) + result = await session.execute(stmt) + tasks_models = result.scalars().all() + tasks = [self._from_orm(task_model) for task_model in tasks_models] + + next_page_token = ( + str(page_number + 1) + if total_count > (page_number + 1) * page_size + else '' + ) + + return TasksPage( + tasks=tasks, + total_size=total_count, + next_page_token=next_page_token, + ) + async def delete( self, task_id: str, context: ServerCallContext | None = None ) -> None: diff --git a/src/a2a/server/tasks/inmemory_task_store.py b/src/a2a/server/tasks/inmemory_task_store.py index 4e192af08..1e562e57a 100644 --- a/src/a2a/server/tasks/inmemory_task_store.py +++ b/src/a2a/server/tasks/inmemory_task_store.py @@ -2,8 +2,9 @@ import logging from a2a.server.context import ServerCallContext -from a2a.server.tasks.task_store import TaskStore -from a2a.types import Task +from a2a.server.tasks.task_store import TaskStore, TasksPage +from a2a.types import ListTasksParams, Task +from a2a.utils.constants import DEFAULT_LIST_TASKS_PAGE_SIZE logger = logging.getLogger(__name__) @@ -43,6 +44,59 @@ async def get( logger.debug('Task %s not found in store.', task_id) return task + async def list( + self, + params: ListTasksParams, + context: ServerCallContext | None = None, + ) -> TasksPage: + """Retrieves a list of tasks from the store.""" + async with self.lock: + tasks = list(self.tasks.values()) + + # Apply filtering + if params.context_id: + tasks = [ + task for task in tasks if task.context_id == params.context_id + ] + if params.status is not None: + tasks = [ + task for task in tasks if task.status.state == params.status + ] + + # Reduce payload + base_updates = {} + if not params.include_artifacts: + base_updates = {'artifacts': []} + for i in range(len(tasks)): + updates = dict(base_updates) + history = tasks[i].history + if params.history_length is not None and history: + limited_history = ( + history[-params.history_length :] + if params.history_length > 0 + else [] + ) + updates['history'] = limited_history + tasks[i] = tasks[i].model_copy(update=updates) + + # Apply pagination + total_size = len(tasks) + page_token = int(params.page_token) if params.page_token else 0 + page_size = params.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE + tasks = tasks[page_token * page_size : (page_token + 1) * page_size] + + next_page_token = ( + str(page_token + 1) + if (page_token + 1) * page_size < total_size + else '' + ) + + return TasksPage( + next_page_token=next_page_token, + tasks=tasks, + total_size=total_size, + ) + async def delete( self, task_id: str, context: ServerCallContext | None = None ) -> None: diff --git a/src/a2a/server/tasks/task_store.py b/src/a2a/server/tasks/task_store.py index 16b36edb9..01c79ff27 100644 --- a/src/a2a/server/tasks/task_store.py +++ b/src/a2a/server/tasks/task_store.py @@ -1,7 +1,17 @@ from abc import ABC, abstractmethod +from pydantic import BaseModel + from a2a.server.context import ServerCallContext -from a2a.types import Task +from a2a.types import ListTasksParams, Task + + +class TasksPage(BaseModel): + """Page with tasks.""" + + next_page_token: str = '' + tasks: list[Task] + total_size: int class TaskStore(ABC): @@ -22,6 +32,14 @@ async def get( ) -> Task | None: """Retrieves a task from the store by ID.""" + @abstractmethod + async def list( + self, + params: ListTasksParams, + context: ServerCallContext | None = None, + ) -> TasksPage: + """Retrieves a list of tasks from the store.""" + @abstractmethod async def delete( self, task_id: str, context: ServerCallContext | None = None diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index acfc2395a..9f2d11420 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -585,6 +585,16 @@ def list_tasks_request( include_artifacts=params.include_artifacts, ) + @classmethod + def list_tasks_response( + cls, result: types.ListTasksResult + ) -> a2a_pb2.ListTasksResponse: + return a2a_pb2.ListTasksResponse( + next_page_token=result.next_page_token, + tasks=[cls.task(t) for t in result.tasks], + total_size=result.total_size, + ) + class FromProto: """Converts proto types to Python types.""" @@ -933,6 +943,22 @@ def task_query_params( metadata=None, ) + @classmethod + def list_tasks_params( + cls, request: a2a_pb2.ListTasksRequest + ) -> types.ListTasksParams: + return types.ListTasksParams( + context_id=request.context_id, + history_length=request.history_length, + include_artifacts=request.include_artifacts, + last_updated_after=request.last_updated_time.ToMilliseconds() + if request.last_updated_time + else None, + page_size=request.page_size, + page_token=request.page_token, + status=cls.task_state(request.status) if request.status else None, + ) + @classmethod def capabilities( cls, capabilities: a2a_pb2.AgentCapabilities diff --git a/tests/server/request_handlers/test_default_request_handler.py b/tests/server/request_handlers/test_default_request_handler.py index 5268af115..a1cb05320 100644 --- a/tests/server/request_handlers/test_default_request_handler.py +++ b/tests/server/request_handlers/test_default_request_handler.py @@ -21,6 +21,7 @@ from a2a.server.context import ServerCallContext from a2a.server.events import EventQueue, InMemoryQueueManager, QueueManager from a2a.server.request_handlers import DefaultRequestHandler +from a2a.server.tasks.task_store import TasksPage from a2a.server.tasks import ( InMemoryPushNotificationConfigStore, InMemoryTaskStore, @@ -36,6 +37,8 @@ InternalError, InvalidParamsError, ListTaskPushNotificationConfigParams, + ListTasksParams, + ListTasksResult, Message, MessageSendConfiguration, MessageSendParams, @@ -145,6 +148,33 @@ async def test_on_get_task_not_found(): mock_task_store.get.assert_awaited_once_with('non_existent_task', context) +@pytest.mark.asyncio +async def test_on_list_tasks_success(): + """Test on_list_tasks successfully returns a page of tasks .""" + mock_task_store = AsyncMock(spec=TaskStore) + mock_page = MagicMock(spec=TasksPage) + mock_page.tasks = [ + create_sample_task(task_id='task1'), + create_sample_task(task_id='task2'), + ] + mock_page.next_page_token = '123' + mock_page.total_size = 2 + mock_task_store.list.return_value = mock_page + request_handler = DefaultRequestHandler( + agent_executor=DummyAgentExecutor(), task_store=mock_task_store + ) + params = ListTasksParams(page_size=10) + context = create_server_call_context() + + result = await request_handler.on_list_tasks(params, context) + + mock_task_store.list.assert_awaited_once_with(params, context) + assert result.tasks == mock_page.tasks + assert result.next_page_token == mock_page.next_page_token + assert result.total_size == mock_page.total_size + assert result.page_size == params.page_size + + @pytest.mark.asyncio async def test_on_cancel_task_task_not_found(): """Test on_cancel_task when the task is not found.""" diff --git a/tests/server/request_handlers/test_grpc_handler.py b/tests/server/request_handlers/test_grpc_handler.py index 05af6cdac..2bcd81fe7 100644 --- a/tests/server/request_handlers/test_grpc_handler.py +++ b/tests/server/request_handlers/test_grpc_handler.py @@ -229,6 +229,42 @@ def modifier(card: types.AgentCard) -> types.AgentCard: assert response.version == sample_agent_card.version +@pytest.mark.asyncio +async def test_list_tasks_success( + grpc_handler: GrpcHandler, + mock_request_handler: AsyncMock, + mock_grpc_context: AsyncMock, +): + """Test successful ListTasks call.""" + mock_request_handler.on_list_tasks.return_value = types.ListTasksResult( + next_page_token='123', + page_size=2, + tasks=[ + types.Task( + id='task-1', + context_id='ctx-1', + status=types.TaskStatus(state=types.TaskState.completed), + ), + types.Task( + id='task-2', + context_id='ctx-1', + status=types.TaskStatus(state=types.TaskState.working), + ), + ], + total_size=10, + ) + + response = await grpc_handler.ListTasks( + a2a_pb2.ListTasksRequest(page_size=2), mock_grpc_context + ) + + mock_request_handler.on_list_tasks.assert_awaited_once() + assert isinstance(response, a2a_pb2.ListTasksResponse) + assert len(response.tasks) == 2 + assert response.tasks[0].id == 'task-1' + assert response.tasks[1].id == 'task-2' + + @pytest.mark.asyncio @pytest.mark.parametrize( 'server_error, grpc_status_code, error_message_part', diff --git a/tests/server/request_handlers/test_jsonrpc_handler.py b/tests/server/request_handlers/test_jsonrpc_handler.py index d1ead0211..fa3af203c 100644 --- a/tests/server/request_handlers/test_jsonrpc_handler.py +++ b/tests/server/request_handlers/test_jsonrpc_handler.py @@ -36,6 +36,7 @@ GetAuthenticatedExtendedCardRequest, GetAuthenticatedExtendedCardResponse, GetAuthenticatedExtendedCardSuccessResponse, + ListTasksResult, GetTaskPushNotificationConfigParams, GetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigResponse, @@ -48,6 +49,10 @@ ListTaskPushNotificationConfigParams, ListTaskPushNotificationConfigRequest, ListTaskPushNotificationConfigSuccessResponse, + ListTasksParams, + ListTasksRequest, + ListTasksResponse, + ListTasksSuccessResponse, Message, MessageSendConfiguration, MessageSendParams, @@ -137,6 +142,36 @@ async def test_on_get_task_not_found(self) -> None: self.assertIsInstance(response.root, JSONRPCErrorResponse) assert response.root.error == TaskNotFoundError() # type: ignore + async def test_on_list_tasks_success(self) -> None: + request_handler = AsyncMock(spec=DefaultRequestHandler) + handler = JSONRPCHandler(self.mock_agent_card, request_handler) + mock_result = ListTasksResult( + next_page_token='123', + page_size=2, + tasks=[ + Task(**MINIMAL_TASK), + Task(**MINIMAL_TASK).model_copy(update={'id': 'task_456'}), + ], + total_size=10, + ) + request_handler.on_list_tasks.return_value = mock_result + request = ListTasksRequest( + id='1', + method='tasks/list', + params=ListTasksParams( + page_size=10, + page_token='token', + filter='filter', + ), + ) + call_context = ServerCallContext(state={'foo': 'bar'}) + + response = await handler.list_tasks(request, call_context) + + request_handler.on_list_tasks.assert_awaited_once() + self.assertIsInstance(response.root, ListTasksSuccessResponse) + self.assertEqual(response.root.result, mock_result) + async def test_on_cancel_task_success(self) -> None: mock_agent_executor = AsyncMock(spec=AgentExecutor) mock_task_store = AsyncMock(spec=TaskStore) diff --git a/tests/server/tasks/test_database_task_store.py b/tests/server/tasks/test_database_task_store.py index 87069be46..f0e65e639 100644 --- a/tests/server/tasks/test_database_task_store.py +++ b/tests/server/tasks/test_database_task_store.py @@ -26,6 +26,7 @@ TaskState, TaskStatus, TextPart, + ListTasksParams, ) @@ -171,6 +172,123 @@ async def test_get_task(db_store_parameterized: DatabaseTaskStore) -> None: await db_store_parameterized.delete(task_to_save.id) # Cleanup +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'params, expected_ids, total_count, next_page_token', + [ + # No parameters, should return all tasks + ( + ListTasksParams(), + ['task-4', 'task-3', 'task-2', 'task-1', 'task-0'], + 5, + '', + ), + # Pagination (first page) + ( + ListTasksParams(page_size=2, page_token='0'), + ['task-4', 'task-3'], + 5, + '1', + ), + # Pagination (second page) + ( + ListTasksParams(page_size=2, page_token='1'), + ['task-2', 'task-1'], + 5, + '2', + ), + # Filtering by context_id + (ListTasksParams(context_id='context-1'), ['task-3', 'task-1'], 2, ''), + # Filtering by status + ( + ListTasksParams(status=TaskState.working), + ['task-3', 'task-1'], + 2, + '', + ), + # Combined filtering (context_id and status) + ( + ListTasksParams(context_id='context-0', status=TaskState.submitted), + ['task-2', 'task-0'], + 2, + '', + ), + # Combined filtering and pagination + ( + ListTasksParams( + context_id='context-0', page_size=1, page_token='0' + ), + ['task-4'], + 3, + '1', + ), + ], +) +async def test_list_tasks( + db_store_parameterized: DatabaseTaskStore, + params: ListTasksParams, + expected_ids: list[str], + total_count: int, + next_page_token: str, +) -> None: + """Test listing tasks with various filters and pagination.""" + tasks_to_create = [ + MINIMAL_TASK_OBJ.model_copy( + update={ + 'id': 'task-0', + 'context_id': 'context-0', + 'status': TaskStatus(state=TaskState.submitted), + 'kind': 'task', + } + ), + MINIMAL_TASK_OBJ.model_copy( + update={ + 'id': 'task-1', + 'context_id': 'context-1', + 'status': TaskStatus(state=TaskState.working), + 'kind': 'task', + } + ), + MINIMAL_TASK_OBJ.model_copy( + update={ + 'id': 'task-2', + 'context_id': 'context-0', + 'status': TaskStatus(state=TaskState.submitted), + 'kind': 'task', + } + ), + MINIMAL_TASK_OBJ.model_copy( + update={ + 'id': 'task-3', + 'context_id': 'context-1', + 'status': TaskStatus(state=TaskState.working), + 'kind': 'task', + } + ), + MINIMAL_TASK_OBJ.model_copy( + update={ + 'id': 'task-4', + 'context_id': 'context-0', + 'status': TaskStatus(state=TaskState.completed), + 'kind': 'task', + } + ), + ] + for task in tasks_to_create: + await db_store_parameterized.save(task) + + page = await db_store_parameterized.list(params) + + retrieved_ids = [task.id for task in page.tasks] + assert retrieved_ids == expected_ids + assert page.total_size == total_count + assert page.next_page_token == next_page_token + + # Cleanup + for task in tasks_to_create: + await db_store_parameterized.delete(task.id) + + @pytest.mark.asyncio async def test_get_nonexistent_task( db_store_parameterized: DatabaseTaskStore, diff --git a/tests/server/tasks/test_inmemory_task_store.py b/tests/server/tasks/test_inmemory_task_store.py index c41e3559f..2a4362a67 100644 --- a/tests/server/tasks/test_inmemory_task_store.py +++ b/tests/server/tasks/test_inmemory_task_store.py @@ -3,7 +3,7 @@ import pytest from a2a.server.tasks import InMemoryTaskStore -from a2a.types import Task +from a2a.types import Task, ListTasksParams, TaskState, TaskStatus MINIMAL_TASK: dict[str, Any] = { @@ -32,6 +32,124 @@ async def test_in_memory_task_store_get_nonexistent() -> None: assert retrieved_task is None +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'params, expected_ids, total_count, next_page_token', + [ + # No parameters, should return all tasks + ( + ListTasksParams(), + ['task-0', 'task-1', 'task-2', 'task-3', 'task-4'], + 5, + '', + ), + # Pagination (first page) + ( + ListTasksParams(page_size=2, page_token='0'), + ['task-0', 'task-1'], + 5, + '1', + ), + # Pagination (second page) + ( + ListTasksParams(page_size=2, page_token='1'), + ['task-2', 'task-3'], + 5, + '2', + ), + # Filtering by context_id + (ListTasksParams(context_id='context-1'), ['task-1', 'task-3'], 2, ''), + # Filtering by status + ( + ListTasksParams(status=TaskState.working), + ['task-1', 'task-3'], + 2, + '', + ), + # Combined filtering (context_id and status) + ( + ListTasksParams(context_id='context-0', status=TaskState.submitted), + ['task-0', 'task-2'], + 2, + '', + ), + # Combined filtering and pagination + ( + ListTasksParams( + context_id='context-0', page_size=1, page_token='0' + ), + ['task-0'], + 3, + '1', + ), + ], +) +async def test_list_tasks( + params: ListTasksParams, + expected_ids: list[str], + total_count: int, + next_page_token: str, +) -> None: + """Test listing tasks with various filters and pagination.""" + store = InMemoryTaskStore() + task = Task(**MINIMAL_TASK) + tasks_to_create = [ + task.model_copy( + update={ + 'id': 'task-0', + 'context_id': 'context-0', + 'status': TaskStatus(state=TaskState.submitted), + 'kind': 'task', + } + ), + task.model_copy( + update={ + 'id': 'task-1', + 'context_id': 'context-1', + 'status': TaskStatus(state=TaskState.working), + 'kind': 'task', + } + ), + task.model_copy( + update={ + 'id': 'task-2', + 'context_id': 'context-0', + 'status': TaskStatus(state=TaskState.submitted), + 'kind': 'task', + } + ), + task.model_copy( + update={ + 'id': 'task-3', + 'context_id': 'context-1', + 'status': TaskStatus(state=TaskState.working), + 'kind': 'task', + } + ), + task.model_copy( + update={ + 'id': 'task-4', + 'context_id': 'context-0', + 'status': TaskStatus(state=TaskState.completed), + 'kind': 'task', + } + ), + ] + for task in tasks_to_create: + await store.save(task) + + page = await store.list(params) + + retrieved_ids = [task.id for task in page.tasks] + assert retrieved_ids == expected_ids + assert page.total_size == total_count + assert page.next_page_token == next_page_token + + # Cleanup + for task in tasks_to_create: + await store.delete(task.id) + + @pytest.mark.asyncio async def test_in_memory_task_store_delete() -> None: """Test deleting a task from the store.""" From a57f24a69c660db53dc8f33526a4a2fb14eb34e9 Mon Sep 17 00:00:00 2001 From: lkawka Date: Tue, 4 Nov 2025 18:49:11 +0000 Subject: [PATCH 05/14] linter fix --- src/a2a/server/tasks/inmemory_task_store.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/a2a/server/tasks/inmemory_task_store.py b/src/a2a/server/tasks/inmemory_task_store.py index 1e562e57a..bea7f1f12 100644 --- a/src/a2a/server/tasks/inmemory_task_store.py +++ b/src/a2a/server/tasks/inmemory_task_store.py @@ -1,6 +1,8 @@ import asyncio import logging +from typing import Any + from a2a.server.context import ServerCallContext from a2a.server.tasks.task_store import TaskStore, TasksPage from a2a.types import ListTasksParams, Task @@ -64,7 +66,7 @@ async def list( ] # Reduce payload - base_updates = {} + base_updates: dict[str, Any] = {} if not params.include_artifacts: base_updates = {'artifacts': []} for i in range(len(tasks)): From 32cc97c3f4f1521436c7f958ca960154755167fe Mon Sep 17 00:00:00 2001 From: lkawka Date: Wed, 5 Nov 2025 20:17:09 +0000 Subject: [PATCH 06/14] remove dead code --- src/a2a/server/request_handlers/default_request_handler.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/a2a/server/request_handlers/default_request_handler.py b/src/a2a/server/request_handlers/default_request_handler.py index 68dc88001..28c469481 100644 --- a/src/a2a/server/request_handlers/default_request_handler.py +++ b/src/a2a/server/request_handlers/default_request_handler.py @@ -596,9 +596,3 @@ async def on_delete_task_push_notification_config( await self._push_config_store.delete_info( params.id, params.push_notification_config_id ) - - -def _next_page_token(current_page_token: str) -> str: - if not current_page_token: - return '1' - return str(int(current_page_token) + 1) From eccaa80c55b6e86c70bedf285407ff4be73c8278 Mon Sep 17 00:00:00 2001 From: lkawka Date: Thu, 6 Nov 2025 20:24:55 +0000 Subject: [PATCH 07/14] Move payload reduction to default_request_handler --- .../default_request_handler.py | 14 +++- src/a2a/server/tasks/inmemory_task_store.py | 18 ---- tests/client/test_grpc_client.py | 2 +- tests/client/test_jsonrpc_client.py | 4 +- .../test_default_request_handler.py | 84 +++++++++++++++++-- .../request_handlers/test_jsonrpc_handler.py | 3 +- .../server/tasks/test_database_task_store.py | 2 +- .../server/tasks/test_inmemory_task_store.py | 2 +- tests/utils/test_proto_utils.py | 3 +- 9 files changed, 98 insertions(+), 34 deletions(-) diff --git a/src/a2a/server/request_handlers/default_request_handler.py b/src/a2a/server/request_handlers/default_request_handler.py index 28c469481..de38648ad 100644 --- a/src/a2a/server/request_handlers/default_request_handler.py +++ b/src/a2a/server/request_handlers/default_request_handler.py @@ -131,10 +131,22 @@ async def on_list_tasks( ) -> ListTasksResult: """Default handler for 'tasks/list'.""" page = await self.task_store.list(params, context) + processed_tasks = [] + for task in page.tasks: + processed_task = task + if params.include_artifacts is not True: + processed_task = processed_task.model_copy( + update={'artifacts': None} + ) + if params.history_length is not None: + processed_task = apply_history_length( + processed_task, params.history_length + ) + processed_tasks.append(processed_task) return ListTasksResult( next_page_token=page.next_page_token, page_size=params.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE, - tasks=page.tasks, + tasks=processed_tasks, total_size=page.total_size, ) diff --git a/src/a2a/server/tasks/inmemory_task_store.py b/src/a2a/server/tasks/inmemory_task_store.py index bea7f1f12..4655f0e14 100644 --- a/src/a2a/server/tasks/inmemory_task_store.py +++ b/src/a2a/server/tasks/inmemory_task_store.py @@ -1,8 +1,6 @@ import asyncio import logging -from typing import Any - from a2a.server.context import ServerCallContext from a2a.server.tasks.task_store import TaskStore, TasksPage from a2a.types import ListTasksParams, Task @@ -65,22 +63,6 @@ async def list( task for task in tasks if task.status.state == params.status ] - # Reduce payload - base_updates: dict[str, Any] = {} - if not params.include_artifacts: - base_updates = {'artifacts': []} - for i in range(len(tasks)): - updates = dict(base_updates) - history = tasks[i].history - if params.history_length is not None and history: - limited_history = ( - history[-params.history_length :] - if params.history_length > 0 - else [] - ) - updates['history'] = limited_history - tasks[i] = tasks[i].model_copy(update=updates) - # Apply pagination total_size = len(tasks) page_token = int(params.page_token) if params.page_token else 0 diff --git a/tests/client/test_grpc_client.py b/tests/client/test_grpc_client.py index bbaae8dae..2a666fef9 100644 --- a/tests/client/test_grpc_client.py +++ b/tests/client/test_grpc_client.py @@ -10,6 +10,7 @@ AgentCard, Artifact, GetTaskPushNotificationConfigParams, + ListTasksParams, Message, MessageSendParams, Part, @@ -25,7 +26,6 @@ TaskStatus, TaskStatusUpdateEvent, TextPart, - ListTasksParams, ) from a2a.utils import get_text_parts, proto_utils from a2a.utils.errors import ServerError diff --git a/tests/client/test_jsonrpc_client.py b/tests/client/test_jsonrpc_client.py index f4b8a94af..e5b6d0a33 100644 --- a/tests/client/test_jsonrpc_client.py +++ b/tests/client/test_jsonrpc_client.py @@ -22,6 +22,8 @@ AgentCard, AgentSkill, InvalidParamsError, + ListTasksParams, + ListTasksResult, Message, MessageSendParams, PushNotificationConfig, @@ -31,8 +33,6 @@ TaskIdParams, TaskPushNotificationConfig, TaskQueryParams, - ListTasksParams, - ListTasksResult, ) from a2a.utils import AGENT_CARD_WELL_KNOWN_PATH diff --git a/tests/server/request_handlers/test_default_request_handler.py b/tests/server/request_handlers/test_default_request_handler.py index a1cb05320..6330c2d59 100644 --- a/tests/server/request_handlers/test_default_request_handler.py +++ b/tests/server/request_handlers/test_default_request_handler.py @@ -21,7 +21,6 @@ from a2a.server.context import ServerCallContext from a2a.server.events import EventQueue, InMemoryQueueManager, QueueManager from a2a.server.request_handlers import DefaultRequestHandler -from a2a.server.tasks.task_store import TasksPage from a2a.server.tasks import ( InMemoryPushNotificationConfigStore, InMemoryTaskStore, @@ -31,14 +30,15 @@ TaskStore, TaskUpdater, ) +from a2a.server.tasks.task_store import TasksPage from a2a.types import ( + Artifact, DeleteTaskPushNotificationConfigParams, GetTaskPushNotificationConfigParams, InternalError, InvalidParamsError, ListTaskPushNotificationConfigParams, ListTasksParams, - ListTasksResult, Message, MessageSendConfiguration, MessageSendParams, @@ -56,9 +56,7 @@ TextPart, UnsupportedOperationError, ) -from a2a.utils import ( - new_task, -) +from a2a.utils import new_agent_text_message, new_task class DummyAgentExecutor(AgentExecutor): @@ -155,7 +153,17 @@ async def test_on_list_tasks_success(): mock_page = MagicMock(spec=TasksPage) mock_page.tasks = [ create_sample_task(task_id='task1'), - create_sample_task(task_id='task2'), + create_sample_task(task_id='task2').model_copy( + update={ + 'artifacts': [ + Artifact( + artifact_id='artifact1', + parts=[Part(root=TextPart(text='Hello world!'))], + name='conversion_result', + ) + ] + } + ), ] mock_page.next_page_token = '123' mock_page.total_size = 2 @@ -163,7 +171,7 @@ async def test_on_list_tasks_success(): request_handler = DefaultRequestHandler( agent_executor=DummyAgentExecutor(), task_store=mock_task_store ) - params = ListTasksParams(page_size=10) + params = ListTasksParams(include_artifacts=True, page_size=10) context = create_server_call_context() result = await request_handler.on_list_tasks(params, context) @@ -175,6 +183,68 @@ async def test_on_list_tasks_success(): assert result.page_size == params.page_size +@pytest.mark.asyncio +async def test_on_list_tasks_excludes_artifacts(): + """Test on_list_tasks excludes artifacts from returned tasks.""" + mock_task_store = AsyncMock(spec=TaskStore) + mock_page = MagicMock(spec=TasksPage) + mock_page.tasks = [ + create_sample_task(task_id='task1'), + create_sample_task(task_id='task2').model_copy( + update={ + 'artifacts': [ + Artifact( + artifact_id='artifact1', + parts=[Part(root=TextPart(text='Hello world!'))], + name='conversion_result', + ) + ] + } + ), + ] + mock_page.next_page_token = '123' + mock_page.total_size = 2 + mock_task_store.list.return_value = mock_page + request_handler = DefaultRequestHandler( + agent_executor=DummyAgentExecutor(), task_store=mock_task_store + ) + params = ListTasksParams(include_artifacts=False, page_size=10) + context = create_server_call_context() + + result = await request_handler.on_list_tasks(params, context) + + assert result.tasks[1].artifacts == None + + +@pytest.mark.asyncio +async def test_on_list_tasks_applies_history_length(): + """Test on_list_tasks applies history length filter.""" + mock_task_store = AsyncMock(spec=TaskStore) + mock_page = MagicMock(spec=TasksPage) + history = [ + new_agent_text_message('Hello 1!'), + new_agent_text_message('Hello 2!'), + ] + mock_page.tasks = [ + create_sample_task(task_id='task1'), + create_sample_task(task_id='task2').model_copy( + update={'history': history} + ), + ] + mock_page.next_page_token = '123' + mock_page.total_size = 2 + mock_task_store.list.return_value = mock_page + request_handler = DefaultRequestHandler( + agent_executor=DummyAgentExecutor(), task_store=mock_task_store + ) + params = ListTasksParams(history_length=1, page_size=10) + context = create_server_call_context() + + result = await request_handler.on_list_tasks(params, context) + + assert result.tasks[1].history == [history[1]] + + @pytest.mark.asyncio async def test_on_cancel_task_task_not_found(): """Test on_cancel_task when the task is not found.""" diff --git a/tests/server/request_handlers/test_jsonrpc_handler.py b/tests/server/request_handlers/test_jsonrpc_handler.py index fa3af203c..298a751d8 100644 --- a/tests/server/request_handlers/test_jsonrpc_handler.py +++ b/tests/server/request_handlers/test_jsonrpc_handler.py @@ -36,7 +36,6 @@ GetAuthenticatedExtendedCardRequest, GetAuthenticatedExtendedCardResponse, GetAuthenticatedExtendedCardSuccessResponse, - ListTasksResult, GetTaskPushNotificationConfigParams, GetTaskPushNotificationConfigRequest, GetTaskPushNotificationConfigResponse, @@ -51,7 +50,7 @@ ListTaskPushNotificationConfigSuccessResponse, ListTasksParams, ListTasksRequest, - ListTasksResponse, + ListTasksResult, ListTasksSuccessResponse, Message, MessageSendConfiguration, diff --git a/tests/server/tasks/test_database_task_store.py b/tests/server/tasks/test_database_task_store.py index f0e65e639..ce6d1952c 100644 --- a/tests/server/tasks/test_database_task_store.py +++ b/tests/server/tasks/test_database_task_store.py @@ -19,6 +19,7 @@ from a2a.server.tasks.database_task_store import DatabaseTaskStore from a2a.types import ( Artifact, + ListTasksParams, Message, Part, Role, @@ -26,7 +27,6 @@ TaskState, TaskStatus, TextPart, - ListTasksParams, ) diff --git a/tests/server/tasks/test_inmemory_task_store.py b/tests/server/tasks/test_inmemory_task_store.py index 2a4362a67..c45abe514 100644 --- a/tests/server/tasks/test_inmemory_task_store.py +++ b/tests/server/tasks/test_inmemory_task_store.py @@ -3,7 +3,7 @@ import pytest from a2a.server.tasks import InMemoryTaskStore -from a2a.types import Task, ListTasksParams, TaskState, TaskStatus +from a2a.types import ListTasksParams, Task, TaskState, TaskStatus MINIMAL_TASK: dict[str, Any] = { diff --git a/tests/utils/test_proto_utils.py b/tests/utils/test_proto_utils.py index cccf1d498..ccd0def62 100644 --- a/tests/utils/test_proto_utils.py +++ b/tests/utils/test_proto_utils.py @@ -2,11 +2,12 @@ import pytest +from google.protobuf.timestamp_pb2 import Timestamp + from a2a import types from a2a.grpc import a2a_pb2 from a2a.utils import proto_utils from a2a.utils.errors import ServerError -from google.protobuf.timestamp_pb2 import Timestamp # --- Test Data --- From ec96994963400b5ca47fa86b03b844107a898cff Mon Sep 17 00:00:00 2001 From: lkawka Date: Fri, 7 Nov 2025 00:01:07 +0000 Subject: [PATCH 08/14] Add integration tests --- src/a2a/utils/proto_utils.py | 4 +- .../test_client_server_integration.py | 65 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/src/a2a/utils/proto_utils.py b/src/a2a/utils/proto_utils.py index 9f2d11420..2910ec96b 100644 --- a/src/a2a/utils/proto_utils.py +++ b/src/a2a/utils/proto_utils.py @@ -590,9 +590,9 @@ def list_tasks_response( cls, result: types.ListTasksResult ) -> a2a_pb2.ListTasksResponse: return a2a_pb2.ListTasksResponse( - next_page_token=result.next_page_token, + next_page_token=result.next_page_token or '', tasks=[cls.task(t) for t in result.tasks], - total_size=result.total_size, + total_size=result.total_size or 0, ) diff --git a/tests/integration/test_client_server_integration.py b/tests/integration/test_client_server_integration.py index 88d4d3d11..45a585995 100644 --- a/tests/integration/test_client_server_integration.py +++ b/tests/integration/test_client_server_integration.py @@ -22,6 +22,8 @@ GetTaskPushNotificationConfigParams, Message, MessageSendParams, + ListTasksParams, + ListTasksResult, Part, PushNotificationConfig, Role, @@ -105,6 +107,12 @@ async def stream_side_effect(*args, **kwargs): lambda params, context: params ) handler.on_get_task_push_notification_config.return_value = CALLBACK_CONFIG + handler.on_list_tasks.return_value = ListTasksResult( + tasks=[TASK_FROM_BLOCKING], + next_page_token='', + page_size=50, + total_size=1, + ) async def resubscribe_side_effect(*args, **kwargs): yield RESUBSCRIBE_EVENT @@ -434,6 +442,63 @@ def channel_factory(address: str) -> Channel: await transport.close() +@pytest.mark.asyncio +@pytest.mark.parametrize( + 'transport_setup_fixture', + [ + pytest.param('jsonrpc_setup', id='JSON-RPC'), + pytest.param('rest_setup', id='REST'), + ], +) +async def test_http_transport_list_tasks( + transport_setup_fixture: str, request +) -> None: + transport_setup: TransportSetup = request.getfixturevalue( + transport_setup_fixture + ) + transport = transport_setup.transport + handler = transport_setup.handler + + print(handler.on_list_tasks.call_args) + + params = ListTasksParams() + result = await transport.list_tasks(params) + + handler.on_list_tasks.assert_awaited_once_with(params, ANY) + assert result.next_page_token == '' + assert result.page_size == 50 + assert len(result.tasks) == 1 + assert result.total_size == 1 + + if hasattr(transport, 'close'): + await transport.close() + + +@pytest.mark.asyncio +async def test_grpc_transport_list_tasks( + grpc_server_and_handler: tuple[str, AsyncMock], + agent_card: AgentCard, +) -> None: + server_address, handler = grpc_server_and_handler + agent_card.url = server_address + + def channel_factory(address: str) -> Channel: + return grpc.aio.insecure_channel(address) + + channel = channel_factory(server_address) + transport = GrpcTransport(channel=channel, agent_card=agent_card) + + result = await transport.list_tasks(ListTasksParams()) + + handler.on_list_tasks.assert_awaited_once() + assert result.next_page_token == '' + assert result.page_size == 50 + assert len(result.tasks) == 1 + assert result.total_size == 1 + + await transport.close() + + @pytest.mark.asyncio @pytest.mark.parametrize( 'transport_setup_fixture', From 7b52ed211d34389d662316e1c2b5fc9f85a40ef0 Mon Sep 17 00:00:00 2001 From: lkawka Date: Fri, 7 Nov 2025 00:01:38 +0000 Subject: [PATCH 09/14] format --- tests/integration/test_client_server_integration.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_client_server_integration.py b/tests/integration/test_client_server_integration.py index 45a585995..65c735827 100644 --- a/tests/integration/test_client_server_integration.py +++ b/tests/integration/test_client_server_integration.py @@ -1,4 +1,5 @@ import asyncio + from collections.abc import AsyncGenerator from typing import NamedTuple from unittest.mock import ANY, AsyncMock @@ -7,6 +8,7 @@ import httpx import pytest import pytest_asyncio + from grpc.aio import Channel from a2a.client.transports import JsonRpcTransport, RestTransport @@ -20,10 +22,10 @@ AgentCard, AgentInterface, GetTaskPushNotificationConfigParams, - Message, - MessageSendParams, ListTasksParams, ListTasksResult, + Message, + MessageSendParams, Part, PushNotificationConfig, Role, @@ -38,6 +40,7 @@ TransportProtocol, ) + # --- Test Constants --- TASK_FROM_STREAM = Task( From d79a2293443f2b4ae7752e61a50cdf796fc0b609 Mon Sep 17 00:00:00 2001 From: Lukasz Kawka Date: Thu, 6 Nov 2025 17:06:07 -0700 Subject: [PATCH 10/14] Apply suggestion from @holtskinner Co-authored-by: Holt Skinner <13262395+holtskinner@users.noreply.github.com> --- src/a2a/server/request_handlers/rest_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/a2a/server/request_handlers/rest_handler.py b/src/a2a/server/request_handlers/rest_handler.py index 1cbaf7689..68f5ebba7 100644 --- a/src/a2a/server/request_handlers/rest_handler.py +++ b/src/a2a/server/request_handlers/rest_handler.py @@ -279,7 +279,7 @@ async def list_tasks( context: Context provided by the server. Returns: - A list of dict representing the`Task` objects. + A list of `dict` representing the `Task` objects. Raises: NotImplementedError: This method is not yet implemented. From d63809384553c996e453398f6450ac4b26461258 Mon Sep 17 00:00:00 2001 From: lkawka Date: Fri, 7 Nov 2025 00:10:12 +0000 Subject: [PATCH 11/14] change next_page_token default val --- src/a2a/server/tasks/task_store.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/a2a/server/tasks/task_store.py b/src/a2a/server/tasks/task_store.py index 01c79ff27..48dd3be72 100644 --- a/src/a2a/server/tasks/task_store.py +++ b/src/a2a/server/tasks/task_store.py @@ -9,7 +9,7 @@ class TasksPage(BaseModel): """Page with tasks.""" - next_page_token: str = '' + next_page_token: str | None = None tasks: list[Task] total_size: int From 50eba6a1f7c370708ff47aa462199f9d46d8a431 Mon Sep 17 00:00:00 2001 From: lkawka Date: Fri, 7 Nov 2025 21:03:04 +0000 Subject: [PATCH 12/14] change empty next_page_token value to None --- src/a2a/server/tasks/database_task_store.py | 2 +- src/a2a/server/tasks/inmemory_task_store.py | 2 +- tests/server/tasks/test_database_task_store.py | 13 +++++++++---- tests/server/tasks/test_inmemory_task_store.py | 13 +++++++++---- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/a2a/server/tasks/database_task_store.py b/src/a2a/server/tasks/database_task_store.py index dc762ed1c..2fa5cc2c3 100644 --- a/src/a2a/server/tasks/database_task_store.py +++ b/src/a2a/server/tasks/database_task_store.py @@ -187,7 +187,7 @@ async def list( next_page_token = ( str(page_number + 1) if total_count > (page_number + 1) * page_size - else '' + else None ) return TasksPage( diff --git a/src/a2a/server/tasks/inmemory_task_store.py b/src/a2a/server/tasks/inmemory_task_store.py index 4655f0e14..6b4f04150 100644 --- a/src/a2a/server/tasks/inmemory_task_store.py +++ b/src/a2a/server/tasks/inmemory_task_store.py @@ -72,7 +72,7 @@ async def list( next_page_token = ( str(page_token + 1) if (page_token + 1) * page_size < total_size - else '' + else None ) return TasksPage( diff --git a/tests/server/tasks/test_database_task_store.py b/tests/server/tasks/test_database_task_store.py index ce6d1952c..32d57cf10 100644 --- a/tests/server/tasks/test_database_task_store.py +++ b/tests/server/tasks/test_database_task_store.py @@ -181,7 +181,7 @@ async def test_get_task(db_store_parameterized: DatabaseTaskStore) -> None: ListTasksParams(), ['task-4', 'task-3', 'task-2', 'task-1', 'task-0'], 5, - '', + None, ), # Pagination (first page) ( @@ -198,20 +198,25 @@ async def test_get_task(db_store_parameterized: DatabaseTaskStore) -> None: '2', ), # Filtering by context_id - (ListTasksParams(context_id='context-1'), ['task-3', 'task-1'], 2, ''), + ( + ListTasksParams(context_id='context-1'), + ['task-3', 'task-1'], + 2, + None, + ), # Filtering by status ( ListTasksParams(status=TaskState.working), ['task-3', 'task-1'], 2, - '', + None, ), # Combined filtering (context_id and status) ( ListTasksParams(context_id='context-0', status=TaskState.submitted), ['task-2', 'task-0'], 2, - '', + None, ), # Combined filtering and pagination ( diff --git a/tests/server/tasks/test_inmemory_task_store.py b/tests/server/tasks/test_inmemory_task_store.py index c45abe514..32fad8faa 100644 --- a/tests/server/tasks/test_inmemory_task_store.py +++ b/tests/server/tasks/test_inmemory_task_store.py @@ -41,7 +41,7 @@ async def test_in_memory_task_store_get_nonexistent() -> None: ListTasksParams(), ['task-0', 'task-1', 'task-2', 'task-3', 'task-4'], 5, - '', + None, ), # Pagination (first page) ( @@ -58,20 +58,25 @@ async def test_in_memory_task_store_get_nonexistent() -> None: '2', ), # Filtering by context_id - (ListTasksParams(context_id='context-1'), ['task-1', 'task-3'], 2, ''), + ( + ListTasksParams(context_id='context-1'), + ['task-1', 'task-3'], + 2, + None, + ), # Filtering by status ( ListTasksParams(status=TaskState.working), ['task-1', 'task-3'], 2, - '', + None, ), # Combined filtering (context_id and status) ( ListTasksParams(context_id='context-0', status=TaskState.submitted), ['task-0', 'task-2'], 2, - '', + None, ), # Combined filtering and pagination ( From 1d712e8f81e5cb12e32821e70ccaaf19fd934073 Mon Sep 17 00:00:00 2001 From: lkawka Date: Fri, 7 Nov 2025 21:06:30 +0000 Subject: [PATCH 13/14] rm random param --- tests/server/request_handlers/test_jsonrpc_handler.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/server/request_handlers/test_jsonrpc_handler.py b/tests/server/request_handlers/test_jsonrpc_handler.py index 298a751d8..608b63492 100644 --- a/tests/server/request_handlers/test_jsonrpc_handler.py +++ b/tests/server/request_handlers/test_jsonrpc_handler.py @@ -160,7 +160,6 @@ async def test_on_list_tasks_success(self) -> None: params=ListTasksParams( page_size=10, page_token='token', - filter='filter', ), ) call_context = ServerCallContext(state={'foo': 'bar'}) From 51ee2c74791c35093b87b6ca1d8cf87763ec908f Mon Sep 17 00:00:00 2001 From: lkawka Date: Thu, 13 Nov 2025 18:09:43 +0000 Subject: [PATCH 14/14] Change last token to '' and add more list_tasks tests --- .../request_handlers/default_request_handler.py | 2 +- tests/server/tasks/test_database_task_store.py | 15 +++++++++++---- tests/server/tasks/test_inmemory_task_store.py | 15 +++++++++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/a2a/server/request_handlers/default_request_handler.py b/src/a2a/server/request_handlers/default_request_handler.py index de38648ad..643f14353 100644 --- a/src/a2a/server/request_handlers/default_request_handler.py +++ b/src/a2a/server/request_handlers/default_request_handler.py @@ -144,7 +144,7 @@ async def on_list_tasks( ) processed_tasks.append(processed_task) return ListTasksResult( - next_page_token=page.next_page_token, + next_page_token=page.next_page_token or '', page_size=params.page_size or DEFAULT_LIST_TASKS_PAGE_SIZE, tasks=processed_tasks, total_size=page.total_size, diff --git a/tests/server/tasks/test_database_task_store.py b/tests/server/tasks/test_database_task_store.py index 32d57cf10..d734001c3 100644 --- a/tests/server/tasks/test_database_task_store.py +++ b/tests/server/tasks/test_database_task_store.py @@ -190,12 +190,19 @@ async def test_get_task(db_store_parameterized: DatabaseTaskStore) -> None: 5, '1', ), - # Pagination (second page) + # Pagination (final page) ( - ListTasksParams(page_size=2, page_token='1'), - ['task-2', 'task-1'], + ListTasksParams(page_size=2, page_token='2'), + ['task-0'], 5, - '2', + None, + ), + # Pagination (out of bounds) + ( + ListTasksParams(page_size=2, page_token='3'), + [], + 5, + None, ), # Filtering by context_id ( diff --git a/tests/server/tasks/test_inmemory_task_store.py b/tests/server/tasks/test_inmemory_task_store.py index 32fad8faa..2708e68f7 100644 --- a/tests/server/tasks/test_inmemory_task_store.py +++ b/tests/server/tasks/test_inmemory_task_store.py @@ -50,12 +50,19 @@ async def test_in_memory_task_store_get_nonexistent() -> None: 5, '1', ), - # Pagination (second page) + # Pagination (final page) ( - ListTasksParams(page_size=2, page_token='1'), - ['task-2', 'task-3'], + ListTasksParams(page_size=2, page_token='2'), + ['task-4'], 5, - '2', + None, + ), + # Pagination (out of bounds) + ( + ListTasksParams(page_size=2, page_token='3'), + [], + 5, + None, ), # Filtering by context_id (