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

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
5387bd6
- init database
laststylebender14 Sep 10, 2025
c88d213
- add repository
laststylebender14 Sep 10, 2025
13c7195
- clean up
laststylebender14 Sep 10, 2025
64800b7
- impl repository
laststylebender14 Sep 11, 2025
17df48e
- use new repo in conversation service
laststylebender14 Sep 11, 2025
8a2ba17
- fix conversion to ctx
laststylebender14 Sep 11, 2025
9364233
- migrations
laststylebender14 Sep 11, 2025
b6b4657
- add indexes
laststylebender14 Sep 11, 2025
2df992d
- drop un-necessary method
laststylebender14 Sep 11, 2025
fd70f7a
Merge branch 'main' into feat/persist-conversations
laststylebender14 Sep 11, 2025
94dc50a
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
d17ccde
- list and allow user to resume conversation
laststylebender14 Sep 11, 2025
aec01ab
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
99ba4b6
- show 20 conversations per workspace.
laststylebender14 Sep 11, 2025
c6cd181
- resume the conversation if resume flag is set.
laststylebender14 Sep 11, 2025
3ad8629
- show conversation id if title doesn't exists
laststylebender14 Sep 11, 2025
c48717e
- generate the title for conversation
laststylebender14 Sep 11, 2025
34c2f30
- update the conversation with title
laststylebender14 Sep 11, 2025
66fae9c
Merge branch 'main' into feat/persist-conversations
laststylebender14 Sep 11, 2025
b8c84bc
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
90bd1fe
- clean up
laststylebender14 Sep 11, 2025
1321b5d
- setup the conversation
laststylebender14 Sep 11, 2025
07ce091
- trim down the system prompt for title generator
laststylebender14 Sep 11, 2025
523c08e
- move logic to title generator
laststylebender14 Sep 11, 2025
58b4beb
- add traces
laststylebender14 Sep 11, 2025
c45afa5
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
def492a
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Sep 11, 2025
c0d7a9a
- update banner
laststylebender14 Sep 11, 2025
c80a9a4
- update title
laststylebender14 Sep 11, 2025
4606fc1
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
5492c96
- avoid cloning
laststylebender14 Sep 11, 2025
f8a368a
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
40459b5
- add tests
laststylebender14 Sep 11, 2025
c8e5aaf
- raise an erorr if conversion not found
laststylebender14 Sep 11, 2025
5b16eb2
- use cleaner syntax
laststylebender14 Sep 11, 2025
1f4fe6a
- move impl to separate method
laststylebender14 Sep 11, 2025
0e46586
- add quotes for selected conversation title
laststylebender14 Sep 11, 2025
e16a90d
- add metadata to conversation
laststylebender14 Sep 11, 2025
b5387e8
- use local time and format list correctly.
laststylebender14 Sep 11, 2025
e87189e
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
bdf267d
[autofix.ci] apply automated fixes (attempt 2/3)
autofix-ci[bot] Sep 11, 2025
3001791
- set context only if its not empty
laststylebender14 Sep 11, 2025
0b4e7be
- clean up
laststylebender14 Sep 11, 2025
efed9df
- fmt changes
laststylebender14 Sep 11, 2025
89f319c
refactor: rename conversation service methods for consistency
tusharmath Sep 11, 2025
d588b7a
refactor: rename find_last_active_conversation to last_conversation f…
tusharmath Sep 11, 2025
9f61dbb
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
48ae94a
refactor: rename find_last_active_conversation to last_conversation f…
tusharmath Sep 11, 2025
d6e33d1
Merge branch 'feat/persist-conversations' of https://github.com/antin…
tusharmath Sep 11, 2025
4a0f6c0
refactor: rename ConversationRepositoryInfra to ConversationRepositor…
tusharmath Sep 11, 2025
1cf909e
refactor: rename find_conversations to get_conversations for consistency
tusharmath Sep 11, 2025
b27a8fd
refactor: rename conversation repository methods for consistency
tusharmath Sep 11, 2025
cb7e2d4
- clean up
laststylebender14 Sep 11, 2025
415bce4
- convert to local time for display
laststylebender14 Sep 11, 2025
428f474
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
c5df6a9
- use same created_at for metadata and metrics
laststylebender14 Sep 11, 2025
3b328a2
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 11, 2025
9eca8ee
- doc clean up
laststylebender14 Sep 11, 2025
68b636b
refactor: include Environment in ConversationRepositoryImpl initializ…
tusharmath Sep 12, 2025
c386c6c
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 12, 2025
9cd941c
refactor: remove WorkspaceId from Conversation and related components
tusharmath Sep 12, 2025
9b540b8
test: fix formatting in test assertion for conversation context
tusharmath Sep 12, 2025
8efd536
refactor: extract title formatting in render_conversation_html for reuse
tusharmath Sep 12, 2025
45f26d2
- use 64 as workspace id & separate out index creation
laststylebender14 Sep 12, 2025
23b46f0
- fix tests
laststylebender14 Sep 12, 2025
057641d
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 12, 2025
97b5da7
- add filter by workspace id
laststylebender14 Sep 12, 2025
c6d770f
- always use if not exists clause
laststylebender14 Sep 12, 2025
2f9304f
- execute user prompt after all the init.
laststylebender14 Sep 12, 2025
b45af2d
refactor: rename list command and update related references to conver…
laststylebender14 Sep 12, 2025
53b40da
- show loader on_message
laststylebender14 Sep 12, 2025
0042409
- fix test
laststylebender14 Sep 12, 2025
e100f17
[autofix.ci] apply automated fixes
autofix-ci[bot] Sep 12, 2025
f1a00d3
fix: update FORGE_CMD to include resume option and correct regex for …
tusharmath Sep 12, 2025
d10095e
fix: update transformation logic to support resume and new conversati…
tusharmath Sep 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
280 changes: 231 additions & 49 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions crates/forge_api/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ pub trait API: Sync + Send {
/// Returns the conversation with the given ID
async fn conversation(&self, conversation_id: &ConversationId) -> Result<Option<Conversation>>;

/// Lists all conversations for the active workspace
async fn list_conversations(&self, limit: Option<usize>) -> Result<Vec<Conversation>>;

/// Finds the last active conversation for the current workspace
async fn last_conversation(&self) -> Result<Option<Conversation>>;

/// Compacts the context of the main agent for the given conversation and
/// persists it. Returns metrics about the compaction (original vs.
/// compacted tokens and messages).
Expand Down
12 changes: 12 additions & 0 deletions crates/forge_api/src/forge_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@ impl<A: Services, F: CommandInfra> API for ForgeAPI<A, F> {
self.services.find_conversation(conversation_id).await
}

async fn list_conversations(&self, limit: Option<usize>) -> anyhow::Result<Vec<Conversation>> {
Ok(self
.services
.get_conversations(limit)
.await?
.unwrap_or_default())
}

async fn last_conversation(&self) -> anyhow::Result<Option<Conversation>> {
self.services.last_conversation().await
}

async fn execute_shell_command(
&self,
command: &str,
Expand Down
1 change: 1 addition & 0 deletions crates/forge_app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod orch;
mod orch_spec;
mod retry;
mod services;
mod title_generator;
mod tool_executor;
mod tool_registry;
mod truncation;
Expand Down
25 changes: 23 additions & 2 deletions crates/forge_app/src/orch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use tracing::{debug, info, warn};

use crate::agent::AgentService;
use crate::compact::Compactor;
use crate::title_generator::TitleGenerator;

#[derive(Clone, Setters)]
#[setters(into, strip_option)]
Expand Down Expand Up @@ -355,6 +356,7 @@ impl<S: AgentService> Orchestrator<S> {

// Store tool calls at turn level
let mut turn_has_tool_calls = false;
let title_generator = TitleGenerator::new(self.services.clone());

let tool_context =
ToolCallContext::new(self.conversation.metrics.clone()).sender(self.sender.clone());
Expand Down Expand Up @@ -383,8 +385,15 @@ impl<S: AgentService> Orchestrator<S> {
}),
);

// Prepare compaction task that runs in parallel
// Generate title only if conversation doesn't have any title.
use futures::future::{Either, ready};
let title_generator_future: Either<_, _> = if self.conversation.title.is_none() {
Either::Left(title_generator.generate(&context, &model_id))
} else {
Either::Right(ready(Ok::<Option<String>, anyhow::Error>(None)))
};

// Prepare compaction task that runs in parallel
// Execute both operations in parallel
let (
ChatCompletionMessageFull {
Expand All @@ -396,7 +405,19 @@ impl<S: AgentService> Orchestrator<S> {
finish_reason,
},
compaction_result,
) = tokio::try_join!(main_request, self.check_and_compact(&context))?;
conversation_title,
) = tokio::try_join!(
main_request,
self.check_and_compact(&context),
title_generator_future
)?;

// If conversation_title is generated then update the conversation with it's
// title.
if let Some(title) = conversation_title {
debug!(conversation_id = %self.conversation.id, title, "Title generated for conversation");
self.conversation.title = Some(title);
}

// Apply compaction result if it completed successfully
match compaction_result {
Expand Down
4 changes: 3 additions & 1 deletion crates/forge_app/src/orch_spec/orch_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,9 @@ impl Runner {
});

let services = Arc::new(Runner::new(setup));
let conversation = Conversation::new(ConversationId::generate());
// setup the conversation
let conversation = Conversation::new(ConversationId::generate()).title(setup.title.clone());

let agent = setup.agent.clone();
let event = setup.event.clone();
let system_tools = setup.tools.clone();
Expand Down
2 changes: 2 additions & 0 deletions crates/forge_app/src/orch_spec/orch_setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct TestContext {
pub files: Vec<String>,
pub env: Environment,
pub current_time: DateTime<Local>,
pub title: Option<String>,

// Final output of the test is store in the context
pub output: TestOutput,
Expand Down Expand Up @@ -77,6 +78,7 @@ impl TestContext {
max_search_result_bytes: 200,
stdout_max_line_length: 200, // 5 MB
},
title: Some("test-conversation".into()),
agent: Agent::new(AgentId::new("forge"))
.system_prompt(Template::new("You are Forge"))
.tools(vec![("fs_read").into(), ("fs_write").into()]),
Expand Down
24 changes: 23 additions & 1 deletion crates/forge_app/src/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,17 @@ pub trait ConversationService: Send + Sync {
/// conversation atomically.
async fn modify_conversation<F, T>(&self, id: &ConversationId, f: F) -> anyhow::Result<T>
where
F: FnOnce(&mut Conversation) -> T + Send;
F: FnOnce(&mut Conversation) -> T + Send,
T: Send;

/// Find conversations with optional limit
async fn get_conversations(
&self,
limit: Option<usize>,
) -> anyhow::Result<Option<Vec<Conversation>>>;

/// Find the last active conversation
async fn last_conversation(&self) -> anyhow::Result<Option<Conversation>>;
}

#[async_trait::async_trait]
Expand Down Expand Up @@ -439,9 +449,21 @@ impl<I: Services> ConversationService for I {
async fn modify_conversation<F, T>(&self, id: &ConversationId, f: F) -> anyhow::Result<T>
where
F: FnOnce(&mut Conversation) -> T + Send,
T: Send,
{
self.conversation_service().modify_conversation(id, f).await
}

async fn get_conversations(
&self,
limit: Option<usize>,
) -> anyhow::Result<Option<Vec<Conversation>>> {
self.conversation_service().get_conversations(limit).await
}

async fn last_conversation(&self) -> anyhow::Result<Option<Conversation>> {
self.conversation_service().last_conversation().await
}
}
#[async_trait::async_trait]
impl<I: Services> ProviderService for I {
Expand Down
64 changes: 64 additions & 0 deletions crates/forge_app/src/title_generator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
use std::sync::Arc;

use forge_domain::{
ChatCompletionMessageFull, Context, ContextMessage, ModelId, ResultStreamExt, Role,
extract_tag_content,
};

use crate::agent::AgentService as AS;

/// Service for generating contextually appropriate titles
pub struct TitleGenerator<S> {
/// Shared reference to the agent services used for AI interactions
services: Arc<S>,
}

impl<S: AS> TitleGenerator<S> {
pub fn new(services: Arc<S>) -> Self {
Self { services }
}

pub async fn generate(
&self,
context: &Context,
model_id: &ModelId,
) -> anyhow::Result<Option<String>> {
let first_user_message = context
.messages
.iter()
.find(|message| message.has_role(Role::User));
if let Some(ContextMessage::Text(text_msg)) = first_user_message
&& let Ok(conversation_title) = self
.generate_internal(text_msg.content.as_str(), model_id)
.await
{
return Ok(conversation_title);
}
Ok(None)
}

/// Generate the appropriate title for given user prompt.
async fn generate_internal(
&self,
user_prompt: &str,
model_id: &ModelId,
) -> anyhow::Result<Option<String>> {
let template = self
.services
.render("{{> forge-system-prompt-title-generation.md }}", &())
.await?;
let ctx = Context::default()
.add_message(ContextMessage::system(template))
.add_message(ContextMessage::user(
user_prompt.to_string(),
Some(model_id.clone()),
));

let stream = self.services.chat_agent(model_id, ctx).await?;
let ChatCompletionMessageFull { content, .. } = stream.into_full(false).await?;
if let Some(extracted) = extract_tag_content(&content, "title") {
return Ok(Some(extracted.to_string()));
}
Ok(None)
}
}
36 changes: 26 additions & 10 deletions crates/forge_domain/src/conversation.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use chrono::{DateTime, Utc};
use derive_more::derive::Display;
use derive_setters::Setters;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -32,25 +33,40 @@ impl ConversationId {
}

#[derive(Debug, Setters, Serialize, Deserialize, Clone)]
#[setters(into, strip_option)]
#[setters(into)]
pub struct Conversation {
pub id: ConversationId,

pub title: Option<String>,
pub context: Option<Context>,
pub metrics: Metrics,
pub metadata: MetaData,
}

impl Conversation {
pub fn reset_metric(&mut self) -> &mut Self {
self.metrics = Metrics::new();
self.metrics.start();
self
#[derive(Debug, Setters, Serialize, Deserialize, Clone)]
#[setters(into)]
pub struct MetaData {
pub created_at: DateTime<Utc>,
pub updated_at: Option<DateTime<Utc>>,
}

impl MetaData {
pub fn new(created_at: DateTime<Utc>) -> Self {
Self { created_at, updated_at: None }
}
}

impl Conversation {
pub fn new(id: ConversationId) -> Self {
let mut metrics = Metrics::new();
metrics.start();

Self { id, context: None, metrics }
let created_at = Utc::now();
let metrics = Metrics::new().with_time(created_at);
Self {
id,
metrics,
metadata: MetaData::new(created_at),
title: None,
context: None,
}
}

/// Generates an HTML representation of the conversation
Expand Down
12 changes: 10 additions & 2 deletions crates/forge_domain/src/conversation_html.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ use crate::context::ContextMessage;
use crate::conversation::Conversation;

pub fn render_conversation_html(conversation: &Conversation) -> String {
let c_title = format!(
"Title: {}",
conversation
.title
.clone()
.unwrap_or(conversation.id.to_string())
);
let html = Element::new("html")
.attr("lang", "en")
.append(
Expand All @@ -15,12 +22,13 @@ pub fn render_conversation_html(conversation: &Conversation) -> String {
.attr("name", "viewport")
.attr("content", "width=device-width, initial-scale=1.0"),
)
.append(Element::new("title").text(format!("Conversation: {}", conversation.id)))
.append(Element::new("title").text(&c_title))
.append(Element::new("style").text(include_str!("conversation_style.css"))),
)
.append(
Element::new("body")
.append(Element::new("h1").text("Conversation"))
.append(Element::new("h2").text(&c_title))
// Basic Information Section
.append(
Element::new("div.section")
Expand Down Expand Up @@ -210,7 +218,7 @@ mod tests {
// and returns a non-empty string for an empty conversation
assert!(actual.contains("<html"));
assert!(actual.contains("</html>"));
assert!(actual.contains("Conversation: "));
assert!(actual.contains("Title: "));
assert!(actual.contains("Basic Information"));
assert!(actual.contains("Conversation Context"));
}
Expand Down
31 changes: 27 additions & 4 deletions crates/forge_domain/src/env.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::PathBuf;

use derive_more::Display;
use derive_setters::Setters;
use serde::{Deserialize, Serialize};
use url::Url;
Expand Down Expand Up @@ -56,10 +58,6 @@ pub struct Environment {
}

impl Environment {
pub fn db_path(&self) -> PathBuf {
self.base_path.clone()
}

pub fn log_path(&self) -> PathBuf {
self.base_path.join("logs")
}
Expand Down Expand Up @@ -90,12 +88,37 @@ impl Environment {
pub fn mcp_local_config(&self) -> PathBuf {
self.cwd.join(".mcp.json")
}

pub fn version(&self) -> String {
VERSION.to_string()
}

pub fn app_config(&self) -> PathBuf {
self.base_path.join(".config.json")
}

pub fn database_path(&self) -> PathBuf {
self.base_path.join(".forge.db")
}

pub fn workspace_id(&self) -> WorkspaceId {
let mut hasher = DefaultHasher::default();
self.cwd.hash(&mut hasher);

WorkspaceId(hasher.finish())
}
}

#[derive(Clone, Copy, Display)]
pub struct WorkspaceId(u64);
impl WorkspaceId {
pub fn new(id: u64) -> Self {
WorkspaceId(id)
}

pub fn id(&self) -> u64 {
self.0
}
}

#[cfg(test)]
Expand Down
5 changes: 3 additions & 2 deletions crates/forge_domain/src/session_metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ impl Metrics {
}

/// Starts tracking session metrics
pub fn start(&mut self) {
self.started_at = Some(Utc::now());
pub fn with_time(mut self, started_at: DateTime<Utc>) -> Self {
self.started_at = Some(started_at);
self
}

pub fn record_file_operation(&mut self, path: String, lines_added: u64, lines_removed: u64) {
Expand Down
3 changes: 3 additions & 0 deletions crates/forge_infra/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ forge_walker.workspace = true
reqwest-eventsource.workspace = true
globset.workspace = true
futures.workspace = true
diesel = { version= "2.2.12", features = ["sqlite", "r2d2", "chrono"] }
diesel_migrations = "2.2.0"
chrono = { version = "0.4", features = ["serde"] }

[dev-dependencies]
serial_test = "3.2"
Loading
Loading