Преглед на файлове

feat: Implement user confirmation mode, request confirmation when running bash/python code in this mode (#2774)

* [feat] confirmation mode for bash actions

* feat: Add modal setting for Confirmation Mode

* fix: frontend tests for confirmation mode switch

* fix: add missing CONFIRMATION_MODE value in SettingsModal.test.tsx

* fix: update test to integrate new setting

* feat: Implement user confirmation for running bash/python code

* fix: don't display rejected actions

* fix: linting, rename/refactor based on feedback

* fix: add property only to commands, pass serialization tests

* fix: package-lock.json, lint test_action_serialization.py

* test: add is_confirmed to integration test outputs

---------

Co-authored-by: Mislav Balunovic <mislav.balunovic@gmail.com>
adragos преди 1 година
родител
ревизия
5f61885e44
променени са 60 файла, в които са добавени 524 реда и са изтрити 106 реда
  1. 18 0
      frontend/src/assets/confirm.tsx
  2. 22 0
      frontend/src/assets/reject.tsx
  3. 39 32
      frontend/src/components/AgentControlBar.tsx
  4. 14 0
      frontend/src/components/AgentStatusBar.tsx
  5. 3 2
      frontend/src/components/chat/Chat.test.tsx
  6. 11 2
      frontend/src/components/chat/Chat.tsx
  7. 5 2
      frontend/src/components/chat/ChatInterface.tsx
  8. 13 2
      frontend/src/components/chat/ChatMessage.test.tsx
  9. 51 1
      frontend/src/components/chat/ChatMessage.tsx
  10. 10 0
      frontend/src/components/modals/settings/SettingsForm.test.tsx
  11. 18 1
      frontend/src/components/modals/settings/SettingsForm.tsx
  12. 4 1
      frontend/src/components/modals/settings/SettingsModal.test.tsx
  13. 7 1
      frontend/src/components/modals/settings/SettingsModal.tsx
  14. 40 0
      frontend/src/i18n/translation.json
  15. 38 2
      frontend/src/services/actions.ts
  16. 1 0
      frontend/src/services/session.test.ts
  17. 5 1
      frontend/src/services/settings.test.ts
  18. 14 4
      frontend/src/services/settings.ts
  19. 3 0
      frontend/src/types/AgentState.tsx
  20. 48 2
      opendevin/controller/agent_controller.py
  21. 1 0
      opendevin/controller/state/state.py
  22. 1 0
      opendevin/core/config.py
  23. 12 0
      opendevin/core/schema/agent.py
  24. 1 0
      opendevin/core/schema/config.py
  25. 2 0
      opendevin/core/schema/observation.py
  26. 2 1
      opendevin/events/action/__init__.py
  27. 7 0
      opendevin/events/action/action.py
  28. 3 1
      opendevin/events/action/commands.py
  29. 2 0
      opendevin/events/observation/__init__.py
  30. 18 0
      opendevin/events/observation/reject.py
  31. 2 0
      opendevin/events/serialization/observation.py
  32. 14 0
      opendevin/runtime/runtime.py
  33. 1 1
      opendevin/server/listen.py
  34. 4 0
      opendevin/server/session/agent.py
  35. 1 1
      tests/integration/mock/DelegatorAgent/test_edits/prompt_002.log
  36. 1 1
      tests/integration/mock/DelegatorAgent/test_edits/prompt_003.log
  37. 1 1
      tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_002.log
  38. 1 1
      tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_005.log
  39. 1 1
      tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_006.log
  40. 1 1
      tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_009.log
  41. 1 1
      tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_003.log
  42. 1 1
      tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_004.log
  43. 1 1
      tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_005.log
  44. 1 1
      tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_004.log
  45. 1 1
      tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_005.log
  46. 1 1
      tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_006.log
  47. 1 1
      tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_009.log
  48. 1 1
      tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_010.log
  49. 4 2
      tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_001.log
  50. 6 3
      tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_002.log
  51. 8 4
      tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_003.log
  52. 8 4
      tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_004.log
  53. 8 4
      tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_005.log
  54. 8 4
      tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_006.log
  55. 10 5
      tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_007.log
  56. 12 6
      tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_008.log
  57. 2 1
      tests/integration/mock/MonologueAgent/test_write_simple_script/response_002.log
  58. 2 1
      tests/integration/mock/PlannerAgent/test_write_simple_script/prompt_010.log
  59. 2 1
      tests/integration/mock/PlannerAgent/test_write_simple_script/prompt_011.log
  60. 6 1
      tests/unit/test_action_serialization.py

+ 18 - 0
frontend/src/assets/confirm.tsx

@@ -0,0 +1,18 @@
+import React from "react";
+
+function ConfirmIcon(): JSX.Element {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      fill="none"
+      viewBox="0 0 24 24"
+      strokeWidth={1.5}
+      stroke="currentColor"
+      className="w-5 h-5"
+    >
+      <path strokeLinecap="round" strokeLinejoin="round" d="M5 13l4 4L19 7" />
+    </svg>
+  );
+}
+
+export default ConfirmIcon;

+ 22 - 0
frontend/src/assets/reject.tsx

@@ -0,0 +1,22 @@
+import React from "react";
+
+function RejectIcon(): JSX.Element {
+  return (
+    <svg
+      xmlns="http://www.w3.org/2000/svg"
+      fill="none"
+      viewBox="0 0 24 24"
+      strokeWidth={1.5}
+      stroke="currentColor"
+      className="w-5 h-5"
+    >
+      <path
+        strokeLinecap="round"
+        strokeLinejoin="round"
+        d="M6 18L18 6M6 6l12 12"
+      />
+    </svg>
+  );
+}
+
+export default RejectIcon;

+ 39 - 32
frontend/src/components/AgentControlBar.tsx

@@ -17,6 +17,7 @@ const IgnoreTaskStateMap: { [k: string]: AgentState[] } = {
     AgentState.FINISHED,
     AgentState.REJECTED,
     AgentState.AWAITING_USER_INPUT,
+    AgentState.AWAITING_USER_CONFIRMATION,
   ],
   [AgentState.RUNNING]: [
     AgentState.INIT,
@@ -25,8 +26,12 @@ const IgnoreTaskStateMap: { [k: string]: AgentState[] } = {
     AgentState.FINISHED,
     AgentState.REJECTED,
     AgentState.AWAITING_USER_INPUT,
+    AgentState.AWAITING_USER_CONFIRMATION,
   ],
   [AgentState.STOPPED]: [AgentState.INIT, AgentState.STOPPED],
+  [AgentState.USER_CONFIRMED]: [AgentState.RUNNING],
+  [AgentState.USER_REJECTED]: [AgentState.RUNNING],
+  [AgentState.AWAITING_USER_CONFIRMATION]: [],
 };
 
 interface ButtonProps {
@@ -101,42 +106,44 @@ function AgentControlBar() {
   }, [curAgentState]);
 
   return (
-    <div className="flex items-center gap-3">
-      {curAgentState === AgentState.PAUSED ? (
+    <div className="flex justify-between items-center gap-20">
+      <div className="flex items-center gap-3">
+        {curAgentState === AgentState.PAUSED ? (
+          <ActionButton
+            isDisabled={
+              isLoading ||
+              IgnoreTaskStateMap[AgentState.RUNNING].includes(curAgentState)
+            }
+            content="Resume the agent task"
+            action={AgentState.RUNNING}
+            handleAction={handleAction}
+            large
+          >
+            <PlayIcon />
+          </ActionButton>
+        ) : (
+          <ActionButton
+            isDisabled={
+              isLoading ||
+              IgnoreTaskStateMap[AgentState.PAUSED].includes(curAgentState)
+            }
+            content="Pause the current task"
+            action={AgentState.PAUSED}
+            handleAction={handleAction}
+            large
+          >
+            <PauseIcon />
+          </ActionButton>
+        )}
         <ActionButton
-          isDisabled={
-            isLoading ||
-            IgnoreTaskStateMap[AgentState.RUNNING].includes(curAgentState)
-          }
-          content="Resume the agent task"
-          action={AgentState.RUNNING}
+          isDisabled={isLoading}
+          content="Start a new task"
+          action={AgentState.STOPPED}
           handleAction={handleAction}
-          large
         >
-          <PlayIcon />
+          <ArrowIcon />
         </ActionButton>
-      ) : (
-        <ActionButton
-          isDisabled={
-            isLoading ||
-            IgnoreTaskStateMap[AgentState.PAUSED].includes(curAgentState)
-          }
-          content="Pause the current task"
-          action={AgentState.PAUSED}
-          handleAction={handleAction}
-          large
-        >
-          <PauseIcon />
-        </ActionButton>
-      )}
-      <ActionButton
-        isDisabled={isLoading}
-        content="Start a new task"
-        action={AgentState.STOPPED}
-        handleAction={handleAction}
-      >
-        <ArrowIcon />
-      </ActionButton>
+      </div>
     </div>
   );
 }

+ 14 - 0
frontend/src/components/AgentStatusBar.tsx

@@ -58,6 +58,20 @@ function AgentStatusBar() {
       message: t(I18nKey.CHAT_INTERFACE$AGENT_ERROR_MESSAGE),
       indicator: IndicatorColor.RED,
     },
+    [AgentState.AWAITING_USER_CONFIRMATION]: {
+      message: t(
+        I18nKey.CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE,
+      ),
+      indicator: IndicatorColor.ORANGE,
+    },
+    [AgentState.USER_CONFIRMED]: {
+      message: t(I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE),
+      indicator: IndicatorColor.GREEN,
+    },
+    [AgentState.USER_REJECTED]: {
+      message: t(I18nKey.CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE),
+      indicator: IndicatorColor.RED,
+    },
   };
 
   // TODO: Extend the agent status, e.g.:

+ 3 - 2
frontend/src/components/chat/Chat.test.tsx

@@ -1,6 +1,7 @@
 import React from "react";
-import { render, screen } from "@testing-library/react";
+import { screen } from "@testing-library/react";
 import { describe, expect, it } from "vitest";
+import { renderWithProviders } from "test-utils";
 import Chat from "./Chat";
 
 const MESSAGES: Message[] = [
@@ -13,7 +14,7 @@ HTMLElement.prototype.scrollTo = vi.fn(() => {});
 
 describe("Chat", () => {
   it("should render chat messages", () => {
-    render(<Chat messages={MESSAGES} />);
+    renderWithProviders(<Chat messages={MESSAGES} />);
 
     const messages = screen.getAllByTestId("message");
 

+ 11 - 2
frontend/src/components/chat/Chat.tsx

@@ -1,15 +1,24 @@
 import React from "react";
 import ChatMessage from "./ChatMessage";
+import AgentState from "#/types/AgentState";
 
 interface ChatProps {
   messages: Message[];
+  curAgentState?: AgentState;
 }
 
-function Chat({ messages }: ChatProps) {
+function Chat({ messages, curAgentState }: ChatProps) {
   return (
     <div className="flex flex-col gap-3">
       {messages.map((message, index) => (
-        <ChatMessage key={index} message={message} />
+        <ChatMessage
+          key={index}
+          message={message}
+          isLastMessage={messages && index === messages.length - 1}
+          awaitingUserConfirmation={
+            curAgentState === AgentState.AWAITING_USER_CONFIRMATION
+          }
+        />
       ))}
     </div>
   );

+ 5 - 2
frontend/src/components/chat/ChatInterface.tsx

@@ -123,7 +123,7 @@ function ChatInterface() {
           className="overflow-y-auto p-3"
           onScroll={(e) => onChatBodyScroll(e.currentTarget)}
         >
-          <Chat messages={messages} />
+          <Chat messages={messages} curAgentState={curAgentState} />
         </div>
       </div>
 
@@ -169,7 +169,10 @@ function ChatInterface() {
       </div>
 
       <ChatInput
-        disabled={curAgentState === AgentState.LOADING}
+        disabled={
+          curAgentState === AgentState.LOADING ||
+          curAgentState === AgentState.AWAITING_USER_CONFIRMATION
+        }
         onSendMessage={handleSendMessage}
       />
       <FeedbackModal

+ 13 - 2
frontend/src/components/chat/ChatMessage.test.tsx

@@ -10,14 +10,24 @@ vi.mock("#/hooks/useTyping", () => ({
 
 describe("Message", () => {
   it("should render a user message", () => {
-    render(<ChatMessage message={{ sender: "user", content: "Hello" }} />);
+    render(
+      <ChatMessage
+        message={{ sender: "user", content: "Hello" }}
+        isLastMessage={false}
+      />,
+    );
 
     expect(screen.getByTestId("message")).toBeInTheDocument();
     expect(screen.getByTestId("message")).toHaveClass("self-end"); // user message should be on the right side
   });
 
   it("should render an assistant message", () => {
-    render(<ChatMessage message={{ sender: "assistant", content: "Hi" }} />);
+    render(
+      <ChatMessage
+        message={{ sender: "assistant", content: "Hi" }}
+        isLastMessage={false}
+      />,
+    );
 
     expect(screen.getByTestId("message")).toBeInTheDocument();
     expect(screen.getByTestId("message")).not.toHaveClass("self-end"); // assistant message should be on the left side
@@ -30,6 +40,7 @@ describe("Message", () => {
           sender: "user",
           content: "```js\nconsole.log('Hello')\n```",
         }}
+        isLastMessage={false}
       />,
     );
 

+ 51 - 1
frontend/src/components/chat/ChatMessage.tsx

@@ -3,15 +3,26 @@ import Markdown from "react-markdown";
 import { FaClipboard, FaClipboardCheck } from "react-icons/fa";
 import { twMerge } from "tailwind-merge";
 import { useTranslation } from "react-i18next";
+import { Tooltip } from "@nextui-org/react";
+import AgentState from "#/types/AgentState";
 import { code } from "../markdown/code";
 import toast from "#/utils/toast";
 import { I18nKey } from "#/i18n/declaration";
+import ConfirmIcon from "#/assets/confirm";
+import RejectIcon from "#/assets/reject";
+import { changeAgentState } from "#/services/agentStateService";
 
 interface MessageProps {
   message: Message;
+  isLastMessage?: boolean;
+  awaitingUserConfirmation?: boolean;
 }
 
-function ChatMessage({ message }: MessageProps) {
+function ChatMessage({
+  message,
+  isLastMessage,
+  awaitingUserConfirmation,
+}: MessageProps) {
   const [isCopy, setIsCopy] = useState(false);
   const [isHovering, setIsHovering] = useState(false);
 
@@ -58,6 +69,45 @@ function ChatMessage({ message }: MessageProps) {
         </button>
       )}
       <Markdown components={{ code }}>{message.content}</Markdown>
+      {isLastMessage &&
+        message.sender === "assistant" &&
+        awaitingUserConfirmation && (
+          <div className="flex justify-between items-center pt-4">
+            <p>{t(I18nKey.CHAT_INTERFACE$USER_ASK_CONFIRMATION)}</p>
+            <div className="flex items-center gap-3">
+              <Tooltip
+                content={t(I18nKey.CHAT_INTERFACE$USER_CONFIRMED)}
+                closeDelay={100}
+              >
+                <button
+                  type="button"
+                  aria-label="Confirm action"
+                  className="bg-neutral-700 rounded-full p-1 hover:bg-neutral-800"
+                  onClick={() => {
+                    changeAgentState(AgentState.USER_CONFIRMED);
+                  }}
+                >
+                  <ConfirmIcon />
+                </button>
+              </Tooltip>
+              <Tooltip
+                content={t(I18nKey.CHAT_INTERFACE$USER_REJECTED)}
+                closeDelay={100}
+              >
+                <button
+                  type="button"
+                  aria-label="Reject action"
+                  className="bg-neutral-700 rounded-full p-1 hover:bg-neutral-800"
+                  onClick={() => {
+                    changeAgentState(AgentState.USER_REJECTED);
+                  }}
+                >
+                  <RejectIcon />
+                </button>
+              </Tooltip>
+            </div>
+          </div>
+        )}
     </div>
   );
 }

+ 10 - 0
frontend/src/components/modals/settings/SettingsForm.test.tsx

@@ -9,6 +9,7 @@ const onModelChangeMock = vi.fn();
 const onAgentChangeMock = vi.fn();
 const onLanguageChangeMock = vi.fn();
 const onAPIKeyChangeMock = vi.fn();
+const onConfirmationModeChangeMock = vi.fn();
 
 const renderSettingsForm = (settings?: Settings) => {
   renderWithProviders(
@@ -20,6 +21,7 @@ const renderSettingsForm = (settings?: Settings) => {
           AGENT: "agent1",
           LANGUAGE: "en",
           LLM_API_KEY: "sk-...",
+          CONFIRMATION_MODE: true,
         }
       }
       models={["model1", "model2", "model3"]}
@@ -28,6 +30,7 @@ const renderSettingsForm = (settings?: Settings) => {
       onAgentChange={onAgentChangeMock}
       onLanguageChange={onLanguageChangeMock}
       onAPIKeyChange={onAPIKeyChangeMock}
+      onConfirmationModeChange={onConfirmationModeChangeMock}
     />,
   );
 };
@@ -40,11 +43,13 @@ describe("SettingsForm", () => {
     const agentInput = screen.getByRole("combobox", { name: "agent" });
     const languageInput = screen.getByRole("combobox", { name: "language" });
     const apiKeyInput = screen.getByTestId("apikey");
+    const confirmationModeInput = screen.getByTestId("confirmationmode");
 
     expect(modelInput).toHaveValue("model1");
     expect(agentInput).toHaveValue("agent1");
     expect(languageInput).toHaveValue("English");
     expect(apiKeyInput).toHaveValue("sk-...");
+    expect(confirmationModeInput).toHaveAttribute("data-selected", "true");
   });
 
   it("should display the existing values if it they are present", () => {
@@ -53,6 +58,7 @@ describe("SettingsForm", () => {
       AGENT: "agent2",
       LANGUAGE: "es",
       LLM_API_KEY: "sk-...",
+      CONFIRMATION_MODE: true,
     });
 
     const modelInput = screen.getByRole("combobox", { name: "model" });
@@ -72,6 +78,7 @@ describe("SettingsForm", () => {
           AGENT: "agent1",
           LANGUAGE: "en",
           LLM_API_KEY: "sk-...",
+          CONFIRMATION_MODE: true,
         }}
         models={["model1", "model2", "model3"]}
         agents={["agent1", "agent2", "agent3"]}
@@ -80,15 +87,18 @@ describe("SettingsForm", () => {
         onAgentChange={onAgentChangeMock}
         onLanguageChange={onLanguageChangeMock}
         onAPIKeyChange={onAPIKeyChangeMock}
+        onConfirmationModeChange={onConfirmationModeChangeMock}
       />,
     );
     const modelInput = screen.getByRole("combobox", { name: "model" });
     const agentInput = screen.getByRole("combobox", { name: "agent" });
     const languageInput = screen.getByRole("combobox", { name: "language" });
+    const confirmationModeInput = screen.getByTestId("confirmationmode");
 
     expect(modelInput).toBeDisabled();
     expect(agentInput).toBeDisabled();
     expect(languageInput).toBeDisabled();
+    expect(confirmationModeInput).toHaveAttribute("data-disabled", "true");
   });
 
   describe("onChange handlers", () => {

+ 18 - 1
frontend/src/components/modals/settings/SettingsForm.tsx

@@ -1,4 +1,4 @@
-import { Input, useDisclosure } from "@nextui-org/react";
+import { Input, Switch, Tooltip, useDisclosure } from "@nextui-org/react";
 import React from "react";
 import { useTranslation } from "react-i18next";
 import { FaEye, FaEyeSlash } from "react-icons/fa";
@@ -17,6 +17,7 @@ interface SettingsFormProps {
   onAPIKeyChange: (apiKey: string) => void;
   onAgentChange: (agent: string) => void;
   onLanguageChange: (language: string) => void;
+  onConfirmationModeChange: (confirmationMode: boolean) => void;
 }
 
 function SettingsForm({
@@ -28,6 +29,7 @@ function SettingsForm({
   onAPIKeyChange,
   onAgentChange,
   onLanguageChange,
+  onConfirmationModeChange,
 }: SettingsFormProps) {
   const { t } = useTranslation();
   const { isOpen: isVisible, onOpenChange: onVisibleChange } = useDisclosure();
@@ -86,6 +88,21 @@ function SettingsForm({
         tooltip={t(I18nKey.SETTINGS$LANGUAGE_TOOLTIP)}
         disabled={disabled}
       />
+      <Switch
+        aria-label="confirmationmode"
+        data-testid="confirmationmode"
+        defaultSelected={settings.CONFIRMATION_MODE}
+        onValueChange={onConfirmationModeChange}
+        isDisabled={disabled}
+      >
+        <Tooltip
+          content={t(I18nKey.SETTINGS$CONFIRMATION_MODE_TOOLTIP)}
+          closeDelay={100}
+          delay={500}
+        >
+          {t(I18nKey.SETTINGS$CONFIRMATION_MODE)}
+        </Tooltip>
+      </Switch>
     </>
   );
 }

+ 4 - 1
frontend/src/components/modals/settings/SettingsModal.test.tsx

@@ -27,12 +27,14 @@ vi.mock("#/services/settings", async (importOriginal) => ({
     AGENT: "CodeActAgent",
     LANGUAGE: "en",
     LLM_API_KEY: "sk-...",
+    CONFIRMATION_MODE: true,
   }),
   getDefaultSettings: vi.fn().mockReturnValue({
     LLM_MODEL: "gpt-4o",
     AGENT: "CodeActAgent",
     LANGUAGE: "en",
     LLM_API_KEY: "",
+    CONFIRMATION_MODE: false,
   }),
   settingsAreUpToDate: vi.fn().mockReturnValue(true),
   saveSettings: vi.fn(),
@@ -107,6 +109,7 @@ describe("SettingsModal", () => {
       AGENT: "CodeActAgent",
       LANGUAGE: "en",
       LLM_API_KEY: "sk-...",
+      CONFIRMATION_MODE: true,
     };
 
     it("should save the settings", async () => {
@@ -196,7 +199,7 @@ describe("SettingsModal", () => {
         await userEvent.click(saveButton);
       });
 
-      expect(toastSpy).toHaveBeenCalledTimes(2);
+      expect(toastSpy).toHaveBeenCalledTimes(3);
     });
 
     it("should change the language", async () => {

+ 7 - 1
frontend/src/components/modals/settings/SettingsModal.tsx

@@ -48,7 +48,8 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
     const isRunning =
       curAgentState === AgentState.RUNNING ||
       curAgentState === AgentState.PAUSED ||
-      curAgentState === AgentState.AWAITING_USER_INPUT;
+      curAgentState === AgentState.AWAITING_USER_INPUT ||
+      curAgentState === AgentState.AWAITING_USER_CONFIRMATION;
     setAgentIsRunning(isRunning);
   }, [curAgentState]);
 
@@ -89,6 +90,10 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
     setSettings((prev) => ({ ...prev, LLM_API_KEY: key }));
   };
 
+  const handleConfirmationModeChange = (confirmationMode: boolean) => {
+    setSettings((prev) => ({ ...prev, CONFIRMATION_MODE: confirmationMode }));
+  };
+
   const handleResetSettings = () => {
     setSettings(getDefaultSettings);
   };
@@ -170,6 +175,7 @@ function SettingsModal({ isOpen, onOpenChange }: SettingsProps) {
           onAgentChange={handleAgentChange}
           onLanguageChange={handleLanguageChange}
           onAPIKeyChange={handleAPIKeyChange}
+          onConfirmationModeChange={handleConfirmationModeChange}
         />
       )}
     </BaseModal>

+ 40 - 0
frontend/src/i18n/translation.json

@@ -567,6 +567,21 @@
     "de": "Agent ist auf einen Fehler gelaufen.",
     "zh-CN": "智能体遇到错误"
   },
+  "CHAT_INTERFACE$AGENT_AWAITING_USER_CONFIRMATION_MESSAGE": {
+    "en": "Agent is awaiting user confirmation for the pending action.",
+    "de": "Agent wartet auf die Bestätigung des Benutzers für die ausstehende Aktion.",
+    "zh-CN": "代理正在等待用户确认待处理的操作。"
+  },
+  "CHAT_INTERFACE$AGENT_ACTION_USER_CONFIRMED_MESSAGE": {
+    "en": "Agent action has been confirmed!",
+    "de": "Die Aktion des Agenten wurde bestätigt!",
+    "zh-CN": "代理操作已确认!"
+  },
+  "CHAT_INTERFACE$AGENT_ACTION_USER_REJECTED_MESSAGE": {
+    "en": "Agent action has been rejected!",
+    "de": "Die Aktion des Agenten wurde abgelehnt!",
+    "zh-CN": "代理操作已被拒绝!"
+  },
   "CHAT_INTERFACE$INPUT_PLACEHOLDER": {
     "en": "Message assistant...",
     "zh-CN": "给助理发消息",
@@ -586,6 +601,21 @@
     "zh-CN": "继续",
     "de": "Fortfahren"
   },
+  "CHAT_INTERFACE$USER_ASK_CONFIRMATION": {
+    "en": "Do you want to continue with this action?",
+    "de": "Möchten Sie mit dieser Aktion fortfahren?",
+    "zh-CN": "您要继续此操作吗?"
+  },
+  "CHAT_INTERFACE$USER_CONFIRMED": {
+    "en": "Confirm the requested action",
+    "de": "Bestätigen Sie die angeforderte Aktion",
+    "zh-CN": "确认请求的操作"
+  },
+  "CHAT_INTERFACE$USER_REJECTED": {
+    "en": "Reject the requested action",
+    "de": "Lehnen Sie die angeforderte Aktion ab",
+    "zh-CN": "拒绝请求的操作"
+  },
   "CHAT_INTERFACE$INPUT_SEND_MESSAGE_BUTTON_CONTENT": {
     "en": "Send",
     "zh-CN": "发送",
@@ -681,6 +711,16 @@
     "zh-TW": "輸入您的 API 金鑰。",
     "de": "Modell API Schlüssel."
   },
+  "SETTINGS$CONFIRMATION_MODE": {
+    "en": "Enable Confirmation Mode",
+    "de": "Bestätigungsmodus aktivieren",
+    "zh-CN": "启用确认模式"
+  },
+  "SETTINGS$CONFIRMATION_MODE_TOOLTIP": {
+    "en": "Awaits for user confirmation before executing code.",
+    "de": "Wartet auf die Bestätigung des Benutzers, bevor der Code ausgeführt wird.",
+    "zh-CN": "在执行代码之前等待用户确认。"
+  },
   "BROWSER$EMPTY_MESSAGE": {
     "en": "No page loaded.",
     "zh-CN": "页面未加载",

+ 38 - 2
frontend/src/services/actions.ts

@@ -46,13 +46,23 @@ const messageActions = {
     if (message.args.thought) {
       store.dispatch(addAssistantMessage(message.args.thought));
     }
-    store.dispatch(appendInput(message.args.command));
+    if (
+      !message.args.is_confirmed ||
+      message.args.is_confirmed !== "rejected"
+    ) {
+      store.dispatch(appendInput(message.args.command));
+    }
   },
   [ActionType.RUN_IPYTHON]: (message: ActionMessage) => {
     if (message.args.thought) {
       store.dispatch(addAssistantMessage(message.args.thought));
     }
-    store.dispatch(appendJupyterInput(message.args.code));
+    if (
+      !message.args.is_confirmed ||
+      message.args.is_confirmed !== "rejected"
+    ) {
+      store.dispatch(appendJupyterInput(message.args.code));
+    }
   },
   [ActionType.ADD_TASK]: () => {
     getRootTask().then((fetchedRootTask) =>
@@ -67,6 +77,32 @@ const messageActions = {
 };
 
 export function handleActionMessage(message: ActionMessage) {
+  if (
+    (message.action === ActionType.RUN ||
+      message.action === ActionType.RUN_IPYTHON) &&
+    message.args.is_confirmed === "awaiting_confirmation"
+  ) {
+    if (message.args.thought) {
+      store.dispatch(addAssistantMessage(message.args.thought));
+    }
+    if (message.args.command) {
+      store.dispatch(
+        addAssistantMessage(
+          `Running this command now: \n\`\`\`\`bash\n${message.args.command}\n\`\`\`\`\n`,
+        ),
+      );
+    } else if (message.args.code) {
+      store.dispatch(
+        addAssistantMessage(
+          `Running this code now: \n\`\`\`\`python\n${message.args.code}\n\`\`\`\`\n`,
+        ),
+      );
+    } else {
+      store.dispatch(addAssistantMessage(message.message));
+    }
+    return;
+  }
+
   if (message.action in messageActions) {
     const actionFn =
       messageActions[message.action as keyof typeof messageActions];

+ 1 - 0
frontend/src/services/session.test.ts

@@ -20,6 +20,7 @@ describe("startNewSession", () => {
       AGENT: "agent_value",
       LANGUAGE: "language_value",
       LLM_API_KEY: "sk-...",
+      CONFIRMATION_MODE: true,
     };
 
     const event = {

+ 5 - 1
frontend/src/services/settings.test.ts

@@ -20,7 +20,8 @@ describe("getSettings", () => {
       .mockReturnValueOnce("llm_value")
       .mockReturnValueOnce("agent_value")
       .mockReturnValueOnce("language_value")
-      .mockReturnValueOnce("api_key");
+      .mockReturnValueOnce("api_key")
+      .mockReturnValueOnce("true");
 
     const settings = getSettings();
 
@@ -29,6 +30,7 @@ describe("getSettings", () => {
       AGENT: "agent_value",
       LANGUAGE: "language_value",
       LLM_API_KEY: "api_key",
+      CONFIRMATION_MODE: true,
     });
   });
 
@@ -46,6 +48,7 @@ describe("getSettings", () => {
       AGENT: DEFAULT_SETTINGS.AGENT,
       LANGUAGE: DEFAULT_SETTINGS.LANGUAGE,
       LLM_API_KEY: "",
+      CONFIRMATION_MODE: DEFAULT_SETTINGS.CONFIRMATION_MODE,
     });
   });
 });
@@ -57,6 +60,7 @@ describe("saveSettings", () => {
       AGENT: "agent_value",
       LANGUAGE: "language_value",
       LLM_API_KEY: "some_key",
+      CONFIRMATION_MODE: true,
     };
 
     saveSettings(settings);

+ 14 - 4
frontend/src/services/settings.ts

@@ -5,13 +5,17 @@ export type Settings = {
   AGENT: string;
   LANGUAGE: string;
   LLM_API_KEY: string;
+  CONFIRMATION_MODE: boolean;
 };
 
+type SettingsInput = Settings[keyof Settings];
+
 export const DEFAULT_SETTINGS: Settings = {
   LLM_MODEL: "gpt-4o",
   AGENT: "CodeActAgent",
   LANGUAGE: "en",
   LLM_API_KEY: "",
+  CONFIRMATION_MODE: false,
 };
 
 const validKeys = Object.keys(DEFAULT_SETTINGS) as (keyof Settings)[];
@@ -51,12 +55,14 @@ export const getSettings = (): Settings => {
   const agent = localStorage.getItem("AGENT");
   const language = localStorage.getItem("LANGUAGE");
   const apiKey = localStorage.getItem("LLM_API_KEY");
+  const confirmationMode = localStorage.getItem("CONFIRMATION_MODE") === "true";
 
   return {
     LLM_MODEL: model || DEFAULT_SETTINGS.LLM_MODEL,
     AGENT: agent || DEFAULT_SETTINGS.AGENT,
     LANGUAGE: language || DEFAULT_SETTINGS.LANGUAGE,
     LLM_API_KEY: apiKey || DEFAULT_SETTINGS.LLM_API_KEY,
+    CONFIRMATION_MODE: confirmationMode || DEFAULT_SETTINGS.CONFIRMATION_MODE,
   };
 };
 
@@ -69,7 +75,8 @@ export const saveSettings = (settings: Partial<Settings>) => {
     const isValid = validKeys.includes(key as keyof Settings);
     const value = settings[key as keyof Settings];
 
-    if (isValid && value) localStorage.setItem(key, value);
+    if (isValid && (value || typeof value === "boolean"))
+      localStorage.setItem(key, value.toString());
   });
   localStorage.setItem("SETTINGS_VERSION", LATEST_SETTINGS_VERSION.toString());
 };
@@ -91,11 +98,14 @@ export const getSettingsDifference = (settings: Partial<Settings>) => {
   const updatedSettings: Partial<Settings> = {};
 
   Object.keys(settings).forEach((key) => {
+    const typedKey = key as keyof Settings;
     if (
-      validKeys.includes(key as keyof Settings) &&
-      settings[key as keyof Settings] !== currentSettings[key as keyof Settings]
+      validKeys.includes(typedKey) &&
+      settings[typedKey] !== currentSettings[typedKey]
     ) {
-      updatedSettings[key as keyof Settings] = settings[key as keyof Settings];
+      (updatedSettings[typedKey] as SettingsInput) = settings[
+        typedKey
+      ] as SettingsInput;
     }
   });
 

+ 3 - 0
frontend/src/types/AgentState.tsx

@@ -8,6 +8,9 @@ enum AgentState {
   FINISHED = "finished",
   REJECTED = "rejected",
   ERROR = "error",
+  AWAITING_USER_CONFIRMATION = "awaiting_user_confirmation",
+  USER_CONFIRMED = "user_confirmed",
+  USER_REJECTED = "user_rejected",
 }
 
 export default AgentState;

+ 48 - 2
opendevin/controller/agent_controller.py

@@ -16,11 +16,14 @@ from opendevin.core.schema import AgentState
 from opendevin.events import EventSource, EventStream, EventStreamSubscriber
 from opendevin.events.action import (
     Action,
+    ActionConfirmationStatus,
     AddTaskAction,
     AgentDelegateAction,
     AgentFinishAction,
     AgentRejectAction,
     ChangeAgentStateAction,
+    CmdRunAction,
+    IPythonRunCellAction,
     MessageAction,
     ModifyTaskAction,
     NullAction,
@@ -49,6 +52,7 @@ class AgentController:
     max_iterations: int
     event_stream: EventStream
     state: State
+    confirmation_mode: bool
     agent_task: Optional[asyncio.Task] = None
     parent: 'AgentController | None' = None
     delegate: 'AgentController | None' = None
@@ -60,6 +64,7 @@ class AgentController:
         event_stream: EventStream,
         sid: str = 'default',
         max_iterations: int | None = MAX_ITERATIONS,
+        confirmation_mode: bool = False,
         max_budget_per_task: float | None = MAX_BUDGET_PER_TASK,
         initial_state: State | None = None,
         is_delegate: bool = False,
@@ -92,6 +97,7 @@ class AgentController:
         self.set_initial_state(
             state=initial_state,
             max_iterations=max_iterations,
+            confirmation_mode=confirmation_mode,
         )
 
         self.max_budget_per_task = max_budget_per_task
@@ -170,8 +176,19 @@ class AgentController:
             self.state.outputs = event.outputs  # type: ignore[attr-defined]
             await self.set_agent_state_to(AgentState.REJECTED)
         elif isinstance(event, Observation):
+            if (
+                self._pending_action
+                and hasattr(self._pending_action, 'is_confirmed')
+                and self._pending_action.is_confirmed
+                == ActionConfirmationStatus.AWAITING_CONFIRMATION
+            ):
+                return
             if self._pending_action and self._pending_action.id == event.cause:
                 self._pending_action = None
+                if self.state.agent_state == AgentState.USER_CONFIRMED:
+                    await self.set_agent_state_to(AgentState.RUNNING)
+                if self.state.agent_state == AgentState.USER_REJECTED:
+                    await self.set_agent_state_to(AgentState.AWAITING_USER_INPUT)
                 logger.info(event, extra={'msg_type': 'OBSERVATION'})
             elif isinstance(event, CmdOutputObservation):
                 logger.info(event, extra={'msg_type': 'OBSERVATION'})
@@ -205,6 +222,18 @@ class AgentController:
         if new_state == AgentState.STOPPED or new_state == AgentState.ERROR:
             self.reset_task()
 
+        if self._pending_action is not None and (
+            new_state == AgentState.USER_CONFIRMED
+            or new_state == AgentState.USER_REJECTED
+        ):
+            if hasattr(self._pending_action, 'thought'):
+                self._pending_action.thought = ''  # type: ignore[union-attr]
+            if new_state == AgentState.USER_CONFIRMED:
+                self._pending_action.is_confirmed = ActionConfirmationStatus.CONFIRMED  # type: ignore[attr-defined]
+            else:
+                self._pending_action.is_confirmed = ActionConfirmationStatus.REJECTED  # type: ignore[attr-defined]
+            self.event_stream.add_event(self._pending_action, EventSource.AGENT)
+
         self.event_stream.add_event(
             AgentStateChangedObservation('', self.state.agent_state), EventSource.AGENT
         )
@@ -346,9 +375,19 @@ class AgentController:
             return
 
         if action.runnable:
+            if self.state.confirmation_mode and (
+                type(action) is CmdRunAction or type(action) is IPythonRunCellAction
+            ):
+                action.is_confirmed = ActionConfirmationStatus.AWAITING_CONFIRMATION
             self._pending_action = action
 
         if not isinstance(action, NullAction):
+            if (
+                hasattr(action, 'is_confirmed')
+                and action.is_confirmed
+                == ActionConfirmationStatus.AWAITING_CONFIRMATION
+            ):
+                await self.set_agent_state_to(AgentState.AWAITING_USER_CONFIRMATION)
             self.event_stream.add_event(action, EventSource.AGENT)
 
         await self.update_state_after_step()
@@ -362,12 +401,19 @@ class AgentController:
         return self.state
 
     def set_initial_state(
-        self, state: State | None, max_iterations: int = MAX_ITERATIONS
+        self,
+        state: State | None,
+        max_iterations: int = MAX_ITERATIONS,
+        confirmation_mode: bool = False,
     ):
         # state from the previous session, state from a parent agent, or a new state
         # note that this is called twice when restoring a previous session, first with state=None
         if state is None:
-            self.state = State(inputs={}, max_iterations=max_iterations)
+            self.state = State(
+                inputs={},
+                max_iterations=max_iterations,
+                confirmation_mode=confirmation_mode,
+            )
         else:
             self.state = state
 

+ 1 - 0
opendevin/controller/state/state.py

@@ -39,6 +39,7 @@ class State:
     root_task: RootTask = field(default_factory=RootTask)
     iteration: int = 0
     max_iterations: int = 100
+    confirmation_mode: bool = False
     history: ShortTermHistory = field(default_factory=ShortTermHistory)
     inputs: dict = field(default_factory=dict)
     outputs: dict = field(default_factory=dict)

+ 1 - 0
opendevin/core/config.py

@@ -223,6 +223,7 @@ class AppConfig(metaclass=Singleton):
     workspace_mount_rewrite: str | None = None
     cache_dir: str = '/tmp/cache'
     run_as_devin: bool = True
+    confirmation_mode: bool = False
     max_iterations: int = 100
     max_budget_per_task: float | None = None
     e2b_api_key: str = ''

+ 12 - 0
opendevin/core/schema/agent.py

@@ -37,3 +37,15 @@ class AgentState(str, Enum):
     ERROR = 'error'
     """An error occurred during the task.
     """
+
+    AWAITING_USER_CONFIRMATION = 'awaiting_user_confirmation'
+    """The agent is awaiting user confirmation.
+    """
+
+    USER_CONFIRMED = 'user_confirmed'
+    """The user confirmed the agent's action.
+    """
+
+    USER_REJECTED = 'user_rejected'
+    """The user rejected the agent's action.
+    """

+ 1 - 0
opendevin/core/schema/config.py

@@ -20,6 +20,7 @@ class ConfigType(str, Enum):
     WORKSPACE_MOUNT_PATH_IN_SANDBOX = 'WORKSPACE_MOUNT_PATH_IN_SANDBOX'
     CACHE_DIR = 'CACHE_DIR'
     LLM_MODEL = 'LLM_MODEL'
+    CONFIRMATION_MODE = 'CONFIRMATION_MODE'
     SANDBOX_CONTAINER_IMAGE = 'SANDBOX_CONTAINER_IMAGE'
     RUN_AS_DEVIN = 'RUN_AS_DEVIN'
     LLM_EMBEDDING_MODEL = 'LLM_EMBEDDING_MODEL'

+ 2 - 0
opendevin/core/schema/observation.py

@@ -44,5 +44,7 @@ class ObservationTypeSchema(BaseModel):
 
     AGENT_STATE_CHANGED: str = Field(default='agent_state_changed')
 
+    USER_REJECTED: str = Field(default='user_rejected')
+
 
 ObservationType = ObservationTypeSchema()

+ 2 - 1
opendevin/events/action/__init__.py

@@ -1,4 +1,4 @@
-from .action import Action
+from .action import Action, ActionConfirmationStatus
 from .agent import (
     AgentDelegateAction,
     AgentFinishAction,
@@ -32,4 +32,5 @@ __all__ = [
     'ChangeAgentStateAction',
     'IPythonRunCellAction',
     'MessageAction',
+    'ActionConfirmationStatus',
 ]

+ 7 - 0
opendevin/events/action/action.py

@@ -1,9 +1,16 @@
 from dataclasses import dataclass
+from enum import Enum
 from typing import ClassVar
 
 from opendevin.events.event import Event
 
 
+class ActionConfirmationStatus(str, Enum):
+    CONFIRMED = 'confirmed'
+    REJECTED = 'rejected'
+    AWAITING_CONFIRMATION = 'awaiting_confirmation'
+
+
 @dataclass
 class Action(Event):
     runnable: ClassVar[bool] = False

+ 3 - 1
opendevin/events/action/commands.py

@@ -3,7 +3,7 @@ from typing import ClassVar
 
 from opendevin.core.schema import ActionType
 
-from .action import Action
+from .action import Action, ActionConfirmationStatus
 
 
 @dataclass
@@ -12,6 +12,7 @@ class CmdRunAction(Action):
     thought: str = ''
     action: str = ActionType.RUN
     runnable: ClassVar[bool] = True
+    is_confirmed: ActionConfirmationStatus = ActionConfirmationStatus.CONFIRMED
 
     @property
     def message(self) -> str:
@@ -31,6 +32,7 @@ class IPythonRunCellAction(Action):
     thought: str = ''
     action: str = ActionType.RUN_IPYTHON
     runnable: ClassVar[bool] = True
+    is_confirmed: ActionConfirmationStatus = ActionConfirmationStatus.CONFIRMED
     kernel_init_code: str = ''  # code to run in the kernel (if the kernel is restarted)
 
     def __str__(self) -> str:

+ 2 - 0
opendevin/events/observation/__init__.py

@@ -7,6 +7,7 @@ from .error import ErrorObservation
 from .files import FileReadObservation, FileWriteObservation
 from .observation import Observation
 from .recall import AgentRecallObservation
+from .reject import RejectObservation
 from .success import SuccessObservation
 
 __all__ = [
@@ -22,4 +23,5 @@ __all__ = [
     'AgentStateChangedObservation',
     'AgentDelegateObservation',
     'SuccessObservation',
+    'RejectObservation',
 ]

+ 18 - 0
opendevin/events/observation/reject.py

@@ -0,0 +1,18 @@
+from dataclasses import dataclass
+
+from opendevin.core.schema import ObservationType
+
+from .observation import Observation
+
+
+@dataclass
+class RejectObservation(Observation):
+    """
+    This data class represents the result of a successful action.
+    """
+
+    observation: str = ObservationType.USER_REJECTED
+
+    @property
+    def message(self) -> str:
+        return self.content

+ 2 - 0
opendevin/events/serialization/observation.py

@@ -10,6 +10,7 @@ from opendevin.events.observation.error import ErrorObservation
 from opendevin.events.observation.files import FileReadObservation, FileWriteObservation
 from opendevin.events.observation.observation import Observation
 from opendevin.events.observation.recall import AgentRecallObservation
+from opendevin.events.observation.reject import RejectObservation
 from opendevin.events.observation.success import SuccessObservation
 
 observations = (
@@ -24,6 +25,7 @@ observations = (
     SuccessObservation,
     ErrorObservation,
     AgentStateChangedObservation,
+    RejectObservation,
 )
 
 OBSERVATION_TYPE_TO_CLASS = {

+ 14 - 0
opendevin/runtime/runtime.py

@@ -7,6 +7,7 @@ from opendevin.core.logger import opendevin_logger as logger
 from opendevin.events import EventStream, EventStreamSubscriber
 from opendevin.events.action import (
     Action,
+    ActionConfirmationStatus,
     AgentRecallAction,
     BrowseInteractiveAction,
     BrowseURLAction,
@@ -20,6 +21,7 @@ from opendevin.events.observation import (
     ErrorObservation,
     NullObservation,
     Observation,
+    RejectObservation,
 )
 from opendevin.events.serialization.action import ACTION_TYPE_TO_CLASS
 from opendevin.runtime import (
@@ -115,6 +117,11 @@ class Runtime:
         """
         if not action.runnable:
             return NullObservation('')
+        if (
+            hasattr(action, 'is_confirmed')
+            and action.is_confirmed == ActionConfirmationStatus.AWAITING_CONFIRMATION
+        ):
+            return NullObservation('')
         action_type = action.action  # type: ignore[attr-defined]
         if action_type not in ACTION_TYPE_TO_CLASS:
             return ErrorObservation(f'Action {action_type} does not exist.')
@@ -122,6 +129,13 @@ class Runtime:
             return ErrorObservation(
                 f'Action {action_type} is not supported in the current runtime.'
             )
+        if (
+            hasattr(action, 'is_confirmed')
+            and action.is_confirmed == ActionConfirmationStatus.REJECTED
+        ):
+            return RejectObservation(
+                'Action has been rejected by the user! Waiting for further user input.'
+            )
         observation = await getattr(self, action_type)(action)
         observation._parent = action.id  # type: ignore[attr-defined]
         return observation

+ 1 - 1
opendevin/server/listen.py

@@ -217,7 +217,7 @@ async def websocket_endpoint(websocket: WebSocket):
         ```
     - Run a command:
         ```json
-        {"action": "run", "args": {"command": "ls -l", "thought": ""}}
+        {"action": "run", "args": {"command": "ls -l", "thought": "", "is_confirmed": "confirmed"}}
         ```
     - Run an IPython command:
         ```json

+ 4 - 0
opendevin/server/session/agent.py

@@ -91,6 +91,9 @@ class AgentSession:
         model = args.get(ConfigType.LLM_MODEL, llm_config.model)
         api_key = args.get(ConfigType.LLM_API_KEY, llm_config.api_key)
         api_base = llm_config.base_url
+        confirmation_mode = args.get(
+            ConfigType.CONFIRMATION_MODE, config.confirmation_mode
+        )
         max_iterations = args.get(ConfigType.MAX_ITERATIONS, config.max_iterations)
 
         logger.info(f'Creating agent {agent_cls} using LLM {model}')
@@ -110,6 +113,7 @@ class AgentSession:
             event_stream=self.event_stream,
             agent=agent,
             max_iterations=int(max_iterations),
+            confirmation_mode=confirmation_mode,
         )
         try:
             agent_state = State.restore_from_session(self.sid)

+ 1 - 1
tests/integration/mock/DelegatorAgent/test_edits/prompt_002.log

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": ""}}, {"source": "agent", "observation": "run", "content": "bad.txt", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
+[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "bad.txt", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
 
 ## Format
 Your response MUST be in JSON format. It must be an object, and it must contain two fields:

+ 1 - 1
tests/integration/mock/DelegatorAgent/test_edits/prompt_003.log

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": ""}}, {"source": "agent", "observation": "run", "content": "bad.txt", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}, {"source": "agent", "action": "read", "args": {"path": "bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "bad.txt"}}]
+[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "bad.txt", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}, {"source": "agent", "action": "read", "args": {"path": "bad.txt", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "This is a stupid typoo.\nReally?\nNo mor typos!\nEnjoy!\n", "extras": {"path": "bad.txt"}}]
 
 ## Format
 Your response MUST be in JSON format. It must be an object, and it must contain two fields:

+ 1 - 1
tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_002.log

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
+[{"source": "agent", "action": "run", "args": {"command": "ls", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "ls", "exit_code": 0}}]
 
 ## Format
 Your response MUST be in JSON format. It must be an object, and it must contain two fields:

+ 1 - 1
tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_005.log

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}]
+[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}]
 
 ## Format
 Your response MUST be in JSON format. It must be an object, and it must contain two fields:

+ 1 - 1
tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_006.log

@@ -41,7 +41,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}, {"source": "agent", "action": "run", "args": {"command": "./hello.sh", "thought": ""}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "./hello.sh", "exit_code": 0}}]
+[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}, {"source": "agent", "action": "run", "args": {"command": "./hello.sh", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "./hello.sh", "exit_code": 0}}]
 
 ## Format
 Your response MUST be in JSON format. It must be an object, and it must contain two fields:

+ 1 - 1
tests/integration/mock/DelegatorAgent/test_write_simple_script/prompt_009.log

@@ -39,7 +39,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'\n", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "bash hello.sh", "thought": ""}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "bash hello.sh", "exit_code": 0}}]
+[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\n# Print hello\necho 'hello'\n", "extras": {"path": "hello.sh"}}, {"source": "agent", "action": "run", "args": {"command": "bash hello.sh", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "bash hello.sh", "exit_code": 0}}]
 
 ## Format
 Your response MUST be in JSON format. It must be an object, and it must contain two fields:

+ 1 - 1
tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_003.log

@@ -28,7 +28,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": ""}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}]
+[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}]
 
 If the last item in the history is an error, you should try to fix it.
 

+ 1 - 1
tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_004.log

@@ -28,7 +28,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": ""}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
+[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
 
 If the last item in the history is an error, you should try to fix it.
 

+ 1 - 1
tests/integration/mock/ManagerAgent/test_simple_task_rejection/prompt_005.log

@@ -28,7 +28,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": ""}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
+[{"source": "agent", "action": "run", "args": {"command": "git status", "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "fatal: not a git repository (or any parent up to mount point /)\r\nStopping at filesystem boundary (GIT_DISCOVERY_ACROSS_FILESYSTEM not set).", "extras": {"command_id": -1, "command": "git status", "exit_code": 128}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}, {"source": "agent", "observation": "error", "content": "action={'action': 'reject', 'args': {'reason': 'Not a valid git repository.'}} has the wrong arguments", "extras": {}}]
 
 If the last item in the history is an error, you should try to fix it.
 

+ 1 - 1
tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_004.log

@@ -38,7 +38,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "background": false, "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}]]
+[[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "background": false, "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}]]
 
 ## Format
 Your response MUST be in JSON format. It must be an object, and it must contain two fields:

+ 1 - 1
tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_005.log

@@ -38,7 +38,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "background": false, "thought": ""}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}], [{"source": "agent", "action": "run", "args": {"command": "./hello.sh", "background": false, "thought": ""}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "./hello.sh", "exit_code": 0}}]]
+[[{"source": "agent", "action": "write", "args": {"path": "hello.sh", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "write", "content": "", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "chmod +x hello.sh", "background": false, "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "", "extras": {"command_id": -1, "command": "chmod +x hello.sh", "exit_code": 0}}], [{"source": "agent", "action": "run", "args": {"command": "./hello.sh", "background": false, "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "./hello.sh", "exit_code": 0}}]]
 
 ## Format
 Your response MUST be in JSON format. It must be an object, and it must contain two fields:

+ 1 - 1
tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_006.log

@@ -60,7 +60,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[[{"source": "user", "action": "message", "args": {"content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.", "wait_for_response": false}}, {"observation": "null", "content": "", "extras": {}}], [{"source": "agent", "action": "delegate", "args": {"agent": "CoderAgent", "inputs": {"task": "Write a shell script 'hello.sh' that prints 'hello'.", "summary": ""}, "thought": ""}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {}}}]]
+[[{"source": "user", "action": "message", "args": {"content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.", "wait_for_response": false}}, {"observation": "null", "content": "", "extras": {}}], [{"source": "agent", "action": "delegate", "args": {"agent": "CoderAgent", "inputs": {"task": "Write a shell script 'hello.sh' that prints 'hello'.", "summary": ""}, "thought": "", "is_confirmed": "confirmed"}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {}}}]]
 
 ## Available Actions
 * `delegate` - send a task to another agent from the list provided. Arguments:

+ 1 - 1
tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_009.log

@@ -40,7 +40,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'\n", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "bash hello.sh", "background": false, "thought": ""}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "bash hello.sh", "exit_code": 0}}]]
+[[{"source": "agent", "action": "read", "args": {"path": "hello.sh", "start": 0, "end": -1, "thought": ""}}, {"source": "agent", "observation": "read", "content": "#!/bin/bash\n\n# Print 'hello'\necho 'hello'\n", "extras": {"path": "hello.sh"}}], [{"source": "agent", "action": "run", "args": {"command": "bash hello.sh", "background": false, "thought": "", "is_confirmed": "confirmed"}}, {"source": "agent", "observation": "run", "content": "hello", "extras": {"command_id": -1, "command": "bash hello.sh", "exit_code": 0}}]]
 
 ## Format
 Your response MUST be in JSON format. It must be an object, and it must contain two fields:

+ 1 - 1
tests/integration/mock/ManagerAgent/test_write_simple_script/prompt_010.log

@@ -60,7 +60,7 @@ as well as observations you've made. This only includes the MOST RECENT
 actions and observations--more may have happened before that.
 They are time-ordered, with your most recent action at the bottom.
 
-[[{"source": "user", "action": "message", "args": {"content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.", "wait_for_response": false}}, {"observation": "null", "content": "", "extras": {}}], [{"source": "agent", "action": "delegate", "args": {"agent": "CoderAgent", "inputs": {"task": "Write a shell script 'hello.sh' that prints 'hello'.", "summary": ""}, "thought": ""}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {}}}], [{"source": "agent", "action": "delegate", "args": {"agent": "VerifierAgent", "inputs": {"task": "Verify that the shell script 'hello.sh' prints 'hello'."}, "thought": ""}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {"completed": true}}}]]
+[[{"source": "user", "action": "message", "args": {"content": "Write a shell script 'hello.sh' that prints 'hello'. Do not ask me for confirmation at any point.", "wait_for_response": false}}, {"observation": "null", "content": "", "extras": {}}], [{"source": "agent", "action": "delegate", "args": {"agent": "CoderAgent", "inputs": {"task": "Write a shell script 'hello.sh' that prints 'hello'.", "summary": ""}, "thought": "", "is_confirmed": "confirmed"}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {}}}], [{"source": "agent", "action": "delegate", "args": {"agent": "VerifierAgent", "inputs": {"task": "Verify that the shell script 'hello.sh' prints 'hello'."}, "thought": "", "is_confirmed": "confirmed"}}, {"observation": "null", "content": "", "extras": {}}], [{"action": "null", "args": {}}, {"source": "agent", "observation": "delegate", "content": "", "extras": {"outputs": {"completed": true}}}]]
 
 ## Available Actions
 * `delegate` - send a task to another agent from the list provided. Arguments:

+ 4 - 2
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_001.log

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "echo \"hello world\"",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "node test.js",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 6 - 3
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_002.log

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "echo \"hello world\"",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "node test.js",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "pwd",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 8 - 4
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_003.log

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "echo \"hello world\"",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "node test.js",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "pwd",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "ls",
-      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
+      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 8 - 4
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_004.log

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "echo \"hello world\"",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "node test.js",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "pwd",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "ls",
-      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
+      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 8 - 4
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_005.log

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "echo \"hello world\"",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "node test.js",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "pwd",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "ls",
-      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
+      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 8 - 4
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_006.log

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "echo \"hello world\"",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "node test.js",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "pwd",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "ls",
-      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
+      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 10 - 5
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_007.log

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "echo \"hello world\"",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "node test.js",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "pwd",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "ls",
-      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
+      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -401,7 +405,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "chmod +x hello.sh",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 12 - 6
tests/integration/mock/MonologueAgent/test_write_simple_script/prompt_008.log

@@ -135,7 +135,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "echo \"hello world\"",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -182,7 +183,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "node test.js",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -330,7 +332,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "pwd",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -348,7 +351,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "ls",
-      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
+      "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -401,7 +405,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "chmod +x hello.sh",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {
@@ -419,7 +424,8 @@ This is your internal monologue, in JSON format:
     "action": "run",
     "args": {
       "command": "./hello.sh",
-      "thought": "I need to run the 'hello.sh' script to verify that it prints 'hello'."
+      "thought": "I need to run the 'hello.sh' script to verify that it prints 'hello'.",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 2 - 1
tests/integration/mock/MonologueAgent/test_write_simple_script/response_002.log

@@ -3,7 +3,8 @@
   "action": "run",
   "args": {
     "command": "ls",
-    "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better."
+    "thought": "I need to see the contents of the current directory to ensure there are no conflicts and to understand the environment better.",
+    "is_confirmed": "confirmed"
   }
 }
 ```

+ 2 - 1
tests/integration/mock/PlannerAgent/test_write_simple_script/prompt_010.log

@@ -196,7 +196,8 @@ ten actions--more happened before that.
     "action": "run",
     "args": {
       "command": "bash hello.sh",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 2 - 1
tests/integration/mock/PlannerAgent/test_write_simple_script/prompt_011.log

@@ -195,7 +195,8 @@ ten actions--more happened before that.
     "action": "run",
     "args": {
       "command": "bash hello.sh",
-      "thought": ""
+      "thought": "",
+      "is_confirmed": "confirmed"
     }
   },
   {

+ 6 - 1
tests/unit/test_action_serialization.py

@@ -13,6 +13,7 @@ from opendevin.events.action import (
     MessageAction,
     ModifyTaskAction,
 )
+from opendevin.events.action.action import ActionConfirmationStatus
 from opendevin.events.serialization import (
     event_from_dict,
     event_to_dict,
@@ -90,7 +91,11 @@ def test_agent_reject_action_serialization_deserialization():
 def test_cmd_run_action_serialization_deserialization():
     original_action_dict = {
         'action': 'run',
-        'args': {'command': 'echo "Hello world"', 'thought': ''},
+        'args': {
+            'command': 'echo "Hello world"',
+            'thought': '',
+            'is_confirmed': ActionConfirmationStatus.CONFIRMED,
+        },
     }
     serialization_deserialization(original_action_dict, CmdRunAction)