Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
1 change: 1 addition & 0 deletions frontend/internal-packages/agent/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ graph TD;
__start__ -.-> validateInitialSchema;
__start__ -.-> leadAgent;
leadAgent -.-> pmAgent;
leadAgent -.-> dbAgent;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the hard tab character.

This line contains a hard tab character. Replace it with spaces for consistency with the rest of the file.

As per coding guidelines (markdownlint-cli2 MD010).

Apply this diff:

-	leadAgent -.-> dbAgent;
+  leadAgent -.-> dbAgent;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
leadAgent -.-> dbAgent;
leadAgent -.-> dbAgent;
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

24-24: Hard tabs
Column: 1

(MD010, no-hard-tabs)

🤖 Prompt for AI Agents
In frontend/internal-packages/agent/README.md around line 24, the line
"leadAgent -.-> dbAgent;" contains a hard tab character; replace the tab with
spaces (use spaces consistent with the file, e.g., a single space or the
project's preferred indentation such as 2 or 4 spaces) so the line contains no
tab characters (fixes markdownlint MD010).

leadAgent -.-> __end__;
classDef default fill:#f2f0ff,line-height:1.2;
classDef first fill-opacity:0;
Expand Down
23 changes: 13 additions & 10 deletions frontend/internal-packages/agent/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
* Important: Node retries do NOT count toward this limit. The limit only
* applies to transitions between nodes.
*
* The workflow has 8 nodes:
* - Normal execution: 9 transitions (START → 8 nodes → END)
* - With error loops: May have additional transitions when errors occur
* (e.g., validateSchema → designSchema)
* TEMPORARY LIMITATION (set to 10):
* Due to issues with schemaDesignTool, the DB Agent cannot resolve schema issues
* on the second and subsequent attempts, causing infinite loops between:
* leadAgent → dbAgent → qaAgent → leadAgent (when schemaIssues exist)
*
* Setting this to 50 ensures:
* - Complete workflow execution under normal conditions
* - Ample headroom for complex error handling loops and retries
* - Protection against infinite loops while allowing for complex workflows
* - Sufficient capacity for finding optimal workflow patterns
* Current behavior with limit=10:
* - Allows multiple iterations of: PM Agent → DB Agent → QA Agent → Lead Agent → DB Agent
* - The workflow will fail after 10 loops if issues persist
* - Provides more opportunities for the DB Agent to refine the schema
*
* TODO: Increase this limit after fixing schemaDesignTool to properly handle
* schema modifications (e.g., unique constraint issues, JSON patch errors)
* See: route06/liam-internal#5642
*/
Comment on lines +8 to 21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify impact of low recursion limit on legitimate workflows.

A recursion limit of 10 is extremely restrictive and, as the comment acknowledges, will cause workflow failures after 10 loops. While the temporary nature is documented, this could break legitimate multi-step design sessions or complex schema scenarios.

Consider the following:

  1. Verification needed: Test that existing design sessions and typical user workflows don't exceed this limit under normal conditions
  2. Error handling: Ensure the workflow provides clear, actionable error messages when the recursion limit is hit, guiding users on next steps (e.g., "Schema validation is taking longer than expected. Please simplify your requirements or try again.")
  3. Monitoring: Add telemetry to track how often users hit this limit to assess real-world impact

Run the following script to check for existing workflows or tests that might be affected:

Additionally, consider searching documentation for guidance on recursion limits:


🏁 Script executed:

#!/bin/bash
# Search for integration tests or workflow configurations that might be sensitive to recursion limits
rg -n -C3 --type=ts 'recursionLimit|streamEvents.*config' | head -50

Length of output: 4241


🌐 Web query:

What are typical recursion limits for LangGraph workflows in production applications?

💡 Result:

Short answer — default is 25 super-steps; in production teams typically keep the default for simple graphs and raise it for complex workflows (common ranges: 50–200, sometimes higher) while adding monitoring/guards. See below for sources and guidance.

  • Default: 25 super-steps (LangGraph will raise GraphRecursionError when hit). [1][2]
  • Docs/how‑to: show how to set recursion_limit per run and recommend increasing it for complex graphs to avoid false positives while guarding against runaway loops. [2][3]
  • Troubleshooting / community guidance: common production practice is to raise the limit (examples use 50–100 or 100+) and pair with tracing, timeouts, and explicit stop conditions to avoid costs from infinite loops. [4][3]

If you want, tell me your graph’s structure (depth, loops, parallel branches, typical LLM call count) and I’ll recommend a concrete recursion_limit and monitoring configuration.

Sources: [1] LangGraph low‑level docs (recursion_limit default 25). [2] LangGraph how‑to: control recursion limit. [3] LangChain docs / migration guide (examples of raising recursionLimit). [4] LangGraph troubleshooting / community discussions recommending higher limits.


🏁 Script executed:

#!/bin/bash
rg -n "DEFAULT_RECURSION_LIMIT" -C2 --type=ts

Length of output: 5998


Raise DEFAULT_RECURSION_LIMIT above 10

  • DEFAULT_RECURSION_LIMIT is set to 10, which is below LangGraph’s default (25) and common production ranges (50–200), risking premature GraphRecursionError in valid workflows.
  • Increase the constant to a higher default (e.g., ≥50), update integration tests that import DEFAULT_RECURSION_LIMIT, and add clear error messaging or telemetry when the limit is reached.
🤖 Prompt for AI Agents
In frontend/internal-packages/agent/src/constants.ts around lines 8 to 21,
DEFAULT_RECURSION_LIMIT is currently set to 10 which is too low; update the
constant to a higher default (recommend ≥50, e.g., 50), then update any
integration/unit tests that import DEFAULT_RECURSION_LIMIT to expect the new
value, and add or update the code path that handles reaching the limit to emit a
clear error message and/or telemetry event indicating the recursion limit was
hit so failures are actionable.

export const DEFAULT_RECURSION_LIMIT = 50
export const DEFAULT_RECURSION_LIMIT = 10
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getTestConfig,
outputStreamEvents,
} from '../test-utils/workflowTestHelpers'
import { DEFAULT_RECURSION_LIMIT } from './constants'
import { createGraph } from './createGraph'
import type { WorkflowState } from './types'

Expand Down Expand Up @@ -36,6 +37,7 @@ describe('createGraph Integration', () => {
// Act
const streamEvents = graph.streamEvents(initialState, {
...config,
recursionLimit: DEFAULT_RECURSION_LIMIT,
streamMode: 'messages',
version: 'v2',
subgraphs: true,
Expand Down
1 change: 1 addition & 0 deletions frontend/internal-packages/agent/src/createGraph.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ graph TD;
__start__ -.-> validateInitialSchema;
__start__ -.-> leadAgent;
leadAgent -.-> pmAgent;
leadAgent -.-> dbAgent;
leadAgent -.-> __end__;
classDef default fill:#f2f0ff,line-height:1.2;
classDef first fill-opacity:0;
Expand Down
4 changes: 3 additions & 1 deletion frontend/internal-packages/agent/src/createGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export const createGraph = (checkpointer?: BaseCheckpointSaver) => {
const modifiedState = { ...state, messages: [], prompt }
const output = await dbAgentSubgraph.invoke(modifiedState, config)

return { ...state, ...output }
// Clear schemaIssues after DB agent processing to prevent infinite loops
return { ...state, ...output, schemaIssues: [] }
}

const callQaAgent = async (state: WorkflowState, config: RunnableConfig) => {
Expand Down Expand Up @@ -92,6 +93,7 @@ export const createGraph = (checkpointer?: BaseCheckpointSaver) => {

.addConditionalEdges('leadAgent', (state) => state.next, {
pmAgent: 'pmAgent',
dbAgent: 'dbAgent',
[END]: END,
})
.addEdge('pmAgent', 'dbAgent')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ describe('convertAnalyzedRequirementsToPrompt', () => {

Design a user management system with authentication

## Test Cases
## Requirements

- authentication: User login (SELECT), User logout (UPDATE), Password reset (UPDATE)
- userManagement: Create new user (INSERT), Update user info (UPDATE), Delete user (DELETE)"
Expand All @@ -96,7 +96,7 @@ describe('convertAnalyzedRequirementsToPrompt', () => {

Build a simple system

## Test Cases"
## Requirements"
`)
})

Expand Down Expand Up @@ -128,7 +128,7 @@ describe('convertAnalyzedRequirementsToPrompt', () => {

Add a basic feature

## Test Cases
## Requirements

- basic: Basic feature test (INSERT)"
`)
Expand Down Expand Up @@ -156,9 +156,13 @@ describe('convertAnalyzedRequirementsToPrompt', () => {

Design a user management system with authentication

## Test Cases
## Requirements

- authentication: User logout (UPDATE)"
- authentication: User logout (UPDATE)

## Schema Issues to Fix

1. Missing logout table"
`)
})

Expand All @@ -185,7 +189,7 @@ describe('convertAnalyzedRequirementsToPrompt', () => {

Design a user management system with authentication

## Test Cases
## Requirements

- authentication: User login (SELECT), User logout (UPDATE), Password reset (UPDATE)
- userManagement: Create new user (INSERT), Update user info (UPDATE), Delete user (DELETE)"
Expand Down Expand Up @@ -213,9 +217,13 @@ describe('convertAnalyzedRequirementsToPrompt', () => {

Design a user management system with authentication

## Test Cases
## Requirements

- authentication: User login (SELECT)

## Schema Issues to Fix

- authentication: User login (SELECT)"
1. Login form missing"
`)
})

Expand Down Expand Up @@ -243,7 +251,12 @@ describe('convertAnalyzedRequirementsToPrompt', () => {

Design a user management system with authentication

## Test Cases"
## Requirements


## Schema Issues to Fix

1. Non-existent testcase issue"
`)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ export const convertRequirementsToPrompt = (
)
.join('\n')

const schemaIssuesSection =
schemaIssues && schemaIssues.length > 0
? `\n\n## Schema Issues to Fix\n\n${schemaIssues.map((issue, index) => `${index + 1}. ${issue.description}`).join('\n')}`
: ''

return `## Session Goal

${requirements.goal}
Expand All @@ -45,7 +50,7 @@ ${requirements.goal}

${userInput}

## Test Cases
## Requirements

${testCasesSection}`.trim()
${testCasesSection}${schemaIssuesSection}`.trim()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { END } from '@langchain/langgraph'
import { describe, expect, it } from 'vitest'
import type { WorkflowState } from '../../types'
import { classifyMessage } from './index'

const createMockState = (
overrides: Partial<WorkflowState> = {},
): WorkflowState => ({
messages: [],
analyzedRequirements: {
goal: '',
testcases: {},
},
schemaIssues: [],
schemaData: { tables: {}, enums: {}, extensions: {} },
buildingSchemaId: 'test-schema-id',
organizationId: 'test-org-id',
userId: 'test-user-id',
designSessionId: 'test-session-id',
next: END,
...overrides,
})

describe('classifyMessage', () => {
it('should route to dbAgent when schema issues exist', async () => {
const state = createMockState({
schemaIssues: [
{
testcaseId: 'test-1',
description: 'Missing foreign key constraint',
},
],
})

const result = await classifyMessage(state)

expect(result.goto).toEqual([END])
expect(result.update).toEqual({ next: 'dbAgent' })
})

it('should route to dbAgent when multiple schema issues exist', async () => {
const state = createMockState({
schemaIssues: [
{
testcaseId: 'test-1',
description: 'Missing foreign key constraint',
},
{ testcaseId: 'test-2', description: 'Invalid table name' },
],
})

const result = await classifyMessage(state)

expect(result.goto).toEqual([END])
expect(result.update).toEqual({ next: 'dbAgent' })
})

it('should prioritize schema issues over QA completion', async () => {
const state = createMockState({
schemaIssues: [
{ testcaseId: 'test-1', description: 'Schema issue found' },
],
analyzedRequirements: {
goal: 'Test goal',
testcases: {
functional: [
{
id: 'test-1',
title: 'Test case',
type: 'SELECT',
sql: 'SELECT 1',
testResults: [],
},
],
},
},
})

const result = await classifyMessage(state)

expect(result.goto).toEqual([END])
expect(result.update).toEqual({ next: 'dbAgent' })
})

it('should route to summarizeWorkflow when QA completed and no schema issues', async () => {
const state = createMockState({
schemaIssues: [],
analyzedRequirements: {
goal: 'Test goal',
testcases: {
functional: [
{
id: 'test-1',
title: 'Test case',
type: 'SELECT',
sql: 'SELECT 1',
testResults: [],
},
],
},
},
})

const result = await classifyMessage(state)

expect(result.goto).toEqual(['summarizeWorkflow'])
expect(result.update).toBeUndefined()
})

it('should route to pmAgent when no schema issues and QA not completed', async () => {
const state = createMockState({
schemaIssues: [],
analyzedRequirements: {
goal: 'Test goal',
testcases: {},
},
})

const result = await classifyMessage(state)

expect(result.goto).toEqual([END])
expect(result.update).toEqual({ next: 'pmAgent' })
})
})
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import { Command, END } from '@langchain/langgraph'
import type { WorkflowState } from '../../types'
import { isQACompleted } from '../utils/workflowStatus'
import { isQACompleted, shouldRouteDBAgent } from '../utils/workflowStatus'

export async function classifyMessage(state: WorkflowState): Promise<Command> {
// 1. Check if QA is completed first (highest priority)
// 1. Check if DB Agent routing is needed (highest priority)
if (shouldRouteDBAgent(state)) {
return new Command({
update: { next: 'dbAgent' },
goto: END,
})
}
Comment on lines +7 to +12
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


// 2. Check if QA is completed (second priority)
if (isQACompleted(state)) {
return new Command({ goto: 'summarizeWorkflow' })
}

// 2. Direct routing to pmAgent
// 3. Direct routing to pmAgent (default)
return new Command({
update: { next: 'pmAgent' },
goto: END,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,11 @@ export function isQACompleted(state: WorkflowState): boolean {

return false
}

function hasSchemaIssues(state: WorkflowState): boolean {
return state.schemaIssues.length > 0
}

export function shouldRouteDBAgent(state: WorkflowState): boolean {
return hasSchemaIssues(state)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { AnalyzedRequirements } from '@liam-hq/artifact'
import type { Schema } from '@liam-hq/schema'
import {
generatedSqlsAnnotation,
schemaIssuesAnnotation,
qaSchemaIssuesAnnotation,
} from '../testcaseGeneration/testcaseAnnotation'

/**
Expand All @@ -27,7 +27,7 @@ export const qaAgentAnnotation = Annotation.Root({
}),
designSessionId: Annotation<string>,
buildingSchemaId: Annotation<string>,
schemaIssues: schemaIssuesAnnotation,
schemaIssues: qaSchemaIssuesAnnotation,
generatedSqls: generatedSqlsAnnotation,
next: Annotation<string>({
reducer: (x, y) => y ?? x ?? END,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { Annotation, MessagesAnnotation } from '@langchain/langgraph'
import type { Schema } from '@liam-hq/schema'
import type { SchemaIssue } from '../../workflowSchemaIssuesAnnotation'
import type { TestCaseData } from '../distributeRequirements'

type SchemaIssue = {
testcaseId: string
description: string
}

export const schemaIssuesAnnotation = Annotation<Array<SchemaIssue>>({
/**
* Schema issues annotation for QA agent's parallel processing.
*
* Uses concat reducer because:
* - Multiple testcase nodes run in parallel
* - Each node may discover different schema issues
* - All issues must be collected together
*
* This is different from workflow-level annotation which uses replacement
* for clearing issues after DB agent processing.
*/
export const qaSchemaIssuesAnnotation = Annotation<Array<SchemaIssue>>({
reducer: (prev, next) => prev.concat(next),
default: () => [],
})
Expand All @@ -27,6 +34,6 @@ export const testcaseAnnotation = Annotation.Root({
currentTestcase: Annotation<TestCaseData>,
schemaData: Annotation<Schema>,
goal: Annotation<string>,
schemaIssues: schemaIssuesAnnotation,
schemaIssues: qaSchemaIssuesAnnotation,
generatedSqls: generatedSqlsAnnotation,
})
Loading
Loading