fix: soft error on AI agent max iterations + rename retries tab to error handling (#8366)

* fix: soft error when AI agent reaches max iterations instead of hard error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: set output to error message when AI agent reaches max iterations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: add error field to AI agent result for max iterations soft error

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: restore default max iterations to 10

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: include partial result in max iterations error message

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: rename retries tab to error handling and reorganize sections

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
centdix
2026-03-16 15:13:22 +01:00
committed by GitHub
parent 020de59fcf
commit 1a1e8a164c
3 changed files with 84 additions and 76 deletions
+9 -4
View File
@@ -762,8 +762,10 @@ pub async fn run_agent(
.map(|m| m.clamp(1, HARD_MAX_AGENT_ITERATIONS))
.unwrap_or(DEFAULT_MAX_AGENT_ITERATIONS);
// Main agent loop
for i in 0..max_iterations {
if used_structured_output_tool {
break;
}
@@ -1036,10 +1038,13 @@ pub async fn run_agent(
if tool_calls.is_empty() {
break;
} else if i == max_iterations - 1 {
return Err(Error::internal_err(
"AI agent reached max iterations, but there are still tool calls"
.to_string(),
));
let partial_result = serde_json::to_string(&serde_json::json!({
"messages": messages,
})).unwrap_or_default();
return Err(Error::internal_err(format!(
"AI agent reached max iterations ({}), you can either increase max_iterations or enable the \"continue on error\" option from the advanced options of the step. Partial result:\n{}",
max_iterations, partial_result
)));
}
messages.push(OpenAIMessage {
@@ -1111,8 +1111,8 @@
<Tabs bind:selected={advancedSelected} wrapperClass="shrink-0">
<Tab
value="retries"
active={flowModule.retry !== undefined}
label="Retries"
active={flowModule.retry !== undefined || flowModule.continue_on_error}
label="Error handling"
/>
{#if !selectedId.includes('failure')}
<Tab value="runtime" label="Runtime" />
@@ -1152,6 +1152,22 @@
{/if}
<div class="flex-1 overflow-auto p-4">
{#if advancedSelected === 'retries'}
<Section label="Continue on error">
{#snippet header()}
<Tooltip>
When enabled, the flow will continue to the next step even if this step fails (after exhausting all retries, if any). This enables to process the error in a branch one for instance.
</Tooltip>
{/snippet}
<Toggle
size="xs"
bind:checked={flowModule.continue_on_error}
options={{
left: 'Stop on error and propagate error up',
right: "Continue on error with error as step's return"
}}
/>
</Section>
<div class="mt-4"></div>
<Section label="Retries">
{#snippet header()}
<Tooltip
@@ -1161,19 +1177,6 @@
maximum number of attempts as defined below.
</Tooltip>
{/snippet}
<Label label="After all retries attempts have been exhausted:">
<Toggle
size="xs"
bind:checked={flowModule.continue_on_error}
options={{
left: 'Stop on error and propagate error up',
right: "Continue on error with error as step's return",
rightTooltip:
'When enabled, the flow will continue to the next step after going through all the retries (if any) even if this step fails. This enables to process the error in a branch one for instance.'
}}
/>
</Label>
<div class="my-8"></div>
<FlowRetries bind:flowModuleRetry={flowModule.retry} bind:flowModule />
</Section>
{:else if advancedSelected === 'runtime' && advancedRuntimeSelected === 'concurrency'}
@@ -110,7 +110,7 @@
const u32Max = 4294967295
</script>
<div class="h-full flex flex-col gap-4">
<div class="flex flex-col gap-4">
<ToggleButtonGroup
bind:selected={delayType}
class={`${disabled ? 'disabled' : ''}`}
@@ -216,6 +216,7 @@
</Section>
{/if}
{#if delayType === 'constant' || delayType === 'exponential'}
<div class="flex h-[calc(100%-22px)]">
<div class="w-1/2 h-full overflow-auto pr-2">
{#if delayType === 'constant'}
@@ -296,66 +297,65 @@
</div>
<div class="w-1/2 h-full overflow-auto pl-2">
{#if true}
{@const { attempts: cAttempts, seconds: cSeconds } = flowModuleRetry?.constant || {}}
{@const {
attempts: eAttempts,
seconds: eSeconds,
multiplier,
random_factor
} = flowModuleRetry?.exponential || {}}
{@const cArray = Array.from({ length: Math.min(cAttempts || 0, 100) }, () => cSeconds)}
{@const eArray = Array.from(
{ length: Math.min(eAttempts || 0, 100) },
(_, i) => (multiplier || 0) * (eSeconds || 0) ** (i + cArray.length + 1)
)}
{@const array = [...cArray, ...eArray]}
<div class="bg-surface-secondary border rounded px-4 py-2">
<div class="text-xs font-medium mb-2">Retry attempts</div>
{#if array.length > 0}
<table class="text-xs">
<thead>
{@const { attempts: cAttempts, seconds: cSeconds } = flowModuleRetry?.constant || {}}
{@const {
attempts: eAttempts,
seconds: eSeconds,
multiplier,
random_factor
} = flowModuleRetry?.exponential || {}}
{@const cArray = Array.from({ length: Math.min(cAttempts || 0, 100) }, () => cSeconds)}
{@const eArray = Array.from(
{ length: Math.min(eAttempts || 0, 100) },
(_, i) => (multiplier || 0) * (eSeconds || 0) ** (i + cArray.length + 1)
)}
{@const array = [...cArray, ...eArray]}
<div class="bg-surface-secondary border rounded px-4 py-2">
<div class="text-xs font-medium mb-2">Retry attempts</div>
{#if array.length > 0}
<table class="text-xs">
<thead>
<tr>
<td class="font-semibold pr-1 pb-1">1:</td>
<td class="pb-1"
>After {array[0]} second{array[0] === 1 ? '' : 's'}
{#if (random_factor ?? 0) > 0}(+/- {((array[0] ?? 0) * (random_factor ?? 0)) /
100}
seconds){/if}</td
>
</tr>
</thead>
<tbody>
{#each array.slice(1, 100) as delay, i}
{@const index = i + 2}
<tr>
<td class="font-semibold pr-1 pb-1">1:</td>
<td class="pb-1"
>After {array[0]} second{array[0] === 1 ? '' : 's'}
{#if (random_factor ?? 0) > 0}(+/- {((array[0] ?? 0) * (random_factor ?? 0)) /
<td class="font-semibold pr-1 align-top">{index}:</td>
<td class="pb-1 whitespace-nowrap">
{delay} second{delay === 1 ? '' : 's'}
{#if (random_factor ?? 0) > 0}(+/- {((delay ?? 0) * (random_factor ?? 0)) /
100}
seconds){/if}</td
>
seconds){/if}
after attempt #{index - 1}
{#if i > cArray.length - 2}
<span class="text-gray-400 pl-2">
({multiplier} * {eSeconds}<sup>{index}</sup>)
</span>
{/if}
</td>
</tr>
</thead>
<tbody>
{#each array.slice(1, 100) as delay, i}
{@const index = i + 2}
<tr>
<td class="font-semibold pr-1 align-top">{index}:</td>
<td class="pb-1 whitespace-nowrap">
{delay} second{delay === 1 ? '' : 's'}
{#if (random_factor ?? 0) > 0}(+/- {((delay ?? 0) * (random_factor ?? 0)) /
100}
seconds){/if}
after attempt #{index - 1}
{#if i > cArray.length - 2}
<span class="text-gray-400 pl-2">
({multiplier} * {eSeconds}<sup>{index}</sup>)
</span>
{/if}
</td>
</tr>
{/each}
{#if (cAttempts ?? 0) > 100 || (eAttempts ?? 0) > 100}
<tr>
<td class="font-semibold pr-1 align-top">...</td>
<td class="pb-1">...</td>
</tr>
{/if}
</tbody>
</table>
{:else}
<div class="text-xs">No retries</div>
{/if}
</div>
{/each}
{#if (cAttempts ?? 0) > 100 || (eAttempts ?? 0) > 100}
<tr>
<td class="font-semibold pr-1 align-top">...</td>
<td class="pb-1">...</td>
</tr>
{/if}
</tbody>
</table>
{/if}
</div>
{/if}
</div>
</div>
{/if}
</div>