-
Notifications
You must be signed in to change notification settings - Fork 176
Add workflow reconnection logic for page reload scenarios #3672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4a0c870
ca1e92b
dd2b7b0
a4ec435
a6724e2
2a676bb
027d620
991254b
96da7bb
d5b74c9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { AIMessage, HumanMessage } from '@langchain/core/messages' | ||
import { describe, expect, it } from 'vitest' | ||
import { determineWorkflowAction } from './determineWorkflowAction' | ||
|
||
describe('determineWorkflowAction', () => { | ||
describe('Priority 1: Already triggered', () => { | ||
it('returns none when hasTriggered is true', () => { | ||
const result = determineWorkflowAction( | ||
[new HumanMessage('test')], | ||
false, // isWorkflowInProgress | ||
true, // hasTriggered | ||
) | ||
expect(result).toEqual({ type: 'none' }) | ||
}) | ||
|
||
it('returns none even when both hasTriggered and isWorkflowInProgress are true', () => { | ||
const result = determineWorkflowAction( | ||
[new HumanMessage('test')], | ||
true, // isWorkflowInProgress | ||
true, // hasTriggered | ||
) | ||
expect(result).toEqual({ type: 'none' }) | ||
}) | ||
}) | ||
|
||
describe('Priority 2: Workflow in progress flag exists', () => { | ||
it('returns replay when isWorkflowInProgress is true', () => { | ||
const result = determineWorkflowAction( | ||
[], | ||
true, // isWorkflowInProgress | ||
false, // hasTriggered | ||
) | ||
expect(result).toEqual({ type: 'replay' }) | ||
}) | ||
|
||
it('returns replay even with multiple messages when isWorkflowInProgress is true', () => { | ||
const result = determineWorkflowAction( | ||
[new HumanMessage('first'), new AIMessage('response')], | ||
true, // isWorkflowInProgress | ||
false, // hasTriggered | ||
) | ||
expect(result).toEqual({ type: 'replay' }) | ||
}) | ||
}) | ||
|
||
describe('Priority 3: Single unanswered user message', () => { | ||
it('returns start when there is a single HumanMessage', () => { | ||
const result = determineWorkflowAction( | ||
[new HumanMessage('hello')], | ||
false, // isWorkflowInProgress | ||
false, // hasTriggered | ||
) | ||
expect(result).toEqual({ | ||
type: 'start', | ||
userInput: 'hello', | ||
}) | ||
}) | ||
|
||
it('handles when content is not a string', () => { | ||
const message = new HumanMessage({ content: 'test' }) | ||
const result = determineWorkflowAction([message], false, false) | ||
expect(result.type).toBe('start') | ||
}) | ||
Comment on lines
+59
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Test name mismatch: actually still string content This case claims “not a string” but still passes string content. Use a non‑string content shape to truly exercise the path. - it('handles when content is not a string', () => {
- const message = new HumanMessage({ content: 'test' })
- const result = determineWorkflowAction([message], false, false)
- expect(result.type).toBe('start')
- })
+ it('handles when content is not a string', () => {
+ const message = new HumanMessage({
+ // non-string content (array of parts)
+ content: [{ type: 'text', text: 'hello from parts' }],
+ })
+ const result = determineWorkflowAction([message], false, false)
+ expect(result.type).toBe('start')
+ }) 🤖 Prompt for AI Agents
|
||
}) | ||
|
||
describe('Priority 4: Do nothing', () => { | ||
it('returns none when messages is empty', () => { | ||
const result = determineWorkflowAction([], false, false) | ||
expect(result).toEqual({ type: 'none' }) | ||
}) | ||
|
||
it('returns none when there are multiple messages', () => { | ||
const result = determineWorkflowAction( | ||
[new HumanMessage('first'), new AIMessage('response')], | ||
false, | ||
false, | ||
) | ||
expect(result).toEqual({ type: 'none' }) | ||
}) | ||
|
||
it('returns none when single message is AIMessage', () => { | ||
const result = determineWorkflowAction( | ||
[new AIMessage('ai message')], | ||
false, | ||
false, | ||
) | ||
expect(result).toEqual({ type: 'none' }) | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,45 @@ | ||||||||||||||||||||||||||||||||||
import type { BaseMessage } from '@langchain/core/messages' | ||||||||||||||||||||||||||||||||||
import { isHumanMessage } from '@langchain/core/messages' | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
type WorkflowAction = | ||||||||||||||||||||||||||||||||||
| { type: 'start'; userInput: string } | ||||||||||||||||||||||||||||||||||
| { type: 'replay' } | ||||||||||||||||||||||||||||||||||
| { type: 'none' } | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||
* Pure function to determine workflow action | ||||||||||||||||||||||||||||||||||
* | ||||||||||||||||||||||||||||||||||
* @param messages - Message history | ||||||||||||||||||||||||||||||||||
* @param isWorkflowInProgress - Whether workflow is in progress (from sessionStorage) | ||||||||||||||||||||||||||||||||||
* @param hasTriggered - Whether workflow has already been triggered | ||||||||||||||||||||||||||||||||||
* @returns Action to execute | ||||||||||||||||||||||||||||||||||
*/ | ||||||||||||||||||||||||||||||||||
export const determineWorkflowAction = ( | ||||||||||||||||||||||||||||||||||
messages: BaseMessage[], | ||||||||||||||||||||||||||||||||||
isWorkflowInProgress: boolean, | ||||||||||||||||||||||||||||||||||
hasTriggered: boolean, | ||||||||||||||||||||||||||||||||||
): WorkflowAction => { | ||||||||||||||||||||||||||||||||||
// 1. Already triggered - do nothing | ||||||||||||||||||||||||||||||||||
if (hasTriggered) { | ||||||||||||||||||||||||||||||||||
return { type: 'none' } | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// 2. Workflow in progress flag exists - resume interrupted workflow | ||||||||||||||||||||||||||||||||||
if (isWorkflowInProgress) { | ||||||||||||||||||||||||||||||||||
return { type: 'replay' } | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// 3. Single unanswered user message - start new workflow | ||||||||||||||||||||||||||||||||||
if (messages.length === 1) { | ||||||||||||||||||||||||||||||||||
const firstMessage = messages[0] | ||||||||||||||||||||||||||||||||||
if (firstMessage && isHumanMessage(firstMessage)) { | ||||||||||||||||||||||||||||||||||
return { | ||||||||||||||||||||||||||||||||||
type: 'start', | ||||||||||||||||||||||||||||||||||
userInput: firstMessage.text, | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+35
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Validate non-empty userInput before starting workflow. The code correctly uses Apply this diff to validate non-empty userInput: if (firstMessage && isHumanMessage(firstMessage)) {
+ const userInput = firstMessage.text.trim()
+ if (!userInput) {
+ return { type: 'none' }
+ }
return {
type: 'start',
- userInput: firstMessage.text,
+ userInput,
}
} 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
// 4. Otherwise - do nothing | ||||||||||||||||||||||||||||||||||
return { type: 'none' } | ||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
const WORKFLOW_KEY_PREFIX = 'liam:workflow:' | ||
|
||
/** | ||
* Get whether workflow is in progress | ||
*/ | ||
export const getWorkflowInProgress = (designSessionId: string): boolean => { | ||
if (typeof window === 'undefined') return false | ||
const key = `${WORKFLOW_KEY_PREFIX}${designSessionId}` | ||
const value = sessionStorage.getItem(key) | ||
return value === 'in_progress' | ||
} | ||
|
||
/** | ||
* Set workflow in progress flag | ||
*/ | ||
export const setWorkflowInProgress = (designSessionId: string): void => { | ||
if (typeof window === 'undefined') return | ||
const key = `${WORKFLOW_KEY_PREFIX}${designSessionId}` | ||
sessionStorage.setItem(key, 'in_progress') | ||
} | ||
|
||
/** | ||
* Clear workflow in progress flag | ||
*/ | ||
export const clearWorkflowInProgress = (designSessionId: string): void => { | ||
if (typeof window === 'undefined') return | ||
const key = `${WORKFLOW_KEY_PREFIX}${designSessionId}` | ||
sessionStorage.removeItem(key) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.