Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 14 additions & 10 deletions src/content/docs/user-guide/concepts/agents/hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -876,8 +876,8 @@ class RetryOnServiceUnavailable(HookProvider):
</Tab>
<Tab label="TypeScript">

```ts
// This feature is not yet available in TypeScript SDK
```typescript
--8<-- "user-guide/concepts/agents/hooks.ts:retry_on_service_unavailable_class"
```
</Tab>
</Tabs>
Expand All @@ -898,8 +898,8 @@ result = agent("What is the capital of France?")
</Tab>
<Tab label="TypeScript">

```ts
// This feature is not yet available in TypeScript SDK
```typescript
--8<-- "user-guide/concepts/agents/hooks.ts:retry_on_service_unavailable_usage"
```
</Tab>
</Tabs>
Expand Down Expand Up @@ -941,8 +941,12 @@ agent = Agent(
</Tab>
<Tab label="TypeScript">

```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"
```
</Tab>
</Tabs>
Expand Down Expand Up @@ -992,8 +996,8 @@ class RetryOnToolError(HookProvider):
</Tab>
<Tab label="TypeScript">

```ts
// This feature is not yet available in TypeScript SDK
```typescript
--8<-- "user-guide/concepts/agents/hooks.ts:retry_on_tool_error_class"
```
</Tab>
</Tabs>
Expand Down Expand Up @@ -1026,8 +1030,8 @@ result = agent("Look up the weather")
</Tab>
<Tab label="TypeScript">

```ts
// This feature is not yet available in TypeScript SDK
```typescript
--8<-- "user-guide/concepts/agents/hooks.ts:retry_on_tool_error_usage"
```
</Tab>
</Tabs>
Expand Down
132 changes: 132 additions & 0 deletions src/content/docs/user-guide/concepts/agents/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<new (...args: any[]) => Error>

constructor(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
allowedExceptions: Array<new (...args: any[]) => 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<string, number>()

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
12 changes: 6 additions & 6 deletions src/content/docs/user-guide/concepts/agents/retry-strategies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ agent = Agent(
</Tab>
<Tab label="TypeScript">

```ts
// Not supported in TypeScript
```typescript
--8<-- "user-guide/concepts/agents/retry-strategies.ts:customizing_retry"
```
</Tab>
</Tabs>
Expand Down Expand Up @@ -69,8 +69,8 @@ agent = Agent(
</Tab>
<Tab label="TypeScript">

```ts
// Not supported in TypeScript
```typescript
--8<-- "user-guide/concepts/agents/retry-strategies.ts:disabling_retry"
```
</Tab>
</Tabs>
Expand Down Expand Up @@ -111,8 +111,8 @@ agent = Agent(hooks=[CustomRetry()])
</Tab>
<Tab label="TypeScript">

```ts
// Not supported in TypeScript
```typescript
--8<-- "user-guide/concepts/agents/retry-strategies.ts:custom_retry_logic"
```
</Tab>
</Tabs>
Expand Down
64 changes: 64 additions & 0 deletions src/content/docs/user-guide/concepts/agents/retry-strategies.ts
Original file line number Diff line number Diff line change
@@ -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]
}