Skip to content
Merged
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
6 changes: 3 additions & 3 deletions QA-CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,8 @@
- [-] Composio (tool integration for Agent) → `composio.spec.ts`
- [x] Playground shows error when LLM run endpoint returns 500 (mocked invalid API key) → `llm-agents/llm-invalid-api-key-ui.spec.ts`
- [x] Playground input remains usable after API error (mocked) → `llm-agents/llm-invalid-api-key-ui.spec.ts`
- [x] Agent stops when configured stop condition is reached → `core-functionality/llm-agents/agent-max-iterations.spec.ts` (`max_iterations` is the Agent's only configurable stop mechanism — no dedicated stop-condition field exists; see #824)
- [x] Agent stops when maximum number of iterations is reached → `core-functionality/llm-agents/agent-max-iterations.spec.ts`
- [~] Agent stops when configured stop condition is reached → `core-functionality/llm-agents/agent-max-iterations.spec.ts` (`max_iterations` is the Agent's only configurable stop mechanism — no dedicated stop-condition field exists; see #824. **Partial for the same reason as the bullet below:** the stop itself is quarantined against #1264)
- [~] Agent stops when maximum number of iterations is reached → `core-functionality/llm-agents/agent-max-iterations.spec.ts` (**partial: the stop assertion is quarantined (`test.fixme`, never `@stable`) against a live product failure — #1264.** With `max_iterations=1` on a task that needs several tool-calling iterations, the agent answers the task normally instead of returning `Model call limits exceeded: run limit (1/1)`; the message renders, so this is a content failure, not a timeout. Reproduced on 1.12.0.dev18 locally, off CI load, on `claude-haiku-4-5`, `claude-opus-5` and `claude-opus-4-5`, which rules out the mid-run backend wedge #1264's triage left open as a cover. The `@stable` causal control — a high limit finishes without the limit message — still runs, so what remains proven is that a high cap does not stop the agent, not that a low one does)
- [x] Agent with multiple configured tools executes correctly → `agent-multi-tool-selection.spec.ts`
- [ ] Agent with configured timeout respects the limit (no product surface on 1.12.x — the Agent component exposes no timeout field: its inputs are `max_iterations`, `max_tokens`, `n_messages`, `system_prompt`, `context_id`, `stream`, and tools; Langflow's only configurable per-component timeouts live on the A2A Agent (`timeout`) and MCP tools (`tool_execution_timeout`), not the Agent, and the Language Model / chat models expose none either. The client/transport execution timeout that bounds any run is covered by `ui-ux/execution-error-notification.spec.ts`. Not automatable as written; #825, same class as #824)
- [x] Connecting an external model in Agent drops the prior model selection (connection-mode isolation, prevents stale provider config) → `llm-agents/agent-model-connection-isolation.spec.ts`
Expand Down Expand Up @@ -436,7 +436,7 @@

#### 7.7 Model Parameters (Agent)
- [x] Maximum token count — response truncated as configured → `llm-agents/agent-max-tokens.spec.ts`
- [x] Maximum agent iterations → `core-functionality/llm-agents/agent-max-iterations.spec.ts`
- [~] Maximum agent iterations → `core-functionality/llm-agents/agent-max-iterations.spec.ts` (partial — the stop assertion is quarantined against #1264; see §6.2 for the measurement)
- [x] Use of custom `context_id` for memory isolation → `agent-context-id-isolation.spec.ts`
- [x] Output formatting (JSON via output_schema, Markdown, plain text) → `agent-structured-output.spec.ts`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ the model through the Playground.
`npx playwright test tests/collect-models.spec.ts`.
- At least one active provider API key in `.env` (OpenAI, Anthropic, or Google).
- Run with `--workers=1` (agent specs create named flows that collide in
parallel). File is serial (`SimpleAgentTemplatePage.load()` wipes all flows).
parallel). File is serial. `SimpleAgentTemplatePage.load()` does **not** wipe
existing flows — the cross-worker delete-all was removed in #553 — and cleanup
is id-scoped via the shared tracker (see *Notes* → flow cleanup).

---

Expand Down Expand Up @@ -181,6 +183,15 @@ The spec generates **2 tests per active model** via `resolveTestTargets()` (defa

## Notes *(optional)*

- **Flow cleanup is id-scoped, captured from the creation POST** (#1108's shared
tracker, wired in #1346). The spec previously had no cleanup at all, which cost
twice: an orphan `Simple Agent` per test on the shared instance, and — because
token attribution lives on the delete path (#1197) — tokens that reached the QA
platform with no spec to claim them (2026-08-06 daily: traces `1027dfd2` and
`6676e05d`, 936 + 918 tokens on `claude-haiku-4-5`, in the run's `unattributed`
bucket). The tracker rather than `load()`'s returned id, because `load()` can
throw **after** creating the flow (the #751/#1072 credential-settle guard throws
exactly there).
- **Why refusal is hard-asserted but empty is not:** capable models reliably obey
"reply with exactly this and nothing else", so a forced refusal (with a
distinctive marker) is deterministic. Genuinely-empty output is not — models
Expand Down
31 changes: 29 additions & 2 deletions docs/core-functionality/llm-agents/agent-max-iterations.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ Two tests establish this **causally**:
> completes). The bug appears **fixed**, so this is authored as a normal passing
> `@stable` test, not an expected-fail. Flagged on the issue/PR.

> **Quarantine (2026-08-06, nightly 1.12.0.dev18):** Test 1 is `test.fixme`
> against **#1264** — the product no longer enforces the cap. With
> `max_iterations = 1` the agent answers the task normally
> (`"I'll fetch that URL for you."` and variants) instead of returning the limit
> message; the bubble renders and the locator resolves 34 times, so this is a
> **content** failure, not a timeout. Reproduced LOCALLY on 1.12.0.dev18, off CI
> load, on `claude-haiku-4-5`, `claude-opus-5` and `claude-opus-4-5` — which is
> what rules out the mid-run backend wedge #1264's triage left open as a possible
> cover. Quarantined rather than left red because the test is `@regression` and
> never `@stable`: the daily does not run it, so a red here only failed the PR
> lane of any diff touching this file — and the file is serial, so it also skipped
> the `@stable` causal control. Test 2 is deliberately NOT quarantined; on its own
> it proves only that a high limit finishes. Lifting the quarantine is #1264's
> call, once the cap is enforced again on `langflowai/langflow-nightly:latest`.

If this fails, the agent no longer honours its iteration cap — a regression in a
core safety/cost control.

Expand All @@ -38,7 +53,9 @@ core safety/cost control.

`@stable` `@regression` `@agents` `@playground`

`@stable` added only after multiple clean `--retries=0` runs on the fresh nightly.
`@stable` added only after multiple clean `--retries=0` runs on the fresh nightly,
and it is carried by **Test 2 only** — Test 1 has never carried it and is now
`test.fixme` (see the quarantine note above; #1264).
`@regression` — guards the max-iterations enforcement from regressing (the bug
#481 documented); `@agents` — agent execution; `@playground` — the flow is run
through the Playground.
Expand All @@ -52,7 +69,9 @@ through the Playground.
`npx playwright test tests/collect-models.spec.ts`.
- At least one active provider API key in `.env`.
- Run with `--workers=1` (agent specs create named flows that collide in
parallel). File is serial (`SimpleAgentTemplatePage.load()` wipes all flows).
parallel). File is serial. `SimpleAgentTemplatePage.load()` does **not** wipe
existing flows — the cross-worker delete-all was removed in #553 — and cleanup
is id-scoped via the shared tracker (see *Notes* → flow cleanup).

---

Expand Down Expand Up @@ -166,6 +185,14 @@ flaky). The fetch is SSRF-blocked backend-side, but that is irrelevant — the

## Notes *(optional)*

- **Flow cleanup is id-scoped, captured from the creation POST** (#1108's shared
tracker, wired in #1346). The spec previously had no cleanup at all, which cost
twice: an orphan `Simple Agent` per test on the shared instance, and — because
token attribution lives on the delete path (#1197) — tokens that reached the QA
platform with no spec to claim them (2026-08-06 daily: trace `e7c60610`, 2,266
tokens over 2 `claude-haiku-4-5` calls, in the run's `unattributed` bucket). The
tracker rather than `load()`'s returned id, because `load()` can throw **after**
creating the flow (the #751/#1072 credential-settle guard throws exactly there).
- **Observable found during reproduction:** setting `max_iterations=1` yields the
AI message `Model call limits exceeded: run limit (1/1)`; a high limit
completes. This is a clean, deterministic signal — far more robust than
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,6 @@ This is a pure UI/state assertion — no LLM call is made.
## Notes *(optional)*

- In connection mode, `selectedModel` is intentionally overridden to `{ name: "Connect other models" }` in `modelInputComponent/index.tsx`, which is why the trigger shows that label rather than the empty `"Select a model"` placeholder.
- Run with `--workers=1`: `SimpleAgentTemplatePage.load()` deletes all flows before loading the template, so parallel agent specs would wipe each other's flows. The spec also sets file-level serial mode.
- Run with `--workers=1`: agent specs create named flows that collide in parallel. The spec also sets file-level serial mode. `SimpleAgentTemplatePage.load()` does **not** delete existing flows — the cross-worker delete-all was removed in #553.
- **Flow cleanup is id-scoped, from the creation POST** (#1108's shared tracker, wired in #1346). The spec previously had no cleanup at all and left an orphan `Simple Agent` behind per run. It never executes the flow, so it produced no trace and no unattributed tokens on the 2026-08-06 daily — the leak was the whole cost here; the tracker also closes the token-attribution path (#1197), which lives on the delete call.
- The spec is provider-agnostic and runs a single target on purpose — the connection-mode clear does not vary by provider, so looping every model would add cost without coverage.
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
import { resolveTestTargets } from "../../../../helpers/provider-setup/test-targets";
import { waitForFlowSaveSettled } from "../../../../helpers/flows/wait-for-flow-save-settled";
import { getAuthToken } from "../../../../helpers/auth/get-auth-token";
import { trackCreatedFlows } from "../../../../helpers/flows/track-created-flows";

/**
* Agent robustness on a degenerate model output (QA-CHECKLIST §6.5,
Expand All @@ -38,6 +39,33 @@ if (!process.env.CI) {
// instruction we set.
const USER_MESSAGE = "What is the capital of France?";

// Id-scoped cleanup for every flow this spec's page creates (#1108's shared
// tracker, never a delete-all sweep — #553). This spec had NO cleanup at all: the
// flow it ran the agent on was left behind, which cost twice. It leaked an orphan
// `Simple Agent` per test on the shared instance, and — because token attribution
// lives on the delete path (#1197) — its tokens reached the platform with no spec
// to claim them. Measured on the 2026-08-06 daily (#1346): traces `1027dfd2` and
// `6676e05d`, 936 + 918 tokens on `claude-haiku-4-5`, in the run's `unattributed`
// bucket. The `attrib_cost` records this spec DID produce came from
// `loadTemplateByName`'s own cleanup of the surplus flows it creates — those never
// ran, so they carried no traces and the attribution read came back empty.
//
// The tracker rather than the returned id: `load()` can throw AFTER creating the
// flow (the #751/#1072 credential-settle guard throws exactly there), and an id
// captured from the creation POST survives that.
let flows: ReturnType<typeof trackCreatedFlows>;

test.beforeEach(({ page }) => {
flows = trackCreatedFlows(page);
});

// Attribution is derived from the running test by `cleanup` itself (#1197 §1.1) —
// no explicit `attribution` option is needed, and the whole sidecar stays inert
// unless the lane sets TOKENS_ATTRIB.
test.afterEach(async ({ request }) => {
await flows.cleanup(request);
});

async function loadAgent(page: Page, options: LoadSimpleAgentOptions): Promise<void> {
try {
await new SimpleAgentTemplatePage(page).load(options);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { Page } from "@playwright/test";
import { expect, test } from "../../../../fixtures/fixtures";
import { SimpleAgentTemplatePage, type LoadSimpleAgentOptions } from "../../../../pages";
import { waitForFlowSaveSettled } from "../../../../helpers/flows/wait-for-flow-save-settled";
import { trackCreatedFlows } from "../../../../helpers/flows/track-created-flows";
import {
closeAdvancedOptions,
openAdvancedOptions,
Expand Down Expand Up @@ -51,6 +52,33 @@ const TASK = `Fetch ${TARGET_URL} and tell me the exact "version" value it retur
const LIMIT_MESSAGE = /model call limits exceeded/i;
const HIGH_LIMIT = "20";

// Id-scoped cleanup for every flow this spec's page creates (#1108's shared
// tracker, never a delete-all sweep — #553). This spec had NO cleanup at all: the
// flow it ran the agent on was left behind, which cost twice. It leaked an orphan
// `Simple Agent` per test on the shared instance, and — because token attribution
// lives on the delete path (#1197) — its tokens reached the platform with no spec
// to claim them. Measured on the 2026-08-06 daily (#1346): trace `e7c60610`,
// 2,266 tokens over 2 `claude-haiku-4-5` calls, in the run's `unattributed`
// bucket. The `attrib_cost` record this spec DID produce came from
// `loadTemplateByName`'s own cleanup of the surplus flows it creates — those never
// ran, so they carried no traces and the attribution read came back empty.
//
// The tracker rather than the returned id: `load()` can throw AFTER creating the
// flow (the #751/#1072 credential-settle guard throws exactly there), and an id
// captured from the creation POST survives that.
let flows: ReturnType<typeof trackCreatedFlows>;

test.beforeEach(({ page }) => {
flows = trackCreatedFlows(page);
});

// Attribution is derived from the running test by `cleanup` itself (#1197 §1.1) —
// no explicit `attribution` option is needed, and the whole sidecar stays inert
// unless the lane sets TOKENS_ATTRIB.
test.afterEach(async ({ request }) => {
await flows.cleanup(request);
});

async function loadAgent(page: Page, options: LoadSimpleAgentOptions): Promise<void> {
try {
await new SimpleAgentTemplatePage(page).load(options);
Expand Down Expand Up @@ -123,15 +151,42 @@ async function runAndGetBubble(page: Page) {

const targets = resolveTestTargets({ tier: "tool-calling" });

// SimpleAgentTemplatePage.load() deletes all flows before loading the template;
// serial mode + --workers=1 keeps the shared instance state deterministic.
// Serial mode + --workers=1 keeps the shared instance state deterministic. Note
// that `SimpleAgentTemplatePage.load()` does NOT wipe existing flows — the
// cross-worker delete-all was removed in #553 — so cleanup is id-scoped, in the
// `afterEach` above.
test.describe.configure({ mode: "serial" });

for (const { label, options, skipReason } of targets) {
const provider = options.provider ?? (Object.keys(providerConfigMap)[0] as Provider);

test.describe(`Agent Max Iterations [${label}]`, () => {
test(
// QUARANTINED — the product no longer enforces the cap (#1264, still open).
// The agent answers the task normally instead of stopping: the assertion below
// reads a real, rendered message (`34 × locator resolved`), so this is a
// content failure, not a timeout. Received on three independent runs of the
// three different Anthropic models tried:
//
// "I'll fetch that URL for you."
// "I'll fetch that URL for you and retrieve the version value."
// "I'll fetch that URL for you and get the version value." (daily #1258)
//
// Reproduced on 1.12.0.dev18 LOCALLY, off CI load, on claude-haiku-4-5,
// claude-opus-5 and claude-opus-4-5 — which is what rules out the mid-run
// backend wedge #1264's triage left open as a possible cover.
//
// `test.fixme` rather than leaving it red, for the reason the same quarantine
// is used in mcp-server.spec.ts (#1266) and openai-compatible-provider-setup:
// this test is `@regression` and never `@stable`, so the daily does not run it
// and the only thing a red here does is fail the PR lane of any diff that
// touches this file — while the file is SERIAL, so its failure also skipped
// the `@stable` causal control below, the half that still works.
//
// Lifting the quarantine (remove `test.fixme`) is #1264's call, once the cap is
// enforced again on `langflowai/langflow-nightly:latest`. The causal control
// below is deliberately NOT quarantined: on its own it proves only that a high
// limit finishes, and it is what will show the pair working again.
test.fixme(
"agent stops when max iterations is reached",
{ tag: ["@regression", "@agents", "@playground"] },
async ({ page }) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import fs from "fs";
import type { Page } from "@playwright/test";
import { expect, test } from "../../../../fixtures/fixtures";
import { SimpleAgentTemplatePage, type LoadSimpleAgentOptions } from "../../../../pages";
import { trackCreatedFlows } from "../../../../helpers/flows/track-created-flows";
import {
hasProviderEnvKeys,
keyedProviderNames,
Expand Down Expand Up @@ -95,10 +96,33 @@ async function loadAgent(page: Page): Promise<void> {
}
}

// SimpleAgentTemplatePage.load() deletes all flows before loading the template.
// Serial mode only serializes the blocks within this file; isolation from
// sibling agent specs that also wipe flows relies on running with --workers=1
// (required by this folder's CLAUDE.md).
// Id-scoped cleanup for every flow this spec's page creates (#1108's shared
// tracker, never a delete-all sweep — #553). This spec had NO cleanup at all, so
// it left an orphan `Simple Agent` behind per run. Unlike the two sibling specs
// #1346 measured, this one never executes the flow, so it produced no trace and no
// unattributed tokens on the 2026-08-06 daily — the leak is the whole cost here,
// and the attribution path is closed for free by the same hook.
//
// The tracker rather than the returned id: `load()` can throw AFTER creating the
// flow (the #751/#1072 credential-settle guard throws exactly there), and an id
// captured from the creation POST survives that.
let flows: ReturnType<typeof trackCreatedFlows>;

test.beforeEach(({ page }) => {
flows = trackCreatedFlows(page);
});

// Attribution is derived from the running test by `cleanup` itself (#1197 §1.1) —
// no explicit `attribution` option is needed, and the whole sidecar stays inert
// unless the lane sets TOKENS_ATTRIB.
test.afterEach(async ({ request }) => {
await flows.cleanup(request);
});

// Serial mode only serializes the blocks within this file; isolation from sibling
// agent specs relies on running with --workers=1 (required by this folder's
// CLAUDE.md). `SimpleAgentTemplatePage.load()` does NOT wipe existing flows — the
// cross-worker delete-all was removed in #553.
test.describe.configure({ mode: "serial" });

test.describe("Agent Model Connection Isolation", () => {
Expand Down
Loading