diff --git a/src/content/docs/user-guide/concepts/agents/hooks.mdx b/src/content/docs/user-guide/concepts/agents/hooks.mdx index 064ab1eca..e871e4b84 100644 --- a/src/content/docs/user-guide/concepts/agents/hooks.mdx +++ b/src/content/docs/user-guide/concepts/agents/hooks.mdx @@ -876,8 +876,8 @@ class RetryOnServiceUnavailable(HookProvider): -```ts -// This feature is not yet available in TypeScript SDK +```typescript +--8<-- "user-guide/concepts/agents/hooks.ts:retry_on_service_unavailable_class" ``` @@ -898,8 +898,8 @@ result = agent("What is the capital of France?") -```ts -// This feature is not yet available in TypeScript SDK +```typescript +--8<-- "user-guide/concepts/agents/hooks.ts:retry_on_service_unavailable_usage" ``` @@ -941,8 +941,12 @@ agent = Agent( -```ts -// This feature is not yet available in TypeScript SDK +```typescript +--8<-- "user-guide/concepts/agents/hooks.ts:propagate_unexpected_exceptions_class" +``` + +```typescript +--8<-- "user-guide/concepts/agents/hooks.ts:propagate_unexpected_exceptions_usage" ``` @@ -992,8 +996,8 @@ class RetryOnToolError(HookProvider): -```ts -// This feature is not yet available in TypeScript SDK +```typescript +--8<-- "user-guide/concepts/agents/hooks.ts:retry_on_tool_error_class" ``` @@ -1026,8 +1030,8 @@ result = agent("Look up the weather") -```ts -// This feature is not yet available in TypeScript SDK +```typescript +--8<-- "user-guide/concepts/agents/hooks.ts:retry_on_tool_error_usage" ``` diff --git a/src/content/docs/user-guide/concepts/agents/hooks.ts b/src/content/docs/user-guide/concepts/agents/hooks.ts index 9bb28dba3..e5f04468b 100644 --- a/src/content/docs/user-guide/concepts/agents/hooks.ts +++ b/src/content/docs/user-guide/concepts/agents/hooks.ts @@ -425,3 +425,135 @@ void orchestratorCallbackExample void conditionalNodeExecutionExample void orchestratorAgnosticDesignExample void layeredHooksExample + +// ===================== +// Cookbook: Retry Examples +// ===================== + +async function retryOnServiceUnavailableExample() { + // --8<-- [start:retry_on_service_unavailable_class] + class RetryOnServiceUnavailable implements Plugin { + readonly name = 'retry-on-service-unavailable' + private readonly maxRetries: number + private retryCount = 0 + + constructor(maxRetries = 3) { + this.maxRetries = maxRetries + } + + initAgent(agent: LocalAgent): void { + agent.addHook(BeforeInvocationEvent, () => { + this.retryCount = 0 + }) + agent.addHook(AfterModelCallEvent, (event) => this.handleRetry(event)) + } + + private async handleRetry(event: AfterModelCallEvent): Promise { + if (event.error) { + if ( + String(event.error).includes('ServiceUnavailable') && + this.retryCount < this.maxRetries + ) { + this.retryCount++ + await new Promise((resolve) => + setTimeout(resolve, 2 ** this.retryCount * 1000), + ) + event.retry = true + } + } else { + this.retryCount = 0 + } + } + } + // --8<-- [end:retry_on_service_unavailable_class] + + // --8<-- [start:retry_on_service_unavailable_usage] + const retryPlugin = new RetryOnServiceUnavailable(3) + const agent = new Agent({ plugins: [retryPlugin] }) + + const result = await agent.invoke('What is the capital of France?') + // --8<-- [end:retry_on_service_unavailable_usage] + void result +} + +async function propagateUnexpectedExceptionsExample() { + // --8<-- [start:propagate_unexpected_exceptions_class] + class PropagateUnexpectedExceptions implements Plugin { + readonly name = 'propagate-unexpected-exceptions' + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private readonly allowedExceptions: Array Error> + + constructor( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + allowedExceptions: Array Error> = [], + ) { + this.allowedExceptions = allowedExceptions + } + + initAgent(agent: LocalAgent): void { + agent.addHook(AfterToolCallEvent, (event) => this.checkException(event)) + } + + private checkException(event: AfterToolCallEvent): void { + if (!event.error) return // Tool succeeded + // Let model retry allowed exception types + if (this.allowedExceptions.some((Exc) => event.error instanceof Exc)) return + throw event.error // Propagate unexpected errors immediately + } + } + // --8<-- [end:propagate_unexpected_exceptions_class] + + // --8<-- [start:propagate_unexpected_exceptions_usage] + const agent = new Agent({ + tools: [myTool], + plugins: [new PropagateUnexpectedExceptions([TypeError])], + }) + // --8<-- [end:propagate_unexpected_exceptions_usage] + void agent +} + +async function retryOnToolErrorExample() { + // --8<-- [start:retry_on_tool_error_class] + class RetryOnToolError implements Plugin { + readonly name = 'retry-on-tool-error' + private readonly maxRetries: number + private readonly attemptCounts = new Map() + + constructor(maxRetries = 1) { + this.maxRetries = maxRetries + } + + initAgent(agent: LocalAgent): void { + agent.addHook(AfterToolCallEvent, (event) => this.handleRetry(event)) + } + + private handleRetry(event: AfterToolCallEvent): void { + const toolUseId = event.toolUse.toolUseId + const toolName = event.toolUse.name + const attempt = (this.attemptCounts.get(toolUseId) ?? 0) + 1 + this.attemptCounts.set(toolUseId, attempt) + + if (event.result.status === 'error' && attempt <= this.maxRetries) { + console.log( + `Retrying tool '${toolName}' (attempt ${attempt}/${this.maxRetries})`, + ) + event.retry = true + } else if (event.result.status !== 'error') { + this.attemptCounts.delete(toolUseId) + } + } + } + // --8<-- [end:retry_on_tool_error_class] + + // --8<-- [start:retry_on_tool_error_usage] + const retryPlugin = new RetryOnToolError(1) + const agent = new Agent({ tools: [calculator], plugins: [retryPlugin] }) + + const result = await agent.invoke('Look up the weather') + // --8<-- [end:retry_on_tool_error_usage] + void result +} + +void retryOnServiceUnavailableExample +void propagateUnexpectedExceptionsExample +void retryOnToolErrorExample diff --git a/src/content/docs/user-guide/concepts/agents/retry-strategies.mdx b/src/content/docs/user-guide/concepts/agents/retry-strategies.mdx index f585a8c8d..0694b64ef 100644 --- a/src/content/docs/user-guide/concepts/agents/retry-strategies.mdx +++ b/src/content/docs/user-guide/concepts/agents/retry-strategies.mdx @@ -38,8 +38,8 @@ agent = Agent( -```ts -// Not supported in TypeScript +```typescript +--8<-- "user-guide/concepts/agents/retry-strategies.ts:customizing_retry" ``` @@ -69,8 +69,8 @@ agent = Agent( -```ts -// Not supported in TypeScript +```typescript +--8<-- "user-guide/concepts/agents/retry-strategies.ts:disabling_retry" ``` @@ -111,8 +111,8 @@ agent = Agent(hooks=[CustomRetry()]) -```ts -// Not supported in TypeScript +```typescript +--8<-- "user-guide/concepts/agents/retry-strategies.ts:custom_retry_logic" ``` diff --git a/src/content/docs/user-guide/concepts/agents/retry-strategies.ts b/src/content/docs/user-guide/concepts/agents/retry-strategies.ts new file mode 100644 index 000000000..77b6d9e5e --- /dev/null +++ b/src/content/docs/user-guide/concepts/agents/retry-strategies.ts @@ -0,0 +1,64 @@ +import { Agent, AfterModelCallEvent, ModelThrottledError } from '@strands-agents/sdk' + +// =========================== +// Customizing Retry Behavior +// =========================== + +async function customizingRetryExample() { + // --8<-- [start:customizing_retry] + const agent = new Agent() + + let attempts = 0 + const maxAttempts = 3 // Total attempts (including first try) + const initialDelay = 2 // Seconds before first retry + const maxDelay = 60 // Cap on backoff delay + + agent.addHook(AfterModelCallEvent, async (event) => { + if (event.error instanceof ModelThrottledError && attempts < maxAttempts - 1) { + const delay = Math.min(initialDelay * 2 ** attempts, maxDelay) + attempts++ + await new Promise((resolve) => setTimeout(resolve, delay * 1000)) + event.retry = true + } else { + attempts = 0 + } + }) + // --8<-- [end:customizing_retry] +} + +// ===================== +// Disabling Retries +// ===================== + +async function disablingRetryExample() { + // --8<-- [start:disabling_retry] + // The TypeScript SDK does not perform built-in automatic retries for + // throttle errors. Retry only occurs when a hook explicitly sets + // event.retry = true. No additional configuration is needed to disable it. + const agent = new Agent() + // --8<-- [end:disabling_retry] +} + +// ===================== +// Custom Retry Logic +// ===================== + +async function customRetryLogicExample() { + // --8<-- [start:custom_retry_logic] + const agent = new Agent() + + let attempts = 0 + const maxRetries = 3 + const delay = 2.0 // seconds + + agent.addHook(AfterModelCallEvent, async (event) => { + if (event.error && attempts < maxRetries) { + attempts++ + await new Promise((resolve) => setTimeout(resolve, delay * 1000)) + event.retry = true + } else { + attempts = 0 + } + }) + // --8<-- [end:custom_retry_logic] +}