Improve workflow suggestion steps and debug info (#16309)

Kirill Bulatov , Nathan Sobo , and Bennet Bo Fenner created

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <nathan@zed.dev>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>

Change summary

assets/prompts/step_resolution.hbs        | 202 +++++++++++++++++-------
crates/assistant/src/assistant_panel.rs   |   2 
crates/assistant/src/context.rs           |   6 
crates/assistant/src/context_inspector.rs |  49 +++--
crates/assistant/src/prompts.rs           |  16 +
crates/assistant/src/workflow.rs          | 191 +++++++++++++++--------
6 files changed, 310 insertions(+), 156 deletions(-)

Detailed changes

assets/prompts/step_resolution.hbs 🔗

@@ -1,22 +1,27 @@
-Your task is to map a step from the conversation above to suggestions on symbols inside the provided source files.
+<overview>
+Your task is to map a step from a workflow to locations in source code where code needs to be changed to fulfill that step.
+Given a workflow containing background context plus a series of <step> tags, you will resolve *one* of these step tags to resolve to one or more locations in the code.
+With each location, you will produce a brief, one-line description of the changes to be made.
 
-Guidelines:
+<guidelines>
 - There's no need to describe *what* to do, just *where* to do it.
+- Only reference locations that actually exist (unless you're creating a file).
 - If creating a file, assume any subsequent updates are included at the time of creation.
-- Don't create and then update a file.
-- We'll create it in one shot.
+- Don't create and then update a file. Always create new files in shot.
 - Prefer updating symbols lower in the syntax tree if possible.
 - Never include suggestions on a parent symbol and one of its children in the same suggestions block.
 - Never nest an operation with another operation or include CDATA or other content. All suggestions are leaf nodes.
-- Include a description attribute for each operation with a brief, one-line description of the change to perform.
 - Descriptions are required for all suggestions except delete.
 - When generating multiple suggestions, ensure the descriptions are specific to each individual operation.
 - Avoid referring to the location in the description. Focus on the change to be made, not the location where it's made. That's implicit with the symbol you provide.
 - Don't generate multiple suggestions at the same location. Instead, combine them together in a single operation with a succinct combined description.
+</guidelines>
+</overview>
 
-Example 1:
-
-User:
+<examples>
+<example>
+<workflow_context>
+<message role="user">
 ```rs src/rectangle.rs
 struct Rectangle {
     width: f64,
@@ -30,12 +35,21 @@ impl Rectangle {
 }
 ```
 
+We need to add methods to calculate the area and perimeter of the rectangle. Can you help with that?
+</message>
+<message role="assistant">
+Sure, I can help with that!
+
 <step>Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct</step>
 <step>Implement the 'Display' trait for the Rectangle struct</step>
+</message>
+</workflow_context>
 
-What are the suggestions for the step: <step>Add a new method 'calculate_area' to the Rectangle struct</step>
+<step_to_resolve>
+Add new methods 'calculate_area' and 'calculate_perimeter' to the Rectangle struct
+</step_to_resolve>
 
-A (wrong):
+<incorrect_output reason="NEVER append multiple children at the same location.">
 {
   "title": "Add Rectangle methods",
   "suggestions": [
@@ -53,10 +67,9 @@ A (wrong):
     }
   ]
 }
+</incorrect_output>
 
-This demonstrates what NOT to do. NEVER append multiple children at the same location.
-
-A (corrected):
+<correct_output>
 {
   "title": "Add Rectangle methods",
   "suggestions": [
@@ -68,11 +81,13 @@ A (corrected):
     }
   ]
 }
+</correct_output>
 
-User:
-What are the suggestions for the step: <step>Implement the 'Display' trait for the Rectangle struct</step>
+<step_to_resolve>
+Implement the 'Display' trait for the Rectangle struct
+</step_to_resolve>
 
-A:
+<output>
 {
   "title": "Implement Display for Rectangle",
   "suggestions": [
@@ -84,10 +99,11 @@ A:
     }
   ]
 }
+</output>
 
-Example 2:
-
-User:
+<example>
+<workflow_context>
+<message role="user">
 ```rs src/user.rs
 struct User {
     pub name: String,
@@ -105,13 +121,19 @@ impl User {
     }
 }
 ```
-
+</message>
+<message role="assistant">
+Certainly!
 <step>Update the 'print_info' method to use formatted output</step>
 <step>Remove the 'email' field from the User struct</step>
+</message>
+</workflow_context>
 
-What are the suggestions for the step: <step>Update the 'print_info' method to use formatted output</step>
+<step_to_resolve>
+Update the 'print_info' method to use formatted output
+</step_to_resolve>
 
-A:
+<output>
 {
   "title": "Use formatted output",
   "suggestions": [
@@ -123,11 +145,13 @@ A:
     }
   ]
 }
+</output>
 
-User:
-What are the suggestions for the step: <step>Remove the 'email' field from the User struct</step>
+<step_to_resolve>
+Remove the 'email' field from the User struct
+</step_to_resolve>
 
-A:
+<output>
 {
   "title": "Remove email field",
   "suggestions": [
@@ -138,10 +162,12 @@ A:
       }
     ]
 }
+</output>
+</example>
 
-Example 3:
-
-User:
+<example>
+<workflow_context>
+<message role="user">
 ```rs src/vehicle.rs
 struct Vehicle {
     make: String,
@@ -159,13 +185,18 @@ impl Vehicle {
     }
 }
 ```
-
+</message>
+<message role="assistant">
 <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
 <step>Add a new method 'start_engine' in the Vehicle impl block</step>
+</message>
+</workflow_context>
 
-What are the suggestions for the step: <step>Add a 'use std::fmt;' statement at the beginning of the file</step>
+<step_to_resolve>
+Add a 'use std::fmt;' statement at the beginning of the file
+</step_to_resolve>
 
-A:
+<output>
 {
   "title": "Add use std::fmt statement",
   "suggestions": [
@@ -176,11 +207,13 @@ A:
     }
   ]
 }
+</output>
 
-User:
-What are the suggestions for the step: <step>Add a new method 'start_engine' in the Vehicle impl block</step>
+<step_to_resolve>
+Add a new method 'start_engine' in the Vehicle impl block
+</step_to_resolve>
 
-A:
+<output>
 {
   "title": "Add start_engine method",
   "suggestions": [
@@ -192,10 +225,12 @@ A:
     }
   ]
 }
+</output>
+</example>
 
-Example 4:
-
-User:
+<example>
+<workflow_context>
+<message role="user">
 ```rs src/employee.rs
 struct Employee {
     name: String,
@@ -219,12 +254,18 @@ impl Employee {
     }
 }
 ```
-
+</message>
+<message role="assistant">
 <step>Make salary an f32</step>
+<step>Remove the 'department' field and update the 'print_details' method</step>
+</message>
+</workflow_context>
 
-What are the suggestions for the step: <step>Make salary an f32</step>
+<step_to_resolve>
+Make salary an f32
+</step_to_resolve>
 
-A (wrong):
+<incorrect_output reason="NEVER include suggestions on a parent symbol and one of its children in the same suggestions block.">
 {
   "title": "Change salary to f32",
   "suggestions": [
@@ -242,10 +283,9 @@ A (wrong):
     }
   ]
 }
+</incorrect_output>
 
-This example demonstrates what not to do. `struct Employee salary` is a child of `struct Employee`.
-
-A (corrected):
+<correct_output>
 {
   "title": "Change salary to f32",
   "suggestions": [
@@ -257,11 +297,13 @@ A (corrected):
     }
   ]
 }
+</correct_output>
 
-User:
-What are the correct suggestions for the step: <step>Remove the 'department' field and update the 'print_details' method</step>
+<step_to_resolve>
+Remove the 'department' field and update the 'print_details' method
+</step_to_resolve>
 
-A:
+<output>
 {
   "title": "Remove department",
   "suggestions": [
@@ -278,10 +320,12 @@ A:
     }
   ]
 }
+</output>
+</example>
 
-Example 5:
-
-User:
+<example>
+<workflow_context>
+<message role="user">
 ```rs src/game.rs
 struct Player {
     name: String,
@@ -305,10 +349,17 @@ impl Game {
     }
 }
 ```
-
+</message>
+<message role="assistant">
 <step>Add a 'level' field to Player and update the 'new' method</step>
+</message>
+</workflow_context>
+
+<step_to_resolve>
+Add a 'level' field to Player and update the 'new' method
+</step_to_resolve>
 
-A:
+<output>
 {
   "title": "Add level field to Player",
   "suggestions": [
@@ -326,10 +377,12 @@ A:
     }
   ]
 }
+</output>
+</example>
 
-Example 6:
-
-User:
+<example>
+<workflow_context>
+<message role="user">
 ```rs src/config.rs
 use std::collections::HashMap;
 
@@ -343,10 +396,17 @@ impl Config {
     }
 }
 ```
-
+</message>
+<message role="assistant">
 <step>Add a 'load_from_file' method to Config and import necessary modules</step>
+</message>
+</workflow_context>
+
+<step_to_resolve>
+Add a 'load_from_file' method to Config and import necessary modules
+</step_to_resolve>
 
-A:
+<output>
 {
   "title": "Add load_from_file method",
   "suggestions": [
@@ -363,10 +423,12 @@ A:
     }
   ]
 }
+</output>
+</example>
 
-Example 7:
-
-User:
+<example>
+<workflow_context>
+<message role="user">
 ```rs src/database.rs
 pub(crate) struct Database {
     connection: Connection,
@@ -383,10 +445,17 @@ impl Database {
     }
 }
 ```
-
+</message>
+<message role="assistant">
 <step>Add error handling to the 'query' method and create a custom error type</step>
+</message>
+</workflow_context>
+
+<step_to_resolve>
+Add error handling to the 'query' method and create a custom error type
+</step_to_resolve>
 
-A:
+<output>
 {
   "title": "Add error handling to query",
   "suggestions": [
@@ -409,5 +478,16 @@ A:
     }
   ]
 }
+</output>
+</example>
+</examples>
 
 Now generate the suggestions for the following step:
+
+<workflow_context>
+{{{workflow_context}}}
+</workflow_context>
+
+<step_to_resolve>
+{{{step_to_resolve}}}
+</step_to_resolve>

crates/assistant/src/assistant_panel.rs 🔗

@@ -2911,7 +2911,7 @@ impl ContextEditor {
         let mut assist_ids = Vec::new();
         for (excerpt_id, suggestion_group) in suggestion_groups {
             for suggestion in &suggestion_group.suggestions {
-                assist_ids.extend(suggestion.show(
+                assist_ids.extend(suggestion.kind.show(
                     &editor,
                     excerpt_id,
                     workspace,

crates/assistant/src/context.rs 🔗

@@ -2974,12 +2974,12 @@ mod tests {
 
         model
             .as_fake()
-            .respond_to_last_tool_use(tool::WorkflowStepResolution {
+            .respond_to_last_tool_use(tool::WorkflowStepResolutionTool {
                 step_title: "Title".into(),
-                suggestions: vec![tool::WorkflowSuggestion {
+                suggestions: vec![tool::WorkflowSuggestionTool {
                     path: "/root/hello.rs".into(),
                     // Simulate a symbol name that's slightly different than our outline query
-                    kind: tool::WorkflowSuggestionKind::Update {
+                    kind: tool::WorkflowSuggestionToolKind::Update {
                         symbol: "fn main()".into(),
                         description: "Extract a greeting function".into(),
                     },

crates/assistant/src/context_inspector.rs 🔗

@@ -11,7 +11,7 @@ use ui::{
     div, h_flex, px, Color, Element as _, ParentElement as _, Styled, ViewContext, WindowContext,
 };
 
-use crate::{Context, ResolvedWorkflowStep, WorkflowSuggestion};
+use crate::{Context, ResolvedWorkflowStep, WorkflowSuggestion, WorkflowSuggestionKind};
 
 type StepRange = Range<language::Anchor>;
 
@@ -68,7 +68,7 @@ impl ContextInspector {
                         .and_then(|file| file.path().to_str())
                         .unwrap_or("untitled");
                     let snapshot = buffer.text_snapshot();
-                    writeln!(output, "  {buffer_path}:").ok()?;
+                    writeln!(output, "Path: {buffer_path}:").ok()?;
                     for group in suggestion_groups {
                         for suggestion in &group.suggestions {
                             pretty_print_workflow_suggestion(&mut output, suggestion, &snapshot);
@@ -163,7 +163,7 @@ impl ContextInspector {
 }
 fn pretty_print_anchor(
     out: &mut String,
-    anchor: &language::Anchor,
+    anchor: language::Anchor,
     snapshot: &text::BufferSnapshot,
 ) {
     use std::fmt::Write;
@@ -177,9 +177,9 @@ fn pretty_print_range(
 ) {
     use std::fmt::Write;
     write!(out, "    Range: ").ok();
-    pretty_print_anchor(out, &range.start, snapshot);
+    pretty_print_anchor(out, range.start, snapshot);
     write!(out, "..").ok();
-    pretty_print_anchor(out, &range.end, snapshot);
+    pretty_print_anchor(out, range.end, snapshot);
 }
 
 fn pretty_print_workflow_suggestion(
@@ -188,37 +188,46 @@ fn pretty_print_workflow_suggestion(
     snapshot: &text::BufferSnapshot,
 ) {
     use std::fmt::Write;
-    let (range, description, position) = match suggestion {
-        WorkflowSuggestion::Update { range, description } => (Some(range), Some(description), None),
-        WorkflowSuggestion::CreateFile { description } => (None, Some(description), None),
-        WorkflowSuggestion::AppendChild {
+    let (position, description, range) = match &suggestion.kind {
+        WorkflowSuggestionKind::Update { range, description } => {
+            (None, Some(description), Some(range))
+        }
+        WorkflowSuggestionKind::CreateFile { description } => (None, Some(description), None),
+        WorkflowSuggestionKind::AppendChild {
             position,
             description,
-        }
-        | WorkflowSuggestion::InsertSiblingBefore {
+        } => (Some(position), Some(description), None),
+        WorkflowSuggestionKind::InsertSiblingBefore {
             position,
             description,
-        }
-        | WorkflowSuggestion::InsertSiblingAfter {
+        } => (Some(position), Some(description), None),
+        WorkflowSuggestionKind::InsertSiblingAfter {
             position,
             description,
-        }
-        | WorkflowSuggestion::PrependChild {
+        } => (Some(position), Some(description), None),
+        WorkflowSuggestionKind::PrependChild {
             position,
             description,
-        } => (None, Some(description), Some(position)),
-
-        WorkflowSuggestion::Delete { range } => (Some(range), None, None),
+        } => (Some(position), Some(description), None),
+        WorkflowSuggestionKind::Delete { range } => (None, None, Some(range)),
     };
+    writeln!(out, "    Tool input: {}", suggestion.tool_input).ok();
+    writeln!(
+        out,
+        "    Tool output: {}",
+        serde_json::to_string_pretty(&suggestion.tool_output)
+            .expect("Should not fail on valid struct serialization")
+    )
+    .ok();
     if let Some(description) = description {
         writeln!(out, "    Description: {description}").ok();
     }
     if let Some(range) = range {
-        pretty_print_range(out, range, snapshot);
+        pretty_print_range(out, &range, snapshot);
     }
     if let Some(position) = position {
         write!(out, "    Position: ").ok();
-        pretty_print_anchor(out, position, snapshot);
+        pretty_print_anchor(out, *position, snapshot);
         write!(out, "\n").ok();
     }
     write!(out, "\n").ok();

crates/assistant/src/prompts.rs 🔗

@@ -31,6 +31,15 @@ pub struct TerminalAssistantPromptContext {
     pub user_prompt: String,
 }
 
+/// Context required to generate a workflow step resolution prompt.
+#[derive(Debug, Serialize)]
+pub struct StepResolutionContext {
+    /// The full context, including <step>...</step> tags
+    pub workflow_context: String,
+    /// The text of the specific step from the context to resolve
+    pub step_to_resolve: String,
+}
+
 pub struct PromptBuilder {
     handlebars: Arc<Mutex<Handlebars<'static>>>,
 }
@@ -278,7 +287,10 @@ impl PromptBuilder {
         self.handlebars.lock().render("edit_workflow", &())
     }
 
-    pub fn generate_step_resolution_prompt(&self) -> Result<String, RenderError> {
-        self.handlebars.lock().render("step_resolution", &())
+    pub fn generate_step_resolution_prompt(
+        &self,
+        context: &StepResolutionContext,
+    ) -> Result<String, RenderError> {
+        self.handlebars.lock().render("step_resolution", context)
     }
 }

crates/assistant/src/workflow.rs 🔗

@@ -1,4 +1,6 @@
-use crate::{AssistantPanel, Context, InlineAssistId, InlineAssistant};
+use crate::{
+    prompts::StepResolutionContext, AssistantPanel, Context, InlineAssistId, InlineAssistant,
+};
 use anyhow::{anyhow, Error, Result};
 use collections::HashMap;
 use editor::Editor;
@@ -10,7 +12,7 @@ use project::Project;
 use rope::Point;
 use serde::{Deserialize, Serialize};
 use smol::stream::StreamExt;
-use std::{cmp, ops::Range, sync::Arc};
+use std::{cmp, fmt::Write, ops::Range, sync::Arc};
 use text::{AnchorRangeExt as _, OffsetRangeExt as _};
 use util::ResultExt as _;
 use workspace::Workspace;
@@ -34,7 +36,36 @@ pub struct WorkflowSuggestionGroup {
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
-pub enum WorkflowSuggestion {
+pub struct WorkflowSuggestion {
+    pub kind: WorkflowSuggestionKind,
+    pub tool_input: String,
+    pub tool_output: tool::WorkflowSuggestionTool,
+}
+
+impl WorkflowSuggestion {
+    pub fn range(&self) -> Range<Anchor> {
+        self.kind.range()
+    }
+
+    pub fn show(
+        &self,
+        editor: &View<Editor>,
+        excerpt_id: editor::ExcerptId,
+        workspace: &WeakView<Workspace>,
+        assistant_panel: &View<AssistantPanel>,
+        cx: &mut ui::ViewContext<crate::assistant_panel::ContextEditor>,
+    ) -> Option<InlineAssistId> {
+        self.kind
+            .show(editor, excerpt_id, workspace, assistant_panel, cx)
+    }
+
+    fn try_merge(&mut self, other: &mut WorkflowSuggestion, snapshot: &BufferSnapshot) -> bool {
+        self.kind.try_merge(&other.kind, snapshot)
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum WorkflowSuggestionKind {
     Update {
         range: Range<language::Anchor>,
         description: String,
@@ -87,6 +118,15 @@ impl WorkflowStepResolution {
             .text_for_range(self.tagged_range.clone())
             .collect::<String>();
 
+        let mut workflow_context = String::new();
+        for message in context.messages(cx) {
+            write!(&mut workflow_context, "<message role={}>", message.role).unwrap();
+            for chunk in context_buffer.read(cx).text_for_range(message.offset_range) {
+                write!(&mut workflow_context, "{chunk}").unwrap();
+            }
+            write!(&mut workflow_context, "</message>").unwrap();
+        }
+
         Some(cx.spawn(|this, mut cx| async move {
             let result = async {
                 let Some(model) = model else {
@@ -99,7 +139,12 @@ impl WorkflowStepResolution {
                     cx.notify();
                 })?;
 
-                let mut prompt = prompt_builder.generate_step_resolution_prompt()?;
+                let resolution_context = StepResolutionContext {
+                    workflow_context,
+                    step_to_resolve: step_text.clone(),
+                };
+                let mut prompt =
+                    prompt_builder.generate_step_resolution_prompt(&resolution_context)?;
                 prompt.push_str(&step_text);
                 request.messages.push(LanguageModelRequestMessage {
                     role: Role::User,
@@ -108,7 +153,7 @@ impl WorkflowStepResolution {
 
                 // Invoke the model to get its edit suggestions for this workflow step.
                 let mut stream = model
-                    .use_tool_stream::<tool::WorkflowStepResolution>(request, &cx)
+                    .use_tool_stream::<tool::WorkflowStepResolutionTool>(request, &cx)
                     .await?;
                 while let Some(chunk) = stream.next().await {
                     let chunk = chunk?;
@@ -119,14 +164,16 @@ impl WorkflowStepResolution {
                 }
 
                 let resolution = this.update(&mut cx, |this, _| {
-                    serde_json::from_str::<tool::WorkflowStepResolution>(&this.output)
+                    serde_json::from_str::<tool::WorkflowStepResolutionTool>(&this.output)
                 })??;
 
                 // Translate the parsed suggestions to our internal types, which anchor the suggestions to locations in the code.
                 let suggestion_tasks: Vec<_> = resolution
                     .suggestions
                     .iter()
-                    .map(|suggestion| suggestion.resolve(project.clone(), cx.clone()))
+                    .map(|suggestion| {
+                        suggestion.resolve(step_text.clone(), project.clone(), cx.clone())
+                    })
                     .collect();
 
                 // Expand the context ranges of each suggestion and group suggestions with overlapping context ranges.
@@ -152,7 +199,7 @@ impl WorkflowStepResolution {
                     suggestions.sort_by(|a, b| a.range().cmp(&b.range(), &snapshot));
 
                     // Merge overlapping suggestions
-                    suggestions.dedup_by(|a, b| b.try_merge(&a, &snapshot));
+                    suggestions.dedup_by(|a, b| b.try_merge(a, &snapshot));
 
                     // Create context ranges for each suggestion
                     for suggestion in suggestions {
@@ -214,40 +261,40 @@ impl WorkflowStepResolution {
     }
 }
 
-impl WorkflowSuggestion {
+impl WorkflowSuggestionKind {
     pub fn range(&self) -> Range<language::Anchor> {
         match self {
-            WorkflowSuggestion::Update { range, .. } => range.clone(),
-            WorkflowSuggestion::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
-            WorkflowSuggestion::InsertSiblingBefore { position, .. }
-            | WorkflowSuggestion::InsertSiblingAfter { position, .. }
-            | WorkflowSuggestion::PrependChild { position, .. }
-            | WorkflowSuggestion::AppendChild { position, .. } => *position..*position,
-            WorkflowSuggestion::Delete { range } => range.clone(),
+            Self::Update { range, .. } => range.clone(),
+            Self::CreateFile { .. } => language::Anchor::MIN..language::Anchor::MAX,
+            Self::InsertSiblingBefore { position, .. }
+            | Self::InsertSiblingAfter { position, .. }
+            | Self::PrependChild { position, .. }
+            | Self::AppendChild { position, .. } => *position..*position,
+            Self::Delete { range } => range.clone(),
         }
     }
 
     pub fn description(&self) -> Option<&str> {
         match self {
-            WorkflowSuggestion::Update { description, .. }
-            | WorkflowSuggestion::CreateFile { description }
-            | WorkflowSuggestion::InsertSiblingBefore { description, .. }
-            | WorkflowSuggestion::InsertSiblingAfter { description, .. }
-            | WorkflowSuggestion::PrependChild { description, .. }
-            | WorkflowSuggestion::AppendChild { description, .. } => Some(description),
-            WorkflowSuggestion::Delete { .. } => None,
+            Self::Update { description, .. }
+            | Self::CreateFile { description }
+            | Self::InsertSiblingBefore { description, .. }
+            | Self::InsertSiblingAfter { description, .. }
+            | Self::PrependChild { description, .. }
+            | Self::AppendChild { description, .. } => Some(description),
+            Self::Delete { .. } => None,
         }
     }
 
     fn description_mut(&mut self) -> Option<&mut String> {
         match self {
-            WorkflowSuggestion::Update { description, .. }
-            | WorkflowSuggestion::CreateFile { description }
-            | WorkflowSuggestion::InsertSiblingBefore { description, .. }
-            | WorkflowSuggestion::InsertSiblingAfter { description, .. }
-            | WorkflowSuggestion::PrependChild { description, .. }
-            | WorkflowSuggestion::AppendChild { description, .. } => Some(description),
-            WorkflowSuggestion::Delete { .. } => None,
+            Self::Update { description, .. }
+            | Self::CreateFile { description }
+            | Self::InsertSiblingBefore { description, .. }
+            | Self::InsertSiblingAfter { description, .. }
+            | Self::PrependChild { description, .. }
+            | Self::AppendChild { description, .. } => Some(description),
+            Self::Delete { .. } => None,
         }
     }
 
@@ -286,16 +333,16 @@ impl WorkflowSuggestion {
         let snapshot = buffer.read(cx).snapshot(cx);
 
         match self {
-            WorkflowSuggestion::Update { range, description } => {
+            Self::Update { range, description } => {
                 initial_prompt = description.clone();
                 suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
                     ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
             }
-            WorkflowSuggestion::CreateFile { description } => {
+            Self::CreateFile { description } => {
                 initial_prompt = description.clone();
                 suggestion_range = editor::Anchor::min()..editor::Anchor::min();
             }
-            WorkflowSuggestion::InsertSiblingBefore {
+            Self::InsertSiblingBefore {
                 position,
                 description,
             } => {
@@ -311,7 +358,7 @@ impl WorkflowSuggestion {
                     line_start..line_start
                 });
             }
-            WorkflowSuggestion::InsertSiblingAfter {
+            Self::InsertSiblingAfter {
                 position,
                 description,
             } => {
@@ -327,7 +374,7 @@ impl WorkflowSuggestion {
                     line_start..line_start
                 });
             }
-            WorkflowSuggestion::PrependChild {
+            Self::PrependChild {
                 position,
                 description,
             } => {
@@ -343,7 +390,7 @@ impl WorkflowSuggestion {
                     line_start..line_start
                 });
             }
-            WorkflowSuggestion::AppendChild {
+            Self::AppendChild {
                 position,
                 description,
             } => {
@@ -359,7 +406,7 @@ impl WorkflowSuggestion {
                     line_start..line_start
                 });
             }
-            WorkflowSuggestion::Delete { range } => {
+            Self::Delete { range } => {
                 initial_prompt = "Delete".to_string();
                 suggestion_range = snapshot.anchor_in_excerpt(excerpt_id, range.start)?
                     ..snapshot.anchor_in_excerpt(excerpt_id, range.end)?;
@@ -392,15 +439,15 @@ pub mod tool {
     use schemars::JsonSchema;
 
     #[derive(Debug, Serialize, Deserialize, JsonSchema)]
-    pub struct WorkflowStepResolution {
+    pub struct WorkflowStepResolutionTool {
         /// An extremely short title for the edit step represented by these operations.
         pub step_title: String,
         /// A sequence of operations to apply to the codebase.
         /// When multiple operations are required for a step, be sure to include multiple operations in this list.
-        pub suggestions: Vec<WorkflowSuggestion>,
+        pub suggestions: Vec<WorkflowSuggestionTool>,
     }
 
-    impl LanguageModelTool for WorkflowStepResolution {
+    impl LanguageModelTool for WorkflowStepResolutionTool {
         fn name() -> String {
             "edit".into()
         }
@@ -429,16 +476,17 @@ pub mod tool {
     /// programmatic changes to source code. It provides a structured way to describe
     /// edits for features like refactoring tools or AI-assisted coding suggestions.
     #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
-    pub struct WorkflowSuggestion {
+    pub struct WorkflowSuggestionTool {
         /// The path to the file containing the relevant operation
         pub path: String,
         #[serde(flatten)]
-        pub kind: WorkflowSuggestionKind,
+        pub kind: WorkflowSuggestionToolKind,
     }
 
-    impl WorkflowSuggestion {
+    impl WorkflowSuggestionTool {
         pub(super) async fn resolve(
             &self,
+            tool_input: String,
             project: Model<Project>,
             mut cx: AsyncAppContext,
         ) -> Result<(Model<Buffer>, super::WorkflowSuggestion)> {
@@ -475,9 +523,8 @@ pub mod tool {
             let snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
             let outline = snapshot.outline(None).context("no outline for buffer")?;
 
-            let suggestion;
-            match kind {
-                WorkflowSuggestionKind::Update {
+            let kind = match kind {
+                WorkflowSuggestionToolKind::Update {
                     symbol,
                     description,
                 } => {
@@ -494,12 +541,12 @@ pub mod tool {
                         snapshot.line_len(symbol.range.end.row),
                     );
                     let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
-                    suggestion = super::WorkflowSuggestion::Update { range, description };
+                    WorkflowSuggestionKind::Update { range, description }
                 }
-                WorkflowSuggestionKind::Create { description } => {
-                    suggestion = super::WorkflowSuggestion::CreateFile { description };
+                WorkflowSuggestionToolKind::Create { description } => {
+                    WorkflowSuggestionKind::CreateFile { description }
                 }
-                WorkflowSuggestionKind::InsertSiblingBefore {
+                WorkflowSuggestionToolKind::InsertSiblingBefore {
                     symbol,
                     description,
                 } => {
@@ -514,12 +561,12 @@ pub mod tool {
                                 annotation_range.start
                             }),
                     );
-                    suggestion = super::WorkflowSuggestion::InsertSiblingBefore {
+                    WorkflowSuggestionKind::InsertSiblingBefore {
                         position,
                         description,
-                    };
+                    }
                 }
-                WorkflowSuggestionKind::InsertSiblingAfter {
+                WorkflowSuggestionToolKind::InsertSiblingAfter {
                     symbol,
                     description,
                 } => {
@@ -528,12 +575,12 @@ pub mod tool {
                         .with_context(|| format!("symbol not found: {:?}", symbol))?
                         .to_point(&snapshot);
                     let position = snapshot.anchor_after(symbol.range.end);
-                    suggestion = super::WorkflowSuggestion::InsertSiblingAfter {
+                    WorkflowSuggestionKind::InsertSiblingAfter {
                         position,
                         description,
-                    };
+                    }
                 }
-                WorkflowSuggestionKind::PrependChild {
+                WorkflowSuggestionToolKind::PrependChild {
                     symbol,
                     description,
                 } => {
@@ -548,18 +595,18 @@ pub mod tool {
                                 .body_range
                                 .map_or(symbol.range.start, |body_range| body_range.start),
                         );
-                        suggestion = super::WorkflowSuggestion::PrependChild {
+                        WorkflowSuggestionKind::PrependChild {
                             position,
                             description,
-                        };
+                        }
                     } else {
-                        suggestion = super::WorkflowSuggestion::PrependChild {
+                        WorkflowSuggestionKind::PrependChild {
                             position: language::Anchor::MIN,
                             description,
-                        };
+                        }
                     }
                 }
-                WorkflowSuggestionKind::AppendChild {
+                WorkflowSuggestionToolKind::AppendChild {
                     symbol,
                     description,
                 } => {
@@ -574,18 +621,18 @@ pub mod tool {
                                 .body_range
                                 .map_or(symbol.range.end, |body_range| body_range.end),
                         );
-                        suggestion = super::WorkflowSuggestion::AppendChild {
+                        WorkflowSuggestionKind::AppendChild {
                             position,
                             description,
-                        };
+                        }
                     } else {
-                        suggestion = super::WorkflowSuggestion::PrependChild {
+                        WorkflowSuggestionKind::PrependChild {
                             position: language::Anchor::MAX,
                             description,
-                        };
+                        }
                     }
                 }
-                WorkflowSuggestionKind::Delete { symbol } => {
+                WorkflowSuggestionToolKind::Delete { symbol } => {
                     let symbol = outline
                         .find_most_similar(&symbol)
                         .with_context(|| format!("symbol not found: {:?}", symbol))?
@@ -599,9 +646,15 @@ pub mod tool {
                         snapshot.line_len(symbol.range.end.row),
                     );
                     let range = snapshot.anchor_before(start)..snapshot.anchor_after(end);
-                    suggestion = super::WorkflowSuggestion::Delete { range };
+                    WorkflowSuggestionKind::Delete { range }
                 }
-            }
+            };
+
+            let suggestion = WorkflowSuggestion {
+                kind,
+                tool_output: self.clone(),
+                tool_input,
+            };
 
             Ok((buffer, suggestion))
         }
@@ -609,7 +662,7 @@ pub mod tool {
 
     #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
     #[serde(tag = "kind")]
-    pub enum WorkflowSuggestionKind {
+    pub enum WorkflowSuggestionToolKind {
         /// Rewrites the specified symbol entirely based on the given description.
         /// This operation completely replaces the existing symbol with new content.
         Update {