Skip to content

Commit f7e0c6c

Browse files
authored
Separate agent controller and server via EventStream (#1538)
* move towards event stream * refactor agent state changes * move agent state logic * fix callbacks * break on finish * closer to working * change frontend to accomodate new flow * handle start action * fix locked stream * revert message * logspam * no async on close * get rid of agent_task * fix up closing * better asyncio handling * sleep to give back control * fix key * logspam * update frontend agent state actions * fix pause and cancel * delint * fix map * delint * wait for agent to finish * fix unit test * event stream enums * fix merge issues * fix lint * fix test * fix test * add user message action * add user message action * fix up user messages * fix main.py flow * refactor message waiting * lint * fix test * fix test
1 parent 4e84aac commit f7e0c6c

36 files changed

+433
-494
lines changed

frontend/src/components/AgentControlBar.tsx

Lines changed: 41 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,47 +4,39 @@ import { useSelector } from "react-redux";
44
import ArrowIcon from "#/assets/arrow";
55
import PauseIcon from "#/assets/pause";
66
import PlayIcon from "#/assets/play";
7-
import { changeTaskState } from "#/services/agentStateService";
7+
import { changeAgentState } from "#/services/agentStateService";
88
import { clearMsgs } from "#/services/session";
99
import store, { RootState } from "#/store";
10-
import AgentTaskAction from "#/types/AgentTaskAction";
11-
import AgentTaskState from "#/types/AgentTaskState";
10+
import AgentState from "#/types/AgentState";
1211
import { clearMessages } from "#/state/chatSlice";
1312

14-
const TaskStateActionMap = {
15-
[AgentTaskAction.START]: AgentTaskState.RUNNING,
16-
[AgentTaskAction.PAUSE]: AgentTaskState.PAUSED,
17-
[AgentTaskAction.RESUME]: AgentTaskState.RUNNING,
18-
[AgentTaskAction.STOP]: AgentTaskState.STOPPED,
19-
};
20-
21-
const IgnoreTaskStateMap: { [k: string]: AgentTaskState[] } = {
22-
[AgentTaskAction.PAUSE]: [
23-
AgentTaskState.INIT,
24-
AgentTaskState.PAUSED,
25-
AgentTaskState.STOPPED,
26-
AgentTaskState.FINISHED,
27-
AgentTaskState.AWAITING_USER_INPUT,
13+
const IgnoreTaskStateMap: { [k: string]: AgentState[] } = {
14+
[AgentState.PAUSED]: [
15+
AgentState.INIT,
16+
AgentState.PAUSED,
17+
AgentState.STOPPED,
18+
AgentState.FINISHED,
19+
AgentState.AWAITING_USER_INPUT,
2820
],
29-
[AgentTaskAction.RESUME]: [
30-
AgentTaskState.INIT,
31-
AgentTaskState.RUNNING,
32-
AgentTaskState.STOPPED,
33-
AgentTaskState.FINISHED,
34-
AgentTaskState.AWAITING_USER_INPUT,
21+
[AgentState.RUNNING]: [
22+
AgentState.INIT,
23+
AgentState.RUNNING,
24+
AgentState.STOPPED,
25+
AgentState.FINISHED,
26+
AgentState.AWAITING_USER_INPUT,
3527
],
36-
[AgentTaskAction.STOP]: [
37-
AgentTaskState.INIT,
38-
AgentTaskState.STOPPED,
39-
AgentTaskState.FINISHED,
28+
[AgentState.STOPPED]: [
29+
AgentState.INIT,
30+
AgentState.STOPPED,
31+
AgentState.FINISHED,
4032
],
4133
};
4234

4335
interface ButtonProps {
4436
isDisabled: boolean;
4537
content: string;
46-
action: AgentTaskAction;
47-
handleAction: (action: AgentTaskAction) => void;
38+
action: AgentState;
39+
handleAction: (action: AgentState) => void;
4840
large?: boolean;
4941
}
5042

@@ -75,53 +67,53 @@ ActionButton.defaultProps = {
7567
};
7668

7769
function AgentControlBar() {
78-
const { curTaskState } = useSelector((state: RootState) => state.agent);
79-
const [desiredState, setDesiredState] = React.useState(AgentTaskState.INIT);
70+
const { curAgentState } = useSelector((state: RootState) => state.agent);
71+
const [desiredState, setDesiredState] = React.useState(AgentState.INIT);
8072
const [isLoading, setIsLoading] = React.useState(false);
8173

82-
const handleAction = (action: AgentTaskAction) => {
83-
if (IgnoreTaskStateMap[action].includes(curTaskState)) {
74+
const handleAction = (action: AgentState) => {
75+
if (IgnoreTaskStateMap[action].includes(curAgentState)) {
8476
return;
8577
}
8678

8779
let act = action;
8880

89-
if (act === AgentTaskAction.STOP) {
90-
act = AgentTaskAction.STOP;
81+
if (act === AgentState.STOPPED) {
82+
act = AgentState.STOPPED;
9183
clearMsgs().then().catch();
9284
store.dispatch(clearMessages());
9385
} else {
9486
setIsLoading(true);
9587
}
9688

97-
setDesiredState(TaskStateActionMap[act]);
98-
changeTaskState(act);
89+
setDesiredState(act);
90+
changeAgentState(act);
9991
};
10092

10193
useEffect(() => {
102-
if (curTaskState === desiredState) {
103-
if (curTaskState === AgentTaskState.STOPPED) {
94+
if (curAgentState === desiredState) {
95+
if (curAgentState === AgentState.STOPPED) {
10496
clearMsgs().then().catch();
10597
store.dispatch(clearMessages());
10698
}
10799
setIsLoading(false);
108-
} else if (curTaskState === AgentTaskState.RUNNING) {
109-
setDesiredState(AgentTaskState.RUNNING);
100+
} else if (curAgentState === AgentState.RUNNING) {
101+
setDesiredState(AgentState.RUNNING);
110102
}
111-
// We only want to run this effect when curTaskState changes
103+
// We only want to run this effect when curAgentState changes
112104
// eslint-disable-next-line react-hooks/exhaustive-deps
113-
}, [curTaskState]);
105+
}, [curAgentState]);
114106

115107
return (
116108
<div className="flex items-center gap-3">
117-
{curTaskState === AgentTaskState.PAUSED ? (
109+
{curAgentState === AgentState.PAUSED ? (
118110
<ActionButton
119111
isDisabled={
120112
isLoading ||
121-
IgnoreTaskStateMap[AgentTaskAction.RESUME].includes(curTaskState)
113+
IgnoreTaskStateMap[AgentState.RUNNING].includes(curAgentState)
122114
}
123115
content="Resume the agent task"
124-
action={AgentTaskAction.RESUME}
116+
action={AgentState.RUNNING}
125117
handleAction={handleAction}
126118
large
127119
>
@@ -131,10 +123,10 @@ function AgentControlBar() {
131123
<ActionButton
132124
isDisabled={
133125
isLoading ||
134-
IgnoreTaskStateMap[AgentTaskAction.PAUSE].includes(curTaskState)
126+
IgnoreTaskStateMap[AgentState.PAUSED].includes(curAgentState)
135127
}
136128
content="Pause the agent task"
137-
action={AgentTaskAction.PAUSE}
129+
action={AgentState.PAUSED}
138130
handleAction={handleAction}
139131
large
140132
>
@@ -144,7 +136,7 @@ function AgentControlBar() {
144136
<ActionButton
145137
isDisabled={isLoading}
146138
content="Restart a new agent task"
147-
action={AgentTaskAction.STOP}
139+
action={AgentState.STOPPED}
148140
handleAction={handleAction}
149141
>
150142
<ArrowIcon />

frontend/src/components/AgentStatusBar.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,40 +3,39 @@ import { useTranslation } from "react-i18next";
33
import { useSelector } from "react-redux";
44
import { I18nKey } from "#/i18n/declaration";
55
import { RootState } from "#/store";
6-
import AgentTaskState from "#/types/AgentTaskState";
6+
import AgentState from "#/types/AgentState";
77

88
const AgentStatusMap: { [k: string]: { message: string; indicator: string } } =
99
{
10-
[AgentTaskState.INIT]: {
10+
[AgentState.INIT]: {
1111
message: "Agent is initialized, waiting for task...",
1212
indicator: "bg-blue-500",
1313
},
14-
[AgentTaskState.RUNNING]: {
14+
[AgentState.RUNNING]: {
1515
message: "Agent is running task...",
1616
indicator: "bg-green-500",
1717
},
18-
[AgentTaskState.AWAITING_USER_INPUT]: {
18+
[AgentState.AWAITING_USER_INPUT]: {
1919
message: "Agent is awaiting user input...",
2020
indicator: "bg-orange-500",
2121
},
22-
[AgentTaskState.PAUSED]: {
22+
[AgentState.PAUSED]: {
2323
message: "Agent has paused.",
2424
indicator: "bg-yellow-500",
2525
},
26-
[AgentTaskState.STOPPED]: {
26+
[AgentState.STOPPED]: {
2727
message: "Agent has stopped.",
2828
indicator: "bg-red-500",
2929
},
30-
[AgentTaskState.FINISHED]: {
30+
[AgentState.FINISHED]: {
3131
message: "Agent has finished the task.",
3232
indicator: "bg-green-500",
3333
},
3434
};
3535

3636
function AgentStatusBar() {
3737
const { t } = useTranslation();
38-
const { initialized } = useSelector((state: RootState) => state.task);
39-
const { curTaskState } = useSelector((state: RootState) => state.agent);
38+
const { curAgentState } = useSelector((state: RootState) => state.agent);
4039

4140
// TODO: Extend the agent status, e.g.:
4241
// - Agent is typing
@@ -46,13 +45,13 @@ function AgentStatusBar() {
4645
// - Agent is not available
4746
return (
4847
<div className="flex items-center">
49-
{initialized ? (
48+
{curAgentState !== AgentState.LOADING ? (
5049
<>
5150
<div
52-
className={`w-3 h-3 mr-2 rounded-full animate-pulse ${AgentStatusMap[curTaskState].indicator}`}
51+
className={`w-3 h-3 mr-2 rounded-full animate-pulse ${AgentStatusMap[curAgentState].indicator}`}
5352
/>
5453
<span className="text-sm text-stone-400">
55-
{AgentStatusMap[curTaskState].message}
54+
{AgentStatusMap[curAgentState].message}
5655
</span>
5756
</>
5857
) : (

frontend/src/components/chat/ChatInterface.test.tsx

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import ChatInterface from "./ChatInterface";
88
import Socket from "#/services/socket";
99
import ActionType from "#/types/ActionType";
1010
import { addAssistantMessage } from "#/state/chatSlice";
11-
import AgentTaskState from "#/types/AgentTaskState";
11+
import AgentState from "#/types/AgentState";
1212

1313
// avoid typing side-effect
1414
vi.mock("#/hooks/useTyping", () => ({
@@ -25,7 +25,6 @@ const renderChatInterface = () =>
2525
renderWithProviders(<ChatInterface />, {
2626
preloadedState: {
2727
task: {
28-
initialized: true,
2928
completed: false,
3029
},
3130
},
@@ -38,7 +37,16 @@ describe("ChatInterface", () => {
3837
});
3938

4039
it("should render the new message the user has typed", async () => {
41-
renderChatInterface();
40+
renderWithProviders(<ChatInterface />, {
41+
preloadedState: {
42+
task: {
43+
completed: false,
44+
},
45+
agent: {
46+
curAgentState: AgentState.INIT,
47+
},
48+
},
49+
});
4250

4351
const input = screen.getByRole("textbox");
4452

@@ -73,11 +81,10 @@ describe("ChatInterface", () => {
7381
renderWithProviders(<ChatInterface />, {
7482
preloadedState: {
7583
task: {
76-
initialized: true,
7784
completed: false,
7885
},
7986
agent: {
80-
curTaskState: AgentTaskState.INIT,
87+
curAgentState: AgentState.INIT,
8188
},
8289
},
8390
});
@@ -95,11 +102,10 @@ describe("ChatInterface", () => {
95102
renderWithProviders(<ChatInterface />, {
96103
preloadedState: {
97104
task: {
98-
initialized: true,
99105
completed: false,
100106
},
101107
agent: {
102-
curTaskState: AgentTaskState.AWAITING_USER_INPUT,
108+
curAgentState: AgentState.AWAITING_USER_INPUT,
103109
},
104110
},
105111
});
@@ -110,8 +116,8 @@ describe("ChatInterface", () => {
110116
});
111117

112118
const event = {
113-
action: ActionType.USER_MESSAGE,
114-
args: { message: "my message" },
119+
action: ActionType.MESSAGE,
120+
args: { content: "my message" },
115121
};
116122
expect(socketSpy).toHaveBeenCalledWith(JSON.stringify(event));
117123
});
@@ -120,9 +126,11 @@ describe("ChatInterface", () => {
120126
renderWithProviders(<ChatInterface />, {
121127
preloadedState: {
122128
task: {
123-
initialized: false,
124129
completed: false,
125130
},
131+
agent: {
132+
curAgentState: AgentState.LOADING,
133+
},
126134
},
127135
});
128136

frontend/src/components/chat/ChatInterface.tsx

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,19 @@
11
import React from "react";
2-
import { useDispatch, useSelector } from "react-redux";
2+
import { useSelector } from "react-redux";
33
import { IoMdChatbubbles } from "react-icons/io";
44
import ChatInput from "./ChatInput";
55
import Chat from "./Chat";
66
import { RootState } from "#/store";
7-
import AgentTaskState from "#/types/AgentTaskState";
8-
import { addUserMessage } from "#/state/chatSlice";
9-
import ActionType from "#/types/ActionType";
10-
import Socket from "#/services/socket";
7+
import AgentState from "#/types/AgentState";
8+
import { sendChatMessage } from "#/services/chatService";
119

1210
function ChatInterface() {
13-
const { initialized } = useSelector((state: RootState) => state.task);
1411
const { messages } = useSelector((state: RootState) => state.chat);
15-
const { curTaskState } = useSelector((state: RootState) => state.agent);
16-
17-
const dispatch = useDispatch();
12+
const { curAgentState } = useSelector((state: RootState) => state.agent);
1813

1914
const handleSendMessage = (content: string) => {
20-
dispatch(addUserMessage(content));
21-
22-
let event;
23-
if (curTaskState === AgentTaskState.INIT) {
24-
event = { action: ActionType.START, args: { task: content } };
25-
} else {
26-
event = { action: ActionType.USER_MESSAGE, args: { message: content } };
27-
}
28-
29-
Socket.send(JSON.stringify(event));
15+
const isTask = curAgentState === AgentState.INIT;
16+
sendChatMessage(content, isTask);
3017
};
3118

3219
return (
@@ -42,7 +29,10 @@ function ChatInterface() {
4229
{/* Fade between messages and input */}
4330
<div className="absolute bottom-0 left-0 right-0 h-4 bg-gradient-to-b from-transparent to-neutral-800" />
4431
</div>
45-
<ChatInput disabled={!initialized} onSendMessage={handleSendMessage} />
32+
<ChatInput
33+
disabled={curAgentState === AgentState.LOADING}
34+
onSendMessage={handleSendMessage}
35+
/>
4636
</div>
4737
);
4838
}

frontend/src/components/modals/settings/SettingsForm.test.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { act, screen } from "@testing-library/react";
22
import userEvent from "@testing-library/user-event";
33
import React from "react";
44
import { renderWithProviders } from "test-utils";
5-
import AgentTaskState from "#/types/AgentTaskState";
5+
import AgentState from "#/types/AgentState";
66
import { Settings } from "#/services/settings";
77
import SettingsForm from "./SettingsForm";
88

@@ -80,7 +80,7 @@ describe("SettingsForm", () => {
8080
onLanguageChange={onLanguageChangeMock}
8181
onAPIKeyChange={onAPIKeyChangeMock}
8282
/>,
83-
{ preloadedState: { agent: { curTaskState: AgentTaskState.RUNNING } } },
83+
{ preloadedState: { agent: { curAgentState: AgentState.RUNNING } } },
8484
);
8585
const modelInput = screen.getByRole("combobox", { name: "model" });
8686
const agentInput = screen.getByRole("combobox", { name: "agent" });

0 commit comments

Comments
 (0)