From 99106a36b407d2e0f2a7300e2c63ed94fea23925 Mon Sep 17 00:00:00 2001 From: waglesathwik Date: Wed, 8 Oct 2025 00:33:36 +0530 Subject: [PATCH 1/2] feat: add waitForAgentReady helper for agent deployment polling --- src/lib/agent-poll.ts | 91 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 src/lib/agent-poll.ts diff --git a/src/lib/agent-poll.ts b/src/lib/agent-poll.ts new file mode 100644 index 0000000..b7c3b6c --- /dev/null +++ b/src/lib/agent-poll.ts @@ -0,0 +1,91 @@ +import Gradient from "@digitalocean/gradient"; + +export type AgentStatus = + | 'STATUS_UNKNOWN' + | 'STATUS_WAITING_FOR_DEPLOYMENT' + | 'STATUS_DEPLOYING' + | 'STATUS_RUNNING' + | 'STATUS_FAILED' + | 'STATUS_WAITING_FOR_UNDEPLOYMENT' + | 'STATUS_UNDEPLOYING' + | 'STATUS_UNDEPLOYMENT_FAILED' + | 'STATUS_DELETED'; + +export interface WaitForAgentOptions { + /** Check interval in ms (default: 3000) */ + interval?: number; + /** Maximum wait time in ms (default: 180000 = 3 mins) */ + timeout?: number; + /** Optional callback for progress updates */ + onUpdate?: (status: AgentStatus, elapsed: number) => void; + signal?: AbortSignal; +} + +export class AgentTimeoutError extends Error { + constructor(public agentId: string, public timeoutMs: number) { + super(`Agent ${agentId} did not become ready within ${timeoutMs}ms`); + this.name = 'AgentTimeoutError'; + } +} + +export class AgentDeploymentError extends Error { + constructor(public agentId: string, public status: AgentStatus) { + super(`Agent ${agentId} deployment failed with status: ${status}`); + this.name = 'AgentDeploymentError'; + } +} + +/** + * Polls an agent until it reaches STATUS_RUNNING. + * + * @example + * ```typescript + * const agent = await waitForAgentReady(client, 'agent-123', { + * timeout: 60000, // 1 minute + * onUpdate: (status, elapsed) => { + * console.log(`Status: ${status} (${elapsed}ms elapsed)`); + * } + * }); + * ``` + * + * @param client - Gradient API client instance + * @param agentId - ID of the agent to monitor + * @param options - Polling configuration + * @returns The agent object once STATUS_RUNNING is reached + * @throws {AgentTimeoutError} When polling exceeds timeout duration + * @throws {AgentDeploymentError} When agent enters a failure state + */ +export async function waitForAgentReady( + client: Gradient, + agentId: string, + options: WaitForAgentOptions = {} +): Promise>> { + const { interval = 3000, timeout = 180000, onUpdate, signal } = options; + const start = Date.now(); + + while (true) { + signal?.throwIfAborted(); + + const elapsed = Date.now() - start; + + // ⏰ Check timeout BEFORE making the API call + if (elapsed > timeout) { + throw new AgentTimeoutError(agentId, timeout); + } + + const agent = await client.agents.retrieve(agentId); + const status = agent.agent?.deployment?.status as AgentStatus || 'STATUS_UNKNOWN'; + + onUpdate?.(status, Date.now() - start); + + if (status === "STATUS_RUNNING") { + return agent; + } + + if (status === "STATUS_FAILED" || status === "STATUS_UNDEPLOYMENT_FAILED") { + throw new AgentDeploymentError(agentId, status); + } + + await new Promise((resolve) => setTimeout(resolve, interval)); + } +} From f00d0949308b957a3239cba66b8eaeebd541ada7 Mon Sep 17 00:00:00 2001 From: "sathwik.w" Date: Tue, 21 Oct 2025 16:13:05 +0530 Subject: [PATCH 2/2] refactor(agent): move waitForAgentReady into Agent class - Converted waitForAgentReady to a method on Agent client for fluent usage - Removed optional onUpdate callback as per review - Reuse existing AgentStatus type from agents.ts - Include request options when calling retrieve() API - Updated sleep helper usage for polling interval Resolves #4 --- src/lib/agent-poll.ts | 91 -------------------------- src/resources/agents/agents.ts | 29 ++++++++ src/resources/agents/wait-for-agent.ts | 82 +++++++++++++++++++++++ 3 files changed, 111 insertions(+), 91 deletions(-) delete mode 100644 src/lib/agent-poll.ts create mode 100644 src/resources/agents/wait-for-agent.ts diff --git a/src/lib/agent-poll.ts b/src/lib/agent-poll.ts deleted file mode 100644 index b7c3b6c..0000000 --- a/src/lib/agent-poll.ts +++ /dev/null @@ -1,91 +0,0 @@ -import Gradient from "@digitalocean/gradient"; - -export type AgentStatus = - | 'STATUS_UNKNOWN' - | 'STATUS_WAITING_FOR_DEPLOYMENT' - | 'STATUS_DEPLOYING' - | 'STATUS_RUNNING' - | 'STATUS_FAILED' - | 'STATUS_WAITING_FOR_UNDEPLOYMENT' - | 'STATUS_UNDEPLOYING' - | 'STATUS_UNDEPLOYMENT_FAILED' - | 'STATUS_DELETED'; - -export interface WaitForAgentOptions { - /** Check interval in ms (default: 3000) */ - interval?: number; - /** Maximum wait time in ms (default: 180000 = 3 mins) */ - timeout?: number; - /** Optional callback for progress updates */ - onUpdate?: (status: AgentStatus, elapsed: number) => void; - signal?: AbortSignal; -} - -export class AgentTimeoutError extends Error { - constructor(public agentId: string, public timeoutMs: number) { - super(`Agent ${agentId} did not become ready within ${timeoutMs}ms`); - this.name = 'AgentTimeoutError'; - } -} - -export class AgentDeploymentError extends Error { - constructor(public agentId: string, public status: AgentStatus) { - super(`Agent ${agentId} deployment failed with status: ${status}`); - this.name = 'AgentDeploymentError'; - } -} - -/** - * Polls an agent until it reaches STATUS_RUNNING. - * - * @example - * ```typescript - * const agent = await waitForAgentReady(client, 'agent-123', { - * timeout: 60000, // 1 minute - * onUpdate: (status, elapsed) => { - * console.log(`Status: ${status} (${elapsed}ms elapsed)`); - * } - * }); - * ``` - * - * @param client - Gradient API client instance - * @param agentId - ID of the agent to monitor - * @param options - Polling configuration - * @returns The agent object once STATUS_RUNNING is reached - * @throws {AgentTimeoutError} When polling exceeds timeout duration - * @throws {AgentDeploymentError} When agent enters a failure state - */ -export async function waitForAgentReady( - client: Gradient, - agentId: string, - options: WaitForAgentOptions = {} -): Promise>> { - const { interval = 3000, timeout = 180000, onUpdate, signal } = options; - const start = Date.now(); - - while (true) { - signal?.throwIfAborted(); - - const elapsed = Date.now() - start; - - // ⏰ Check timeout BEFORE making the API call - if (elapsed > timeout) { - throw new AgentTimeoutError(agentId, timeout); - } - - const agent = await client.agents.retrieve(agentId); - const status = agent.agent?.deployment?.status as AgentStatus || 'STATUS_UNKNOWN'; - - onUpdate?.(status, Date.now() - start); - - if (status === "STATUS_RUNNING") { - return agent; - } - - if (status === "STATUS_FAILED" || status === "STATUS_UNDEPLOYMENT_FAILED") { - throw new AgentDeploymentError(agentId, status); - } - - await new Promise((resolve) => setTimeout(resolve, interval)); - } -} diff --git a/src/resources/agents/agents.ts b/src/resources/agents/agents.ts index 74d3329..eb18b98 100644 --- a/src/resources/agents/agents.ts +++ b/src/resources/agents/agents.ts @@ -106,6 +106,7 @@ import { import { APIPromise } from '../../core/api-promise'; import { RequestOptions } from '../../internal/request-options'; import { path } from '../../internal/utils/path'; +import { waitForAgentReady } from './wait-for-agent'; export class Agents extends APIResource { apiKeys: APIKeysAPI.APIKeys = new APIKeysAPI.APIKeys(this._client); @@ -266,6 +267,23 @@ export class Agents extends APIResource { ...options, }); } + + /** + * Polls for agent readiness. + * + * @example + * ```typescript + * const agent = await waitForAgentReady(client, 'agent-123', { + * timeout: 60000, // 1 minute + * }); + * ``` + */ + waitForReady( + uuid: string, + options: import('./wait-for-agent').WaitForAgentOptions, + ): Promise { + return waitForAgentReady(this._client, uuid, options); + } } /** @@ -1067,6 +1085,16 @@ export interface AgentRetrieveResponse { agent?: APIAgent; } +/** + * One Agent + */ +export interface AgentReadinessResponse { + /** + * An Agent + */ + agent?: APIAgent; +} + /** * Information about an updated agent */ @@ -1758,6 +1786,7 @@ export declare namespace Agents { type APIWorkspace as APIWorkspace, type AgentCreateResponse as AgentCreateResponse, type AgentRetrieveResponse as AgentRetrieveResponse, + type AgentReadinessResponse as AgentReadinessResponse, type AgentUpdateResponse as AgentUpdateResponse, type AgentListResponse as AgentListResponse, type AgentDeleteResponse as AgentDeleteResponse, diff --git a/src/resources/agents/wait-for-agent.ts b/src/resources/agents/wait-for-agent.ts new file mode 100644 index 0000000..693d31f --- /dev/null +++ b/src/resources/agents/wait-for-agent.ts @@ -0,0 +1,82 @@ +import { Gradient } from '../../client'; +import { RequestOptions } from '../../internal/request-options'; +import { GradientError } from '../../core/error'; +import { sleep } from '../../internal/utils'; +import { AgentReadinessResponse, APIAgent } from './agents'; + +type AgentStatus = NonNullable; + +export interface WaitForAgentOptions extends RequestOptions { + /** Check interval in ms (default: 3000) */ + interval?: number; +} + +export class AgentTimeoutError extends GradientError { + constructor( + public agentId: string, + public timeoutMs: number, + ) { + super(`Agent ${agentId} did not become ready within ${timeoutMs}ms`); + this.name = 'AgentTimeoutError'; + } +} + +export class AgentDeploymentError extends GradientError { + constructor( + public agentId: string, + public status: AgentStatus, + ) { + super(`Agent ${agentId} deployment failed with status: ${status}`); + this.name = 'AgentDeploymentError'; + } +} + +/** + * Polls an agent until it reaches STATUS_RUNNING. + * + * @example + * ```typescript + * const agent = await waitForAgentReady(client, 'agent-123', { + * timeout: 60000, // 1 minute + * }); + * ``` + * + * @param client - Gradient API client instance + * @param uuid - UUID of the agent to monitor + * @param options - Polling configuration + * @returns The agent object once STATUS_RUNNING is reached + * @throws {AgentTimeoutError} When polling exceeds timeout duration + * @throws {AgentDeploymentError} When agent enters a failure state + */ +export async function waitForAgentReady( + client: Gradient, + uuid: string, + options: WaitForAgentOptions = {}, +): Promise { + const { interval = 3000, timeout = 180000, signal } = options; + const start = Date.now(); + + while (true) { + signal?.throwIfAborted(); + + const elapsed = Date.now() - start; + + // ⏰ Check timeout BEFORE making the API call + if (elapsed > timeout) { + throw new AgentTimeoutError(uuid, timeout); + } + + const agent = await client.agents.retrieve(uuid, options); + const status = (agent.agent?.deployment?.status as AgentStatus) || 'STATUS_UNKNOWN'; + + if (status === 'STATUS_RUNNING') { + return agent; + } + + if (status === 'STATUS_FAILED' || status === 'STATUS_UNDEPLOYMENT_FAILED') { + throw new AgentDeploymentError(uuid, status); + } + + await sleep(interval); + } +}