diff --git a/.github/conventional-commit-lint.yaml b/.github/conventional-commit-lint.yaml deleted file mode 100644 index c967ffa..0000000 --- a/.github/conventional-commit-lint.yaml +++ /dev/null @@ -1,2 +0,0 @@ -enabled: true -always_check_pr_title: true diff --git a/.github/release-please.yml b/.github/release-please.yml deleted file mode 100644 index c8b6617..0000000 --- a/.github/release-please.yml +++ /dev/null @@ -1,4 +0,0 @@ -releaseType: node -handleGHRelease: true -bumpMinorPreMajor: false -bumpPatchForMinorPreMajor: true diff --git a/.github/release-trigger.yml b/.github/release-trigger.yml deleted file mode 100644 index d4ca941..0000000 --- a/.github/release-trigger.yml +++ /dev/null @@ -1 +0,0 @@ -enabled: true diff --git a/.github/workflows/conventional-commits.yml b/.github/workflows/conventional-commits.yml new file mode 100644 index 0000000..d23da45 --- /dev/null +++ b/.github/workflows/conventional-commits.yml @@ -0,0 +1,26 @@ +name: "Conventional Commits" + +on: + pull_request: + types: + - opened + - edited + - synchronize + +permissions: + contents: read + +jobs: + main: + permissions: + pull-requests: read + statuses: write + name: Validate PR Title + runs-on: ubuntu-latest + steps: + - name: semantic-pull-request + uses: amannn/action-semantic-pull-request@v5.5.3 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + validateSingleCommit: false diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 18bdff0..a105b21 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,35 +1,32 @@ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages -name: Node.js Package +name: Publish to NPM on: release: types: [created] jobs: - build: + publish-npm: runs-on: ubuntu-latest + permissions: + contents: read + id-token: write steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 18 registry-url: 'https://registry.npmjs.org' - cache: 'npm' - run: npm ci - - run: npm test - - publish-npm: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 + - run: npm run build + + # Now configure with the publish service for install. - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 18 registry-url: 'https://wombat-dressing-room.appspot.com/' - - run: npm ci - run: npm publish --provenance --access public env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..21421d9 --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,19 @@ +on: + push: + branches: + - main + +permissions: + contents: write + pull-requests: write + +name: release-please + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - uses: googleapis/release-please-action@v4 + with: + token: ${{ secrets.A2A_BOT_PAT }} + release-type: node diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000..f94abca --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,25 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Run Unit Tests + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + test: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + registry-url: 'https://registry.npmjs.org' + cache: 'npm' + - run: npm ci + - run: npm test diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a0f701..41f503a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,40 @@ # Changelog +## [0.3.1](https://github.com/a2aproject/a2a-js/compare/v0.3.0...v0.3.1) (2025-08-06) + + +### Bug Fixes + +* add missing express entrypoint to tsup config ([#96](https://github.com/a2aproject/a2a-js/issues/96)) ([8e990e4](https://github.com/a2aproject/a2a-js/commit/8e990e497927e3554699f8ebb005829b170d9bc3)) + +## [0.3.0](https://github.com/a2aproject/a2a-js/compare/v0.2.5...v0.3.0) (2025-08-05) + + +### ⚠ BREAKING CHANGES + +* upgrade to a2a 0.3.0 spec version ([#87](https://github.com/a2aproject/a2a-js/issues/87)) +* make Express dependency optional + +### Features + +* make Express dependency optional ([60899c5](https://github.com/a2aproject/a2a-js/commit/60899c51e2910570402d1207f6b50952bed8862f)) +* upgrade to a2a 0.3.0 spec version ([#87](https://github.com/a2aproject/a2a-js/issues/87)) ([ae53da1](https://github.com/a2aproject/a2a-js/commit/ae53da1e36ff58912e01fefa854c5b3174edf7d8)) + +## [0.2.5](https://github.com/a2aproject/a2a-js/compare/v0.2.4...v0.2.5) (2025-07-30) + + +### Features + +* add support for custom agent card url. resolves [#68](https://github.com/a2aproject/a2a-js/issues/68) ([#79](https://github.com/a2aproject/a2a-js/issues/79)) ([dc92d32](https://github.com/a2aproject/a2a-js/commit/dc92d321ac7c142ff5232cdca0db8a24b4d76da0)) +* Export ExecutionEventQueue in server ([#61](https://github.com/a2aproject/a2a-js/issues/61)) ([530c0b9](https://github.com/a2aproject/a2a-js/commit/530c0b9f1fd50fafd991f640c119837860ae8c3f)) +* Export type AgentExecutionEvent ([#66](https://github.com/a2aproject/a2a-js/issues/66)) ([f4c81f4](https://github.com/a2aproject/a2a-js/commit/f4c81f41866c24d83823b5db7d24b5fdb56b37b4)) + + +### Bug Fixes + +* correct the example code ([#64](https://github.com/a2aproject/a2a-js/issues/64)) ([126eee4](https://github.com/a2aproject/a2a-js/commit/126eee4e3b79e9475a5af5cbebb0e98b68f286fa)) +* setting context id in _createRequestContext ([#49](https://github.com/a2aproject/a2a-js/issues/49)) ([1abc8a1](https://github.com/a2aproject/a2a-js/commit/1abc8a1f3590f78647d94c5a1e31444203e1131f)) + ## [0.2.4](https://github.com/a2aproject/a2a-js/compare/v0.2.3...v0.2.4) (2025-07-14) diff --git a/README.md b/README.md index ce8f776..18d5800 100644 --- a/README.md +++ b/README.md @@ -21,12 +21,25 @@ You can install the A2A SDK using either `npm`. npm install @a2a-js/sdk ``` +### For Server Usage + +If you plan to use the A2A server functionality (A2AExpressApp), you'll also need to install Express as it's a peer dependency: + +```bash +npm install express +``` + You can also find JavaScript samples [here](https://github.com/google-a2a/a2a-samples/tree/main/samples/js). ## A2A Server This directory contains a TypeScript server implementation for the Agent-to-Agent (A2A) communication protocol, built using Express.js. +**Note:** Express is a peer dependency for server functionality. Make sure to install it separately: +```bash +npm install express +``` + ### 1. Define Agent Card ```typescript @@ -42,6 +55,7 @@ const movieAgentCard: AgentCard = { organization: "A2A Agents", url: "https://example.com/a2a-agents", // Added provider URL }, + protocolVersion: "0.3.0", // A2A protocol this agent supports. version: "0.0.2", // Incremented version capabilities: { streaming: true, // Supports streaming @@ -81,12 +95,12 @@ const movieAgentCard: AgentCard = { import { InMemoryTaskStore, TaskStore, - A2AExpressApp, AgentExecutor, RequestContext, ExecutionEventBus, DefaultRequestHandler, } from "@a2a-js/sdk/server"; +import { A2AExpressApp } from "@a2a-js/sdk/server/express"; // 1. Define your agent's logic as a AgentExecutor class MyAgentExecutor implements AgentExecutor { @@ -234,7 +248,7 @@ expressApp.listen(PORT, () => { `[MyAgent] Server using new framework started on http://localhost:${PORT}` ); console.log( - `[MyAgent] Agent Card: http://localhost:${PORT}/.well-known/agent.json` + `[MyAgent] Agent Card: http://localhost:${PORT}/.well-known/agent-card.json` ); console.log("[MyAgent] Press Ctrl+C to stop the server"); }); diff --git a/package-lock.json b/package-lock.json index 85d9219..5837784 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@a2a-js/sdk", - "version": "0.2.4", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@a2a-js/sdk", - "version": "0.2.4", + "version": "0.3.1", "dependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.23", @@ -3096,7 +3096,7 @@ }, "node_modules/@types/express": { "version": "4.17.23", - "resolved": "https://npm.dev.wixpress.com/api/npm/npm-repos/@types/express/-/express-4.17.23.tgz", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", "license": "MIT", "dependencies": { @@ -3108,7 +3108,7 @@ }, "node_modules/@types/express-serve-static-core": { "version": "4.19.6", - "resolved": "https://npm.dev.wixpress.com/api/npm/npm-repos/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", "license": "MIT", "dependencies": { @@ -3232,13 +3232,13 @@ }, "node_modules/@types/qs": { "version": "6.14.0", - "resolved": "https://npm.dev.wixpress.com/api/npm/npm-repos/@types/qs/-/qs-6.14.0.tgz", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", "license": "MIT" }, "node_modules/@types/range-parser": { "version": "1.2.7", - "resolved": "https://npm.dev.wixpress.com/api/npm/npm-repos/@types/range-parser/-/range-parser-1.2.7.tgz", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", "license": "MIT" }, diff --git a/package.json b/package.json index 6ca6bd5..f3a471c 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,11 @@ { "name": "@a2a-js/sdk", - "version": "0.2.4", + "version": "0.3.1", "description": "Server & Client SDK for Agent2Agent protocol", - "repository": "google-a2a/a2a-js.git", + "repository": { + "type": "git", + "url": "git+https://github.com/a2aproject/a2a-js.git" + }, "engines": { "node": ">=18" }, @@ -20,6 +23,11 @@ "import": "./dist/server/index.js", "require": "./dist/server/index.cjs" }, + "./server/express": { + "types": "./dist/server/express/index.d.ts", + "import": "./dist/server/express/index.js", + "require": "./dist/server/express/index.cjs" + }, "./client": { "types": "./dist/client/index.d.ts", "import": "./dist/client/index.js", @@ -35,11 +43,13 @@ "@genkit-ai/googleai": "^1.8.0", "@genkit-ai/vertexai": "^1.8.0", "@types/chai": "^5.2.2", + "@types/express": "^4.17.23", "@types/mocha": "^10.0.10", "@types/node": "^22.13.14", "@types/sinon": "^17.0.4", "c8": "^10.1.3", "chai": "^5.2.0", + "express": "^4.21.2", "genkit": "^1.8.0", "gts": "^6.0.2", "json-schema-to-typescript": "^15.0.4", @@ -53,18 +63,24 @@ "clean": "gts clean", "build": "tsup", "pretest": "npm run build", - "test": "mocha build/test/**/*.js", + "test": "mocha test/**/*.spec.ts", "coverage": "c8 npm run test", "generate": "curl https://raw.githubusercontent.com/google-a2a/A2A/refs/heads/main/specification/json/a2a.json > spec.json && node scripts/generateTypes.js && rm spec.json", "sample:cli": "tsx src/samples/cli.ts", "sample:movie-agent": "tsx src/samples/agents/movie-agent/index.ts" }, "dependencies": { - "@types/cors": "^2.8.17", - "@types/express": "^4.17.23", - "body-parser": "^2.2.0", - "cors": "^2.8.5", - "express": "^4.21.2", "uuid": "^11.1.0" + }, + "peerDependencies": { + "express": "^4.21.2" + }, + "peerDependenciesMeta": { + "express": { + "optional": true + } + }, + "mocha": { + "require": "tsx" } } diff --git a/src/a2a_response.ts b/src/a2a_response.ts index 4004aed..f3c4d3b 100644 --- a/src/a2a_response.ts +++ b/src/a2a_response.ts @@ -1,4 +1,4 @@ -import { SendMessageResponse, SendStreamingMessageResponse, GetTaskResponse, CancelTaskResponse, SetTaskPushNotificationConfigResponse, GetTaskPushNotificationConfigResponse, JSONRPCErrorResponse } from "./types.js"; +import { SendMessageResponse, SendStreamingMessageResponse, GetTaskResponse, CancelTaskResponse, SetTaskPushNotificationConfigResponse, GetTaskPushNotificationConfigResponse, JSONRPCErrorResponse, ListTaskPushNotificationConfigSuccessResponse, DeleteTaskPushNotificationConfigSuccessResponse, GetAuthenticatedExtendedCardSuccessResponse } from "./types.js"; /** * Represents any valid JSON-RPC response defined in the A2A protocol. @@ -10,5 +10,8 @@ export type A2AResponse = | CancelTaskResponse | SetTaskPushNotificationConfigResponse | GetTaskPushNotificationConfigResponse + | ListTaskPushNotificationConfigSuccessResponse + | DeleteTaskPushNotificationConfigSuccessResponse + | GetAuthenticatedExtendedCardSuccessResponse | JSONRPCErrorResponse; // Catch-all for other error responses \ No newline at end of file diff --git a/src/client/auth-handler.ts b/src/client/auth-handler.ts new file mode 100644 index 0000000..e46dc5e --- /dev/null +++ b/src/client/auth-handler.ts @@ -0,0 +1,114 @@ +export interface HttpHeaders { [key: string]: string }; + +/** + * Generic interface for handling authentication for HTTP requests. + * + * - For each HTTP request, this handler is called to provide additional headers to the request through + * the headers() function. + * - After the server returns a response, the shouldRetryWithHeaders() function is called. Usually this + * function responds to a 401 or 403 response or JSON-RPC codes, but can respond to any other signal - + * that is an implementation detail of the AuthenticationHandler. + * - If the shouldRetryWithHeaders() function returns new headers, then the request should retried with the provided + * revised headers. These provisional headers may, or may not, be optimistically stored for subsequent requests - + * that is an implementation detail of the AuthenticationHandler. + * - If the request is successful and the onSuccessfulRetry() is defined, then the onSuccessfulRetry() function is + * called with the headers that were used to successfully complete the request. This callback provides an + * opportunity to save the headers for subsequent requests if they were not already saved. + * + */ +export interface AuthenticationHandler { + /** + * Provides additional HTTP request headers. + * @returns HTTP headers which may include Authorization if available. + */ + headers: () => Promise; + + /** + * For every HTTP response (even 200s) the shouldRetryWithHeaders() method is called. + * This method is supposed to check if the request needs to be retried and if, yes, + * return a set of headers. An A2A server might indicate auth failures in its response + * by JSON-rpc codes, HTTP codes like 401, 403 or headers like WWW-Authenticate. + * + * @param req The RequestInit object used to invoke fetch() + * @param res The fetch Response object + * @returns If the HTTP request should be retried then returns the HTTP headers to use, + * or returns undefined if no retry should be made. + */ + shouldRetryWithHeaders: (req:RequestInit, res:Response) => Promise; + + /** + * If the last HTTP request using the headers from shouldRetryWithHeaders() was successful, and + * this function is implemented, then it will be called with the headers provided from + * shouldRetryWithHeaders(). + * + * This callback allows transient headers to be saved for subsequent requests only when they + * are validated by the server. + */ + onSuccessfulRetry?: (headers:HttpHeaders) => Promise +} + +/** + * Higher-order function that wraps fetch with authentication handling logic. + * Returns a new fetch function that automatically handles authentication retries for 401/403 responses. + * + * @param fetchImpl The underlying fetch implementation to wrap + * @param authHandler Authentication handler for managing auth headers and retries + * @returns A new fetch function with authentication handling capabilities + * + * Usage examples: + * - const authFetch = createAuthHandlingFetch(fetch, authHandler); + * - const response = await authFetch(url, options); + * - const response = await authFetch(url); // Direct function call + */ +export function createAuthenticatingFetchWithRetry( + fetchImpl: typeof fetch, + authHandler: AuthenticationHandler +): typeof fetch { + /** + * Executes a fetch request with authentication handling. + * If the auth handler provides new headers for the shouldRetryWithHeaders() function, + * then the request is retried. + * @param url The URL to fetch + * @param init The fetch request options + * @returns A Promise that resolves to the Response + */ + async function authFetch(url: RequestInfo | URL, init?: RequestInit): Promise { + // Merge auth headers with provided headers + const authHeaders = await authHandler.headers() || {}; + const mergedInit: RequestInit = { + ...(init || {}), + headers: { + ...authHeaders, + ...(init?.headers || {}), + }, + }; + + let response = await fetchImpl(url, mergedInit); + + // Check if the auth handler wants to retry the request with new headers + const updatedHeaders = await authHandler.shouldRetryWithHeaders(mergedInit, response); + if (updatedHeaders) { + // Retry request with revised headers + const retryInit: RequestInit = { + ...(init || {}), + headers: { + ...updatedHeaders, + ...(init?.headers || {}), + }, + }; + response = await fetchImpl(url, retryInit); + + if (response.ok && authHandler.onSuccessfulRetry) { + await authHandler.onSuccessfulRetry(updatedHeaders); // Remember headers that worked + } + } + + return response; + } + + // Copy fetch properties to maintain compatibility + Object.setPrototypeOf(authFetch, Object.getPrototypeOf(fetchImpl)); + Object.defineProperties(authFetch, Object.getOwnPropertyDescriptors(fetchImpl)); + + return authFetch; +} diff --git a/src/client/client.ts b/src/client/client.ts index 9679b50..d4091e9 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -31,40 +31,49 @@ import { A2AError, SendMessageSuccessResponse } from '../types.js'; // Assuming schema.ts is in the same directory or appropriately pathed +import { AGENT_CARD_PATH } from "../constants.js"; // Helper type for the data yielded by streaming methods type A2AStreamEventData = Message | Task | TaskStatusUpdateEvent | TaskArtifactUpdateEvent; +export interface A2AClientOptions { + agentCardPath?: string; + fetchImpl?: typeof fetch; +} + /** * A2AClient is a TypeScript HTTP client for interacting with A2A-compliant agents. */ export class A2AClient { - private agentBaseUrl: string; private agentCardPromise: Promise; private requestIdCounter: number = 1; private serviceEndpointUrl?: string; // To be populated from AgentCard after fetching + private fetchImpl: typeof fetch; /** * Constructs an A2AClient instance. * It initiates fetching the agent card from the provided agent baseUrl. - * The Agent Card is expected at `${agentBaseUrl}/.well-known/agent.json`. + * The Agent Card is fetched from a path relative to the agentBaseUrl, which defaults to '.well-known/agent-card.json'. * The `url` field from the Agent Card will be used as the RPC service endpoint. - * @param agentBaseUrl The base URL of the A2A agent (e.g., https://agent.example.com). + * @param agentBaseUrl The base URL of the A2A agent (e.g., https://agent.example.com) + * @param options Optional. The options for the A2AClient including the fetch implementation, agent card path, and authentication handler. */ - constructor(agentBaseUrl: string) { - this.agentBaseUrl = agentBaseUrl.replace(/\/$/, ""); // Remove trailing slash if any - this.agentCardPromise = this._fetchAndCacheAgentCard(); + constructor(agentBaseUrl: string, options?: A2AClientOptions) { + this.fetchImpl = options?.fetchImpl ?? fetch; + this.agentCardPromise = this._fetchAndCacheAgentCard( agentBaseUrl, options?.agentCardPath ); } /** * Fetches the Agent Card from the agent's well-known URI and caches its service endpoint URL. * This method is called by the constructor. + * @param agentBaseUrl The base URL of the A2A agent (e.g., https://agent.example.com) + * @param agentCardPath path to the agent card, defaults to .well-known/agent-card.json * @returns A Promise that resolves to the AgentCard. */ - private async _fetchAndCacheAgentCard(): Promise { - const agentCardUrl = `${this.agentBaseUrl}/.well-known/agent.json`; + private async _fetchAndCacheAgentCard( agentBaseUrl: string, agentCardPath?: string ): Promise { try { - const response = await fetch(agentCardUrl, { + const agentCardUrl = this.resolveAgentCardUrl( agentBaseUrl, agentCardPath ); + const response = await this.fetchImpl(agentCardUrl, { headers: { 'Accept': 'application/json' }, }); if (!response.ok) { @@ -77,7 +86,7 @@ export class A2AClient { this.serviceEndpointUrl = agentCard.url; // Cache the service endpoint URL from the agent card return agentCard; } catch (error) { - console.error("Error fetching or parsing Agent Card:"); + console.error("Error fetching or parsing Agent Card:", error); // Allow the promise to reject so users of agentCardPromise can handle it. throw error; } @@ -88,14 +97,15 @@ export class A2AClient { * If an `agentBaseUrl` is provided, it fetches the card from that specific URL. * Otherwise, it returns the card fetched and cached during client construction. * @param agentBaseUrl Optional. The base URL of the agent to fetch the card from. + * @param agentCardPath path to the agent card, defaults to .well-known/agent-card.json * If provided, this will fetch a new card, not use the cached one from the constructor's URL. * @returns A Promise that resolves to the AgentCard. */ - public async getAgentCard(agentBaseUrl?: string): Promise { + public async getAgentCard(agentBaseUrl?: string, agentCardPath?: string): Promise { if (agentBaseUrl) { - const specificAgentBaseUrl = agentBaseUrl.replace(/\/$/, ""); - const agentCardUrl = `${specificAgentBaseUrl}/.well-known/agent.json`; - const response = await fetch(agentCardUrl, { + const agentCardUrl = this.resolveAgentCardUrl( agentBaseUrl, agentCardPath ); + + const response = await this.fetchImpl(agentCardUrl, { headers: { 'Accept': 'application/json' }, }); if (!response.ok) { @@ -107,6 +117,15 @@ export class A2AClient { return this.agentCardPromise; } + /** + * Determines the agent card URL based on the agent URL. + * @param agentBaseUrl The agent URL. + * @param agentCardPath Optional relative path to the agent card, defaults to .well-known/agent-card.json + */ + private resolveAgentCardUrl( agentBaseUrl: string, agentCardPath: string = AGENT_CARD_PATH ): string { + return `${agentBaseUrl.replace(/\/$/, "")}/${agentCardPath.replace(/^\//, "")}`; + } + /** * Gets the RPC service endpoint URL. Ensures the agent card has been fetched first. * @returns A Promise that resolves to the service endpoint URL string. @@ -144,24 +163,18 @@ export class A2AClient { id: requestId, }; - const httpResponse = await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "application/json", // Expect JSON response for non-streaming requests - }, - body: JSON.stringify(rpcRequest), - }); + const httpResponse = await this._fetchRpc( endpoint, rpcRequest ); if (!httpResponse.ok) { let errorBodyText = '(empty or non-JSON response)'; try { errorBodyText = await httpResponse.text(); const errorJson = JSON.parse(errorBodyText); - // If the body is a valid JSON-RPC error response, let it be handled by the standard parsing below. - // However, if it's not even a JSON-RPC structure but still an error, throw based on HTTP status. - if (!errorJson.jsonrpc && errorJson.error) { // Check if it's a JSON-RPC error structure - throw new Error(`RPC error for ${method}: ${errorJson.error.message} (Code: ${errorJson.error.code}, HTTP Status: ${httpResponse.status}) Data: ${JSON.stringify(errorJson.error.data)}`); + // If the body is a valid JSON-RPC error response, return it as a proper JSON-RPC error response. + if (errorJson.jsonrpc && errorJson.error) { + return errorJson as TResponse; + } else if (!errorJson.jsonrpc && errorJson.error) { // Check if it's a JSON-RPC error structure + throw new Error(`RPC error for ${method}: ${errorJson.error.message} (Code: ${errorJson.error.code}, HTTP Status: ${httpResponse.status}) Data: ${JSON.stringify(errorJson.error.data || {})}`); } else if (!errorJson.jsonrpc) { throw new Error(`HTTP error for ${method}! Status: ${httpResponse.status} ${httpResponse.statusText}. Response: ${errorBodyText}`); } @@ -185,6 +198,25 @@ export class A2AClient { return rpcResponse as TResponse; } + /** + * Internal helper method to fetch the RPC service endpoint. + * @param url The URL to fetch. + * @param rpcRequest The JSON-RPC request to send. + * @param acceptHeader The Accept header to use. Defaults to "application/json". + * @returns A Promise that resolves to the fetch HTTP response. + */ + private async _fetchRpc( url: string, rpcRequest: JSONRPCRequest, acceptHeader: string = "application/json" ): Promise { + const requestInit: RequestInit = { + method: "POST", + headers: { + "Content-Type": "application/json", + "Accept": acceptHeader, // Expect JSON response for non-streaming requests + }, + body: JSON.stringify(rpcRequest) + }; + + return this.fetchImpl(url, requestInit); + } /** * Sends a message to the agent. @@ -222,14 +254,7 @@ export class A2AClient { id: clientRequestId, }; - const response = await fetch(endpoint, { - method: "POST", - headers: { - "Content-Type": "application/json", - "Accept": "text/event-stream", // Crucial for SSE - }, - body: JSON.stringify(rpcRequest), - }); + const response = await this._fetchRpc( endpoint, rpcRequest, "text/event-stream" ); if (!response.ok) { // Attempt to read error body for more details @@ -329,7 +354,7 @@ export class A2AClient { id: clientRequestId, }; - const response = await fetch(endpoint, { + const response = await this.fetchImpl(endpoint, { method: "POST", headers: { "Content-Type": "application/json", @@ -456,7 +481,7 @@ export class A2AClient { if (this.isErrorResponse(a2aStreamResponse)) { const err = a2aStreamResponse.error as (JSONRPCError | A2AError); - throw new Error(`SSE event contained an error: ${err.message} (Code: ${err.code}) Data: ${JSON.stringify(err.data)}`); + throw new Error(`SSE event contained an error: ${err.message} (Code: ${err.code}) Data: ${JSON.stringify(err.data || {})}`); } // Check if 'result' exists, as it's mandatory for successful JSON-RPC responses @@ -477,6 +502,7 @@ export class A2AClient { } } + isErrorResponse(response: JSONRPCResponse): response is JSONRPCErrorResponse { return "error" in response; } diff --git a/src/client/index.ts b/src/client/index.ts index a3e9e7c..c6689cc 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -3,3 +3,5 @@ */ export { A2AClient } from "./client.js"; +export type { A2AClientOptions } from "./client.js"; +export * from "./auth-handler.js"; diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..d84b820 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,8 @@ +/** + * Shared constants for the A2A library + */ + +/** + * The well-known path for the agent card + */ +export const AGENT_CARD_PATH = ".well-known/agent-card.json"; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 5a1a02e..34fe000 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,10 @@ /** - * Main entry point for the A2A Server V2 library. * Exports the common types. + * + * Use the client/index.ts file to import the client-only codebase. + * Use the server/index.ts file to import the server-only codebase. */ export * from "./types.js"; export type { A2AResponse } from "./a2a_response.js"; +export { AGENT_CARD_PATH } from "./constants.js"; diff --git a/src/samples/agents/movie-agent/index.ts b/src/samples/agents/movie-agent/index.ts index be3d242..6c6621d 100644 --- a/src/samples/agents/movie-agent/index.ts +++ b/src/samples/agents/movie-agent/index.ts @@ -12,12 +12,12 @@ import { import { InMemoryTaskStore, TaskStore, - A2AExpressApp, AgentExecutor, RequestContext, ExecutionEventBus, DefaultRequestHandler } from "../../../server/index.js"; +import { A2AExpressApp } from "../../../server/express/index.js"; import { MessageData } from "genkit"; import { ai } from "./genkit.js"; import { searchMovies, searchPeople } from "./tools.js"; @@ -314,7 +314,7 @@ async function main() { const PORT = process.env.PORT || 41241; expressApp.listen(PORT, () => { console.log(`[MovieAgent] Server using new framework started on http://localhost:${PORT}`); - console.log(`[MovieAgent] Agent Card: http://localhost:${PORT}/.well-known/agent.json`); + console.log(`[MovieAgent] Agent Card: http://localhost:${PORT}/.well-known/agent-card.json`); console.log('[MovieAgent] Press Ctrl+C to stop the server'); }); } diff --git a/src/server/error.ts b/src/server/error.ts index 291d587..7b30f40 100644 --- a/src/server/error.ts +++ b/src/server/error.ts @@ -93,4 +93,11 @@ export class A2AError extends Error { `Unsupported operation: ${operation}` ); } + + static authenticatedExtendedCardNotConfigured(): A2AError { + return new A2AError( + -32007, + `Extended card not configured.` + ); + } } diff --git a/src/server/a2a_express_app.ts b/src/server/express/a2a_express_app.ts similarity index 88% rename from src/server/a2a_express_app.ts rename to src/server/express/a2a_express_app.ts index 0181d47..e4b352c 100644 --- a/src/server/a2a_express_app.ts +++ b/src/server/express/a2a_express_app.ts @@ -1,9 +1,10 @@ import express, { Request, Response, Express, RequestHandler, ErrorRequestHandler } from 'express'; -import { A2AError } from "./error.js"; -import { A2AResponse, JSONRPCErrorResponse, JSONRPCSuccessResponse } from "../index.js"; -import { A2ARequestHandler } from "./request_handler/a2a_request_handler.js"; -import { JsonRpcTransportHandler } from "./transports/jsonrpc_transport_handler.js"; +import { A2AError } from "../error.js"; +import { JSONRPCErrorResponse, JSONRPCSuccessResponse, JSONRPCResponse } from "../../index.js"; +import { A2ARequestHandler } from "../request_handler/a2a_request_handler.js"; +import { JsonRpcTransportHandler } from "../transports/jsonrpc_transport_handler.js"; +import { AGENT_CARD_PATH } from "../../constants.js"; export class A2AExpressApp { private requestHandler: A2ARequestHandler; // Kept for getAgentCard @@ -19,17 +20,19 @@ export class A2AExpressApp { * @param app Optional existing Express app. * @param baseUrl The base URL for A2A endpoints (e.g., "/a2a/api"). * @param middlewares Optional array of Express middlewares to apply to the A2A routes. + * @param agentCardPath Optional custom path for the agent card endpoint (defaults to /.well-known/agent-card.json). * @returns The Express app with A2A routes. */ public setupRoutes( app: Express, baseUrl: string = "", - middlewares?: Array + middlewares?: Array, + agentCardPath: string = AGENT_CARD_PATH ): Express { const router = express.Router(); router.use(express.json(), ...(middlewares ?? [])); - router.get("/.well-known/agent.json", async (req: Request, res: Response) => { + router.get(`/${agentCardPath}`, async (req: Request, res: Response) => { try { // getAgentCard is on A2ARequestHandler, which DefaultRequestHandler implements const agentCard = await this.requestHandler.getAgentCard(); @@ -82,7 +85,7 @@ export class A2AExpressApp { } } } else { // Single JSON-RPC response - const rpcResponse = rpcResponseOrStream as A2AResponse; + const rpcResponse = rpcResponseOrStream as JSONRPCResponse; res.status(200).json(rpcResponse); } } catch (error: any) { // Catch errors from jsonRpcTransportHandler.handle itself (e.g., initial parse error) diff --git a/src/server/express/index.ts b/src/server/express/index.ts new file mode 100644 index 0000000..18a61fa --- /dev/null +++ b/src/server/express/index.ts @@ -0,0 +1,6 @@ +/** + * Express integration for the A2A Server library. + * This module provides Express.js specific functionality. + */ + +export { A2AExpressApp } from "./a2a_express_app.js"; diff --git a/src/server/index.ts b/src/server/index.ts index bb30695..682d196 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -19,5 +19,4 @@ export type { TaskStore } from "./store.js"; export { InMemoryTaskStore } from "./store.js"; export { JsonRpcTransportHandler } from "./transports/jsonrpc_transport_handler.js"; -export { A2AExpressApp } from "./a2a_express_app.js"; export { A2AError } from "./error.js"; diff --git a/src/server/request_handler/a2a_request_handler.ts b/src/server/request_handler/a2a_request_handler.ts index 38b56c7..e7c4cba 100644 --- a/src/server/request_handler/a2a_request_handler.ts +++ b/src/server/request_handler/a2a_request_handler.ts @@ -1,8 +1,23 @@ -import { Message, AgentCard, MessageSendParams, Task, TaskStatusUpdateEvent, TaskArtifactUpdateEvent, TaskQueryParams, TaskIdParams, TaskPushNotificationConfig } from "../../types.js"; +import { + Message, + AgentCard, + MessageSendParams, + Task, + TaskStatusUpdateEvent, + TaskArtifactUpdateEvent, + TaskQueryParams, + TaskIdParams, + TaskPushNotificationConfig, + GetTaskPushNotificationConfigParams, + ListTaskPushNotificationConfigParams, + DeleteTaskPushNotificationConfigParams, +} from "../../types.js"; export interface A2ARequestHandler { getAgentCard(): Promise; + getAuthenticatedExtendedAgentCard(): Promise; + sendMessage( params: MessageSendParams ): Promise; @@ -26,9 +41,17 @@ export interface A2ARequestHandler { ): Promise; getTaskPushNotificationConfig( - params: TaskIdParams + params: TaskIdParams | GetTaskPushNotificationConfigParams ): Promise; + listTaskPushNotificationConfigs( + params: ListTaskPushNotificationConfigParams + ): Promise; + + deleteTaskPushNotificationConfig( + params: DeleteTaskPushNotificationConfigParams + ): Promise; + resubscribe( params: TaskIdParams ): AsyncGenerator< diff --git a/src/server/request_handler/default_request_handler.ts b/src/server/request_handler/default_request_handler.ts index bcec00c..71cf187 100644 --- a/src/server/request_handler/default_request_handler.ts +++ b/src/server/request_handler/default_request_handler.ts @@ -1,6 +1,6 @@ import { v4 as uuidv4 } from 'uuid'; // For generating unique IDs -import { Message, AgentCard, PushNotificationConfig, Task, MessageSendParams, TaskState, TaskStatusUpdateEvent, TaskArtifactUpdateEvent, TaskQueryParams, TaskIdParams, TaskPushNotificationConfig } from "../../types.js"; +import { Message, AgentCard, PushNotificationConfig, Task, MessageSendParams, TaskState, TaskStatusUpdateEvent, TaskArtifactUpdateEvent, TaskQueryParams, TaskIdParams, TaskPushNotificationConfig, DeleteTaskPushNotificationConfigParams, GetTaskPushNotificationConfigParams, ListTaskPushNotificationConfigParams } from "../../types.js"; import { AgentExecutor } from "../agent_execution/agent_executor.js"; import { RequestContext } from "../agent_execution/request_context.js"; import { A2AError } from "../error.js"; @@ -15,11 +15,12 @@ const terminalStates: TaskState[] = ["completed", "failed", "canceled", "rejecte export class DefaultRequestHandler implements A2ARequestHandler { private readonly agentCard: AgentCard; + private readonly extendedAgentCard?: AgentCard; private readonly taskStore: TaskStore; private readonly agentExecutor: AgentExecutor; private readonly eventBusManager: ExecutionEventBusManager; // Store for push notification configurations (could be part of TaskStore or separate) - private readonly pushNotificationConfigs: Map = new Map(); + private readonly pushNotificationConfigs: Map = new Map(); constructor( @@ -27,17 +28,27 @@ export class DefaultRequestHandler implements A2ARequestHandler { taskStore: TaskStore, agentExecutor: AgentExecutor, eventBusManager: ExecutionEventBusManager = new DefaultExecutionEventBusManager(), + extendedAgentCard?: AgentCard, ) { this.agentCard = agentCard; this.taskStore = taskStore; this.agentExecutor = agentExecutor; this.eventBusManager = eventBusManager; + this.extendedAgentCard = extendedAgentCard; } async getAgentCard(): Promise { return this.agentCard; } + async getAuthenticatedExtendedAgentCard(): Promise { + if(!this.extendedAgentCard) { + throw A2AError.authenticatedExtendedCardNotConfigured() + } + + return this.extendedAgentCard; + } + private async _createRequestContext( incomingMessage: Message, taskId: string, @@ -73,12 +84,12 @@ export class DefaultRequestHandler implements A2ARequestHandler { } // Ensure contextId is present - const messageForContext = { ...incomingMessage }; - if (!messageForContext.contextId) { - messageForContext.contextId = task?.contextId || uuidv4(); - } + const contextId = incomingMessage.contextId || task?.contextId || uuidv4(); - const contextId = incomingMessage.contextId || uuidv4(); + const messageForContext = { + ...incomingMessage, + contextId, + }; return new RequestContext( messageForContext, @@ -339,32 +350,107 @@ export class DefaultRequestHandler implements A2ARequestHandler { if (!this.agentCard.capabilities.pushNotifications) { throw A2AError.pushNotificationNotSupported(); } - const taskAndHistory = await this.taskStore.load(params.taskId); - if (!taskAndHistory) { + const task = await this.taskStore.load(params.taskId); + if (!task) { throw A2AError.taskNotFound(params.taskId); } - // Store the config. In a real app, this might be stored in the TaskStore - // or a dedicated push notification service. - this.pushNotificationConfigs.set(params.taskId, params.pushNotificationConfig); + + const { taskId, pushNotificationConfig } = params; + + // Default the config ID to the task ID if not provided for backward compatibility. + if (!pushNotificationConfig.id) { + pushNotificationConfig.id = taskId; + } + + const configs = this.pushNotificationConfigs.get(taskId) || []; + + // Remove existing config with the same ID to replace it + const updatedConfigs = configs.filter(c => c.id !== pushNotificationConfig.id); + + updatedConfigs.push(pushNotificationConfig); + + this.pushNotificationConfigs.set(taskId, updatedConfigs); + return params; } async getTaskPushNotificationConfig( - params: TaskIdParams + params: TaskIdParams | GetTaskPushNotificationConfigParams ): Promise { if (!this.agentCard.capabilities.pushNotifications) { throw A2AError.pushNotificationNotSupported(); } - const taskAndHistory = await this.taskStore.load(params.id); // Ensure task exists - if (!taskAndHistory) { + const task = await this.taskStore.load(params.id); + if (!task) { throw A2AError.taskNotFound(params.id); } - const config = this.pushNotificationConfigs.get(params.id); - if (!config) { + + const configs = this.pushNotificationConfigs.get(params.id) || []; + if (configs.length === 0) { throw A2AError.internalError(`Push notification config not found for task ${params.id}.`); } + + let configId: string; + if ('pushNotificationConfigId' in params && params.pushNotificationConfigId) { + configId = params.pushNotificationConfigId; + } else { + // For backward compatibility, if no config ID is given, assume it's the task ID. + configId = params.id; + } + + const config = configs.find(c => c.id === configId); + + if (!config) { + throw A2AError.internalError(`Push notification config with id '${configId}' not found for task ${params.id}.`); + } return { taskId: params.id, pushNotificationConfig: config }; } + + async listTaskPushNotificationConfigs( + params: ListTaskPushNotificationConfigParams + ): Promise { + if (!this.agentCard.capabilities.pushNotifications) { + throw A2AError.pushNotificationNotSupported(); + } + const task = await this.taskStore.load(params.id); + if (!task) { + throw A2AError.taskNotFound(params.id); + } + + const configs = this.pushNotificationConfigs.get(params.id) || []; + + return configs.map(config => ({ + taskId: params.id, + pushNotificationConfig: config, + })); + } + + async deleteTaskPushNotificationConfig( + params: DeleteTaskPushNotificationConfigParams + ): Promise { + if (!this.agentCard.capabilities.pushNotifications) { + throw A2AError.pushNotificationNotSupported(); + } + const task = await this.taskStore.load(params.id); + if (!task) { + throw A2AError.taskNotFound(params.id); + } + + const { id: taskId, pushNotificationConfigId } = params; + + const configs = this.pushNotificationConfigs.get(taskId); + if (!configs) { + return; + } + + const updatedConfigs = configs.filter(c => c.id !== pushNotificationConfigId); + + if (updatedConfigs.length === 0) { + this.pushNotificationConfigs.delete(taskId); + } else if (updatedConfigs.length < configs.length) { + this.pushNotificationConfigs.set(taskId, updatedConfigs); + } + } async *resubscribe( params: TaskIdParams diff --git a/src/server/transports/jsonrpc_transport_handler.ts b/src/server/transports/jsonrpc_transport_handler.ts index 618e17e..eed545b 100644 --- a/src/server/transports/jsonrpc_transport_handler.ts +++ b/src/server/transports/jsonrpc_transport_handler.ts @@ -1,5 +1,4 @@ -import { A2AResponse } from "../../a2a_response.js"; -import { JSONRPCRequest, JSONRPCErrorResponse, MessageSendParams, TaskQueryParams, TaskIdParams, TaskPushNotificationConfig, JSONRPCSuccessResponse, SendStreamingMessageSuccessResponse, A2ARequest } from "../../types.js"; +import { JSONRPCErrorResponse, MessageSendParams, TaskQueryParams, TaskIdParams, TaskPushNotificationConfig, A2ARequest, JSONRPCResponse, DeleteTaskPushNotificationConfigParams, ListTaskPushNotificationConfigParams } from "../../types.js"; import { A2AError } from "../error.js"; import { A2ARequestHandler } from "../request_handler/a2a_request_handler.js"; @@ -20,7 +19,7 @@ export class JsonRpcTransportHandler { */ public async handle( requestBody: any - ): Promise> { + ): Promise> { let rpcRequest: A2ARequest; try { @@ -50,10 +49,24 @@ export class JsonRpcTransportHandler { } as JSONRPCErrorResponse; } - const { method, params = {}, id: requestId = null } = rpcRequest; + const { method, id: requestId = null } = rpcRequest; try { + if(method === 'agent/getAuthenticatedExtendedCard') { + const result = await this.requestHandler.getAuthenticatedExtendedAgentCard(); + return { + jsonrpc: '2.0', + id: requestId, + result: result, + } as JSONRPCResponse; + } + + if (!rpcRequest.params) { + throw A2AError.invalidParams(`'params' is required for '${method}'`); + } + if (method === 'message/stream' || method === 'tasks/resubscribe') { + const params = rpcRequest.params; const agentCard = await this.requestHandler.getAgentCard(); if (!agentCard.capabilities.streaming) { throw A2AError.unsupportedOperation(`Method ${method} requires streaming capability.`); @@ -63,7 +76,7 @@ export class JsonRpcTransportHandler { : this.requestHandler.resubscribe(params as TaskIdParams); // Wrap the agent event stream into a JSON-RPC result stream - return (async function* jsonRpcEventStream(): AsyncGenerator { + return (async function* jsonRpcEventStream(): AsyncGenerator { try { for await (const event of agentEventStream) { yield { @@ -89,22 +102,33 @@ export class JsonRpcTransportHandler { let result: any; switch (method) { case 'message/send': - result = await this.requestHandler.sendMessage(params as MessageSendParams); + result = await this.requestHandler.sendMessage(rpcRequest.params); break; case 'tasks/get': - result = await this.requestHandler.getTask(params as TaskQueryParams); + result = await this.requestHandler.getTask(rpcRequest.params); break; case 'tasks/cancel': - result = await this.requestHandler.cancelTask(params as TaskIdParams); + result = await this.requestHandler.cancelTask(rpcRequest.params); break; case 'tasks/pushNotificationConfig/set': result = await this.requestHandler.setTaskPushNotificationConfig( - params as TaskPushNotificationConfig + rpcRequest.params ); break; case 'tasks/pushNotificationConfig/get': result = await this.requestHandler.getTaskPushNotificationConfig( - params as TaskIdParams + rpcRequest.params + ); + break; + case 'tasks/pushNotificationConfig/delete': + await this.requestHandler.deleteTaskPushNotificationConfig( + rpcRequest.params + ); + result = null; + break; + case 'tasks/pushNotificationConfig/list': + result = await this.requestHandler.listTaskPushNotificationConfigs( + rpcRequest.params ); break; default: @@ -114,7 +138,7 @@ export class JsonRpcTransportHandler { jsonrpc: '2.0', id: requestId, result: result, - } as A2AResponse; + } as JSONRPCResponse; } } catch (error: any) { const a2aError = error instanceof A2AError ? error : A2AError.internalError(error.message || 'An unexpected error occurred.'); diff --git a/src/types.ts b/src/types.ts index f53bc11..bbcf065 100644 --- a/src/types.ts +++ b/src/types.ts @@ -6,6 +6,8 @@ */ /** + * A discriminated union of all standard JSON-RPC and A2A-specific error types. + * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "A2AError". */ @@ -20,9 +22,10 @@ export type A2AError = | PushNotificationNotSupportedError | UnsupportedOperationError | ContentTypeNotSupportedError - | InvalidAgentResponseError; + | InvalidAgentResponseError + | AuthenticatedExtendedCardNotConfiguredError; /** - * A2A supported request types + * A discriminated union representing all possible JSON-RPC 2.0 requests supported by the A2A specification. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "A2ARequest". @@ -34,17 +37,21 @@ export type A2ARequest = | CancelTaskRequest | SetTaskPushNotificationConfigRequest | GetTaskPushNotificationConfigRequest - | TaskResubscriptionRequest; + | TaskResubscriptionRequest + | ListTaskPushNotificationConfigRequest + | DeleteTaskPushNotificationConfigRequest + | GetAuthenticatedExtendedCardRequest; /** - * Represents a part of a message, which can be text, a file, or structured data. + * A discriminated union representing a part of a message or artifact, which can + * be text, a file, or structured data. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "Part". */ export type Part = TextPart | FilePart | DataPart; /** - * Mirrors the OpenAPI Security Scheme Object - * (https://swagger.io/specification/#security-scheme-object) + * Defines a security scheme that can be used to secure an agent's endpoints. + * This is a discriminated union type based on the OpenAPI 3.0 Security Scheme Object. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SecurityScheme". @@ -53,46 +60,48 @@ export type SecurityScheme = | APIKeySecurityScheme | HTTPAuthSecurityScheme | OAuth2SecurityScheme - | OpenIdConnectSecurityScheme; + | OpenIdConnectSecurityScheme + | MutualTLSSecurityScheme; /** - * JSON-RPC response for the 'tasks/cancel' method. + * Represents a JSON-RPC response for the `tasks/cancel` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "CancelTaskResponse". */ export type CancelTaskResponse = JSONRPCErrorResponse | CancelTaskSuccessResponse; /** - * Represents the possible states of a Task. + * Represents a JSON-RPC response for the `tasks/pushNotificationConfig/delete` method. * * This interface was referenced by `MySchema`'s JSON-Schema - * via the `definition` "TaskState". + * via the `definition` "DeleteTaskPushNotificationConfigResponse". */ -export type TaskState = - | "submitted" - | "working" - | "input-required" - | "completed" - | "canceled" - | "failed" - | "rejected" - | "auth-required" - | "unknown"; +export type DeleteTaskPushNotificationConfigResponse = + | JSONRPCErrorResponse + | DeleteTaskPushNotificationConfigSuccessResponse; /** - * JSON-RPC response for the 'tasks/pushNotificationConfig/set' method. + * Represents a JSON-RPC response for the `agent/getAuthenticatedExtendedCard` method. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "GetAuthenticatedExtendedCardResponse". + */ +export type GetAuthenticatedExtendedCardResponse = JSONRPCErrorResponse | GetAuthenticatedExtendedCardSuccessResponse; +/** + * Represents a JSON-RPC response for the `tasks/pushNotificationConfig/get` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "GetTaskPushNotificationConfigResponse". */ export type GetTaskPushNotificationConfigResponse = JSONRPCErrorResponse | GetTaskPushNotificationConfigSuccessResponse; /** - * JSON-RPC response for the 'tasks/get' method. + * Represents a JSON-RPC response for the `tasks/get` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "GetTaskResponse". */ export type GetTaskResponse = JSONRPCErrorResponse | GetTaskSuccessResponse; /** - * Represents a JSON-RPC 2.0 Response object. + * A discriminated union representing all possible JSON-RPC 2.0 responses + * for the A2A specification methods. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "JSONRPCResponse". @@ -104,767 +113,960 @@ export type JSONRPCResponse = | GetTaskSuccessResponse | CancelTaskSuccessResponse | SetTaskPushNotificationConfigSuccessResponse - | GetTaskPushNotificationConfigSuccessResponse; + | GetTaskPushNotificationConfigSuccessResponse + | ListTaskPushNotificationConfigSuccessResponse + | DeleteTaskPushNotificationConfigSuccessResponse + | GetAuthenticatedExtendedCardSuccessResponse; +/** + * Represents a JSON-RPC response for the `tasks/pushNotificationConfig/list` method. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "ListTaskPushNotificationConfigResponse". + */ +export type ListTaskPushNotificationConfigResponse = + | JSONRPCErrorResponse + | ListTaskPushNotificationConfigSuccessResponse; /** - * JSON-RPC response model for the 'message/send' method. + * Represents a JSON-RPC response for the `message/send` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SendMessageResponse". */ export type SendMessageResponse = JSONRPCErrorResponse | SendMessageSuccessResponse; /** - * JSON-RPC response model for the 'message/stream' method. + * Represents a JSON-RPC response for the `message/stream` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SendStreamingMessageResponse". */ export type SendStreamingMessageResponse = JSONRPCErrorResponse | SendStreamingMessageSuccessResponse; /** - * JSON-RPC response for the 'tasks/pushNotificationConfig/set' method. + * Represents a JSON-RPC response for the `tasks/pushNotificationConfig/set` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SetTaskPushNotificationConfigResponse". */ export type SetTaskPushNotificationConfigResponse = JSONRPCErrorResponse | SetTaskPushNotificationConfigSuccessResponse; +/** + * Defines the lifecycle states of a Task. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "TaskState". + */ +export type TaskState = + | "submitted" + | "working" + | "input-required" + | "completed" + | "canceled" + | "failed" + | "rejected" + | "auth-required" + | "unknown"; +/** + * Supported A2A transport protocols. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "TransportProtocol". + */ +export type TransportProtocol = "JSONRPC" | "GRPC" | "HTTP+JSON"; export interface MySchema { [k: string]: unknown; } /** - * JSON-RPC error indicating invalid JSON was received by the server. + * An error indicating that the server received invalid JSON. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "JSONParseError". */ export interface JSONParseError { /** - * A Number that indicates the error type that occurred. + * The error code for a JSON parse error. */ code: -32700; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * JSON-RPC error indicating the JSON sent is not a valid Request object. + * An error indicating that the JSON sent is not a valid Request object. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "InvalidRequestError". */ export interface InvalidRequestError { /** - * A Number that indicates the error type that occurred. + * The error code for an invalid request. */ code: -32600; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * JSON-RPC error indicating the method does not exist or is not available. + * An error indicating that the requested method does not exist or is not available. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "MethodNotFoundError". */ export interface MethodNotFoundError { /** - * A Number that indicates the error type that occurred. + * The error code for a method not found error. */ code: -32601; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * JSON-RPC error indicating invalid method parameter(s). + * An error indicating that the method parameters are invalid. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "InvalidParamsError". */ export interface InvalidParamsError { /** - * A Number that indicates the error type that occurred. + * The error code for an invalid parameters error. */ code: -32602; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * JSON-RPC error indicating an internal JSON-RPC error on the server. + * An error indicating an internal error on the server. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "InternalError". */ export interface InternalError { /** - * A Number that indicates the error type that occurred. + * The error code for an internal server error. */ code: -32603; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * A2A specific error indicating the requested task ID was not found. + * An A2A-specific error indicating that the requested task ID was not found. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "TaskNotFoundError". */ export interface TaskNotFoundError { /** - * A Number that indicates the error type that occurred. + * The error code for a task not found error. */ code: -32001; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * A2A specific error indicating the task is in a state where it cannot be canceled. + * An A2A-specific error indicating that the task is in a state where it cannot be canceled. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "TaskNotCancelableError". */ export interface TaskNotCancelableError { /** - * A Number that indicates the error type that occurred. + * The error code for a task that cannot be canceled. */ code: -32002; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * A2A specific error indicating the agent does not support push notifications. + * An A2A-specific error indicating that the agent does not support push notifications. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "PushNotificationNotSupportedError". */ export interface PushNotificationNotSupportedError { /** - * A Number that indicates the error type that occurred. + * The error code for when push notifications are not supported. */ code: -32003; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * A2A specific error indicating the requested operation is not supported by the agent. + * An A2A-specific error indicating that the requested operation is not supported by the agent. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "UnsupportedOperationError". */ export interface UnsupportedOperationError { /** - * A Number that indicates the error type that occurred. + * The error code for an unsupported operation. */ code: -32004; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * A2A specific error indicating incompatible content types between request and agent capabilities. + * An A2A-specific error indicating an incompatibility between the requested + * content types and the agent's capabilities. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "ContentTypeNotSupportedError". */ export interface ContentTypeNotSupportedError { /** - * A Number that indicates the error type that occurred. + * The error code for an unsupported content type. */ code: -32005; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * A2A specific error indicating agent returned invalid response for the current method + * An A2A-specific error indicating that the agent returned a response that + * does not conform to the specification for the current method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "InvalidAgentResponseError". */ export interface InvalidAgentResponseError { /** - * A Number that indicates the error type that occurred. + * The error code for an invalid agent response. */ code: -32006; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * The error message. */ message: string; } /** - * JSON-RPC request model for the 'message/send' method. + * An A2A-specific error indicating that the agent does not have an Authenticated Extended Card configured + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "AuthenticatedExtendedCardNotConfiguredError". + */ +export interface AuthenticatedExtendedCardNotConfiguredError { + /** + * The error code for when an authenticated extended card is not configured. + */ + code: -32007; + /** + * A primitive or structured value containing additional information about the error. + * This may be omitted. + */ + data?: { + [k: string]: unknown; + }; + /** + * The error message. + */ + message: string; +} +/** + * Represents a JSON-RPC request for the `message/send` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SendMessageRequest". */ export interface SendMessageRequest { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier for this request. */ id: string | number; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * A String containing the name of the method to be invoked. + * The method name. Must be 'message/send'. */ method: "message/send"; params: MessageSendParams; } /** - * A Structured value that holds the parameter values to be used during the invocation of the method. + * The parameters for sending a message. */ export interface MessageSendParams { configuration?: MessageSendConfiguration; message: Message; /** - * Extension metadata. + * Optional metadata for extensions. */ metadata?: { [k: string]: unknown; }; } /** - * Send message configuration. + * Optional configuration for the send request. */ export interface MessageSendConfiguration { /** - * Accepted output modalities by the client. + * A list of output MIME types the client is prepared to accept in the response. */ - acceptedOutputModes: string[]; + acceptedOutputModes?: string[]; /** - * If the server should treat the client as a blocking request. + * If true, the client will wait for the task to complete. The server may reject this if the task is long-running. */ blocking?: boolean; /** - * Number of recent messages to be retrieved. + * The number of most recent messages from the task's history to retrieve in the response. */ historyLength?: number; pushNotificationConfig?: PushNotificationConfig; } /** - * Where the server should send notifications when disconnected. + * Configuration for the agent to send push notifications for updates after the initial response. */ export interface PushNotificationConfig { authentication?: PushNotificationAuthenticationInfo; /** - * Push Notification ID - created by server to support multiple callbacks + * A unique ID for the push notification configuration, set by the client + * to support multiple notification callbacks. */ id?: string; /** - * Token unique to this task/session. + * A unique token for this task or session to validate incoming push notifications. */ token?: string; /** - * URL for sending the push notifications. + * The callback URL where the agent should send push notifications. */ url: string; } /** - * Defines authentication details for push notifications. - * - * This interface was referenced by `MySchema`'s JSON-Schema - * via the `definition` "PushNotificationAuthenticationInfo". + * Optional authentication details for the agent to use when calling the notification URL. */ export interface PushNotificationAuthenticationInfo { /** - * Optional credentials + * Optional credentials required by the push notification endpoint. */ credentials?: string; /** - * Supported authentication schemes - e.g. Basic, Bearer + * A list of supported authentication schemes (e.g., 'Basic', 'Bearer'). */ schemes: string[]; } /** - * The message being sent to the server. + * The message object being sent to the agent. */ export interface Message { /** - * The context the message is associated with + * The context identifier for this message, used to group related interactions. */ contextId?: string; /** - * The URIs of extensions that are present or contributed to this Message. + * The URIs of extensions that are relevant to this message. */ extensions?: string[]; /** - * Event type + * The type of this object, used as a discriminator. Always 'message' for a Message. */ kind: "message"; /** - * Identifier created by the message creator + * A unique identifier for the message, typically a UUID, generated by the sender. */ messageId: string; /** - * Extension metadata. + * Optional metadata for extensions. The key is an extension-specific identifier. */ metadata?: { [k: string]: unknown; }; /** - * Message content + * An array of content parts that form the message body. A message can be + * composed of multiple parts of different types (e.g., text and files). */ parts: Part[]; /** - * List of tasks referenced as context by this message. + * A list of other task IDs that this message references for additional context. */ referenceTaskIds?: string[]; /** - * Message sender's role + * Identifies the sender of the message. `user` for the client, `agent` for the service. */ role: "agent" | "user"; /** - * Identifier of task the message is related to + * The identifier of the task this message is part of. Can be omitted for the first message of a new task. */ taskId?: string; } /** - * Represents a text segment within parts. + * Represents a text segment within a message or artifact. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "TextPart". */ export interface TextPart { /** - * Part type - text for TextParts + * The type of this part, used as a discriminator. Always 'text'. */ kind: "text"; /** - * Optional metadata associated with the part. + * Optional metadata associated with this part. */ metadata?: { [k: string]: unknown; }; /** - * Text content + * The string content of the text part. */ text: string; } /** - * Represents a File segment within parts. + * Represents a file segment within a message or artifact. The file content can be + * provided either directly as bytes or as a URI. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "FilePart". */ export interface FilePart { /** - * File content either as url or bytes + * The file content, represented as either a URI or as base64-encoded bytes. */ file: FileWithBytes | FileWithUri; /** - * Part type - file for FileParts + * The type of this part, used as a discriminator. Always 'file'. */ kind: "file"; /** - * Optional metadata associated with the part. + * Optional metadata associated with this part. */ metadata?: { [k: string]: unknown; }; } /** - * Define the variant where 'bytes' is present and 'uri' is absent + * Represents a file with its content provided directly as a base64-encoded string. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "FileWithBytes". */ export interface FileWithBytes { /** - * base64 encoded content of the file + * The base64-encoded content of the file. */ bytes: string; /** - * Optional mimeType for the file + * The MIME type of the file (e.g., "application/pdf"). */ mimeType?: string; /** - * Optional name for the file + * An optional name for the file (e.g., "document.pdf"). */ name?: string; } /** - * Define the variant where 'uri' is present and 'bytes' is absent + * Represents a file with its content located at a specific URI. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "FileWithUri". */ export interface FileWithUri { /** - * Optional mimeType for the file + * The MIME type of the file (e.g., "application/pdf"). */ mimeType?: string; /** - * Optional name for the file + * An optional name for the file (e.g., "document.pdf"). */ name?: string; /** - * URL for the File content + * A URL pointing to the file's content. */ uri: string; } /** - * Represents a structured data segment within a message part. + * Represents a structured data segment (e.g., JSON) within a message or artifact. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "DataPart". */ export interface DataPart { /** - * Structured data content + * The structured data content. */ data: { [k: string]: unknown; }; /** - * Part type - data for DataParts + * The type of this part, used as a discriminator. Always 'data'. */ kind: "data"; /** - * Optional metadata associated with the part. + * Optional metadata associated with this part. */ metadata?: { [k: string]: unknown; }; } /** - * JSON-RPC request model for the 'message/stream' method. + * Represents a JSON-RPC request for the `message/stream` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SendStreamingMessageRequest". */ export interface SendStreamingMessageRequest { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier for this request. */ id: string | number; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * A String containing the name of the method to be invoked. + * The method name. Must be 'message/stream'. */ method: "message/stream"; params: MessageSendParams1; } /** - * A Structured value that holds the parameter values to be used during the invocation of the method. + * The parameters for sending a message. */ export interface MessageSendParams1 { configuration?: MessageSendConfiguration; message: Message; /** - * Extension metadata. + * Optional metadata for extensions. */ metadata?: { [k: string]: unknown; }; } /** - * JSON-RPC request model for the 'tasks/get' method. + * Represents a JSON-RPC request for the `tasks/get` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "GetTaskRequest". */ export interface GetTaskRequest { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier for this request. */ id: string | number; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * A String containing the name of the method to be invoked. + * The method name. Must be 'tasks/get'. */ method: "tasks/get"; params: TaskQueryParams; } /** - * A Structured value that holds the parameter values to be used during the invocation of the method. + * The parameters for querying a task. */ export interface TaskQueryParams { /** - * Number of recent messages to be retrieved. + * The number of most recent messages from the task's history to retrieve. */ historyLength?: number; /** - * Task id. + * The unique identifier of the task. */ id: string; + /** + * Optional metadata associated with the request. + */ metadata?: { [k: string]: unknown; }; } /** - * JSON-RPC request model for the 'tasks/cancel' method. + * Represents a JSON-RPC request for the `tasks/cancel` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "CancelTaskRequest". */ export interface CancelTaskRequest { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier for this request. */ id: string | number; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * A String containing the name of the method to be invoked. + * The method name. Must be 'tasks/cancel'. */ method: "tasks/cancel"; params: TaskIdParams; } /** - * A Structured value that holds the parameter values to be used during the invocation of the method. + * The parameters identifying the task to cancel. */ export interface TaskIdParams { /** - * Task id. + * The unique identifier of the task. */ id: string; + /** + * Optional metadata associated with the request. + */ metadata?: { [k: string]: unknown; }; } /** - * JSON-RPC request model for the 'tasks/pushNotificationConfig/set' method. + * Represents a JSON-RPC request for the `tasks/pushNotificationConfig/set` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SetTaskPushNotificationConfigRequest". */ export interface SetTaskPushNotificationConfigRequest { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier for this request. */ id: string | number; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * A String containing the name of the method to be invoked. + * The method name. Must be 'tasks/pushNotificationConfig/set'. */ method: "tasks/pushNotificationConfig/set"; params: TaskPushNotificationConfig; } /** - * A Structured value that holds the parameter values to be used during the invocation of the method. + * The parameters for setting the push notification configuration. */ export interface TaskPushNotificationConfig { pushNotificationConfig: PushNotificationConfig1; /** - * Task id. + * The ID of the task. */ taskId: string; } /** - * Push notification configuration. + * The push notification configuration for this task. */ export interface PushNotificationConfig1 { authentication?: PushNotificationAuthenticationInfo; /** - * Push Notification ID - created by server to support multiple callbacks + * A unique ID for the push notification configuration, set by the client + * to support multiple notification callbacks. */ id?: string; /** - * Token unique to this task/session. + * A unique token for this task or session to validate incoming push notifications. */ token?: string; /** - * URL for sending the push notifications. + * The callback URL where the agent should send push notifications. */ url: string; } /** - * JSON-RPC request model for the 'tasks/pushNotificationConfig/get' method. + * Represents a JSON-RPC request for the `tasks/pushNotificationConfig/get` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "GetTaskPushNotificationConfigRequest". */ export interface GetTaskPushNotificationConfigRequest { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier for this request. */ id: string | number; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * A String containing the name of the method to be invoked. + * The method name. Must be 'tasks/pushNotificationConfig/get'. */ method: "tasks/pushNotificationConfig/get"; - params: TaskIdParams1; + /** + * The parameters for getting a push notification configuration. + */ + params: TaskIdParams1 | GetTaskPushNotificationConfigParams; } /** - * A Structured value that holds the parameter values to be used during the invocation of the method. + * Defines parameters containing a task ID, used for simple task operations. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "TaskIdParams". */ export interface TaskIdParams1 { /** - * Task id. + * The unique identifier of the task. + */ + id: string; + /** + * Optional metadata associated with the request. + */ + metadata?: { + [k: string]: unknown; + }; +} +/** + * Defines parameters for fetching a specific push notification configuration for a task. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "GetTaskPushNotificationConfigParams". + */ +export interface GetTaskPushNotificationConfigParams { + /** + * The unique identifier of the task. */ id: string; + /** + * Optional metadata associated with the request. + */ metadata?: { [k: string]: unknown; }; + /** + * The ID of the push notification configuration to retrieve. + */ + pushNotificationConfigId?: string; } /** - * JSON-RPC request model for the 'tasks/resubscribe' method. + * Represents a JSON-RPC request for the `tasks/resubscribe` method, used to resume a streaming connection. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "TaskResubscriptionRequest". */ export interface TaskResubscriptionRequest { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier for this request. */ id: string | number; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * A String containing the name of the method to be invoked. + * The method name. Must be 'tasks/resubscribe'. */ method: "tasks/resubscribe"; params: TaskIdParams2; } /** - * A Structured value that holds the parameter values to be used during the invocation of the method. + * Defines parameters containing a task ID, used for simple task operations. */ export interface TaskIdParams2 { /** - * Task id. + * The unique identifier of the task. + */ + id: string; + /** + * Optional metadata associated with the request. + */ + metadata?: { + [k: string]: unknown; + }; +} +/** + * Represents a JSON-RPC request for the `tasks/pushNotificationConfig/list` method. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "ListTaskPushNotificationConfigRequest". + */ +export interface ListTaskPushNotificationConfigRequest { + /** + * The identifier for this request. + */ + id: string | number; + /** + * The version of the JSON-RPC protocol. MUST be exactly "2.0". + */ + jsonrpc: "2.0"; + /** + * The method name. Must be 'tasks/pushNotificationConfig/list'. + */ + method: "tasks/pushNotificationConfig/list"; + params: ListTaskPushNotificationConfigParams; +} +/** + * The parameters identifying the task whose configurations are to be listed. + */ +export interface ListTaskPushNotificationConfigParams { + /** + * The unique identifier of the task. */ id: string; + /** + * Optional metadata associated with the request. + */ metadata?: { [k: string]: unknown; }; } /** - * API Key security scheme. + * Represents a JSON-RPC request for the `tasks/pushNotificationConfig/delete` method. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "DeleteTaskPushNotificationConfigRequest". + */ +export interface DeleteTaskPushNotificationConfigRequest { + /** + * The identifier for this request. + */ + id: string | number; + /** + * The version of the JSON-RPC protocol. MUST be exactly "2.0". + */ + jsonrpc: "2.0"; + /** + * The method name. Must be 'tasks/pushNotificationConfig/delete'. + */ + method: "tasks/pushNotificationConfig/delete"; + params: DeleteTaskPushNotificationConfigParams; +} +/** + * The parameters identifying the push notification configuration to delete. + */ +export interface DeleteTaskPushNotificationConfigParams { + /** + * The unique identifier of the task. + */ + id: string; + /** + * Optional metadata associated with the request. + */ + metadata?: { + [k: string]: unknown; + }; + /** + * The ID of the push notification configuration to delete. + */ + pushNotificationConfigId: string; +} +/** + * Represents a JSON-RPC request for the `agent/getAuthenticatedExtendedCard` method. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "GetAuthenticatedExtendedCardRequest". + */ +export interface GetAuthenticatedExtendedCardRequest { + /** + * The identifier for this request. + */ + id: string | number; + /** + * The version of the JSON-RPC protocol. MUST be exactly "2.0". + */ + jsonrpc: "2.0"; + /** + * The method name. Must be 'agent/getAuthenticatedExtendedCard'. + */ + method: "agent/getAuthenticatedExtendedCard"; +} +/** + * Defines a security scheme using an API key. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "APIKeySecurityScheme". */ export interface APIKeySecurityScheme { /** - * Description of this security scheme. + * An optional description for the security scheme. */ description?: string; /** - * The location of the API key. Valid values are "query", "header", or "cookie". + * The location of the API key. */ in: "cookie" | "header" | "query"; /** - * The name of the header, query or cookie parameter to be used. + * The name of the header, query, or cookie parameter to be used. */ name: string; + /** + * The type of the security scheme. Must be 'apiKey'. + */ type: "apiKey"; } /** @@ -875,192 +1077,256 @@ export interface APIKeySecurityScheme { */ export interface AgentCapabilities { /** - * extensions supported by this agent. + * A list of protocol extensions supported by the agent. */ extensions?: AgentExtension[]; /** - * true if the agent can notify updates to client. + * Indicates if the agent supports sending push notifications for asynchronous task updates. */ pushNotifications?: boolean; /** - * true if the agent exposes status change history for tasks. + * Indicates if the agent provides a history of state transitions for a task. */ stateTransitionHistory?: boolean; /** - * true if the agent supports SSE. + * Indicates if the agent supports Server-Sent Events (SSE) for streaming responses. */ streaming?: boolean; } /** - * A declaration of an extension supported by an Agent. + * A declaration of a protocol extension supported by an Agent. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "AgentExtension". */ export interface AgentExtension { /** - * A description of how this agent uses this extension. + * A human-readable description of how this agent uses the extension. */ description?: string; /** - * Optional configuration for the extension. + * Optional, extension-specific configuration parameters. */ params?: { [k: string]: unknown; }; /** - * Whether the client must follow specific requirements of the extension. + * If true, the client must understand and comply with the extension's requirements + * to interact with the agent. */ required?: boolean; /** - * The URI of the extension. + * The unique URI identifying the extension. */ uri: string; } /** - * An AgentCard conveys key information: - * - Overall details (version, name, description, uses) - * - Skills: A set of capabilities the agent can perform - * - Default modalities/content types supported by the agent. - * - Authentication requirements + * The AgentCard is a self-describing manifest for an agent. It provides essential + * metadata including the agent's identity, capabilities, skills, supported + * communication methods, and security requirements. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "AgentCard". */ export interface AgentCard { + /** + * A list of additional supported interfaces (transport and URL combinations). + * This allows agents to expose multiple transports, potentially at different URLs. + * + * Best practices: + * - SHOULD include all supported transports for completeness + * - SHOULD include an entry matching the main 'url' and 'preferredTransport' + * - MAY reuse URLs if multiple transports are available at the same endpoint + * - MUST accurately declare the transport available at each URL + * + * Clients can select any interface from this list based on their transport capabilities + * and preferences. This enables transport negotiation and fallback scenarios. + */ + additionalInterfaces?: AgentInterface[]; capabilities: AgentCapabilities1; /** - * The set of interaction modes that the agent supports across all skills. This can be overridden per-skill. - * Supported media types for input. + * Default set of supported input MIME types for all skills, which can be + * overridden on a per-skill basis. */ defaultInputModes: string[]; /** - * Supported media types for output. + * Default set of supported output MIME types for all skills, which can be + * overridden on a per-skill basis. */ defaultOutputModes: string[]; /** - * A human-readable description of the agent. Used to assist users and - * other agents in understanding what the agent can do. + * A human-readable description of the agent, assisting users and other agents + * in understanding its purpose. */ description: string; /** - * A URL to documentation for the agent. + * An optional URL to the agent's documentation. */ documentationUrl?: string; /** - * A URL to an icon for the agent. + * An optional URL to an icon for the agent. */ iconUrl?: string; /** - * Human readable name of the agent. + * A human-readable name for the agent. */ name: string; + /** + * The transport protocol for the preferred endpoint (the main 'url' field). + * If not specified, defaults to 'JSONRPC'. + * + * IMPORTANT: The transport specified here MUST be available at the main 'url'. + * This creates a binding between the main URL and its supported transport protocol. + * Clients should prefer this transport and URL combination when both are supported. + */ + preferredTransport?: string; + /** + * The version of the A2A protocol this agent supports. + */ + protocolVersion: string; provider?: AgentProvider; /** - * Security requirements for contacting the agent. + * A list of security requirement objects that apply to all agent interactions. Each object + * lists security schemes that can be used. Follows the OpenAPI 3.0 Security Requirement Object. + * This list can be seen as an OR of ANDs. Each object in the list describes one possible + * set of security requirements that must be present on a request. This allows specifying, + * for example, "callers must either use OAuth OR an API Key AND mTLS." */ security?: { [k: string]: string[]; }[]; /** - * Security scheme details used for authenticating with this agent. + * A declaration of the security schemes available to authorize requests. The key is the + * scheme name. Follows the OpenAPI 3.0 Security Scheme Object. */ securitySchemes?: { [k: string]: SecurityScheme; }; /** - * Skills are a unit of capability that an agent can perform. + * JSON Web Signatures computed for this AgentCard. + */ + signatures?: AgentCardSignature[]; + /** + * The set of skills, or distinct capabilities, that the agent can perform. */ skills: AgentSkill[]; /** - * true if the agent supports providing an extended agent card when the user is authenticated. - * Defaults to false if not specified. + * If true, the agent can provide an extended agent card with additional details + * to authenticated users. Defaults to false. */ supportsAuthenticatedExtendedCard?: boolean; /** - * A URL to the address the agent is hosted at. + * The preferred endpoint URL for interacting with the agent. + * This URL MUST support the transport specified by 'preferredTransport'. */ url: string; /** - * The version of the agent - format is up to the provider. + * The agent's own version number. The format is defined by the provider. */ version: string; } /** - * Optional capabilities supported by the agent. + * Declares a combination of a target URL and a transport protocol for interacting with the agent. + * This allows agents to expose the same functionality over multiple transport mechanisms. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "AgentInterface". + */ +export interface AgentInterface { + /** + * The transport protocol supported at this URL. + */ + transport: string; + /** + * The URL where this interface is available. Must be a valid absolute HTTPS URL in production. + */ + url: string; +} +/** + * A declaration of optional capabilities supported by the agent. */ export interface AgentCapabilities1 { /** - * extensions supported by this agent. + * A list of protocol extensions supported by the agent. */ extensions?: AgentExtension[]; /** - * true if the agent can notify updates to client. + * Indicates if the agent supports sending push notifications for asynchronous task updates. */ pushNotifications?: boolean; /** - * true if the agent exposes status change history for tasks. + * Indicates if the agent provides a history of state transitions for a task. */ stateTransitionHistory?: boolean; /** - * true if the agent supports SSE. + * Indicates if the agent supports Server-Sent Events (SSE) for streaming responses. */ streaming?: boolean; } /** - * The service provider of the agent + * Information about the agent's service provider. */ export interface AgentProvider { /** - * Agent provider's organization name. + * The name of the agent provider's organization. */ organization: string; /** - * Agent provider's URL. + * A URL for the agent provider's website or relevant documentation. */ url: string; } /** - * HTTP Authentication security scheme. + * Defines a security scheme using HTTP authentication. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "HTTPAuthSecurityScheme". */ export interface HTTPAuthSecurityScheme { /** - * A hint to the client to identify how the bearer token is formatted. Bearer tokens are usually - * generated by an authorization server, so this information is primarily for documentation - * purposes. + * A hint to the client to identify how the bearer token is formatted (e.g., "JWT"). + * This is primarily for documentation purposes. */ bearerFormat?: string; /** - * Description of this security scheme. + * An optional description for the security scheme. */ description?: string; /** - * The name of the HTTP Authentication scheme to be used in the Authorization header as defined - * in RFC7235. The values used SHOULD be registered in the IANA Authentication Scheme registry. - * The value is case-insensitive, as defined in RFC7235. + * The name of the HTTP Authentication scheme to be used in the Authorization header, + * as defined in RFC7235 (e.g., "Bearer"). + * This value should be registered in the IANA Authentication Scheme registry. */ scheme: string; + /** + * The type of the security scheme. Must be 'http'. + */ type: "http"; } /** - * OAuth2.0 security scheme configuration. + * Defines a security scheme using OAuth 2.0. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "OAuth2SecurityScheme". */ export interface OAuth2SecurityScheme { /** - * Description of this security scheme. + * An optional description for the security scheme. */ description?: string; flows: OAuthFlows; + /** + * URL to the oauth2 authorization server metadata + * [RFC8414](https://datatracker.ietf.org/doc/html/rfc8414). TLS is required. + */ + oauth2MetadataUrl?: string; + /** + * The type of the security scheme. Must be 'oauth2'. + */ type: "oauth2"; } /** - * An object containing configuration information for the flow types supported. + * An object containing configuration information for the supported OAuth 2.0 flows. */ export interface OAuthFlows { authorizationCode?: AuthorizationCodeOAuthFlow; @@ -1073,148 +1339,192 @@ export interface OAuthFlows { */ export interface AuthorizationCodeOAuthFlow { /** - * The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS + * The authorization URL to be used for this flow. + * This MUST be a URL and use TLS. */ authorizationUrl: string; /** - * The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS. + * The URL to be used for obtaining refresh tokens. + * This MUST be a URL and use TLS. */ refreshUrl?: string; /** - * The available scopes for the OAuth2 security scheme. A map between the scope name and a short - * description for it. The map MAY be empty. + * The available scopes for the OAuth2 security scheme. A map between the scope + * name and a short description for it. */ scopes: { [k: string]: string; }; /** - * The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard - * requires the use of TLS. + * The token URL to be used for this flow. + * This MUST be a URL and use TLS. */ tokenUrl: string; } /** - * Configuration for the OAuth Client Credentials flow. Previously called application in OpenAPI 2.0 + * Configuration for the OAuth Client Credentials flow. Previously called application in OpenAPI 2.0. */ export interface ClientCredentialsOAuthFlow { /** - * The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS. + * The URL to be used for obtaining refresh tokens. This MUST be a URL. */ refreshUrl?: string; /** - * The available scopes for the OAuth2 security scheme. A map between the scope name and a short - * description for it. The map MAY be empty. + * The available scopes for the OAuth2 security scheme. A map between the scope + * name and a short description for it. */ scopes: { [k: string]: string; }; /** - * The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard - * requires the use of TLS. + * The token URL to be used for this flow. This MUST be a URL. */ tokenUrl: string; } /** - * Configuration for the OAuth Implicit flow + * Configuration for the OAuth Implicit flow. */ export interface ImplicitOAuthFlow { /** - * The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS + * The authorization URL to be used for this flow. This MUST be a URL. */ authorizationUrl: string; /** - * The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS. + * The URL to be used for obtaining refresh tokens. This MUST be a URL. */ refreshUrl?: string; /** - * The available scopes for the OAuth2 security scheme. A map between the scope name and a short - * description for it. The map MAY be empty. + * The available scopes for the OAuth2 security scheme. A map between the scope + * name and a short description for it. */ scopes: { [k: string]: string; }; } /** - * Configuration for the OAuth Resource Owner Password flow + * Configuration for the OAuth Resource Owner Password flow. */ export interface PasswordOAuthFlow { /** - * The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS. + * The URL to be used for obtaining refresh tokens. This MUST be a URL. */ refreshUrl?: string; /** - * The available scopes for the OAuth2 security scheme. A map between the scope name and a short - * description for it. The map MAY be empty. + * The available scopes for the OAuth2 security scheme. A map between the scope + * name and a short description for it. */ scopes: { [k: string]: string; }; /** - * The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard - * requires the use of TLS. + * The token URL to be used for this flow. This MUST be a URL. */ tokenUrl: string; } /** - * OpenID Connect security scheme configuration. + * Defines a security scheme using OpenID Connect. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "OpenIdConnectSecurityScheme". */ export interface OpenIdConnectSecurityScheme { /** - * Description of this security scheme. + * An optional description for the security scheme. */ description?: string; /** - * Well-known URL to discover the [[OpenID-Connect-Discovery]] provider metadata. + * The OpenID Connect Discovery URL for the OIDC provider's metadata. */ openIdConnectUrl: string; + /** + * The type of the security scheme. Must be 'openIdConnect'. + */ type: "openIdConnect"; } /** - * Represents a unit of capability that an agent can perform. + * Defines a security scheme using mTLS authentication. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "MutualTLSSecurityScheme". + */ +export interface MutualTLSSecurityScheme { + /** + * An optional description for the security scheme. + */ + description?: string; + /** + * The type of the security scheme. Must be 'mutualTLS'. + */ + type: "mutualTLS"; +} +/** + * AgentCardSignature represents a JWS signature of an AgentCard. + * This follows the JSON format of an RFC 7515 JSON Web Signature (JWS). + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "AgentCardSignature". + */ +export interface AgentCardSignature { + /** + * The unprotected JWS header values. + */ + header?: { + [k: string]: unknown; + }; + /** + * The protected JWS header for the signature. This is a Base64url-encoded + * JSON object, as per RFC 7515. + */ + protected: string; + /** + * The computed signature, Base64url-encoded. + */ + signature: string; +} +/** + * Represents a distinct capability or function that an agent can perform. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "AgentSkill". */ export interface AgentSkill { /** - * Description of the skill - will be used by the client or a human - * as a hint to understand what the skill does. + * A detailed description of the skill, intended to help clients or users + * understand its purpose and functionality. */ description: string; /** - * The set of example scenarios that the skill can perform. - * Will be used by the client as a hint to understand how the skill can be used. + * Example prompts or scenarios that this skill can handle. Provides a hint to + * the client on how to use the skill. */ examples?: string[]; /** - * Unique identifier for the agent's skill. + * A unique identifier for the agent's skill. */ id: string; /** - * The set of interaction modes that the skill supports - * (if different than the default). - * Supported media types for input. + * The set of supported input MIME types for this skill, overriding the agent's defaults. */ inputModes?: string[]; /** - * Human readable name of the skill. + * A human-readable name for the skill. */ name: string; /** - * Supported media types for output. + * The set of supported output MIME types for this skill, overriding the agent's defaults. */ outputModes?: string[]; /** - * Set of tagwords describing classes of capabilities for this specific skill. + * Security schemes necessary for the agent to leverage this skill. + * As in the overall AgentCard.security, this list represents a logical OR of security + * requirement objects. Each object is a set of security schemes that must be used together + * (a logical AND). + */ + security?: { + [k: string]: string[]; + }[]; + /** + * A set of keywords describing the skill's capabilities. */ tags: string[]; } @@ -1226,75 +1536,75 @@ export interface AgentSkill { */ export interface AgentProvider1 { /** - * Agent provider's organization name. + * The name of the agent provider's organization. */ organization: string; /** - * Agent provider's URL. + * A URL for the agent provider's website or relevant documentation. */ url: string; } /** - * Represents an artifact generated for a task. + * Represents a file, data structure, or other resource generated by an agent during a task. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "Artifact". */ export interface Artifact { /** - * Unique identifier for the artifact. + * A unique identifier for the artifact within the scope of the task. */ artifactId: string; /** - * Optional description for the artifact. + * An optional, human-readable description of the artifact. */ description?: string; /** - * The URIs of extensions that are present or contributed to this Artifact. + * The URIs of extensions that are relevant to this artifact. */ extensions?: string[]; /** - * Extension metadata. + * Optional metadata for extensions. The key is an extension-specific identifier. */ metadata?: { [k: string]: unknown; }; /** - * Optional name for the artifact. + * An optional, human-readable name for the artifact. */ name?: string; /** - * Artifact parts. + * An array of content parts that make up the artifact. */ parts: Part[]; } /** - * Configuration details for a supported OAuth Flow + * Defines configuration details for the OAuth 2.0 Authorization Code flow. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "AuthorizationCodeOAuthFlow". */ export interface AuthorizationCodeOAuthFlow1 { /** - * The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS + * The authorization URL to be used for this flow. + * This MUST be a URL and use TLS. */ authorizationUrl: string; /** - * The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS. + * The URL to be used for obtaining refresh tokens. + * This MUST be a URL and use TLS. */ refreshUrl?: string; /** - * The available scopes for the OAuth2 security scheme. A map between the scope name and a short - * description for it. The map MAY be empty. + * The available scopes for the OAuth2 security scheme. A map between the scope + * name and a short description for it. */ scopes: { [k: string]: string; }; /** - * The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard - * requires the use of TLS. + * The token URL to be used for this flow. + * This MUST be a URL and use TLS. */ tokenUrl: string; } @@ -1305,6 +1615,9 @@ export interface AuthorizationCodeOAuthFlow1 { * via the `definition` "JSONRPCErrorResponse". */ export interface JSONRPCErrorResponse { + /** + * An object describing the error that occurred. + */ error: | JSONRPCError | JSONParseError @@ -1317,82 +1630,83 @@ export interface JSONRPCErrorResponse { | PushNotificationNotSupportedError | UnsupportedOperationError | ContentTypeNotSupportedError - | InvalidAgentResponseError; + | InvalidAgentResponseError + | AuthenticatedExtendedCardNotConfiguredError; /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier established by the client. */ id: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; } /** - * Represents a JSON-RPC 2.0 Error object. - * This is typically included in a JSONRPCErrorResponse when an error occurs. + * Represents a JSON-RPC 2.0 Error object, included in an error response. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "JSONRPCError". */ export interface JSONRPCError { /** - * A Number that indicates the error type that occurred. + * A number that indicates the error type that occurred. */ code: number; /** - * A Primitive or Structured value that contains additional information about the error. + * A primitive or structured value containing additional information about the error. * This may be omitted. */ data?: { [k: string]: unknown; }; /** - * A String providing a short description of the error. + * A string providing a short description of the error. */ message: string; } /** - * JSON-RPC success response model for the 'tasks/cancel' method. + * Represents a successful JSON-RPC response for the `tasks/cancel` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "CancelTaskSuccessResponse". */ export interface CancelTaskSuccessResponse { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier established by the client. */ id: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; result: Task; } /** - * The result object on success. + * The result, containing the final state of the canceled Task object. */ export interface Task { /** - * Collection of artifacts created by the agent. + * A collection of artifacts generated by the agent during the execution of the task. */ artifacts?: Artifact[]; /** - * Server-generated id for contextual alignment across interactions + * A server-generated identifier for maintaining context across multiple related tasks or interactions. */ contextId: string; + /** + * An array of messages exchanged during the task, representing the conversation history. + */ history?: Message1[]; /** - * Unique identifier for the task + * A unique identifier for the task, generated by the server for a new task. */ id: string; /** - * Event type + * The type of this object, used as a discriminator. Always 'task' for a Task. */ kind: "task"; /** - * Extension metadata. + * Optional metadata for extensions. The key is an extension-specific identifier. */ metadata?: { [k: string]: unknown; @@ -1400,215 +1714,387 @@ export interface Task { status: TaskStatus; } /** - * Represents a single message exchanged between user and agent. + * Represents a single message in the conversation between a user and an agent. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "Message". */ export interface Message1 { /** - * The context the message is associated with + * The context identifier for this message, used to group related interactions. */ contextId?: string; /** - * The URIs of extensions that are present or contributed to this Message. + * The URIs of extensions that are relevant to this message. */ extensions?: string[]; /** - * Event type + * The type of this object, used as a discriminator. Always 'message' for a Message. */ kind: "message"; /** - * Identifier created by the message creator + * A unique identifier for the message, typically a UUID, generated by the sender. */ messageId: string; /** - * Extension metadata. + * Optional metadata for extensions. The key is an extension-specific identifier. */ metadata?: { [k: string]: unknown; }; /** - * Message content + * An array of content parts that form the message body. A message can be + * composed of multiple parts of different types (e.g., text and files). */ parts: Part[]; /** - * List of tasks referenced as context by this message. + * A list of other task IDs that this message references for additional context. */ referenceTaskIds?: string[]; /** - * Message sender's role + * Identifies the sender of the message. `user` for the client, `agent` for the service. */ role: "agent" | "user"; /** - * Identifier of task the message is related to + * The identifier of the task this message is part of. Can be omitted for the first message of a new task. */ taskId?: string; } /** - * Current status of the task + * The current status of the task, including its state and a descriptive message. */ export interface TaskStatus { message?: Message2; - state: TaskState; /** - * ISO 8601 datetime string when the status was recorded. + * The current state of the task's lifecycle. + */ + state: + | "submitted" + | "working" + | "input-required" + | "completed" + | "canceled" + | "failed" + | "rejected" + | "auth-required" + | "unknown"; + /** + * An ISO 8601 datetime string indicating when this status was recorded. */ timestamp?: string; } /** - * Represents a single message exchanged between user and agent. + * Represents a single message in the conversation between a user and an agent. */ export interface Message2 { /** - * The context the message is associated with + * The context identifier for this message, used to group related interactions. */ contextId?: string; /** - * The URIs of extensions that are present or contributed to this Message. + * The URIs of extensions that are relevant to this message. */ extensions?: string[]; /** - * Event type + * The type of this object, used as a discriminator. Always 'message' for a Message. */ kind: "message"; /** - * Identifier created by the message creator + * A unique identifier for the message, typically a UUID, generated by the sender. */ messageId: string; /** - * Extension metadata. + * Optional metadata for extensions. The key is an extension-specific identifier. */ metadata?: { [k: string]: unknown; }; /** - * Message content + * An array of content parts that form the message body. A message can be + * composed of multiple parts of different types (e.g., text and files). */ parts: Part[]; /** - * List of tasks referenced as context by this message. + * A list of other task IDs that this message references for additional context. */ referenceTaskIds?: string[]; /** - * Message sender's role + * Identifies the sender of the message. `user` for the client, `agent` for the service. */ role: "agent" | "user"; /** - * Identifier of task the message is related to + * The identifier of the task this message is part of. Can be omitted for the first message of a new task. */ taskId?: string; } /** - * Configuration details for a supported OAuth Flow + * Defines configuration details for the OAuth 2.0 Client Credentials flow. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "ClientCredentialsOAuthFlow". */ export interface ClientCredentialsOAuthFlow1 { /** - * The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS. + * The URL to be used for obtaining refresh tokens. This MUST be a URL. */ refreshUrl?: string; /** - * The available scopes for the OAuth2 security scheme. A map between the scope name and a short - * description for it. The map MAY be empty. + * The available scopes for the OAuth2 security scheme. A map between the scope + * name and a short description for it. */ scopes: { [k: string]: string; }; /** - * The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard - * requires the use of TLS. + * The token URL to be used for this flow. This MUST be a URL. */ tokenUrl: string; } /** - * Represents the base entity for FileParts + * Defines parameters for deleting a specific push notification configuration for a task. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "DeleteTaskPushNotificationConfigParams". + */ +export interface DeleteTaskPushNotificationConfigParams1 { + /** + * The unique identifier of the task. + */ + id: string; + /** + * Optional metadata associated with the request. + */ + metadata?: { + [k: string]: unknown; + }; + /** + * The ID of the push notification configuration to delete. + */ + pushNotificationConfigId: string; +} +/** + * Represents a successful JSON-RPC response for the `tasks/pushNotificationConfig/delete` method. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "DeleteTaskPushNotificationConfigSuccessResponse". + */ +export interface DeleteTaskPushNotificationConfigSuccessResponse { + /** + * The identifier established by the client. + */ + id: string | number | null; + /** + * The version of the JSON-RPC protocol. MUST be exactly "2.0". + */ + jsonrpc: "2.0"; + /** + * The result is null on successful deletion. + */ + result: null; +} +/** + * Defines base properties for a file. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "FileBase". */ export interface FileBase { /** - * Optional mimeType for the file + * The MIME type of the file (e.g., "application/pdf"). */ mimeType?: string; /** - * Optional name for the file + * An optional name for the file (e.g., "document.pdf"). */ name?: string; } /** - * JSON-RPC success response model for the 'tasks/pushNotificationConfig/get' method. + * Represents a successful JSON-RPC response for the `agent/getAuthenticatedExtendedCard` method. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "GetAuthenticatedExtendedCardSuccessResponse". + */ +export interface GetAuthenticatedExtendedCardSuccessResponse { + /** + * The identifier established by the client. + */ + id: string | number | null; + /** + * The version of the JSON-RPC protocol. MUST be exactly "2.0". + */ + jsonrpc: "2.0"; + result: AgentCard1; +} +/** + * The result is an Agent Card object. + */ +export interface AgentCard1 { + /** + * A list of additional supported interfaces (transport and URL combinations). + * This allows agents to expose multiple transports, potentially at different URLs. + * + * Best practices: + * - SHOULD include all supported transports for completeness + * - SHOULD include an entry matching the main 'url' and 'preferredTransport' + * - MAY reuse URLs if multiple transports are available at the same endpoint + * - MUST accurately declare the transport available at each URL + * + * Clients can select any interface from this list based on their transport capabilities + * and preferences. This enables transport negotiation and fallback scenarios. + */ + additionalInterfaces?: AgentInterface[]; + capabilities: AgentCapabilities1; + /** + * Default set of supported input MIME types for all skills, which can be + * overridden on a per-skill basis. + */ + defaultInputModes: string[]; + /** + * Default set of supported output MIME types for all skills, which can be + * overridden on a per-skill basis. + */ + defaultOutputModes: string[]; + /** + * A human-readable description of the agent, assisting users and other agents + * in understanding its purpose. + */ + description: string; + /** + * An optional URL to the agent's documentation. + */ + documentationUrl?: string; + /** + * An optional URL to an icon for the agent. + */ + iconUrl?: string; + /** + * A human-readable name for the agent. + */ + name: string; + /** + * The transport protocol for the preferred endpoint (the main 'url' field). + * If not specified, defaults to 'JSONRPC'. + * + * IMPORTANT: The transport specified here MUST be available at the main 'url'. + * This creates a binding between the main URL and its supported transport protocol. + * Clients should prefer this transport and URL combination when both are supported. + */ + preferredTransport?: string; + /** + * The version of the A2A protocol this agent supports. + */ + protocolVersion: string; + provider?: AgentProvider; + /** + * A list of security requirement objects that apply to all agent interactions. Each object + * lists security schemes that can be used. Follows the OpenAPI 3.0 Security Requirement Object. + * This list can be seen as an OR of ANDs. Each object in the list describes one possible + * set of security requirements that must be present on a request. This allows specifying, + * for example, "callers must either use OAuth OR an API Key AND mTLS." + */ + security?: { + [k: string]: string[]; + }[]; + /** + * A declaration of the security schemes available to authorize requests. The key is the + * scheme name. Follows the OpenAPI 3.0 Security Scheme Object. + */ + securitySchemes?: { + [k: string]: SecurityScheme; + }; + /** + * JSON Web Signatures computed for this AgentCard. + */ + signatures?: AgentCardSignature[]; + /** + * The set of skills, or distinct capabilities, that the agent can perform. + */ + skills: AgentSkill[]; + /** + * If true, the agent can provide an extended agent card with additional details + * to authenticated users. Defaults to false. + */ + supportsAuthenticatedExtendedCard?: boolean; + /** + * The preferred endpoint URL for interacting with the agent. + * This URL MUST support the transport specified by 'preferredTransport'. + */ + url: string; + /** + * The agent's own version number. The format is defined by the provider. + */ + version: string; +} +/** + * Represents a successful JSON-RPC response for the `tasks/pushNotificationConfig/get` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "GetTaskPushNotificationConfigSuccessResponse". */ export interface GetTaskPushNotificationConfigSuccessResponse { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier established by the client. */ id: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; result: TaskPushNotificationConfig1; } /** - * The result object on success. + * The result, containing the requested push notification configuration. */ export interface TaskPushNotificationConfig1 { pushNotificationConfig: PushNotificationConfig1; /** - * Task id. + * The ID of the task. */ taskId: string; } /** - * JSON-RPC success response for the 'tasks/get' method. + * Represents a successful JSON-RPC response for the `tasks/get` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "GetTaskSuccessResponse". */ export interface GetTaskSuccessResponse { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier established by the client. */ id: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; result: Task1; } /** - * The result object on success. + * The result, containing the requested Task object. */ export interface Task1 { /** - * Collection of artifacts created by the agent. + * A collection of artifacts generated by the agent during the execution of the task. */ artifacts?: Artifact[]; /** - * Server-generated id for contextual alignment across interactions + * A server-generated identifier for maintaining context across multiple related tasks or interactions. */ contextId: string; + /** + * An array of messages exchanged during the task, representing the conversation history. + */ history?: Message1[]; /** - * Unique identifier for the task + * A unique identifier for the task, generated by the server for a new task. */ id: string; /** - * Event type + * The type of this object, used as a discriminator. Always 'task' for a Task. */ kind: "task"; /** - * Extension metadata. + * Optional metadata for extensions. The key is an extension-specific identifier. */ metadata?: { [k: string]: unknown; @@ -1616,44 +2102,42 @@ export interface Task1 { status: TaskStatus; } /** - * Configuration details for a supported OAuth Flow + * Defines configuration details for the OAuth 2.0 Implicit flow. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "ImplicitOAuthFlow". */ export interface ImplicitOAuthFlow1 { /** - * The authorization URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS + * The authorization URL to be used for this flow. This MUST be a URL. */ authorizationUrl: string; /** - * The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS. + * The URL to be used for obtaining refresh tokens. This MUST be a URL. */ refreshUrl?: string; /** - * The available scopes for the OAuth2 security scheme. A map between the scope name and a short - * description for it. The map MAY be empty. + * The available scopes for the OAuth2 security scheme. A map between the scope + * name and a short description for it. */ scopes: { [k: string]: string; }; } /** - * Base interface for any JSON-RPC 2.0 request or response. + * Defines the base structure for any JSON-RPC 2.0 request, response, or notification. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "JSONRPCMessage". */ export interface JSONRPCMessage { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * 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. */ id?: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; } @@ -1665,70 +2149,74 @@ export interface JSONRPCMessage { */ export interface JSONRPCRequest { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * 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. */ id?: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * A String containing the name of the method to be invoked. + * A string containing the name of the method to be invoked. */ method: string; /** - * A Structured value that holds the parameter values to be used during the invocation of the method. + * A structured value holding the parameter values to be used during the method invocation. */ params?: { [k: string]: unknown; }; } /** - * JSON-RPC success response model for the 'message/send' method. + * Represents a successful JSON-RPC response for the `message/send` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SendMessageSuccessResponse". */ export interface SendMessageSuccessResponse { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier established by the client. */ id: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * The result object on success + * The result, which can be a direct reply Message or the initial Task object. */ result: Task2 | Message1; } /** + * Represents a single, stateful operation or conversation between a client and an agent. + * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "Task". */ export interface Task2 { /** - * Collection of artifacts created by the agent. + * A collection of artifacts generated by the agent during the execution of the task. */ artifacts?: Artifact[]; /** - * Server-generated id for contextual alignment across interactions + * A server-generated identifier for maintaining context across multiple related tasks or interactions. */ contextId: string; + /** + * An array of messages exchanged during the task, representing the conversation history. + */ history?: Message1[]; /** - * Unique identifier for the task + * A unique identifier for the task, generated by the server for a new task. */ id: string; /** - * Event type + * The type of this object, used as a discriminator. Always 'task' for a Task. */ kind: "task"; /** - * Extension metadata. + * Optional metadata for extensions. The key is an extension-specific identifier. */ metadata?: { [k: string]: unknown; @@ -1736,208 +2224,272 @@ export interface Task2 { status: TaskStatus; } /** - * JSON-RPC success response model for the 'message/stream' method. + * Represents a successful JSON-RPC response for the `message/stream` method. + * The server may send multiple response objects for a single request. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SendStreamingMessageSuccessResponse". */ export interface SendStreamingMessageSuccessResponse { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier established by the client. */ id: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * The result object on success + * The result, which can be a Message, Task, or a streaming update event. */ result: Task2 | Message1 | TaskStatusUpdateEvent | TaskArtifactUpdateEvent; } /** - * Sent by server during sendStream or subscribe requests + * An event sent by the agent to notify the client of a change in a task's status. + * This is typically used in streaming or subscription models. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "TaskStatusUpdateEvent". */ export interface TaskStatusUpdateEvent { /** - * The context the task is associated with + * The context ID associated with the task. */ contextId: string; /** - * Indicates the end of the event stream + * If true, this is the final event in the stream for this interaction. */ final: boolean; /** - * Event type + * The type of this event, used as a discriminator. Always 'status-update'. */ kind: "status-update"; /** - * Extension metadata. + * Optional metadata for extensions. */ metadata?: { [k: string]: unknown; }; status: TaskStatus1; /** - * Task id + * The ID of the task that was updated. */ taskId: string; } /** - * Current status of the task + * The new status of the task. */ export interface TaskStatus1 { message?: Message2; - state: TaskState; /** - * ISO 8601 datetime string when the status was recorded. + * The current state of the task's lifecycle. + */ + state: + | "submitted" + | "working" + | "input-required" + | "completed" + | "canceled" + | "failed" + | "rejected" + | "auth-required" + | "unknown"; + /** + * An ISO 8601 datetime string indicating when this status was recorded. */ timestamp?: string; } /** - * Sent by server during sendStream or subscribe requests + * An event sent by the agent to notify the client that an artifact has been + * generated or updated. This is typically used in streaming models. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "TaskArtifactUpdateEvent". */ export interface TaskArtifactUpdateEvent { /** - * Indicates if this artifact appends to a previous one + * If true, the content of this artifact should be appended to a previously sent artifact with the same ID. */ append?: boolean; artifact: Artifact1; /** - * The context the task is associated with + * The context ID associated with the task. */ contextId: string; /** - * Event type + * The type of this event, used as a discriminator. Always 'artifact-update'. */ kind: "artifact-update"; /** - * Indicates if this is the last chunk of the artifact + * If true, this is the final chunk of the artifact. */ lastChunk?: boolean; /** - * Extension metadata. + * Optional metadata for extensions. */ metadata?: { [k: string]: unknown; }; /** - * Task id + * The ID of the task this artifact belongs to. */ taskId: string; } /** - * Represents an artifact generated for a task. + * Represents a file, data structure, or other resource generated by an agent during a task. */ export interface Artifact1 { /** - * Unique identifier for the artifact. + * A unique identifier for the artifact within the scope of the task. */ artifactId: string; /** - * Optional description for the artifact. + * An optional, human-readable description of the artifact. */ description?: string; /** - * The URIs of extensions that are present or contributed to this Artifact. + * The URIs of extensions that are relevant to this artifact. */ extensions?: string[]; /** - * Extension metadata. + * Optional metadata for extensions. The key is an extension-specific identifier. */ metadata?: { [k: string]: unknown; }; /** - * Optional name for the artifact. + * An optional, human-readable name for the artifact. */ name?: string; /** - * Artifact parts. + * An array of content parts that make up the artifact. */ parts: Part[]; } /** - * JSON-RPC success response model for the 'tasks/pushNotificationConfig/set' method. + * Represents a successful JSON-RPC response for the `tasks/pushNotificationConfig/set` method. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SetTaskPushNotificationConfigSuccessResponse". */ export interface SetTaskPushNotificationConfigSuccessResponse { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier established by the client. */ id: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; result: TaskPushNotificationConfig2; } /** - * The result object on success. + * The result, containing the configured push notification settings. */ export interface TaskPushNotificationConfig2 { pushNotificationConfig: PushNotificationConfig1; /** - * Task id. + * The ID of the task. + */ + taskId: string; +} +/** + * Represents a successful JSON-RPC response for the `tasks/pushNotificationConfig/list` method. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "ListTaskPushNotificationConfigSuccessResponse". + */ +export interface ListTaskPushNotificationConfigSuccessResponse { + /** + * The identifier established by the client. + */ + id: string | number | null; + /** + * The version of the JSON-RPC protocol. MUST be exactly "2.0". + */ + jsonrpc: "2.0"; + /** + * The result, containing an array of all push notification configurations for the task. + */ + result: TaskPushNotificationConfig3[]; +} +/** + * A container associating a push notification configuration with a specific task. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "TaskPushNotificationConfig". + */ +export interface TaskPushNotificationConfig3 { + pushNotificationConfig: PushNotificationConfig1; + /** + * The ID of the task. */ taskId: string; } /** - * Represents a JSON-RPC 2.0 Success Response object. + * Represents a successful JSON-RPC 2.0 Response object. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "JSONRPCSuccessResponse". */ export interface JSONRPCSuccessResponse { /** - * An identifier established by the Client that MUST contain a String, Number. - * Numbers SHOULD NOT contain fractional parts. + * The identifier established by the client. */ id: string | number | null; /** - * Specifies the version of the JSON-RPC protocol. MUST be exactly "2.0". + * The version of the JSON-RPC protocol. MUST be exactly "2.0". */ jsonrpc: "2.0"; /** - * The result object on success + * The value of this member is determined by the method invoked on the Server. */ result: { [k: string]: unknown; }; } /** - * Configuration for the send message request. + * Defines parameters for listing all push notification configurations associated with a task. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "ListTaskPushNotificationConfigParams". + */ +export interface ListTaskPushNotificationConfigParams1 { + /** + * The unique identifier of the task. + */ + id: string; + /** + * Optional metadata associated with the request. + */ + metadata?: { + [k: string]: unknown; + }; +} +/** + * Defines configuration options for a `message/send` or `message/stream` request. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "MessageSendConfiguration". */ export interface MessageSendConfiguration1 { /** - * Accepted output modalities by the client. + * A list of output MIME types the client is prepared to accept in the response. */ - acceptedOutputModes: string[]; + acceptedOutputModes?: string[]; /** - * If the server should treat the client as a blocking request. + * If true, the client will wait for the task to complete. The server may reject this if the task is long-running. */ blocking?: boolean; /** - * Number of recent messages to be retrieved. + * The number of most recent messages from the task's history to retrieve in the response. */ historyLength?: number; pushNotificationConfig?: PushNotificationConfig; } /** - * Sent by the client to the agent as a request. May create, continue or restart a task. + * Defines the parameters for a request to send a message to an agent. This can be used + * to create a new task, continue an existing one, or restart a task. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "MessageSendParams". @@ -1946,14 +2498,14 @@ export interface MessageSendParams2 { configuration?: MessageSendConfiguration; message: Message; /** - * Extension metadata. + * Optional metadata for extensions. */ metadata?: { [k: string]: unknown; }; } /** - * Allows configuration of the supported OAuth Flows + * Defines the configuration for the supported OAuth 2.0 flows. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "OAuthFlows". @@ -1965,46 +2517,60 @@ export interface OAuthFlows1 { password?: PasswordOAuthFlow; } /** - * Base properties common to all message parts. + * Defines base properties common to all message or artifact parts. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "PartBase". */ export interface PartBase { /** - * Optional metadata associated with the part. + * Optional metadata associated with this part. */ metadata?: { [k: string]: unknown; }; } /** - * Configuration details for a supported OAuth Flow + * Defines configuration details for the OAuth 2.0 Resource Owner Password flow. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "PasswordOAuthFlow". */ export interface PasswordOAuthFlow1 { /** - * The URL to be used for obtaining refresh tokens. This MUST be in the form of a URL. The OAuth2 - * standard requires the use of TLS. + * The URL to be used for obtaining refresh tokens. This MUST be a URL. */ refreshUrl?: string; /** - * The available scopes for the OAuth2 security scheme. A map between the scope name and a short - * description for it. The map MAY be empty. + * The available scopes for the OAuth2 security scheme. A map between the scope + * name and a short description for it. */ scopes: { [k: string]: string; }; /** - * The token URL to be used for this flow. This MUST be in the form of a URL. The OAuth2 standard - * requires the use of TLS. + * The token URL to be used for this flow. This MUST be a URL. */ tokenUrl: string; } /** - * Configuration for setting up push notifications for task updates. + * Defines authentication details for a push notification endpoint. + * + * This interface was referenced by `MySchema`'s JSON-Schema + * via the `definition` "PushNotificationAuthenticationInfo". + */ +export interface PushNotificationAuthenticationInfo1 { + /** + * Optional credentials required by the push notification endpoint. + */ + credentials?: string; + /** + * A list of supported authentication schemes (e.g., 'Basic', 'Bearer'). + */ + schemes: string[]; +} +/** + * Defines the configuration for setting up push notifications for task updates. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "PushNotificationConfig". @@ -2012,88 +2578,76 @@ export interface PasswordOAuthFlow1 { export interface PushNotificationConfig2 { authentication?: PushNotificationAuthenticationInfo; /** - * Push Notification ID - created by server to support multiple callbacks + * A unique ID for the push notification configuration, set by the client + * to support multiple notification callbacks. */ id?: string; /** - * Token unique to this task/session. + * A unique token for this task or session to validate incoming push notifications. */ token?: string; /** - * URL for sending the push notifications. + * The callback URL where the agent should send push notifications. */ url: string; } /** - * Base properties shared by all security schemes. + * Defines base properties shared by all security scheme objects. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "SecuritySchemeBase". */ export interface SecuritySchemeBase { /** - * Description of this security scheme. + * An optional description for the security scheme. */ description?: string; } /** - * Parameters containing only a task ID, used for simple task operations. - * - * This interface was referenced by `MySchema`'s JSON-Schema - * via the `definition` "TaskIdParams". - */ -export interface TaskIdParams3 { - /** - * Task id. - */ - id: string; - metadata?: { - [k: string]: unknown; - }; -} -/** - * Parameters for setting or getting push notification configuration for a task - * - * This interface was referenced by `MySchema`'s JSON-Schema - * via the `definition` "TaskPushNotificationConfig". - */ -export interface TaskPushNotificationConfig3 { - pushNotificationConfig: PushNotificationConfig1; - /** - * Task id. - */ - taskId: string; -} -/** - * Parameters for querying a task, including optional history length. + * Defines parameters for querying a task, with an option to limit history length. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "TaskQueryParams". */ export interface TaskQueryParams1 { /** - * Number of recent messages to be retrieved. + * The number of most recent messages from the task's history to retrieve. */ historyLength?: number; /** - * Task id. + * The unique identifier of the task. */ id: string; + /** + * Optional metadata associated with the request. + */ metadata?: { [k: string]: unknown; }; } /** - * TaskState and accompanying message. + * Represents the status of a task at a specific point in time. * * This interface was referenced by `MySchema`'s JSON-Schema * via the `definition` "TaskStatus". */ export interface TaskStatus2 { message?: Message2; - state: TaskState; /** - * ISO 8601 datetime string when the status was recorded. + * The current state of the task's lifecycle. + */ + state: + | "submitted" + | "working" + | "input-required" + | "completed" + | "canceled" + | "failed" + | "rejected" + | "auth-required" + | "unknown"; + /** + * An ISO 8601 datetime string indicating when this status was recorded. */ timestamp?: string; } diff --git a/test/client/client.spec.ts b/test/client/client.spec.ts new file mode 100644 index 0000000..3a002d1 --- /dev/null +++ b/test/client/client.spec.ts @@ -0,0 +1,279 @@ +import { describe, it, beforeEach, afterEach } from 'mocha'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { A2AClient } from '../../src/client/client.js'; +import { MessageSendParams, TextPart, SendMessageResponse, SendMessageSuccessResponse } from '../../src/types.js'; +import { AGENT_CARD_PATH } from '../../src/constants.js'; +import { extractRequestId, createResponse, createAgentCardResponse, createMockAgentCard, createMockFetch } from './util.js'; + +// Helper function to check if response is a success response +function isSuccessResponse(response: SendMessageResponse): response is SendMessageSuccessResponse { + return 'result' in response; +} + +describe('A2AClient Basic Tests', () => { + let client: A2AClient; + let mockFetch: sinon.SinonStub; + let originalConsoleError: typeof console.error; + + beforeEach(() => { + // Suppress console.error during tests to avoid noise + originalConsoleError = console.error; + console.error = () => {}; + + // Create a fresh mock fetch for each test + mockFetch = createMockFetch(); + client = new A2AClient('https://test-agent.example.com', { + fetchImpl: mockFetch + }); + }); + + afterEach(() => { + // Restore console.error + console.error = originalConsoleError; + sinon.restore(); + }); + + describe('Client Initialization', () => { + it('should initialize client with default options', () => { + // Use a mock fetch to avoid real HTTP requests during testing + const mockFetchForDefault = createMockFetch(); + const basicClient = new A2AClient('https://test-agent.example.com', { + fetchImpl: mockFetchForDefault + }); + expect(basicClient).to.be.instanceOf(A2AClient); + }); + + it('should initialize client with custom fetch implementation', () => { + const customFetch = sinon.stub(); + const clientWithCustomFetch = new A2AClient('https://test-agent.example.com', { + fetchImpl: customFetch + }); + expect(clientWithCustomFetch).to.be.instanceOf(A2AClient); + }); + + it('should fetch agent card during initialization', async () => { + // Wait for agent card to be fetched + await client.getAgentCard(); + + expect(mockFetch.callCount).to.be.greaterThan(0); + const agentCardCall = mockFetch.getCalls().find(call => + call.args[0].includes(AGENT_CARD_PATH) + ); + expect(agentCardCall).to.exist; + }); + }); + + describe('Agent Card Handling', () => { + it('should fetch and parse agent card correctly', async () => { + const agentCard = await client.getAgentCard(); + + expect(agentCard).to.have.property('name', 'Test Agent'); + expect(agentCard).to.have.property('description', 'A test agent for basic client testing'); + expect(agentCard).to.have.property('url', 'https://test-agent.example.com/api'); + expect(agentCard).to.have.property('capabilities'); + expect(agentCard.capabilities).to.have.property('streaming', true); + expect(agentCard.capabilities).to.have.property('pushNotifications', true); + }); + + it('should cache agent card for subsequent requests', async () => { + // First call + await client.getAgentCard(); + + // Second call - should not fetch agent card again + await client.getAgentCard(); + + const agentCardCalls = mockFetch.getCalls().filter(call => + call.args[0].includes(AGENT_CARD_PATH) + ); + + expect(agentCardCalls).to.have.length(1); + }); + + it('should handle agent card fetch errors', async () => { + const errorFetch = sinon.stub().callsFake(async (url: string) => { + if (url.includes(AGENT_CARD_PATH)) { + return new Response('Not found', { status: 404 }); + } + return new Response('Not found', { status: 404 }); + }); + + // Create client after setting up the mock to avoid console.error during construction + const errorClient = new A2AClient('https://test-agent.example.com', { + fetchImpl: errorFetch + }); + + try { + await errorClient.getAgentCard(); + expect.fail('Expected error to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Failed to fetch Agent Card'); + } + }); + }); + + describe('Message Sending', () => { + it('should send message successfully', async () => { + const messageParams: MessageSendParams = { + message: { + kind: 'message', + messageId: 'test-msg-1', + role: 'user', + parts: [{ + kind: 'text', + text: 'Hello, agent!' + } as TextPart] + } + }; + + const result = await client.sendMessage(messageParams); + + // Verify fetch was called + expect(mockFetch.callCount).to.be.greaterThan(0); + + // Verify RPC call was made + const rpcCall = mockFetch.getCalls().find(call => + call.args[0].includes('/api') + ); + expect(rpcCall).to.exist; + expect(rpcCall.args[1]).to.deep.include({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + expect(rpcCall.args[1].body).to.include('"method":"message/send"'); + + // Verify the result + expect(isSuccessResponse(result)).to.be.true; + if (isSuccessResponse(result)) { + expect(result.result).to.have.property('kind', 'message'); + expect(result.result).to.have.property('messageId', 'msg-123'); + } + }); + + it('should handle message sending errors', async () => { + const errorFetch = sinon.stub().callsFake(async (url: string, options?: RequestInit) => { + if (url.includes(AGENT_CARD_PATH)) { + const mockAgentCard = createMockAgentCard({ + description: 'A test agent for error testing' + }); + return createAgentCardResponse(mockAgentCard); + } + + if (url.includes('/api')) { + // Extract request ID from the request body + const requestId = extractRequestId(options); + + return createResponse(requestId, undefined, { + code: -32603, + message: 'Internal error' + }, 500); + } + + return new Response('Not found', { status: 404 }); + }); + + const errorClient = new A2AClient('https://test-agent.example.com', { + fetchImpl: errorFetch + }); + + const messageParams: MessageSendParams = { + message: { + kind: 'message', + messageId: 'test-msg-error', + role: 'user', + parts: [{ + kind: 'text', + text: 'This should fail' + } as TextPart] + } + }; + + try { + await errorClient.sendMessage(messageParams); + expect.fail('Expected error to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + } + }); + }); + + describe('Error Handling', () => { + it('should handle network errors gracefully', async () => { + const networkErrorFetch = sinon.stub().rejects(new Error('Network error')); + + const networkErrorClient = new A2AClient('https://test-agent.example.com', { + fetchImpl: networkErrorFetch + }); + + try { + await networkErrorClient.getAgentCard(); + expect.fail('Expected error to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Network error'); + } + }); + + it('should handle malformed JSON responses', async () => { + const malformedFetch = sinon.stub().callsFake(async (url: string) => { + if (url.includes(AGENT_CARD_PATH)) { + return new Response('Invalid JSON', { + status: 200, + headers: { 'Content-Type': 'application/json' } + }); + } + return new Response('Not found', { status: 404 }); + }); + + const malformedClient = new A2AClient('https://test-agent.example.com', { + fetchImpl: malformedFetch + }); + + try { + await malformedClient.getAgentCard(); + expect.fail('Expected error to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + } + }); + + it('should handle missing agent card URL', async () => { + const missingUrlFetch = sinon.stub().callsFake(async (url: string) => { + if (url.includes(AGENT_CARD_PATH)) { + const invalidAgentCard = { + name: 'Test Agent', + description: 'A test agent without URL', + protocolVersion: '1.0.0', + version: '1.0.0', + // Missing url field + defaultInputModes: ['text'], + defaultOutputModes: ['text'], + capabilities: { + streaming: true, + pushNotifications: true + }, + skills: [] + }; + return createAgentCardResponse(invalidAgentCard); + } + return new Response('Not found', { status: 404 }); + }); + + const missingUrlClient = new A2AClient('https://test-agent.example.com', { + fetchImpl: missingUrlFetch + }); + + try { + await missingUrlClient.getAgentCard(); + expect.fail('Expected error to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include("does not contain a valid 'url'"); + } + }); + }); +}); diff --git a/test/client/client_auth.spec.ts b/test/client/client_auth.spec.ts new file mode 100644 index 0000000..662f204 --- /dev/null +++ b/test/client/client_auth.spec.ts @@ -0,0 +1,512 @@ +import { describe, it, beforeEach, afterEach } from 'mocha'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { A2AClient } from '../../src/client/client.js'; +import { AuthenticationHandler, HttpHeaders, createAuthenticatingFetchWithRetry } from '../../src/client/auth-handler.js'; +import {SendMessageResponse, SendMessageSuccessResponse } from '../../src/types.js'; +import { AGENT_CARD_PATH } from '../../src/constants.js'; +import { + createMessageParams, + createMockFetch +} from './util.js'; + + +// Challenge manager class for authentication testing +class ChallengeManager { + private challengeStore: Set = new Set(); + + createChallenge(): string { + const challenge = Math.random().toString(36).substring(2, 18); // just a random string + this.challengeStore.add(challenge); + return challenge; + } + + // used by clients to sign challenges + static signChallenge(challenge: string): string { + return challenge + '.' + challenge.split('.').reverse().join(''); + } + + // verify the "signature" as simply the reverse of the challenge + verifyToken(token: string): boolean { + const [challenge, signature] = token.split('.'); + if (!this.challengeStore.has(challenge)) + return false; + + return signature === challenge.split('.').reverse().join(''); + } + + clearStore(): void { + this.challengeStore.clear(); + } +} + +const challengeManager = new ChallengeManager(); + +// Mock authentication handler that simulates generating tokens and confirming signatures +class MockAuthHandler implements AuthenticationHandler { + private authorization: string | null = null; + + async headers(): Promise { + return this.authorization ? { 'Authorization': this.authorization } : {}; + } + + async shouldRetryWithHeaders(req: RequestInit, res: Response): Promise { + // Simulate 401/403 response handling + if (res.status !== 401 && res.status !== 403) + return undefined; + + // Parse WWW-Authenticate header to extract the token68/challenge value + const [scheme, challenge] = res.headers.get('WWW-Authenticate')?.split(/\s+/) || []; + if (scheme !== 'Bearer') + return undefined; // Not the type we expected for this test + + // Use the ChallengeManager to sign the challenge + const token = ChallengeManager.signChallenge(challenge); + + // have the client try the token, BUT don't save it in case the client doesn't accept it + return { 'Authorization': `Bearer ${token}` }; + } + + async onSuccessfulRetry(headers: HttpHeaders): Promise { + // Remember successful authorization header + const auth = headers['Authorization']; + if (auth) + this.authorization = auth; + } +} + +// Helper function to check if response is a success response +function isSuccessResponse(response: SendMessageResponse): response is SendMessageSuccessResponse { + return 'result' in response; +} + +describe('A2AClient Authentication Tests', () => { + let client: A2AClient; + let authHandler: MockAuthHandler; + let mockFetch: sinon.SinonStub; + let originalConsoleError: typeof console.error; + + beforeEach(() => { + // Suppress console.error during tests to avoid noise + originalConsoleError = console.error; + console.error = () => {}; + + // Create a fresh mock fetch for each test + mockFetch = createMockFetch({ + requiresAuth: true, + agentDescription: 'A test agent for authentication testing', + authErrorConfig: { + code: -32001, + message: 'Authentication required', + challenge: challengeManager.createChallenge() + } + }); + + authHandler = new MockAuthHandler(); + // Use AuthHandlingFetch to wrap the mock fetch with authentication handling + const authHandlingFetch = createAuthenticatingFetchWithRetry(mockFetch, authHandler); + client = new A2AClient('https://test-agent.example.com', { + fetchImpl: authHandlingFetch + }); + }); + + afterEach(() => { + // Restore console.error + console.error = originalConsoleError; + sinon.restore(); + }); + + describe('Authentication Flow', () => { + it('should handle authentication flow correctly', async () => { + const messageParams = createMessageParams({ + messageId: 'test-msg-1', + text: 'Hello, agent!' + }); + + // This should trigger the authentication flow + const result = await client.sendMessage(messageParams); + + // Verify fetch was called multiple times + expect(mockFetch.callCount).to.equal(3); + + // First call: agent card fetch + expect(mockFetch.firstCall.args[0]).to.equal(`https://test-agent.example.com/${AGENT_CARD_PATH}`); + expect(mockFetch.firstCall.args[1]).to.deep.include({ + headers: { 'Accept': 'application/json' } + }); + + // Second call: RPC request without auth header + expect(mockFetch.secondCall.args[0]).to.equal('https://test-agent.example.com/api'); + expect(mockFetch.secondCall.args[1]).to.deep.include({ + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + }); + expect(mockFetch.secondCall.args[1].body).to.include('"method":"message/send"'); + + // Third call: RPC request with auth header + expect(mockFetch.thirdCall.args[0]).to.equal('https://test-agent.example.com/api'); + expect(mockFetch.thirdCall.args[1]).to.deep.include({ + method: 'POST' + }); + // Check headers separately to avoid issues with Authorization header + expect(mockFetch.thirdCall.args[1].headers).to.have.property('Content-Type', 'application/json'); + expect(mockFetch.thirdCall.args[1].headers).to.have.property('Accept', 'application/json'); + expect(mockFetch.thirdCall.args[1].headers).to.have.property('Authorization'); + + expect(mockFetch.thirdCall.args[1].headers['Authorization']).to.match(/^Bearer .+$/); + expect(mockFetch.thirdCall.args[1].body).to.include('"method":"message/send"'); + + // Verify the result + expect(isSuccessResponse(result)).to.be.true; + if (isSuccessResponse(result)) { + expect(result.result).to.have.property('kind', 'message'); + } + }); + + it('should reuse authentication token for subsequent requests', async () => { + const messageParams = createMessageParams({ + messageId: 'test-msg-2', + text: 'Second message' + }); + + // First request - should trigger auth flow + const result1 = await client.sendMessage(messageParams); + + // Capture the token from the first request + const firstRequestAuthCall = mockFetch.getCalls().find(call => + call.args[0].includes('/api') && + call.args[1]?.headers?.['Authorization'] + ); + const firstRequestToken = firstRequestAuthCall?.args[1]?.headers?.['Authorization']; + + // Second request - should use existing token + const result2 = await client.sendMessage(messageParams); + + // Total calls should be 4: 3 for first request + 1 for second request (both agent card and auth token cached) + expect(mockFetch.callCount).to.equal(4); + + // Second request should start from call #4 (after the first 3 calls) + const secondRequestCalls = mockFetch.getCalls().slice(3); + + // Only one call for second request: RPC request with auth header (agent card and token cached) + expect(secondRequestCalls[0].args[0]).to.equal('https://test-agent.example.com/api'); + expect(secondRequestCalls[0].args[1].headers).to.have.property('Authorization'); + + // Should use the exact same token from the first request + expect(secondRequestCalls[0].args[1].headers['Authorization']).to.equal(firstRequestToken); + + expect(isSuccessResponse(result2)).to.be.true; + }); + }); + + describe('Authentication Handler Integration', () => { + it('should call auth handler methods correctly', async () => { + const authHandlerSpy = { + headers: sinon.spy(authHandler, 'headers'), + shouldRetryWithHeaders: sinon.spy(authHandler, 'shouldRetryWithHeaders'), + onSuccess: sinon.spy(authHandler, 'onSuccessfulRetry') + }; + + const messageParams = createMessageParams({ + messageId: 'test-msg-4', + text: 'Test auth handler' + }); + + await client.sendMessage(messageParams); + + // Verify auth handler methods were called + expect(authHandlerSpy.headers.called).to.be.true; + expect(authHandlerSpy.shouldRetryWithHeaders.called).to.be.true; + expect(authHandlerSpy.onSuccess.called).to.be.true; + }); + + it('should handle auth handler returning undefined for retry', async () => { + // Create a mock that doesn't retry + const noRetryHandler = new MockAuthHandler(); + const originalShouldRetry = noRetryHandler.shouldRetryWithHeaders.bind(noRetryHandler); + noRetryHandler.shouldRetryWithHeaders = sinon.stub().resolves(undefined); + + const clientNoRetry = new A2AClient('https://test-agent.example.com', { + fetchImpl: mockFetch + }); + + const messageParams = createMessageParams({ + messageId: 'test-msg-5', + text: 'No retry test' + }); + + // This should fail because we're not retrying with auth + try { + await clientNoRetry.sendMessage(messageParams); + expect.fail('Expected error to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + } + }); + + it('should retry with new auth headers', async () => { + // Create a mock that tracks the Authorization headers sent + const authRetryTestFetch = createMockFetch({ + agentDescription: 'A test agent for authentication testing', + messageConfig: { + messageId: 'msg-auth-retry', + text: 'Test auth retry' + }, + captureAuthHeaders: true, + behavior: 'authRetry' + }); + const { capturedAuthHeaders } = authRetryTestFetch; + + const authHandlingFetch = createAuthenticatingFetchWithRetry(authRetryTestFetch, authHandler); + const clientAuthTest = new A2AClient('https://test-agent.example.com', { + fetchImpl: authHandlingFetch + }); + + const messageParams = createMessageParams({ + messageId: 'test-msg-auth-retry', + text: 'Test auth retry' + }); + + // This should trigger the auth flow and succeed + const result = await clientAuthTest.sendMessage(messageParams); + + // Verify the Authorization headers were sent correctly + // With AuthHandlingFetch, the auth handler makes the retry internally, so we see both calls + expect(capturedAuthHeaders).to.have.length(2); + expect(capturedAuthHeaders[0]).to.equal(''); // First call: no Authorization header + expect(capturedAuthHeaders[1]).to.be.a('string').and.not.be.empty; // Second call: with Authorization header + + // Verify the result + expect(isSuccessResponse(result)).to.be.true; + }); + + it('should continue without authentication when server does not return 401', async () => { + // Create a mock that doesn't require authentication + const noAuthRequiredFetch = createMockFetch({ + requiresAuth: false, + agentDescription: 'A test agent that does not require authentication', + messageConfig: { + messageId: 'msg-no-auth-required', + text: 'Test without authentication' + }, + captureAuthHeaders: true + }); + const { capturedAuthHeaders } = noAuthRequiredFetch; + + const clientNoAuth = new A2AClient('https://test-agent.example.com', { + fetchImpl: noAuthRequiredFetch + }); + + const messageParams = createMessageParams({ + messageId: 'test-msg-no-auth', + text: 'Test without authentication' + }); + + // This should succeed without any authentication flow + const result = await clientNoAuth.sendMessage(messageParams); + + // Verify that no Authorization headers were sent + expect(capturedAuthHeaders).to.have.length(1); + expect(capturedAuthHeaders[0]).to.equal(''); // No auth header sent + + // Verify the result + expect(isSuccessResponse(result)).to.be.true; + if (isSuccessResponse(result)) { + // Check if result is a Message1 (which has messageId) or Task2 + if ('messageId' in result.result) { + expect(result.result.messageId).to.equal('msg-no-auth-required'); + } + } + }); + + it('Client pipes server errors when no auth handler is specified', async () => { + // Create a mock that returns 401 without authHandler + const fetchWithApiError = createMockFetch({ + agentDescription: 'A test agent that requires authentication', + behavior: 'alwaysFail' + }); + + // Create client WITHOUT authHandler + const clientNoAuthHandler = new A2AClient('https://test-agent.example.com', { + fetchImpl: fetchWithApiError + }); + + const messageParams = createMessageParams({ + messageId: 'test-msg-no-auth-handler', + text: 'Test without auth handler' + }); + + // The client should return a JSON-RPC error response rather than throwing an error + const result = await clientNoAuthHandler.sendMessage(messageParams); + + // Verify that the result is a JSON-RPC error response + expect(result).to.have.property('jsonrpc', '2.0'); + expect(result).to.have.property('error'); + expect((result as any).error).to.have.property('code', -32001); + expect((result as any).error).to.have.property('message', 'Authentication required'); + + // Verify that fetch was called only once (no retry attempted) + expect(fetchWithApiError.callCount).to.equal(2); // One for agent card, one for API call + }); + }); +}); + +describe('AuthHandlingFetch Tests', () => { + let mockFetch: sinon.SinonStub; + let authHandler: MockAuthHandler; + let authHandlingFetch: ReturnType; + + beforeEach(() => { + mockFetch = createMockFetch({ + requiresAuth: true, + agentDescription: 'A test agent for authentication testing', + authErrorConfig: { + code: -32001, + message: 'Authentication required', + challenge: challengeManager.createChallenge() + } + }); + authHandler = new MockAuthHandler(); + authHandlingFetch = createAuthenticatingFetchWithRetry(mockFetch, authHandler); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe('Constructor and Function Call', () => { + it('should create a callable instance', () => { + expect(typeof authHandlingFetch).to.equal('function'); + }); + + it('should support direct function calls', async () => { + const response = await authHandlingFetch('https://test.example.com/api'); + expect(response).to.be.instanceOf(Response); + }); + }); + + describe('Header Merging', () => { + it('should merge auth headers with provided headers when auth headers exist', async () => { + // Create an auth handler that has stored authorization headers + const authHandlerWithHeaders = new MockAuthHandler(); + + // Simulate a successful authentication by calling onSuccessfulRetry + // This will store the Authorization header in the auth handler + await authHandlerWithHeaders.onSuccessfulRetry({ + 'Authorization': 'Bearer test-token-123' + }); + + const authHandlingFetchWithHeaders = createAuthenticatingFetchWithRetry(mockFetch, authHandlerWithHeaders); + + await authHandlingFetchWithHeaders('https://test.example.com/api', { + headers: { + 'Content-Type': 'application/json', + 'Custom-Header': 'custom-value' + } + }); + + // Verify that the fetch was called with merged headers including auth headers + const fetchCall = mockFetch.getCall(0); + const headers = fetchCall.args[1]?.headers as Record; + + // Should include both user headers and auth headers + expect(headers).to.include({ + 'Content-Type': 'application/json', + 'Custom-Header': 'custom-value', + 'Authorization': 'Bearer test-token-123' + }); + + // Verify the auth handler's headers method returns the stored authorization + const storedHeaders = await authHandlerWithHeaders.headers(); + expect(storedHeaders['Authorization']).to.equal('Bearer test-token-123'); + }); + + it('should handle empty headers gracefully', async () => { + const emptyAuthHandler = new MockAuthHandler(); + const emptyAuthFetch = createAuthenticatingFetchWithRetry(mockFetch, emptyAuthHandler); + + await emptyAuthFetch('https://test.example.com/api'); + + const fetchCall = mockFetch.getCall(0); + expect(fetchCall.args[1]).to.exist; + }); + }); + + describe('Success Callback', () => { + it('should call onSuccessfulRetry when retry succeeds', async () => { + const successAuthHandler = new MockAuthHandler(); + const onSuccessSpy = sinon.spy(successAuthHandler, 'onSuccessfulRetry'); + + // Create a modified version of the existing mockFetch that returns 401 first, then 200 + const successMockFetch = createMockFetch({ + messageConfig: { + messageId: 'msg-success', + text: 'Success after retry' + }, + behavior: 'authRetry' + }); + + const successAuthFetch = createAuthenticatingFetchWithRetry(successMockFetch, successAuthHandler); + + await successAuthFetch('https://test.example.com/api'); + + expect(onSuccessSpy.called).to.be.true; + expect(onSuccessSpy.firstCall.args[0]).to.deep.include({ + 'Authorization': 'Bearer challenge123.challenge123' + }); + }); + + it('should not call onSuccessfulRetry when retry fails', async () => { + const failAuthHandler = new MockAuthHandler(); + const onSuccessSpy = sinon.spy(failAuthHandler, 'onSuccessfulRetry'); + + const failFetch = createAuthenticatingFetchWithRetry(mockFetch, failAuthHandler); + + // Mock fetch to return 401 first, then 401 again + const failMockFetch = createMockFetch({ + behavior: 'alwaysFail' + }); + + const failAuthFetch = createAuthenticatingFetchWithRetry(failMockFetch, failAuthHandler); + + const response = await failAuthFetch('https://test.example.com/api'); + + expect(onSuccessSpy.called).to.be.false; + expect(response.status).to.equal(401); + }); + }); + + describe('Error Handling', () => { + it('should propagate fetch errors', async () => { + const errorFetch = sinon.stub().rejects(new Error('Network error')); + const errorAuthFetch = createAuthenticatingFetchWithRetry(errorFetch, authHandler); + + try { + await errorAuthFetch('https://test.example.com/api'); + expect.fail('Expected error to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Network error'); + } + }); + + it('should handle auth handler errors gracefully', async () => { + const errorAuthHandler = new MockAuthHandler(); + const shouldRetrySpy = sinon.stub(errorAuthHandler, 'shouldRetryWithHeaders'); + shouldRetrySpy.rejects(new Error('Auth handler error')); + + const errorAuthFetch = createAuthenticatingFetchWithRetry(mockFetch, errorAuthHandler); + + try { + await errorAuthFetch('https://test.example.com/api'); + expect.fail('Expected error to be thrown'); + } catch (error) { + expect(error).to.be.instanceOf(Error); + expect((error as Error).message).to.include('Auth handler error'); + } + }); + }); +}); diff --git a/test/client/util.ts b/test/client/util.ts new file mode 100644 index 0000000..9ed265f --- /dev/null +++ b/test/client/util.ts @@ -0,0 +1,329 @@ +/** + * Utility functions for A2A client tests + */ + +import sinon from 'sinon'; +import { AGENT_CARD_PATH } from '../../src/constants.js'; + +/** + * Extracts the request ID from a RequestInit options object. + * Parses the JSON body and returns the 'id' field, or 1 as default. + * + * @param options - The RequestInit options object containing the request body + * @returns The request ID as a number, defaults to 1 if not found or parsing fails + */ +export function extractRequestId(options?: RequestInit): number { + if (!options?.body) { + return 1; + } + + try { + const requestBody = JSON.parse(options.body as string); + return requestBody.id || 1; + } catch (e) { + // If parsing fails, use default ID + return 1; + } +} + +/** + * Factory function to create fresh Response objects for agent card endpoints. + * Agent cards are returned as raw JSON, not JSON-RPC responses. + * + * @param data - The agent card data to include in the response + * @param status - HTTP status code (defaults to 200) + * @param headers - Additional headers to include in the response + * @returns A fresh Response object with the specified data + */ +export function createAgentCardResponse( + data: any, + status: number = 200, + headers: Record = {} +): Response { + const defaultHeaders = { 'Content-Type': 'application/json' }; + const responseHeaders = { ...defaultHeaders, ...headers }; + + // Create a fresh body each time to avoid "Body is unusable" errors + const body = JSON.stringify(data); + + return new Response(body, { + status, + headers: responseHeaders + }); +} + +/** + * Factory function to create fresh Response objects that can be read multiple times. + * Creates a proper JSON-RPC 2.0 response structure. + * + * @param id - The response ID (used for JSON-RPC responses) + * @param result - The result data to include in the response (for success responses) + * @param error - Optional error object for error responses (mutually exclusive with result) + * @param status - HTTP status code (defaults to 200 for success, 500 for errors) + * @param headers - Additional headers to include in the response + * @returns A fresh Response object with the specified data + */ +export function createResponse( + id: number, + result?: any, + error?: { code: number; message: string; data?: any }, + status: number = 200, + headers: Record = {} +): Response { + const defaultHeaders = { 'Content-Type': 'application/json' }; + const responseHeaders = { ...defaultHeaders, ...headers }; + + // Construct the JSON-RPC response structure + const jsonRpcResponse: any = { + jsonrpc: "2.0", + id: id + }; + + // Add either result or error (mutually exclusive) + if (error) { + jsonRpcResponse.error = error; + // Use provided status or default to 500 for errors + status = status !== 200 ? status : 500; + } else { + jsonRpcResponse.result = result; + } + + return new Response(JSON.stringify(jsonRpcResponse), { + status, + headers: responseHeaders + }); +} + +/** + * Factory function to create mock agent cards for testing. + * + * @param options - Configuration options for the mock agent card + * @param options.name - Agent name (defaults to 'Test Agent') + * @param options.description - Agent description (defaults to 'A test agent for testing') + * @param options.url - Service endpoint URL (defaults to 'https://test-agent.example.com/api') + * @param options.protocolVersion - Protocol version (defaults to '1.0.0') + * @param options.version - Agent version (defaults to '1.0.0') + * @param options.defaultInputModes - Default input modes (defaults to ['text']) + * @param options.defaultOutputModes - Default output modes (defaults to ['text']) + * @param options.capabilities - Agent capabilities (defaults to { streaming: true, pushNotifications: true }) + * @param options.skills - Agent skills (defaults to []) + * @returns A mock AgentCard object + */ +export function createMockAgentCard(options: { + name?: string; + description?: string; + url?: string; + protocolVersion?: string; + version?: string; + defaultInputModes?: string[]; + defaultOutputModes?: string[]; + capabilities?: { + streaming?: boolean; + pushNotifications?: boolean; + }; + skills?: any[]; +} = {}): any { + return { + name: options.name ?? 'Test Agent', + description: options.description ?? 'A test agent for testing', + protocolVersion: options.protocolVersion ?? '1.0.0', + version: options.version ?? '1.0.0', + url: options.url ?? 'https://test-agent.example.com/api', + defaultInputModes: options.defaultInputModes ?? ['text'], + defaultOutputModes: options.defaultOutputModes ?? ['text'], + capabilities: { + streaming: options.capabilities?.streaming ?? true, + pushNotifications: options.capabilities?.pushNotifications ?? true, + ...options.capabilities + }, + skills: options.skills ?? [] + }; +} + +/** + * Factory function to create common message parameters for testing. + * Creates a MessageSendParams object with a text message that can be used + * across multiple test scenarios. + * + * @param options - Configuration options for the message parameters + * @param options.messageId - Message ID (defaults to 'test-msg') + * @param options.text - Message text content (defaults to 'Hello, agent!') + * @param options.role - Message role (defaults to 'user') + * @returns A MessageSendParams object with the specified configuration + */ +export function createMessageParams(options: { + messageId?: string; + text?: string; + role?: 'user' | 'assistant'; +} = {}): any { + const messageId = options.messageId ?? 'test-msg'; + const text = options.text ?? 'Hello, agent!'; + const role = options.role ?? 'user'; + + return { + message: { + kind: 'message', + messageId: messageId, + role: role, + parts: [{ + kind: 'text', + text: text + }] + } + }; +} + +/** + * Factory function to create common mock message objects for testing. + * Creates a Message object with text content that can be used + * across multiple test scenarios. + * + * @param options - Configuration options for the mock message + * @param options.messageId - Message ID (defaults to 'msg-123') + * @param options.text - Message text content (defaults to 'Hello, agent!') + * @param options.role - Message role (defaults to 'user') + * @returns A Message object with the specified configuration + */ +export function createMockMessage(options: { + messageId?: string; + text?: string; + role?: 'user' | 'assistant'; +} = {}): any { + const messageId = options.messageId ?? 'msg-123'; + const text = options.text ?? 'Hello, agent!'; + const role = options.role ?? 'user'; + + return { + kind: 'message', + messageId: messageId, + role: role, + parts: [{ + kind: 'text', + text: text + }] + }; +} + +/** + * Configuration options for creating mock fetch functions + */ +export interface MockFetchConfig { + /** Whether the mock should require authentication */ + requiresAuth?: boolean; + /** Custom agent card description */ + agentDescription?: string; + /** Custom message configuration */ + messageConfig?: { + messageId?: string; + text?: string; + }; + /** Custom error configuration for auth failures */ + authErrorConfig?: { + code?: number; + message?: string; + challenge?: string; + }; + /** Whether to capture auth headers for testing */ + captureAuthHeaders?: boolean; + /** Behavior mode for the mock fetch */ + behavior?: 'standard' | 'authRetry' | 'alwaysFail'; +} + +/** + * Creates a mock fetch function with configurable behavior. + * This is the single function that replaces all previous mock fetch utilities. + * + * @param config - Configuration options for the mock fetch behavior + * @returns A sinon stub that can be used as a mock fetch implementation, with capturedAuthHeaders attached as a property + */ +export function createMockFetch(config: MockFetchConfig = {}): sinon.SinonStub & { capturedAuthHeaders: string[] } { + const { + requiresAuth = false, // Default to no auth required for basic testing + agentDescription = 'A test agent for basic client testing', + messageConfig = { + messageId: 'msg-123', + text: 'Hello, agent!' + }, + authErrorConfig = { + code: -32001, + message: 'Authentication required', + challenge: 'challenge123' + }, + captureAuthHeaders = false, + behavior = 'standard' + } = config; + + let callCount = 0; + const capturedAuthHeaders: string[] = []; + + const mockFetch = sinon.stub().callsFake(async (url: string, options?: RequestInit) => { + // Handle agent card requests + if (url.includes(AGENT_CARD_PATH)) { + const mockAgentCard = createMockAgentCard({ + description: agentDescription + }); + return createAgentCardResponse(mockAgentCard); + } + + // Handle API requests + if (url.includes('/api')) { + const authHeader = options?.headers?.['Authorization'] as string; + + // Capture auth headers if requested + if (captureAuthHeaders) { + capturedAuthHeaders.push(authHeader || ''); + } + + const requestId = extractRequestId(options); + + // Determine response based on behavior + switch (behavior) { + case 'alwaysFail': + // Always return 401 for API calls + return createResponse(requestId, undefined, { + code: authErrorConfig.code!, + message: authErrorConfig.message! + }, 401, { 'WWW-Authenticate': `Bearer ${authErrorConfig.challenge}` }); + + case 'authRetry': + // First call: return 401 to trigger auth flow + if (callCount === 0) { + callCount++; + return createResponse(requestId, undefined, { + code: authErrorConfig.code!, + message: authErrorConfig.message! + }, 401, { 'WWW-Authenticate': `Bearer ${authErrorConfig.challenge}` }); + } + // Subsequent calls: return success + break; + + case 'standard': + default: + // If authentication is required and no valid header is present + if (requiresAuth && !authHeader) { + return createResponse(requestId, undefined, { + code: authErrorConfig.code!, + message: authErrorConfig.message! + }, 401, { 'WWW-Authenticate': `Bearer ${authErrorConfig.challenge}` }); + } + break; + } + + // Return success response + const mockMessage = createMockMessage({ + messageId: messageConfig.messageId || 'msg-123', + text: messageConfig.text || 'Hello, agent!' + }); + + return createResponse(requestId, mockMessage); + } + + // Default: return 404 for unknown endpoints + return new Response('Not found', { status: 404 }); + }); + + // Attach the capturedAuthHeaders as a property to the mock fetch function + (mockFetch as any).capturedAuthHeaders = capturedAuthHeaders; + + return mockFetch as sinon.SinonStub & { capturedAuthHeaders: string[] }; +} diff --git a/test/server/default_request_handler.spec.ts b/test/server/default_request_handler.spec.ts index c6b7c28..03a8f4b 100644 --- a/test/server/default_request_handler.spec.ts +++ b/test/server/default_request_handler.spec.ts @@ -3,9 +3,8 @@ import { assert, expect } from 'chai'; import sinon, { SinonStub, SinonFakeTimers } from 'sinon'; import { AgentExecutor } from '../../src/server/agent_execution/agent_executor.js'; -import { describe, beforeEach, afterEach, it } from 'node:test'; -import { AgentCard, Artifact, Message, MessageSendParams, PushNotificationConfig, Task, TaskIdParams, TaskPushNotificationConfig, TaskState, TaskStatusUpdateEvent } from '../../src/index.js'; -import { RequestContext, ExecutionEventBus, TaskStore, InMemoryTaskStore, DefaultRequestHandler, ExecutionEventQueue } from '../../src/server/index.js'; +import { RequestContext, ExecutionEventBus, TaskStore, InMemoryTaskStore, DefaultRequestHandler, ExecutionEventQueue, A2AError } from '../../src/server/index.js'; +import { AgentCard, Artifact, DeleteTaskPushNotificationConfigParams, GetTaskPushNotificationConfigParams, ListTaskPushNotificationConfigParams, Message, MessageSendParams, PushNotificationConfig, Task, TaskIdParams, TaskPushNotificationConfig, TaskState, TaskStatusUpdateEvent } from '../../src/index.js'; import { DefaultExecutionEventBusManager, ExecutionEventBusManager } from '../../src/server/events/execution_event_bus_manager.js'; import { A2ARequestHandler } from '../../src/server/request_handler/a2a_request_handler.js'; @@ -487,6 +486,7 @@ describe('DefaultRequestHandler as A2ARequestHandler', () => { await mockTaskStore.save(fakeTask); const pushConfig: PushNotificationConfig = { + id: 'config-1', url: 'https://example.com/notify', token: 'secret-token' }; @@ -495,10 +495,129 @@ describe('DefaultRequestHandler as A2ARequestHandler', () => { const setResponse = await handler.setTaskPushNotificationConfig(setParams); assert.deepEqual(setResponse.pushNotificationConfig, pushConfig, "Set response should return the config"); - const getParams: TaskIdParams = { id: taskId }; + const getParams: GetTaskPushNotificationConfigParams = { id: taskId, pushNotificationConfigId: 'config-1' }; const getResponse = await handler.getTaskPushNotificationConfig(getParams); assert.deepEqual(getResponse.pushNotificationConfig, pushConfig, "Get response should return the saved config"); }); + + it('set/getTaskPushNotificationConfig: should save and retrieve config by task ID for backward compatibility', async () => { + const taskId = 'task-push-compat'; + await mockTaskStore.save({ id: taskId, contextId: 'ctx-compat', status: { state: 'working' }, kind: 'task' }); + + // Config ID defaults to task ID + const pushConfig: PushNotificationConfig = { url: 'https://example.com/notify-compat' }; + await handler.setTaskPushNotificationConfig({ taskId, pushNotificationConfig: pushConfig }); + + const getResponse = await handler.getTaskPushNotificationConfig({ id: taskId }); + expect(getResponse.pushNotificationConfig.id).to.equal(taskId); + expect(getResponse.pushNotificationConfig.url).to.equal(pushConfig.url); + }); + + it('setTaskPushNotificationConfig: should overwrite an existing config with the same ID', async () => { + const taskId = 'task-overwrite'; + await mockTaskStore.save({ id: taskId, contextId: 'ctx-overwrite', status: { state: 'working' }, kind: 'task' }); + const initialConfig: PushNotificationConfig = { id: 'config-same', url: 'https://initial.url' }; + await handler.setTaskPushNotificationConfig({ taskId, pushNotificationConfig: initialConfig }); + + const newConfig: PushNotificationConfig = { id: 'config-same', url: 'https://new.url' }; + await handler.setTaskPushNotificationConfig({ taskId, pushNotificationConfig: newConfig }); + + const configs = await handler.listTaskPushNotificationConfigs({ id: taskId }); + expect(configs).to.have.lengthOf(1); + expect(configs[0].pushNotificationConfig.url).to.equal('https://new.url'); + }); + + it('listTaskPushNotificationConfigs: should return all configs for a task', async () => { + const taskId = 'task-list-configs'; + await mockTaskStore.save({ id: taskId, contextId: 'ctx-list', status: { state: 'working' }, kind: 'task' }); + const config1: PushNotificationConfig = { id: 'cfg1', url: 'https://url1.com' }; + const config2: PushNotificationConfig = { id: 'cfg2', url: 'https://url2.com' }; + await handler.setTaskPushNotificationConfig({ taskId, pushNotificationConfig: config1 }); + await handler.setTaskPushNotificationConfig({ taskId, pushNotificationConfig: config2 }); + + const listParams: ListTaskPushNotificationConfigParams = { id: taskId }; + const listResponse = await handler.listTaskPushNotificationConfigs(listParams); + + expect(listResponse).to.be.an('array').with.lengthOf(2); + assert.deepInclude(listResponse, { taskId, pushNotificationConfig: config1 }); + assert.deepInclude(listResponse, { taskId, pushNotificationConfig: config2 }); + }); + + it('deleteTaskPushNotificationConfig: should remove a specific config', async () => { + const taskId = 'task-delete-config'; + await mockTaskStore.save({ id: taskId, contextId: 'ctx-delete', status: { state: 'working' }, kind: 'task' }); + const config1: PushNotificationConfig = { id: 'cfg-del-1', url: 'https://url1.com' }; + const config2: PushNotificationConfig = { id: 'cfg-del-2', url: 'https://url2.com' }; + await handler.setTaskPushNotificationConfig({ taskId, pushNotificationConfig: config1 }); + await handler.setTaskPushNotificationConfig({ taskId, pushNotificationConfig: config2 }); + + const deleteParams: DeleteTaskPushNotificationConfigParams = { id: taskId, pushNotificationConfigId: 'cfg-del-1' }; + await handler.deleteTaskPushNotificationConfig(deleteParams); + + const remainingConfigs = await handler.listTaskPushNotificationConfigs({ id: taskId }); + expect(remainingConfigs).to.have.lengthOf(1); + expect(remainingConfigs[0].pushNotificationConfig.id).to.equal('cfg-del-2'); + }); + + it('deleteTaskPushNotificationConfig: should remove the whole entry if last config is deleted', async () => { + const taskId = 'task-delete-last-config'; + await mockTaskStore.save({ id: taskId, contextId: 'ctx-delete-last', status: { state: 'working' }, kind: 'task' }); + const config: PushNotificationConfig = { id: 'cfg-last', url: 'https://last.com' }; + await handler.setTaskPushNotificationConfig({ taskId, pushNotificationConfig: config }); + + await handler.deleteTaskPushNotificationConfig({ id: taskId, pushNotificationConfigId: 'cfg-last' }); + + const configs = await handler.listTaskPushNotificationConfigs({ id: taskId }); + expect(configs).to.be.an('array').with.lengthOf(0); + }); + + it('Push Notification methods should throw error if task does not exist', async () => { + const nonExistentTaskId = 'task-non-existent'; + const config: PushNotificationConfig = { id: 'cfg-x', url: 'https://x.com' }; + + const methodsToTest = [ + { name: 'setTaskPushNotificationConfig', params: { taskId: nonExistentTaskId, pushNotificationConfig: config } }, + { name: 'getTaskPushNotificationConfig', params: { id: nonExistentTaskId, pushNotificationConfigId: 'cfg-x' } }, + { name: 'listTaskPushNotificationConfigs', params: { id: nonExistentTaskId } }, + { name: 'deleteTaskPushNotificationConfig', params: { id: nonExistentTaskId, pushNotificationConfigId: 'cfg-x' } }, + ]; + + for (const method of methodsToTest) { + try { + await (handler as any)[method.name](method.params); + assert.fail(`Method ${method.name} should have thrown for non-existent task.`); + } catch (error: any) { + expect(error).to.be.instanceOf(A2AError); + expect(error.code).to.equal(-32001); // Task Not Found + } + } + }); + + it('Push Notification methods should throw error if pushNotifications are not supported', async () => { + const unsupportedAgentCard = { ...testAgentCard, capabilities: { ...testAgentCard.capabilities, pushNotifications: false } }; + handler = new DefaultRequestHandler(unsupportedAgentCard, mockTaskStore, mockAgentExecutor, executionEventBusManager); + + const taskId = 'task-unsupported'; + await mockTaskStore.save({ id: taskId, contextId: 'ctx-unsupported', status: { state: 'working' }, kind: 'task' }); + const config: PushNotificationConfig = { id: 'cfg-u', url: 'https://u.com' }; + + const methodsToTest = [ + { name: 'setTaskPushNotificationConfig', params: { taskId, pushNotificationConfig: config } }, + { name: 'getTaskPushNotificationConfig', params: { id: taskId, pushNotificationConfigId: 'cfg-u' } }, + { name: 'listTaskPushNotificationConfigs', params: { id: taskId } }, + { name: 'deleteTaskPushNotificationConfig', params: { id: taskId, pushNotificationConfigId: 'cfg-u' } }, + ]; + + for (const method of methodsToTest) { + try { + await (handler as any)[method.name](method.params); + assert.fail(`Method ${method.name} should have thrown for unsupported push notifications.`); + } catch (error: any) { + expect(error).to.be.instanceOf(A2AError); + expect(error.code).to.equal(-32003); // Push Notification Not Supported + } + } + }); it('cancelTask: should cancel a running task and notify listeners', async () => { clock = sinon.useFakeTimers(); @@ -564,6 +683,88 @@ describe('DefaultRequestHandler as A2ARequestHandler', () => { assert.isFalse((mockAgentExecutor as MockAgentExecutor).cancelTask.called); }); + it('should use contextId from incomingMessage if present (contextId assignment logic)', async () => { + const params: MessageSendParams = { + message: { + messageId: 'msg-ctx', + role: 'user', + parts: [{ kind: 'text', text: 'Hello' }], + kind: 'message', + contextId: 'incoming-ctx-id', + }, + }; + let capturedContextId: string | undefined; + (mockAgentExecutor.execute as SinonStub).callsFake(async (ctx, bus) => { + capturedContextId = ctx.contextId; + bus.publish({ + id: ctx.taskId, + contextId: ctx.contextId, + status: { state: "submitted" }, + kind: 'task' + }); + bus && bus.finished && bus.finished(); + }); + await handler.sendMessage(params); + expect(capturedContextId).to.equal('incoming-ctx-id'); + }); + + it('should use contextId from task if not present in incomingMessage (contextId assignment logic)', async () => { + const taskId = 'task-ctx-id'; + const taskContextId = 'task-context-id'; + await mockTaskStore.save({ + id: taskId, + contextId: taskContextId, + status: { state: 'working' }, + kind: 'task', + }); + const params: MessageSendParams = { + message: { + messageId: 'msg-ctx2', + role: 'user', + parts: [{ kind: 'text', text: 'Hi' }], + kind: 'message', + taskId, + }, + }; + let capturedContextId: string | undefined; + (mockAgentExecutor.execute as SinonStub).callsFake(async (ctx, bus) => { + capturedContextId = ctx.contextId; + bus.publish({ + id: ctx.taskId, + contextId: ctx.contextId, + status: { state: "submitted" }, + kind: 'task' + }); + bus && bus.finished && bus.finished(); + }); + await handler.sendMessage(params); + expect(capturedContextId).to.equal(taskContextId); + }); + + it('should generate a new contextId if not present in message or task (contextId assignment logic)', async () => { + const params: MessageSendParams = { + message: { + messageId: 'msg-ctx3', + role: 'user', + parts: [{ kind: 'text', text: 'Hey' }], + kind: 'message', + }, + }; + let capturedContextId: string | undefined; + (mockAgentExecutor.execute as SinonStub).callsFake(async (ctx, bus) => { + capturedContextId = ctx.contextId; + bus.publish({ + id: ctx.taskId, + contextId: ctx.contextId, + status: { state: "submitted" }, + kind: 'task' + }); + bus && bus.finished && bus.finished(); + }); + await handler.sendMessage(params); + expect(capturedContextId).to.be.a('string').and.not.empty; + }); + it('ExecutionEventQueue should be instantiable and return an object', () => { const fakeBus = { on: () => {}, diff --git a/tsup.config.ts b/tsup.config.ts index 22c189e..e6182bc 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/index.ts", "src/server/index.ts", "src/client/index.ts"], + entry: ["src/index.ts", "src/server/index.ts", "src/server/express/index.ts", "src/client/index.ts"], format: ["esm", "cjs"], dts: true, });