Repository: griptape-ai/griptape

Issue:
Conversation Memory Directly on Prompt Tasks Doesn't Work
```python
from griptape.memory.structure import ConversationMemory
from griptape.tasks import PromptTask

conversation_memory = ConversationMemory()
print(
    PromptTask("Output a single random word", conversation_memory=conversation_memory)
    .run()
    .value
)

print(
    PromptTask("What was that word?", conversation_memory=conversation_memory)
    .run()
    .value
)
```

Hints from issue comments:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f50ac9c8..a5e9fedd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - References being lost on Artifacts during chunking.
 - `FootnotePromptResponseRagModule`'s system prompt causing it to not answer even with relevant chunks. 
 - Chunker occasionally dropping suffix chunk separators.
+- `PromptTask.conversation_memory` not working when set without a Structure.
 
 ### Deprecated
 
diff --git a/docs/griptape-framework/structures/conversation-memory.md b/docs/griptape-framework/structures/conversation-memory.md
index e4e30ad7..b16cb845 100644
--- a/docs/griptape-framework/structures/conversation-memory.md
+++ b/docs/griptape-framework/structures/conversation-memory.md
@@ -57,7 +57,7 @@ Now, each _Task_ creates a Conversation Memory Run when it runs. This eliminates
 To blend the two approaches, you can disable Conversation Memory on individual tasks by setting [PromptTask.conversation_memory](../../reference/griptape/tasks/prompt_task.md#griptape.tasks.prompt_task.PromptTask.conversation_memory) to `None`.
 
 ```python
---8<-- "docs/griptape-framework/structures/src/conversation_memory_per_task_with_none.py"
+--8<-- "docs/griptape-framework/structures/src/conversation_memory_per_task_with_disabled.py"
 ```
 
 ## Types of Memory
diff --git a/griptape/tasks/prompt_task.py b/griptape/tasks/prompt_task.py
index d2de27ac..472e0f4a 100644
--- a/griptape/tasks/prompt_task.py
+++ b/griptape/tasks/prompt_task.py
@@ -43,8 +43,8 @@ class PromptTask(BaseTask, RuleMixin, ActionsSubtaskOriginMixin):
         default=Factory(lambda self: self.default_generate_system_template, takes_self=True),
         kw_only=True,
     )
-    conversation_memory: Union[Optional[BaseConversationMemory], NothingType] = field(
-        default=Factory(lambda: NOTHING), kw_only=True
+    _conversation_memory: Union[Optional[BaseConversationMemory], NothingType] = field(
+        default=Factory(lambda: NOTHING), kw_only=True, alias="conversation_memory"
     )
     _input: Union[str, list, tuple, BaseArtifact, Callable[[BaseTask], BaseArtifact]] = field(
         default=lambda task: task.full_context["args"][0] if task.full_context["args"] else TextArtifact(value=""),
@@ -88,10 +88,24 @@ class PromptTask(BaseTask, RuleMixin, ActionsSubtaskOriginMixin):
     def input(self, value: str | list | tuple | BaseArtifact | Callable[[BaseTask], BaseArtifact]) -> None:
         self._input = value
 
+    @property
+    def conversation_memory(self) -> Optional[BaseConversationMemory]:
+        if self._conversation_memory is NOTHING:
+            if self.structure is None:
+                return None
+            else:
+                return self.structure.conversation_memory
+        else:
+            return self._conversation_memory
+
+    @conversation_memory.setter
+    def conversation_memory(self, value: Optional[BaseConversationMemory]) -> None:
+        self._conversation_memory = value
+
     @property
     def prompt_stack(self) -> PromptStack:
         stack = PromptStack(tools=self.tools, output_schema=self.output_schema)
-        memory = self.structure.conversation_memory if self.structure is not None else None
+        memory = self.conversation_memory
 
         system_template = self.generate_system_template(self)
         if system_template:
@@ -154,7 +168,6 @@ class PromptTask(BaseTask, RuleMixin, ActionsSubtaskOriginMixin):
         if (
             (self.structure is None or self.structure.conversation_memory_strategy == "per_task")
             and conversation_memory is not None
-            and conversation_memory is not NOTHING
             and self.output is not None
         ):
             run = Run(input=self.input, output=self.output)
@@ -197,12 +210,6 @@ class PromptTask(BaseTask, RuleMixin, ActionsSubtaskOriginMixin):
     def preprocess(self, structure: Structure) -> BaseTask:
         super().preprocess(structure)
 
-        if self.conversation_memory is NOTHING:
-            if structure.conversation_memory is not None:
-                self.conversation_memory = structure.conversation_memory
-            else:
-                self.conversation_memory = None
-
         if self.task_memory is None and structure.task_memory:
             self.set_default_tools_memory(structure.task_memory)
 


Test patch:
diff --git a/tests/unit/tasks/test_prompt_task.py b/tests/unit/tasks/test_prompt_task.py
index d146d224..4bbb2de6 100644
--- a/tests/unit/tasks/test_prompt_task.py
+++ b/tests/unit/tasks/test_prompt_task.py
@@ -212,6 +212,7 @@ class TestPromptTask:
         task.run()
 
         assert len(conversation_memory.runs) == 0
+        assert len(task.prompt_stack.messages) == 2
 
         task.conversation_memory = conversation_memory
 
@@ -219,6 +220,7 @@ class TestPromptTask:
         task.run()
 
         assert len(conversation_memory.runs) == 2
+        assert len(task.prompt_stack.messages) == 6
 
         task.conversation_memory = None
 
@@ -226,6 +228,7 @@ class TestPromptTask:
         task.run()
 
         assert len(conversation_memory.runs) == 2
+        assert len(task.prompt_stack.messages) == 2
 
     def test_subtasks(self):
         task = PromptTask(