Skip to content
Merged

V2 #21

Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 24 additions & 2 deletions crates/cowork-core/src/agents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -545,7 +545,7 @@ pub fn create_project_manager_agent(model: Arc<dyn Llm>, iteration_id: String) -
let agent = LlmAgentBuilder::new("project_manager_agent")
.instruction(&instruction)
.model(model)
.tool(Arc::new(PMGotoStageTool))
.tool(Arc::new(PMGotoStageTool::new(iteration_id.clone())))
.tool(Arc::new(PMCreateIterationTool::new(iteration_id.clone())))
.tool(Arc::new(PMRespondTool))
.tool(Arc::new(PMSaveDecisionTool::new(iteration_id.clone())))
Expand Down Expand Up @@ -877,9 +877,31 @@ pub async fn execute_pm_agent_message_streaming(
agent_message = "处理完成".to_string();
}

// Deduplicate actions - keep only the first occurrence of each unique action
let mut seen_stages: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut seen_iterations: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut unique_actions: Vec<PMAgentAction> = Vec::new();

for action in detected_actions {
match &action {
PMAgentAction::GotoStage { target_stage, .. } => {
if !seen_stages.contains(target_stage) {
seen_stages.insert(target_stage.clone());
unique_actions.push(action);
}
}
PMAgentAction::CreateIteration { iteration_id, .. } => {
if !seen_iterations.contains(iteration_id) {
seen_iterations.insert(iteration_id.clone());
unique_actions.push(action);
}
}
}
}

Ok(PMAgentResult {
message: agent_message,
actions: detected_actions,
actions: unique_actions,
parts: all_parts,
})
}
Expand Down
5 changes: 3 additions & 2 deletions crates/cowork-core/src/domain/memory.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

/// Project-level memory (across iterations)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
Expand Down Expand Up @@ -153,7 +154,7 @@ impl Decision {
let now = Utc::now();
let iteration_id = iteration_id.into();
Self {
id: format!("dec-{}-{}", iteration_id, now.timestamp()),
id: format!("dec-{}-{}", iteration_id, Uuid::new_v4()),
title: title.into(),
context: context.into(),
decision: decision.into(),
Expand Down Expand Up @@ -186,7 +187,7 @@ impl Pattern {
let now = Utc::now();
let iteration_id = iteration_id.into();
Self {
id: format!("pat-{}-{}", iteration_id, now.timestamp()),
id: format!("pat-{}-{}", iteration_id, Uuid::new_v4()),
name: name.into(),
description: description.into(),
usage: Vec::new(),
Expand Down
13 changes: 12 additions & 1 deletion crates/cowork-core/src/tools/pm_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,15 @@ use super::get_optional_string_param;
// PM Goto Stage Tool - Restart pipeline from a specific stage
// ============================================================================

pub struct PMGotoStageTool;
pub struct PMGotoStageTool {
current_iteration_id: String,
}

impl PMGotoStageTool {
pub fn new(current_iteration_id: String) -> Self {
Self { current_iteration_id }
}
}

#[async_trait]
impl Tool for PMGotoStageTool {
Expand Down Expand Up @@ -70,6 +78,9 @@ impl Tool for PMGotoStageTool {
}
};

// Set iteration ID for storage operations BEFORE saving feedback
crate::storage::set_iteration_id(self.current_iteration_id.clone());

// Save feedback for the target stage (not "pm_agent")
// This allows the target stage to find its feedback when loading from storage
let feedback = Feedback {
Expand Down
1 change: 1 addition & 0 deletions crates/cowork-gui/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ tokio = { version = "1", features = ["sync", "full"] }
tokio-stream = "0.1"
async-trait = "0.1"
chrono = "0.4"
uuid = { workspace = true }
anyhow = "1"
thiserror = "2"
futures = "0.3"
Expand Down
44 changes: 22 additions & 22 deletions crates/cowork-gui/src-tauri/src/commands/memory.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use cowork_core::persistence::MemoryStore;
use uuid::Uuid;

#[tauri::command]
pub async fn query_memory_index(
Expand Down Expand Up @@ -50,7 +51,8 @@ pub async fn query_memory_index(
for i in &mem.insights {
if matches_stage(Some(&i.stage)) {
results.push(serde_json::json!({
"id": format!("insight-{}", i.created_at.timestamp()),
"id": format!("insight-{}", Uuid::new_v4()),
"_ts": i.created_at.timestamp(),
"title": format!("Insight from {} stage", i.stage),
"summary": i.content.chars().take(200).collect::<String>(),
"category": "decision",
Expand All @@ -68,7 +70,8 @@ pub async fn query_memory_index(
for i in &mem.issues {
if matches_stage(Some(&i.stage)) {
results.push(serde_json::json!({
"id": format!("issue-{}", i.created_at.timestamp()),
"id": format!("issue-{}", Uuid::new_v4()),
"_ts": i.created_at.timestamp(),
"title": format!("Issue from {} stage", i.stage),
"summary": i.content.chars().take(200).collect::<String>(),
"category": "experience",
Expand All @@ -85,7 +88,8 @@ pub async fn query_memory_index(
if matches_category("pattern") || category.is_none() || category.as_deref() == Some("all") {
for l in &mem.learnings {
results.push(serde_json::json!({
"id": format!("learning-{}", l.created_at.timestamp()),
"id": format!("learning-{}", Uuid::new_v4()),
"_ts": l.created_at.timestamp(),
"title": "Learning",
"summary": l.content.chars().take(200).collect::<String>(),
"category": "pattern",
Expand Down Expand Up @@ -153,6 +157,7 @@ pub async fn load_memory_detail(
memory_id: String,
file: Option<String>,
iteration_id: Option<String>,
ts: Option<i64>,
) -> Result<serde_json::Value, String> {
let store = MemoryStore::new();

Expand Down Expand Up @@ -189,14 +194,14 @@ pub async fn load_memory_detail(
}
}

// Check iteration memory
// Check iteration memory using ts parameter for lookup
if let Some(iter_id) = iteration_id {
let iter_mem = store.load_iteration_memory(&iter_id).map_err(|e| e.to_string())?;

if memory_id.starts_with("insight-") {
if let Ok(ts) = memory_id.replace("insight-", "").parse::<i64>() {
if let Some(timestamp) = ts {
if memory_id.starts_with("insight-") {
for i in &iter_mem.insights {
if i.created_at.timestamp() == ts {
if i.created_at.timestamp() == timestamp {
return Ok(serde_json::json!({
"id": memory_id,
"content": i.content,
Expand All @@ -209,12 +214,10 @@ pub async fn load_memory_detail(
}
}
}
}

if memory_id.starts_with("issue-") {
if let Ok(ts) = memory_id.replace("issue-", "").parse::<i64>() {
if memory_id.starts_with("issue-") {
for i in &iter_mem.issues {
if i.created_at.timestamp() == ts {
if i.created_at.timestamp() == timestamp {
return Ok(serde_json::json!({
"id": memory_id,
"content": i.content,
Expand All @@ -227,12 +230,10 @@ pub async fn load_memory_detail(
}
}
}
}

if memory_id.starts_with("learning-") {
if let Ok(ts) = memory_id.replace("learning-", "").parse::<i64>() {
if memory_id.starts_with("learning-") {
for l in &iter_mem.learnings {
if l.created_at.timestamp() == ts {
if l.created_at.timestamp() == timestamp {
return Ok(serde_json::json!({
"id": memory_id,
"content": l.content,
Expand Down Expand Up @@ -296,14 +297,15 @@ pub async fn save_session_memory(
pub async fn promote_to_project_memory(
memory_id: String,
iteration_id: String,
ts: Option<i64>,
) -> Result<serde_json::Value, String> {
let store = MemoryStore::new();
let iter_mem = store.load_iteration_memory(&iteration_id).map_err(|e| e.to_string())?;

if memory_id.starts_with("insight-") {
if let Ok(ts) = memory_id.replace("insight-", "").parse::<i64>() {
if let Some(timestamp) = ts {
if memory_id.starts_with("insight-") {
for i in &iter_mem.insights {
if i.created_at.timestamp() == ts {
if i.created_at.timestamp() == timestamp {
let dec = cowork_core::domain::Decision::new(
"Insight",
"",
Expand All @@ -315,12 +317,10 @@ pub async fn promote_to_project_memory(
}
}
}
}

if memory_id.starts_with("learning-") {
if let Ok(ts) = memory_id.replace("learning-", "").parse::<i64>() {
if memory_id.starts_with("learning-") {
for l in &iter_mem.learnings {
if l.created_at.timestamp() == ts {
if l.created_at.timestamp() == timestamp {
let pat = cowork_core::domain::Pattern::new(
"Learning",
&l.content,
Expand Down
3 changes: 2 additions & 1 deletion crates/cowork-gui/src/components/MemoryPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface Memory {
impact?: string;
tags?: string[];
file?: string;
_ts?: number;
}

interface MemoryDetail {
Expand Down Expand Up @@ -81,7 +82,7 @@ const MemoryPanel: React.FC<MemoryPanelProps> = ({ currentSession, refreshTrigge
setDetailLoading(true);
try {
const detail = await invoke<MemoryDetail>("load_memory_detail", {
memoryId: memory.id, file: memory.file, iterationId: currentSession || null,
memoryId: memory.id, file: memory.file, iterationId: currentSession || null, ts: memory._ts,
});
setMemoryDetail(detail);
} catch (error) {
Expand Down
Loading