Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 26edbc6

Browse files
feat: Add Act and Plan modes (tailcallhq#460)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 336a57e commit 26edbc6

24 files changed

Lines changed: 369 additions & 115 deletions

File tree

.config/nextest.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[profile.default]
22
# Set default test timeouts - mark tests as SLOW after 1 sec.
3-
slow-timeout = { period = "1s", terminate-after = 1, grace-period = "0s" }
3+
slow-timeout = { period = "1s", terminate-after = 30, grace-period = "0s" }

README.md

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ Forge is a comprehensive coding agent that integrates AI capabilities with your
3636
- [Autocomplete](#autocomplete)
3737
- [WYSIWYG Shell Experience](#wysiwyg-shell-experience)
3838
- [Command Interruption](#command-interruption)
39+
- [Operation Modes](#operation-modes)
3940
- [Custom Workflows and Multi-Agent Systems](#custom-workflows-and-multi-agent-systems)
4041
- [Creating Custom Workflows](#creating-custom-workflows)
4142
- [Workflow Configuration](#workflow-configuration)
@@ -136,10 +137,12 @@ Additional security features include:
136137

137138
Forge offers several built-in commands to enhance your interaction:
138139

139-
- `\new` - Start a new task when you've completed your current one
140-
- `\info` - View environment summary, logs folder location, and command history
141-
- `\models` - List all available AI models with capabilities and context limits
142-
- `\dump` - Save the current conversation in JSON format to a file for reference
140+
- `/new` - Start a new task when you've completed your current one
141+
- `/info` - View environment summary, logs folder location, and command history
142+
- `/models` - List all available AI models with capabilities and context limits
143+
- `/dump` - Save the current conversation in JSON format to a file for reference
144+
- `/act` - Switch to ACT mode (default), allowing Forge to execute commands and implement changes
145+
- `/plan` - Switch to PLAN mode, where Forge analyzes and plans but doesn't modify files
143146

144147
### Autocomplete
145148

@@ -161,6 +164,44 @@ Stay in control of your shell environment with intuitive command handling:
161164
- **Cancel with `CTRL+C`:** Gracefully interrupt ongoing operations, providing the flexibility to halt processes that no longer need execution.
162165
- **Exit with `CTRL+D`:** Easily exit the shell session without hassle, ensuring you can quickly terminate your operations when needed.
163166

167+
### Operation Modes
168+
169+
Forge operates in two distinct modes to provide flexible assistance based on your needs:
170+
171+
#### ACT Mode (Default)
172+
173+
In ACT mode, which is the default when you start Forge, the assistant is empowered to directly implement changes to your codebase and execute commands:
174+
175+
- **Full Execution**: Forge can modify files, create new ones, and execute shell commands
176+
- **Implementation**: Directly implements the solutions it proposes
177+
- **Verification**: Performs verification steps to ensure changes work as intended
178+
- **Best For**: When you want Forge to handle implementation details and fix issues directly
179+
180+
**Example**:
181+
182+
```bash
183+
# Switch to ACT mode within a Forge session
184+
/act
185+
```
186+
187+
#### PLAN Mode
188+
189+
In PLAN mode, Forge analyzes and plans but doesn't modify your codebase:
190+
191+
- **Read-Only Operations**: Can only read files and run non-destructive commands
192+
- **Detailed Analysis**: Thoroughly examines code, identifies issues, and proposes solutions
193+
- **Structured Planning**: Provides step-by-step action plans for implementing changes
194+
- **Best For**: When you want to understand what changes are needed before implementing them yourself
195+
196+
**Example**:
197+
198+
```bash
199+
# Switch to PLAN mode within a Forge session
200+
/plan
201+
```
202+
203+
You can easily switch between modes during a session using the `/act` and `/plan` commands. PLAN mode is especially useful for reviewing potential changes before they're implemented, while ACT mode streamlines the development process by handling implementation details for you.
204+
164205
## Custom Workflows and Multi-Agent Systems
165206

166207
For complex tasks, a single agent may not be sufficient. Forge allows you to create custom workflows with multiple specialized agents working together to accomplish sophisticated tasks.

crates/forge_api/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@ forge_app = { path = "../forge_app" }
1212
forge_walker = { path = "../forge_walker" }
1313
forge_infra = { path = "../forge_infra" }
1414
serde_yaml = "0.9.34"
15+
serde_json = { version = "1.0" }
1516

1617
[dev-dependencies]
1718
tempfile = "3.8.1"
1819
tokio = { version = "1.0", features = ["full"] }
1920
insta = "1.41.1"
20-
serde_json = "1.0.133"
2121
serde = { version = "1.0" }

crates/forge_api/src/api.rs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use forge_app::{EnvironmentService, ForgeApp, Infrastructure};
66
use forge_domain::*;
77
use forge_infra::ForgeInfra;
88
use forge_stream::MpscStream;
9+
use serde_json::Value;
910

1011
use crate::executor::ForgeExecutorService;
1112
use crate::loader::ForgeLoaderService;
@@ -77,4 +78,27 @@ impl<F: App + Infrastructure> API for ForgeAPI<F> {
7778
) -> anyhow::Result<Option<Conversation>> {
7879
self.app.conversation_service().get(conversation_id).await
7980
}
81+
82+
async fn get_variable(
83+
&self,
84+
conversation_id: &ConversationId,
85+
key: &str,
86+
) -> anyhow::Result<Option<Value>> {
87+
self.app
88+
.conversation_service()
89+
.get_variable(conversation_id, key)
90+
.await
91+
}
92+
93+
async fn set_variable(
94+
&self,
95+
conversation_id: &ConversationId,
96+
key: String,
97+
value: Value,
98+
) -> anyhow::Result<()> {
99+
self.app
100+
.conversation_service()
101+
.set_variable(conversation_id, key, value)
102+
.await
103+
}
80104
}

crates/forge_api/src/lib.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::path::Path;
88
pub use api::*;
99
pub use forge_domain::*;
1010
use forge_stream::MpscStream;
11+
use serde_json::Value;
1112

1213
#[async_trait::async_trait]
1314
pub trait API {
@@ -44,4 +45,19 @@ pub trait API {
4445
&self,
4546
conversation_id: &ConversationId,
4647
) -> anyhow::Result<Option<Conversation>>;
48+
49+
/// Gets a variable from the conversation
50+
async fn get_variable(
51+
&self,
52+
conversation_id: &ConversationId,
53+
key: &str,
54+
) -> anyhow::Result<Option<Value>>;
55+
56+
/// Sets a variable in the conversation
57+
async fn set_variable(
58+
&self,
59+
conversation_id: &ConversationId,
60+
key: String,
61+
value: Value,
62+
) -> anyhow::Result<()>;
4763
}
Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::collections::HashMap;
22
use std::sync::Arc;
33

4+
use anyhow::{anyhow, Result};
45
use forge_domain::{
56
AgentId, Context, Conversation, ConversationId, ConversationService, Event, Workflow,
67
};
8+
use serde_json::Value;
79
use tokio::sync::Mutex;
810

911
pub struct ForgeConversationService {
@@ -20,46 +22,80 @@ impl ForgeConversationService {
2022
pub fn new() -> Self {
2123
Self { workflows: Arc::new(Mutex::new(HashMap::new())) }
2224
}
25+
26+
// Helper method for operations requiring mutable access to a conversation
27+
async fn write<F, T>(&self, id: &ConversationId, f: F) -> Result<T>
28+
where
29+
F: FnOnce(&mut Conversation) -> T,
30+
{
31+
let mut guard = self.workflows.lock().await;
32+
let conversation = guard
33+
.get_mut(id)
34+
.ok_or_else(|| anyhow!("Conversation not found"))?;
35+
Ok(f(conversation))
36+
}
37+
38+
// Helper method for operations requiring immutable access to a conversation
39+
async fn read<F, T>(&self, id: &ConversationId, f: F) -> Result<Option<T>>
40+
where
41+
F: FnOnce(&Conversation) -> Option<T>,
42+
{
43+
let guard = self.workflows.lock().await;
44+
Ok(guard.get(id).and_then(f))
45+
}
2346
}
2447

2548
#[async_trait::async_trait]
2649
impl ConversationService for ForgeConversationService {
27-
async fn get(&self, id: &ConversationId) -> anyhow::Result<Option<Conversation>> {
50+
async fn get(&self, id: &ConversationId) -> Result<Option<Conversation>> {
2851
Ok(self.workflows.lock().await.get(id).cloned())
2952
}
3053

31-
async fn create(&self, workflow: Workflow) -> anyhow::Result<ConversationId> {
54+
async fn create(&self, workflow: Workflow) -> Result<ConversationId> {
3255
let id = ConversationId::generate();
3356
let conversation = Conversation::new(id.clone(), workflow);
3457
self.workflows.lock().await.insert(id.clone(), conversation);
3558
Ok(id)
3659
}
3760

38-
async fn inc_turn(&self, id: &ConversationId, agent: &AgentId) -> anyhow::Result<()> {
61+
async fn inc_turn(&self, id: &ConversationId, agent: &AgentId) -> Result<()> {
3962
if let Some(c) = self.workflows.lock().await.get_mut(id) {
4063
c.state.entry(agent.clone()).or_default().turn_count += 1;
4164
}
4265
Ok(())
4366
}
67+
4468
async fn set_context(
4569
&self,
4670
id: &ConversationId,
4771
agent: &AgentId,
4872
context: Context,
49-
) -> anyhow::Result<()> {
73+
) -> Result<()> {
5074
if let Some(c) = self.workflows.lock().await.get_mut(id) {
5175
c.state.entry(agent.clone()).or_default().context = Some(context);
5276
}
5377
Ok(())
5478
}
5579

56-
async fn insert_event(&self, id: &ConversationId, event: Event) -> anyhow::Result<()> {
57-
let mut guard = self.workflows.lock().await;
58-
guard
59-
.get_mut(id)
60-
.ok_or_else(|| anyhow::anyhow!("Conversation not found"))?
61-
.events
62-
.push(event);
63-
Ok(())
80+
async fn insert_event(&self, id: &ConversationId, event: Event) -> Result<()> {
81+
self.write(id, |c| {
82+
c.events.push(event);
83+
})
84+
.await
85+
}
86+
87+
async fn get_variable(&self, id: &ConversationId, key: &str) -> Result<Option<Value>> {
88+
self.read(id, |c| c.get_variable(key).cloned()).await
89+
}
90+
91+
async fn set_variable(&self, id: &ConversationId, key: String, value: Value) -> Result<()> {
92+
self.write(id, |c| {
93+
c.set_variable(key, value);
94+
})
95+
.await
96+
}
97+
98+
async fn delete_variable(&self, id: &ConversationId, key: &str) -> Result<bool> {
99+
self.write(id, |c| c.delete_variable(key)).await
64100
}
65101
}

crates/forge_app/src/template.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::collections::HashMap;
12
use std::sync::Arc;
23

34
use forge_domain::{
@@ -6,6 +7,7 @@ use forge_domain::{
67
use forge_walker::Walker;
78
use handlebars::Handlebars;
89
use rust_embed::Embed;
10+
use serde_json::Value;
911
use tracing::debug;
1012

1113
use crate::{EmbeddingService, EnvironmentService, Infrastructure, VectorIndex};
@@ -74,10 +76,14 @@ impl<F: Infrastructure, T: ToolService> TemplateService for ForgeTemplateService
7476
agent: &Agent,
7577
prompt: &Template<EventContext>,
7678
event: &Event,
79+
variables: &HashMap<String, Value>,
7780
) -> anyhow::Result<String> {
7881
// Create an EventContext with the provided event
7982
let mut event_context = EventContext::new(event.clone());
8083

84+
// Add variables to the context
85+
event_context = event_context.variables(variables.clone());
86+
8187
// Only add suggestions if the agent has suggestions enabled
8288
if agent.suggestions {
8389
// Query the vector index directly for suggestions
@@ -101,6 +107,8 @@ impl<F: Infrastructure, T: ToolService> TemplateService for ForgeTemplateService
101107
event_context = event_context.suggestions(suggestion_strings);
102108
}
103109

110+
debug!(event_context = ?event_context, "Event context");
111+
104112
// Render the template with the event context
105113
Ok(self
106114
.hb

crates/forge_domain/src/conversation.rs

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use anyhow::Result;
44
use derive_more::derive::Display;
55
use derive_setters::Setters;
66
use serde::{Deserialize, Serialize};
7+
use serde_json::Value;
78
use uuid::Uuid;
89

910
use crate::{Agent, AgentId, Context, Error, Event, Workflow};
@@ -35,6 +36,7 @@ pub struct Conversation {
3536
pub state: HashMap<AgentId, AgentState>,
3637
pub events: Vec<Event>,
3738
pub workflow: Workflow,
39+
pub variables: HashMap<String, Value>,
3840
}
3941

4042
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -47,10 +49,11 @@ impl Conversation {
4749
pub fn new(id: ConversationId, workflow: Workflow) -> Self {
4850
Self {
4951
id,
50-
workflow,
5152
archived: false,
5253
state: Default::default(),
5354
events: Default::default(),
55+
variables: workflow.variables.clone().unwrap_or_default(),
56+
workflow,
5457
}
5558
}
5659

@@ -76,4 +79,26 @@ impl Conversation {
7679
pub fn rfind_event(&self, event_name: &str) -> Option<&Event> {
7780
self.events.iter().rfind(|event| event.name == event_name)
7881
}
82+
83+
/// Get a variable value by its key
84+
///
85+
/// Returns None if the variable doesn't exist
86+
pub fn get_variable(&self, key: &str) -> Option<&Value> {
87+
self.variables.get(key)
88+
}
89+
90+
/// Set a variable with the given key and value
91+
///
92+
/// If the key already exists, its value will be updated
93+
pub fn set_variable(&mut self, key: String, value: Value) -> &mut Self {
94+
self.variables.insert(key, value);
95+
self
96+
}
97+
98+
/// Delete a variable by its key
99+
///
100+
/// Returns true if the variable was present and removed, false otherwise
101+
pub fn delete_variable(&mut self, key: &str) -> bool {
102+
self.variables.remove(key).is_some()
103+
}
79104
}

crates/forge_domain/src/event.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
use std::collections::HashMap;
2+
13
use derive_setters::Setters;
24
use schemars::{schema_for, JsonSchema};
35
use serde::{Deserialize, Serialize};
6+
use serde_json::Value;
47

58
use crate::{NamedTool, ToolCallFull, ToolDefinition, ToolName};
69

@@ -17,11 +20,16 @@ pub struct Event {
1720
pub struct EventContext {
1821
event: Event,
1922
suggestions: Vec<String>,
23+
variables: HashMap<String, Value>,
2024
}
2125

2226
impl EventContext {
2327
pub fn new(event: Event) -> Self {
24-
Self { event, suggestions: Default::default() }
28+
Self {
29+
event,
30+
suggestions: Default::default(),
31+
variables: Default::default(),
32+
}
2533
}
2634
}
2735

0 commit comments

Comments
 (0)