Skip to content

Commit a7ecba2

Browse files
authored
Merge pull request #8 from sopaco/v2
V2
2 parents c1945c7 + 27b84b6 commit a7ecba2

File tree

12 files changed

+811
-247
lines changed

12 files changed

+811
-247
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/cowork-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ adk-tool = "0.2.1"
1818
# Async runtime
1919
tokio = { workspace = true }
2020
async-trait = "0.1"
21+
async-stream = "0.3"
2122
futures = { workspace = true }
2223

2324
# Error handling

crates/cowork-core/src/agents/hitl.rs

Lines changed: 65 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,83 @@
11
use adk_core::{Agent, Event, AdkError, InvocationContext};
22
use async_trait::async_trait;
33
use std::sync::Arc;
4+
use std::sync::atomic::{AtomicU32, Ordering};
45
use std::pin::Pin;
56
use std::task::{Context as TaskContext, Poll};
67
use futures::{Stream, Future};
78
use dialoguer::{Select, Input, theme::ColorfulTheme};
89

10+
type AgentOutput = Pin<Box<dyn Stream<Item = Result<Event, AdkError>> + Send>>;
11+
912
pub struct ResilientAgent {
1013
inner: Arc<dyn Agent>,
1114
subs: Vec<Arc<dyn Agent>>,
15+
retry_count: Arc<AtomicU32>,
1216
}
1317

1418
impl ResilientAgent {
19+
const MAX_RETRY_ATTEMPTS: u32 = 3;
20+
1521
pub fn new(inner: Arc<dyn Agent>) -> Self {
1622
Self {
1723
inner: inner.clone(),
1824
subs: vec![inner],
25+
retry_count: Arc::new(AtomicU32::new(0)),
1926
}
2027
}
28+
29+
// Helper for immediate errors (recursion in async fn)
30+
async fn handle_error(&self, context: Arc<dyn InvocationContext>, e: AdkError) -> Result<AgentOutput, AdkError> {
31+
let current_retry = self.retry_count.fetch_add(1, Ordering::SeqCst);
32+
33+
// Check if max retry attempts reached
34+
if current_retry >= Self::MAX_RETRY_ATTEMPTS {
35+
println!("\n❌ Maximum retry attempts ({}) reached.", Self::MAX_RETRY_ATTEMPTS);
36+
self.retry_count.store(0, Ordering::SeqCst); // Reset counter
37+
return Err(AdkError::Tool(format!(
38+
"Agent '{}' failed after {} retry attempts",
39+
self.name(),
40+
Self::MAX_RETRY_ATTEMPTS
41+
)));
42+
}
43+
44+
println!("\n⚠️ Agent '{}' encountered error: {}", self.name(), e);
45+
println!("The agent loop limit has been exceeded.");
46+
println!("Retry attempt {}/{}", current_retry + 1, Self::MAX_RETRY_ATTEMPTS);
47+
48+
let selections = &["Retry (reset counter)", "Provide Guidance & Retry", "Abort"];
49+
let selection = Select::with_theme(&ColorfulTheme::default())
50+
.with_prompt("How would you like to proceed?")
51+
.default(0)
52+
.items(&selections[..])
53+
.interact()
54+
.unwrap_or(2);
55+
56+
match selection {
57+
0 => {
58+
println!("🔄 Retrying agent execution...");
59+
return self.run(context).await;
60+
},
61+
1 => {
62+
let input: String = Input::with_theme(&ColorfulTheme::default())
63+
.with_prompt("Please provide guidance for the agent")
64+
.interact_text()
65+
.unwrap_or_default();
66+
67+
if !input.is_empty() {
68+
println!("(Note: User guidance provided: '{}' - but context injection is not implemented. Retrying anyway.)", input);
69+
}
70+
println!("🔄 Retrying with new guidance...");
71+
return self.run(context).await;
72+
},
73+
_ => {
74+
self.retry_count.store(0, Ordering::SeqCst); // Reset counter on abort
75+
return Err(e);
76+
}
77+
}
78+
}
2179
}
2280

23-
type AgentOutput = Pin<Box<dyn Stream<Item = Result<Event, AdkError>> + Send>>;
24-
2581
#[async_trait]
2682
impl Agent for ResilientAgent {
2783
fn name(&self) -> &str {
@@ -40,11 +96,14 @@ impl Agent for ResilientAgent {
4096
// Initial run
4197
match self.inner.run(context.clone()).await {
4298
Ok(stream) => {
99+
// Success - reset retry counter
100+
self.retry_count.store(0, Ordering::SeqCst);
43101
// Wrap the stream to handle errors during iteration
44102
Ok(Box::pin(ResilientStream::new(
45103
self.inner.clone(),
46104
context,
47-
stream
105+
stream,
106+
self.retry_count.clone(),
48107
)))
49108
},
50109
Err(e) => {
@@ -61,42 +120,6 @@ impl Agent for ResilientAgent {
61120
}
62121
}
63122

64-
impl ResilientAgent {
65-
// Helper for immediate errors (recursion in async fn)
66-
async fn handle_error(&self, context: Arc<dyn InvocationContext>, e: AdkError) -> Result<AgentOutput, AdkError> {
67-
println!("\n⚠️ Agent '{}' encountered error: {}", self.name(), e);
68-
println!("The agent loop limit has been exceeded.");
69-
70-
let selections = &["Retry (reset counter)", "Provide Guidance & Retry", "Abort"];
71-
let selection = Select::with_theme(&ColorfulTheme::default())
72-
.with_prompt("How would you like to proceed?")
73-
.default(0)
74-
.items(&selections[..])
75-
.interact()
76-
.unwrap_or(2);
77-
78-
match selection {
79-
0 => {
80-
println!("🔄 Retrying agent execution...");
81-
return self.run(context).await;
82-
},
83-
1 => {
84-
let input: String = Input::with_theme(&ColorfulTheme::default())
85-
.with_prompt("Please provide guidance for the agent")
86-
.interact_text()
87-
.unwrap_or_default();
88-
89-
if !input.is_empty() {
90-
println!("(Note: User guidance provided: '{}' - but context injection is not implemented. Retrying anyway.)", input);
91-
}
92-
println!("🔄 Retrying with new guidance...");
93-
return self.run(context).await;
94-
},
95-
_ => return Err(e),
96-
}
97-
}
98-
}
99-
100123
// ============================================================================
101124
// ResilientStream Implementation
102125
// ============================================================================
@@ -111,20 +134,23 @@ struct ResilientStream {
111134
context: Arc<dyn InvocationContext>,
112135
state: StreamState,
113136
agent_name: String, // Cached for logging
137+
retry_count: Arc<AtomicU32>,
114138
}
115139

116140
impl ResilientStream {
117141
fn new(
118142
inner_agent: Arc<dyn Agent>,
119143
context: Arc<dyn InvocationContext>,
120144
stream: AgentOutput,
145+
retry_count: Arc<AtomicU32>,
121146
) -> Self {
122147
let agent_name = inner_agent.name().to_string();
123148
Self {
124149
inner_agent,
125150
context,
126151
state: StreamState::Streaming(stream),
127152
agent_name,
153+
retry_count,
128154
}
129155
}
130156

crates/cowork-core/src/agents/mod.rs

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
// Agents module - Agent builders using adk-rust
22
//
3-
// IMPORTANT: This file solves a CRITICAL bug where SequentialAgent stops after
4-
// the first LoopAgent completes.
3+
// IMPORTANT: This file uses StageExecutor instead of SequentialAgent to allow
4+
// LoopAgents to use ExitLoopTool without affecting other stages.
55
//
6-
// PROBLEM: When a sub-agent in LoopAgent calls exit_loop(), it terminates the
7-
// ENTIRE SequentialAgent, not just the LoopAgent. This is adk-rust's design.
8-
//
9-
// SOLUTION: Remove exit_loop tools and use max_iterations=1 to let LoopAgent
10-
// complete naturally, allowing SequentialAgent to continue to next agent.
6+
// SOLUTION: StageExecutor isolates each stage's escalate flag, so when a
7+
// sub-agent in LoopAgent calls exit_loop(), it only terminates that specific
8+
// LoopAgent, not the entire workflow.
119

1210
use crate::instructions::*;
1311
use crate::tools::*;
1412
use adk_agent::{LlmAgentBuilder, LoopAgent};
1513
use adk_core::{Llm, IncludeContents};
14+
use adk_tool::ExitLoopTool;
1615
use anyhow::Result;
1716
use std::sync::Arc;
1817

@@ -60,15 +59,14 @@ pub fn create_prd_loop(model: Arc<dyn Llm>, session_id: &str) -> Result<Arc<dyn
6059
.model(model)
6160
.tool(Arc::new(ReadFileTool))
6261
.tool(Arc::new(GetRequirementsTool::new(session.clone())))
63-
.tool(Arc::new(ProvideFeedbackTool::new(session.clone())))
64-
.include_contents(IncludeContents::None)
62+
.tool(Arc::new(ProvideFeedbackTool::new(session.clone()))) // Write feedback to file when checks fail
63+
.tool(Arc::new(ExitLoopTool::new())) // Exit loop when checks pass
64+
.tool(Arc::new(RequestHumanReviewTool::new(session.clone())))
65+
.include_contents(IncludeContents::Default)
6566
.build()?;
6667

67-
let mut loop_agent = LoopAgent::new(
68-
"prd_loop",
69-
vec![Arc::new(prd_actor), Arc::new(prd_critic)],
70-
);
71-
loop_agent = loop_agent.with_max_iterations(3); // Allow up to 3 attempts for Actor to fix issues
68+
let mut loop_agent = LoopAgent::new("prd_loop", vec![Arc::new(prd_actor), Arc::new(prd_critic)]);
69+
loop_agent = loop_agent.with_max_iterations(3); // Loop will complete naturally after 3 iterations
7270

7371
Ok(Arc::new(ResilientAgent::new(Arc::new(loop_agent))))
7472
}
@@ -99,11 +97,13 @@ pub fn create_design_loop(model: Arc<dyn Llm>, session_id: &str) -> Result<Arc<d
9997
.tool(Arc::new(GetDesignTool::new(session.clone())))
10098
.tool(Arc::new(CheckFeatureCoverageTool::new(session.clone())))
10199
.tool(Arc::new(ProvideFeedbackTool::new(session.clone())))
102-
.include_contents(IncludeContents::None)
100+
.tool(Arc::new(ExitLoopTool::new()))
101+
.tool(Arc::new(RequestHumanReviewTool::new(session.clone())))
102+
.include_contents(IncludeContents::Default)
103103
.build()?;
104104

105105
let mut loop_agent = LoopAgent::new("design_loop", vec![Arc::new(design_actor), Arc::new(design_critic)]);
106-
loop_agent = loop_agent.with_max_iterations(3); // Allow up to 3 attempts
106+
loop_agent = loop_agent.with_max_iterations(3); // Loop will complete naturally after 3 iterations
107107

108108
Ok(Arc::new(ResilientAgent::new(Arc::new(loop_agent))))
109109
}
@@ -137,11 +137,13 @@ pub fn create_plan_loop(model: Arc<dyn Llm>, session_id: &str) -> Result<Arc<dyn
137137
.tool(Arc::new(GetDesignTool::new(session.clone())))
138138
.tool(Arc::new(CheckTaskDependenciesTool::new(session.clone())))
139139
.tool(Arc::new(ProvideFeedbackTool::new(session.clone())))
140-
.include_contents(IncludeContents::None)
140+
.tool(Arc::new(ExitLoopTool::new()))
141+
.tool(Arc::new(RequestHumanReviewTool::new(session.clone())))
142+
.include_contents(IncludeContents::Default)
141143
.build()?;
142144

143145
let mut loop_agent = LoopAgent::new("plan_loop", vec![Arc::new(plan_actor), Arc::new(plan_critic)]);
144-
loop_agent = loop_agent.with_max_iterations(3); // Allow up to 3 attempts
146+
loop_agent = loop_agent.with_max_iterations(3); // Loop will complete naturally after 3 iterations
145147

146148
Ok(Arc::new(ResilientAgent::new(Arc::new(loop_agent))))
147149
}
@@ -157,9 +159,10 @@ pub fn create_coding_loop(model: Arc<dyn Llm>, session_id: &str) -> Result<Arc<d
157159
.instruction(CODING_ACTOR_INSTRUCTION)
158160
.model(model.clone())
159161
.tool(Arc::new(GetPlanTool::new(session.clone())))
162+
.tool(Arc::new(ReviewWithFeedbackContentTool)) // Read Critic feedback
160163
.tool(Arc::new(UpdateTaskStatusTool::new(session.clone())))
161164
.tool(Arc::new(UpdateFeatureStatusTool::new(session.clone())))
162-
// Task management tools - NEW
165+
// Task management tools
163166
.tool(Arc::new(CreateTaskTool::new(session.clone())))
164167
.tool(Arc::new(UpdateTaskTool::new(session.clone())))
165168
.tool(Arc::new(DeleteTaskTool::new(session.clone())))
@@ -180,13 +183,13 @@ pub fn create_coding_loop(model: Arc<dyn Llm>, session_id: &str) -> Result<Arc<d
180183
.tool(Arc::new(ListFilesTool))
181184
.tool(Arc::new(RunCommandTool))
182185
.tool(Arc::new(ProvideFeedbackTool::new(session.clone())))
183-
// Replanning request - NEW
186+
.tool(Arc::new(ExitLoopTool::new()))
184187
.tool(Arc::new(RequestReplanningTool::new(session.clone())))
185-
.include_contents(IncludeContents::None)
188+
.include_contents(IncludeContents::Default)
186189
.build()?;
187190

188191
let mut loop_agent = LoopAgent::new("coding_loop", vec![Arc::new(coding_actor), Arc::new(coding_critic)]);
189-
loop_agent = loop_agent.with_max_iterations(5);
192+
loop_agent = loop_agent.with_max_iterations(5); // Loop will complete naturally after 5 iterations
190193

191194
Ok(Arc::new(ResilientAgent::new(Arc::new(loop_agent))))
192195
}

crates/cowork-core/src/instructions/coding.rs

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,30 @@ During implementation, you may discover that the plan needs adjustments. You now
5454
- **Stay focused**: Don't over-plan; focus on what's needed for current implementation
5555
- **Maintain consistency**: Keep task IDs, dependencies, and status aligned
5656
57+
## Handle Critic Feedback (IF IN ITERATION 2+):
58+
**IMPORTANT**: In iterations after the first one, check the conversation history for Critic's feedback:
59+
60+
1. **Look at the previous messages** - Critic's feedback is in the conversation history
61+
2. **If Critic said code is incomplete or has issues**:
62+
- Read exactly what issues were mentioned
63+
- Complete any missing tasks
64+
- Fix any code quality issues
65+
- Simplify over-engineered code if needed
66+
3. **If Critic requested replanning**: Acknowledge (human will review)
67+
4. **If no issues mentioned** - Critic approved and you're done!
68+
69+
**Remember**: You can SEE Critic's messages in the conversation. Read them and take action.
70+
5771
# Tools
5872
- get_plan()
5973
- read_file(path)
6074
- write_file(path, content)
6175
- list_files(path)
6276
- update_task_status(task_id, status)
6377
- update_feature_status(feature_id, status)
64-
- create_task(title, description, feature_id, component_id, files_to_create, dependencies, acceptance_criteria) ← NEW
65-
- update_task(task_id, reason, title?, description?, dependencies?, files_to_create?, acceptance_criteria?) ← NEW
66-
- delete_task(task_id, reason) ← NEW
78+
- create_task(title, description, feature_id, component_id, files_to_create, dependencies, acceptance_criteria)
79+
- update_task(task_id, reason, title?, description?, dependencies?, files_to_create?, acceptance_criteria?)
80+
- delete_task(task_id, reason)
6781
6882
# Code Style - SIMPLE APPROACH
6983
```
@@ -152,13 +166,19 @@ The request will be recorded and reviewed by the Check Agent, which can trigger
152166
# Exit Condition
153167
- When ALL tasks show status="completed" AND key files exist, approve immediately and stop
154168
169+
## Decision Flow:
170+
1. If major architectural issues found → call `request_replanning()`
171+
2. If minor issues or incomplete work → call `provide_feedback(feedback_type="incomplete", severity="medium", details="<what's pending>", suggested_fix="<complete these tasks>")`
172+
3. If everything is complete and good → call `exit_loop()`
173+
155174
# Tools
156175
- get_plan()
157176
- read_file(path)
158177
- list_files(path) ← Use this to verify files exist!
159178
- run_command(command) ← Only for simple checks, not for tests/lint
160-
- provide_feedback(feedback_type, severity, details, suggested_fix)
161-
- request_replanning(issue_type, severity, details, affected_features, suggested_approach) ← NEW
179+
- provide_feedback(feedback_type, severity, details, suggested_fix) - Report incomplete work (Actor will read this)
180+
- exit_loop() - **MUST CALL** when all tasks completed and code is good (exits this loop only, other stages continue)
181+
- request_replanning(issue_type, severity, details, affected_features, suggested_approach) - Call for major issues
162182
163183
# Example - All Tasks Done
164184
```
@@ -168,8 +188,20 @@ The request will be recorded and reviewed by the Check Agent, which can trigger
168188
4. # Returns: ["index.html", "style.css", "script.js"] - files exist!
169189
5. read_file("index.html")
170190
6. # Looks good, simple HTML structure
171-
7. "✅ All 12 tasks completed. Files created: index.html, style.css, script.js. Code is simple and clear. Project ready!"
172-
8. STOP (no more iterations)
191+
7. exit_loop() - Exit the coding loop, workflow continues to check stage
192+
```
193+
194+
# Example - Issues Found
195+
```
196+
1. get_plan()
197+
2. # Returns: 8 completed, 4 pending
198+
3. provide_feedback(
199+
feedback_type="incomplete",
200+
severity="medium",
201+
details="4 tasks still pending: TASK-009 (Create login form), TASK-010 (Add validation), TASK-011 (Style buttons), TASK-012 (Add error messages)",
202+
suggested_fix="Complete the 4 pending tasks before marking work as done"
203+
)
204+
# Actor will read this feedback file and complete the tasks
173205
```
174206
175207
# Example - Tasks Complete but Files Missing

0 commit comments

Comments
 (0)