import { getLogger as getLogger$1 } from "../../../logger.js";
import { unwrapError } from "@inkeep/agents-core";

//#region src/domains/run/a2a/client.ts
const logger = getLogger$1("a2aClient");
const MAX_ERROR_BODY_LENGTH = 4096;
async function readErrorBody(response) {
	try {
		const text = await response.text();
		return text.length > MAX_ERROR_BODY_LENGTH ? `${text.slice(0, MAX_ERROR_BODY_LENGTH)}…(truncated)` : text;
	} catch {
		return;
	}
}
function throwAgentCardError(url, response, responseBody) {
	logger.error({
		url,
		status: response.status,
		statusText: response.statusText,
		responseBody
	}, "Agent Card fetch failed");
	const parts = [`Failed to fetch Agent Card from ${url}: ${response.status} ${response.statusText}`];
	if (responseBody) parts.push(responseBody);
	throw new Error(parts.join(" - "));
}
const DEFAULT_BACKOFF = {
	initialInterval: 500,
	maxInterval: 6e4,
	exponent: 1.5,
	maxElapsedTime: 3e4
};
const DEFAULT_RETRY_STATUS_CODES = [
	"429",
	"500",
	"502",
	"503",
	"504"
];
/**
* Error classes for retry logic
*/
var PermanentError = class PermanentError extends Error {
	cause;
	type;
	constructor(message, options) {
		let msg = message;
		if (options?.cause) msg += `: ${options.cause}`;
		super(msg, options);
		this.name = options?.type || "PermanentError";
		if (typeof this.cause === "undefined") this.cause = options?.cause;
		Object.setPrototypeOf(this, PermanentError.prototype);
	}
};
var TemporaryError = class TemporaryError extends Error {
	response;
	constructor(message, response) {
		super(message);
		this.response = response;
		this.name = "TemporaryError";
		Object.setPrototypeOf(this, TemporaryError.prototype);
	}
};
/**
* A2AClient is a TypeScript HTTP client for interacting with A2A-compliant agents.
*
* Features:
* - Configurable retry behavior with exponential backoff
* - Automatic retry on network errors and configurable HTTP status codes
* - Support for custom retry strategies and timeouts
*
* Default retry behavior:
* - Retries on: 429, 500, 502, 503, 504 status codes and network errors
* - Initial delay: 500ms, max delay: 60s, exponential backoff (1.5x)
* - Max total retry time: 30 seconds
*
* @example
* // Default retry behavior
* const client = new A2AClient('https://agent.example.com');
*
* // Custom retry configuration
* const client = new A2AClient('https://agent.example.com', {
*   retryConfig: {
*     strategy: 'backoff',
*     retryConnectionErrors: true,
*     statusCodes: ['429', '502', '503', '504'],
*     backoff: {
*       initialInterval: 1000,
*       maxInterval: 30000,
*       exponent: 2,
*       maxElapsedTime: 60000
*     }
*   }
* });
*
* // Disable retries
* const client = new A2AClient('https://agent.example.com', {
*   retryConfig: { strategy: 'none' }
* });
*/
var A2AClient = class {
	agentBaseUrl;
	agentCardPromise;
	requestIdCounter = 1;
	serviceEndpointUrl;
	options;
	fetchFn;
	/**
	* 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 `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 options Optional configuration including retry behavior.
	*/
	constructor(agentBaseUrl, options) {
		this.agentBaseUrl = agentBaseUrl.replace(/\/$/, "");
		this.fetchFn = options?.fetchFn ?? fetch;
		this.options = {
			retryConfig: {
				strategy: "backoff",
				backoff: DEFAULT_BACKOFF,
				retryConnectionErrors: true,
				statusCodes: DEFAULT_RETRY_STATUS_CODES
			},
			...options
		};
		this.agentCardPromise = this._fetchAndCacheAgentCard();
	}
	/**
	* Fetches the Agent Card from the agent's well-known URI and caches its service endpoint URL.
	* This method is called by the constructor.
	* @returns A Promise that resolves to the AgentCard.
	*/
	async _fetchAndCacheAgentCard() {
		const agentCardUrl = `${this.agentBaseUrl}/.well-known/agent.json`;
		const url = new URL(agentCardUrl);
		if (this.options.ref) url.searchParams.set("ref", this.options.ref.name);
		getLogger$1("a2a").info({
			agentCardUrl: url.toString(),
			agentBaseUrl: this.agentBaseUrl
		}, "agentCardUrl");
		try {
			const response = await this.fetchFn(url.toString(), { headers: {
				Accept: "application/json",
				...this.options.headers || {}
			} });
			if (!response.ok) {
				const responseBody = await readErrorBody(response);
				throwAgentCardError(url.toString(), response, responseBody);
			}
			const agentCard = await response.json();
			if (!agentCard.url) throw new Error("Fetched Agent Card does not contain a valid 'url' for the service endpoint.");
			this.serviceEndpointUrl = agentCard.url;
			return agentCard;
		} catch (error) {
			const rootCause = unwrapError(error);
			logger.error({ error: rootCause }, "Error fetching or parsing Agent Card");
			throw rootCause;
		}
	}
	/**
	* Retrieves the Agent Card.
	* 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.
	* 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.
	*/
	async getAgentCard(agentBaseUrl) {
		if (agentBaseUrl) {
			const agentCardUrl = `${agentBaseUrl.replace(/\/$/, "")}/.well-known/agent.json`;
			const url = new URL(agentCardUrl);
			if (this.options.ref) url.searchParams.set("ref", this.options.ref.name);
			const response = await this.fetchFn(url.toString(), { headers: {
				Accept: "application/json",
				...this.options.headers || {}
			} });
			if (!response.ok) {
				const responseBody = await readErrorBody(response);
				throwAgentCardError(url.toString(), response, responseBody);
			}
			return await response.json();
		}
		return this.agentCardPromise;
	}
	/**
	* Gets the RPC service endpoint URL. Ensures the agent card has been fetched first.
	* Adds ref as query parameter if it exists.
	* @returns A Promise that resolves to the service endpoint URL string.
	*/
	async _getServiceEndpoint() {
		let endpoint;
		if (this.serviceEndpointUrl) endpoint = this.serviceEndpointUrl;
		else {
			await this.agentCardPromise;
			if (!this.serviceEndpointUrl) throw new Error("Agent Card URL for RPC endpoint is not available. Fetching might have failed.");
			endpoint = this.serviceEndpointUrl;
		}
		if (this.options.ref) {
			const url = new URL(endpoint);
			url.searchParams.set("ref", this.options.ref.name);
			return url.toString();
		}
		return endpoint;
	}
	/**
	* Retry utility functions
	*/
	async retry(fetchFn) {
		const config = this.options.retryConfig;
		if (!config || config.strategy === "none") return await fetchFn();
		const statusCodes = config.statusCodes || DEFAULT_RETRY_STATUS_CODES;
		return this.retryBackoff(this.wrapFetcher(fetchFn, {
			statusCodes,
			retryConnectionErrors: !!config.retryConnectionErrors
		}), config.backoff ?? DEFAULT_BACKOFF);
	}
	wrapFetcher(fn, options) {
		return async () => {
			try {
				const res = await fn();
				if (this.isRetryableResponse(res, options.statusCodes)) throw new TemporaryError("Response failed with retryable status code", res);
				return res;
			} catch (err) {
				if (err instanceof TemporaryError) throw err;
				if (options.retryConnectionErrors && this.isRetryableError(err)) throw err;
				throw new PermanentError("Permanent error", { cause: err });
			}
		};
	}
	isRetryableResponse(res, statusCodes) {
		const actual = `${res.status}`;
		const codeRangeRE = /^[0-9]xx$/i;
		return statusCodes.some((code) => {
			if (!codeRangeRE.test(code)) return code === actual;
			const expectFamily = code.charAt(0);
			if (!expectFamily) throw new Error("Invalid status code range");
			const actualFamily = actual.charAt(0);
			if (!actualFamily) throw new Error(`Invalid response status code: ${actual}`);
			return actualFamily === expectFamily;
		});
	}
	isRetryableError(error) {
		if (error instanceof Error) {
			const message = error.message.toLowerCase();
			return message.includes("network") || message.includes("timeout") || message.includes("connection") || message.includes("econnreset") || message.includes("econnrefused") || message.includes("enotfound") || message.includes("fetch");
		}
		return false;
	}
	async retryBackoff(fn, strategy) {
		const { maxElapsedTime, initialInterval, exponent, maxInterval } = strategy;
		const start = Date.now();
		let attempt = 0;
		while (true) try {
			const res = await fn();
			if (attempt > 0) logger.info({
				attempts: attempt + 1,
				elapsedTime: Date.now() - start
			}, "A2A request succeeded after retries");
			return res;
		} catch (err) {
			if (err instanceof PermanentError) throw unwrapError(err.cause);
			const elapsed = Date.now() - start;
			if (elapsed > maxElapsedTime) {
				logger.warn({
					attempts: attempt + 1,
					elapsedTime: elapsed,
					maxElapsedTime
				}, "A2A request max retry time exceeded");
				if (err instanceof TemporaryError) return err.response;
				throw unwrapError(err);
			}
			let retryInterval = 0;
			if (err instanceof TemporaryError) retryInterval = this.retryIntervalFromResponse(err.response);
			if (retryInterval <= 0) retryInterval = initialInterval * attempt ** exponent + Math.random() * 1e3;
			const delayMs = Math.min(retryInterval, maxInterval);
			logger.info({
				attempt: attempt + 1,
				delayMs,
				error: err instanceof TemporaryError ? `HTTP ${err.response.status}` : "Network error"
			}, "A2A request failed, retrying after delay");
			await this.delay(delayMs);
			attempt++;
		}
	}
	retryIntervalFromResponse(res) {
		const retryVal = res.headers.get("retry-after") || "";
		if (!retryVal) return 0;
		const parsedNumber = Number(retryVal);
		if (Number.isInteger(parsedNumber)) return parsedNumber * 1e3;
		const parsedDate = Date.parse(retryVal);
		if (Number.isInteger(parsedDate)) {
			const deltaMS = parsedDate - Date.now();
			return deltaMS > 0 ? Math.ceil(deltaMS) : 0;
		}
		return 0;
	}
	async delay(delayMs) {
		return new Promise((resolve) => setTimeout(resolve, delayMs));
	}
	/**
	* Helper method to make a generic JSON-RPC POST request.
	* @param method The RPC method name.
	* @param params The parameters for the RPC method.
	* @returns A Promise that resolves to the RPC response.
	*/
	async _postRpcRequest(method, params) {
		const endpoint = await this._getServiceEndpoint();
		const requestId = this.requestIdCounter++;
		const rpcRequest = {
			jsonrpc: "2.0",
			method,
			params,
			id: requestId
		};
		const httpResponse = await this.retry(async () => {
			return this.fetchFn(endpoint, {
				method: "POST",
				headers: {
					"Content-Type": "application/json",
					Accept: "application/json",
					...this.options.headers || {}
				},
				body: JSON.stringify(rpcRequest)
			});
		});
		if (!httpResponse.ok) {
			let errorBodyText = "(empty or non-JSON response)";
			try {
				errorBodyText = await httpResponse.text();
				const errorJson = JSON.parse(errorBodyText);
				if (!errorJson.jsonrpc && errorJson.error) 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 (!errorJson.jsonrpc) throw new Error(`HTTP error for ${method}! Status: ${httpResponse.status} ${httpResponse.statusText}. Response: ${errorBodyText}`);
			} catch (e) {
				if (e.message.startsWith("RPC error for") || e.message.startsWith("HTTP error for")) throw e;
				throw new Error(`HTTP error for ${method}! Status: ${httpResponse.status} ${httpResponse.statusText}. Response: ${errorBodyText}`);
			}
		}
		const rpcResponse = await httpResponse.json();
		if (rpcResponse.error?.data?.type === "connection_refused") throw new PermanentError(rpcResponse.error.message, {
			cause: new Error(rpcResponse.error.message),
			type: "connection_refused"
		});
		if (rpcResponse.id !== requestId) logger.warn({
			method,
			expectedId: requestId,
			receivedId: rpcResponse.id
		}, "RPC response ID mismatch - this may lead to incorrect response handling");
		return rpcResponse;
	}
	/**
	* Sends a message to the agent.
	* The behavior (blocking/non-blocking) and push notification configuration
	* are specified within the `params.configuration` object.
	* Optionally, `params.message.contextId` or `params.message.taskId` can be provided.
	* @param params The parameters for sending the message, including the message content and configuration.
	* @returns A Promise resolving to SendMessageResponse, which can be a Message, Task, or an error.
	*/
	async sendMessage(params) {
		return this._postRpcRequest("message/send", params);
	}
	/**
	* Sends a message to the agent and streams back responses using Server-Sent Events (SSE).
	* Push notification configuration can be specified in `params.configuration`.
	* Optionally, `params.message.contextId` or `params.message.taskId` can be provided.
	* Requires the agent to support streaming (`capabilities.streaming: true` in AgentCard).
	* @param params The parameters for sending the message.
	* @returns An AsyncGenerator yielding A2AStreamEventData (Message, Task, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent).
	* The generator throws an error if streaming is not supported or if an HTTP/SSE error occurs.
	*/
	async *sendMessageStream(params) {
		const endpoint = await this._getServiceEndpoint();
		const clientRequestId = this.requestIdCounter++;
		const rpcRequest = {
			jsonrpc: "2.0",
			method: "message/stream",
			params,
			id: clientRequestId
		};
		const response = await this.fetchFn(endpoint, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				Accept: "text/event-stream",
				...this.options.headers || {}
			},
			body: JSON.stringify(rpcRequest)
		});
		if (!response.ok) {
			let errorBody = "";
			try {
				errorBody = await response.text();
				const errorJson = JSON.parse(errorBody);
				if (errorJson.error) throw new Error(`HTTP error establishing stream for message/stream: ${response.status} ${response.statusText}. RPC Error: ${errorJson.error.message} (Code: ${errorJson.error.code})`);
			} catch (e) {
				if (e.message.startsWith("HTTP error establishing stream")) throw e;
				throw new Error(`HTTP error establishing stream for message/stream: ${response.status} ${response.statusText}. Response: ${errorBody || "(empty)"}`);
			}
			throw new Error(`HTTP error establishing stream for message/stream: ${response.status} ${response.statusText}`);
		}
		if (!response.headers.get("Content-Type")?.startsWith("text/event-stream")) throw new Error("Invalid response Content-Type for SSE stream. Expected 'text/event-stream'.");
		yield* this._parseA2ASseStream(response, clientRequestId);
	}
	/**
	* Sets or updates the push notification configuration for a given task.
	* Requires the agent to support push notifications (`capabilities.pushNotifications: true` in AgentCard).
	* @param params Parameters containing the taskId and the TaskPushNotificationConfig.
	* @returns A Promise resolving to SetTaskPushNotificationConfigResponse.
	*/
	async setTaskPushNotificationConfig(params) {
		if (!(await this.agentCardPromise).capabilities?.pushNotifications) throw new Error("Agent does not support push notifications (AgentCard.capabilities.pushNotifications is not true).");
		return this._postRpcRequest("tasks/pushNotificationConfig/set", params);
	}
	/**
	* Gets the push notification configuration for a given task.
	* @param params Parameters containing the taskId.
	* @returns A Promise resolving to GetTaskPushNotificationConfigResponse.
	*/
	async getTaskPushNotificationConfig(params) {
		return this._postRpcRequest("tasks/pushNotificationConfig/get", params);
	}
	/**
	* Retrieves a task by its ID.
	* @param params Parameters containing the taskId and optional historyLength.
	* @returns A Promise resolving to GetTaskResponse, which contains the Task object or an error.
	*/
	async getTask(params) {
		return this._postRpcRequest("tasks/get", params);
	}
	/**
	* Cancels a task by its ID.
	* @param params Parameters containing the taskId.
	* @returns A Promise resolving to CancelTaskResponse, which contains the updated Task object or an error.
	*/
	async cancelTask(params) {
		return this._postRpcRequest("tasks/cancel", params);
	}
	/**
	* Resubscribes to a task's event stream using Server-Sent Events (SSE).
	* This is used if a previous SSE connection for an active task was broken.
	* Requires the agent to support streaming (`capabilities.streaming: true` in AgentCard).
	* @param params Parameters containing the taskId.
	* @returns An AsyncGenerator yielding A2AStreamEventData (Message, Task, TaskStatusUpdateEvent, or TaskArtifactUpdateEvent).
	*/
	async *resubscribeTask(params) {
		if (!(await this.agentCardPromise).capabilities?.streaming) throw new Error("Agent does not support streaming (required for tasks/resubscribe).");
		const endpoint = await this._getServiceEndpoint();
		const clientRequestId = this.requestIdCounter++;
		const rpcRequest = {
			jsonrpc: "2.0",
			method: "tasks/resubscribe",
			params,
			id: clientRequestId
		};
		const response = await this.fetchFn(endpoint, {
			method: "POST",
			headers: {
				"Content-Type": "application/json",
				Accept: "text/event-stream",
				...this.options.headers || {}
			},
			body: JSON.stringify(rpcRequest)
		});
		if (!response.ok) {
			let errorBody = "";
			try {
				errorBody = await response.text();
				const errorJson = JSON.parse(errorBody);
				if (errorJson.error) throw new Error(`HTTP error establishing stream for tasks/resubscribe: ${response.status} ${response.statusText}. RPC Error: ${errorJson.error.message} (Code: ${errorJson.error.code})`);
			} catch (e) {
				if (e.message.startsWith("HTTP error establishing stream")) throw e;
				throw new Error(`HTTP error establishing stream for tasks/resubscribe: ${response.status} ${response.statusText}. Response: ${errorBody || "(empty)"}`);
			}
			throw new Error(`HTTP error establishing stream for tasks/resubscribe: ${response.status} ${response.statusText}`);
		}
		if (!response.headers.get("Content-Type")?.startsWith("text/event-stream")) throw new Error("Invalid response Content-Type for SSE stream on resubscribe. Expected 'text/event-stream'.");
		yield* this._parseA2ASseStream(response, clientRequestId);
	}
	/**
	* Parses an HTTP response body as an A2A Server-Sent Event stream.
	* Each 'data' field of an SSE event is expected to be a JSON-RPC 2.0 Response object,
	* specifically a SendStreamingMessageResponse (or similar structure for resubscribe).
	* @param response The HTTP Response object whose body is the SSE stream.
	* @param originalRequestId The ID of the client's JSON-RPC request that initiated this stream.
	* Used to validate the `id` in the streamed JSON-RPC responses.
	* @returns An AsyncGenerator yielding the `result` field of each valid JSON-RPC success response from the stream.
	*/
	async *_parseA2ASseStream(response, originalRequestId) {
		if (!response.body) throw new Error("SSE response body is undefined. Cannot read stream.");
		const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
		let buffer = "";
		let eventDataBuffer = "";
		try {
			while (true) {
				const { done, value } = await reader.read();
				logger.info({
					done,
					value
				}, "parseA2ASseStream");
				if (done) {
					if (eventDataBuffer.trim()) yield this._processSseEventData(eventDataBuffer, originalRequestId);
					break;
				}
				buffer += value;
				let lineEndIndex;
				lineEndIndex = buffer.indexOf("\n");
				while (lineEndIndex >= 0) {
					const line = buffer.substring(0, lineEndIndex).trim();
					buffer = buffer.substring(lineEndIndex + 1);
					if (line === "") {
						if (eventDataBuffer) {
							yield this._processSseEventData(eventDataBuffer, originalRequestId);
							eventDataBuffer = "";
						}
					} else if (line.startsWith("data:")) eventDataBuffer += `${line.substring(5).trimStart()}\n`;
					else if (line.startsWith(":")) {} else if (line.includes(":")) {}
					lineEndIndex = buffer.indexOf("\n");
				}
			}
		} catch (error) {
			const rootCause = unwrapError(error);
			logger.error({ error: rootCause }, "Error reading or parsing SSE stream");
			throw rootCause;
		} finally {
			reader.releaseLock();
		}
	}
	/**
	* Processes a single SSE event's data string, expecting it to be a JSON-RPC response.
	* @param jsonData The string content from one or more 'data:' lines of an SSE event.
	* @param originalRequestId The ID of the client's request that initiated the stream.
	* @returns The `result` field of the parsed JSON-RPC success response.
	* @throws Error if data is not valid JSON, not a valid JSON-RPC response, an error response, or ID mismatch.
	*/
	_processSseEventData(jsonData, originalRequestId) {
		if (!jsonData.trim()) throw new Error("Attempted to process empty SSE event data.");
		try {
			const a2aStreamResponse = JSON.parse(jsonData.replace(/\n$/, ""));
			if (a2aStreamResponse.id !== originalRequestId) console.warn(`SSE Event's JSON-RPC response ID mismatch. Client request ID: ${originalRequestId}, event response ID: ${a2aStreamResponse.id}.`);
			if (a2aStreamResponse.error) {
				const err = a2aStreamResponse.error;
				throw new Error(`SSE event contained an error: ${err.message} (Code: ${err.code}) Data: ${JSON.stringify(err.data)}`);
			}
			if (!("result" in a2aStreamResponse) || typeof a2aStreamResponse.result === "undefined") throw new Error(`SSE event JSON-RPC response is missing 'result' field. Data: ${jsonData}`);
			return a2aStreamResponse.result;
		} catch (e) {
			if (e.message.startsWith("SSE event contained an error") || e.message.startsWith("SSE event JSON-RPC response is missing 'result' field")) throw e;
			console.error("Failed to parse SSE event data string or unexpected JSON-RPC structure:", jsonData, e);
			throw new Error(`Failed to parse SSE event data: "${jsonData.substring(0, 100)}...". Original error: ${e.message}`);
		}
	}
};

//#endregion
export { A2AClient };