Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["core", "cli"]
resolver = "2"

[workspace.package]
version = "0.0.3"
version = "0.0.4"
edition = "2021"
authors = ["Blushyes"]
license = "MIT OR Apache-2.0"
Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,11 @@ Create a `coro.json` file:
"base_url": "https://api.deepseek.com",
"api_key": "your-api-key",
"model": "deepseek-chat",
"max_token": 8192
"params": {
"max_tokens": 131072,
"temperature": 0.7,
"top_p": 0.9
}
}
```

Expand Down Expand Up @@ -152,12 +156,12 @@ coro --config custom.json "Analyze this codebase"
<details>
<summary><strong>🤖 Phase 3: Intelligence & Performance</strong></summary>

| Priority | Status | Feature | Description |
| --------- | ------ | ---------------------------------- | ----------------------------------------------------------------------------- |
| 🟡 Medium | 📋 | **Multi-model & Auto Routing** | Auto model selection by task type, failure auto-downgrade & retry strategies |
| 🟡 Medium | 📋 | **Context Optimization & Caching** | File summary caching, duplicate reference deduplication, token budget control |
| 🟡 Medium | ✅ | **Token Compression** | Intelligent context compression, selective token reduction, adaptive context windows |
| 🔵 Low | 📋 | **MCP Extension Ecosystem** | Common provider presets & templates, one-click start/stop external tools |
| Priority | Status | Feature | Description |
| --------- | ------ | ---------------------------------- | ------------------------------------------------------------------------------------ |
| 🟡 Medium | 📋 | **Multi-model & Auto Routing** | Auto model selection by task type, failure auto-downgrade & retry strategies |
| 🟡 Medium | 📋 | **Context Optimization & Caching** | File summary caching, duplicate reference deduplication, token budget control |
| 🟡 Medium | ✅ | **Token Compression** | Intelligent context compression, selective token reduction, adaptive context windows |
| 🔵 Low | 📋 | **MCP Extension Ecosystem** | Common provider presets & templates, one-click start/stop external tools |

</details>

Expand Down
16 changes: 10 additions & 6 deletions README_zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,11 @@ export CORO_MODEL="custom-model"
"base_url": "https://api.deepseek.com",
"api_key": "your-api-key",
"model": "deepseek-chat",
"max_token": 8192
"params": {
"max_tokens": 131072,
"temperature": 0.7,
"top_p": 0.9
}
}
```

Expand Down Expand Up @@ -138,12 +142,12 @@ export CORO_MODEL="custom-model"
<details>
<summary><strong>🤖 第三阶段:智能化与性能</strong></summary>

| 优先级 | 状态 | 功能特性 | 描述 |
| ------ | ---- | -------------------- | ---------------------------------------------- |
| 🟡 中 | 📋 | **多模型与自动路由** | 按任务类型自动选择模型,失败自动降级与重试策略 |
| 🟡 中 | 📋 | **上下文优化与缓存** | 文件摘要缓存、重复引用去重、Token 预算控制 |
| 优先级 | 状态 | 功能特性 | 描述 |
| ------ | ---- | -------------------- | --------------------------------------------------- |
| 🟡 中 | 📋 | **多模型与自动路由** | 按任务类型自动选择模型,失败自动降级与重试策略 |
| 🟡 中 | 📋 | **上下文优化与缓存** | 文件摘要缓存、重复引用去重、Token 预算控制 |
| 🟡 中 | ✅ | **Token 压缩** | 智能上下文压缩、选择性 Token 减少、自适应上下文窗口 |
| 🔵 低 | 📋 | **MCP 扩展生态** | 常用 Provider 预设与模板,一键启停外部工具 |
| 🔵 低 | 📋 | **MCP 扩展生态** | 常用 Provider 预设与模板,一键启停外部工具 |

</details>

Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use tracing::info;
pub async fn test_command() -> Result<()> {
info!("Testing basic functionality");

println!("🧪 Running Trae Agent Tests");
println!("🧪 Running Coro Code Tests");

// Test 1: Configuration loading
println!("📋 Test 1: Configuration system");
Expand Down
2 changes: 1 addition & 1 deletion cli/src/commands/tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pub async fn tools_command() -> Result<()> {

println!("💡 Use these tools in your tasks to accomplish complex workflows!");
println!(
"📋 All tools follow the exact same specifications as the Python version of Trae Agent."
"📋 All tools follow the exact same specifications as the Python version of Coro Code."
);

Ok(())
Expand Down
20 changes: 20 additions & 0 deletions cli/src/interactive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,26 @@ Manages agent task execution with UI integration and token tracking.

### `terminal_output.rs` - Terminal Output Abstraction

#### Cancellation (Interrupt)

- UI sends `AppMessage::AgentExecutionInterrupted` (e.g., on Ctrl+C)
- Task executor forwards this to core by calling the agent's global AbortController
- Core emits `AgentEvent::ExecutionInterrupted { context, reason }` and stops gracefully

Programmatic example (non-UI):

```rust
let (abort_controller, _reg) = coro_core::agent::AbortController::new();
let mut agent = coro_core::agent::AgentCore::new_with_llm_config(
agent_config,
llm_config,
Box::new(coro_core::output::events::NullOutput),
Some(abort_controller.clone()),
).await?;
// In another task/thread:
abort_controller.cancel();
```

Provides terminal output utilities and formatting functions that work with the AgentOutput system.

**Key Traits:**
Expand Down
19 changes: 15 additions & 4 deletions cli/src/interactive/task_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ pub async fn execute_agent_task_with_context(
llm_config,
token_tracking_output,
tool_registry,
None,
)
.await?;

Expand All @@ -147,11 +148,15 @@ pub async fn execute_agent_task_with_context(
// Execute task with conversation continuation
let task_future = agent_ref.execute_task_with_context(&task, &project_path);

// Listen for interruption signals
let interrupt_future = async {
// Listen for interruption signals - cancel the persistent agent when triggered
let agent_for_cancel = agent.clone();
let interrupt_future = async move {
loop {
match interrupt_receiver.recv().await {
Ok(AppMessage::AgentExecutionInterrupted { .. }) => {
if let Some(a) = agent_for_cancel.lock().await.as_ref() {
a.cancel();
}
tracing::warn!("Task interrupted by user");
return Err(anyhow::anyhow!("Task interrupted by user"));
}
Expand Down Expand Up @@ -223,23 +228,29 @@ pub async fn execute_agent_task(
ui_sender.clone(),
)));

// Create an AbortController for this single-run agent
let (abort_controller, _reg) = coro_core::agent::AbortController::new();

// Create and execute agent task
let mut agent = coro_core::agent::AgentCore::new_with_output_and_registry(
agent_config,
llm_config,
token_tracking_output,
tool_registry,
Some(abort_controller.clone()),
)
.await?;

// Execute task with interruption support
let task_future = agent.execute_task_with_context(&task, &project_path);

// Listen for interruption signals
let interrupt_future = async {
// Listen for interruption signals - cancel via AbortController when triggered
let abort_controller_for_cancel = abort_controller.clone();
let interrupt_future = async move {
loop {
match interrupt_receiver.recv().await {
Ok(AppMessage::AgentExecutionInterrupted { .. }) => {
abort_controller_for_cancel.cancel();
tracing::warn!("Task interrupted by user");
return Err(anyhow::anyhow!("Task interrupted by user"));
}
Expand Down
9 changes: 9 additions & 0 deletions cli/src/output/cli_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ impl AgentOutput for CliOutputHandler {
}
}

AgentEvent::ExecutionInterrupted { context, reason } => {
warn!("Task interrupted: {}", reason);
debug!(
"Interrupted after {} steps, duration: {:.2}s",
context.current_step,
context.execution_time.as_secs_f64()
);
}

AgentEvent::StepStarted { step_info } => {
debug!("Step {}: {}", step_info.step_number, step_info.task);
}
Expand Down
3 changes: 2 additions & 1 deletion core/examples/custom_system_prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {

// Create agent with custom system prompt
let mut agent =
AgentCore::new_with_llm_config(agent_config, llm_config, Box::new(NullOutput)).await?;
AgentCore::new_with_llm_config(agent_config, llm_config, Box::new(NullOutput), None)
.await?;

// Verify the system prompt is set
if let Some(prompt) = agent.get_configured_system_prompt() {
Expand Down
147 changes: 147 additions & 0 deletions core/src/agent/abort.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
//! Abort (cancellation) controller for AgentCore

#[derive(Clone)]
pub struct AbortController {
tx: tokio::sync::watch::Sender<bool>,
}

#[derive(Clone)]
pub struct AbortRegistration {
rx: tokio::sync::watch::Receiver<bool>,
}

impl AbortController {
/// Create a new controller and its registration
pub fn new() -> (Self, AbortRegistration) {
let (tx, rx) = tokio::sync::watch::channel(false);
(Self { tx: tx.clone() }, AbortRegistration { rx })
}

/// Subscribe to this controller to obtain a registration
pub fn subscribe(&self) -> AbortRegistration {
AbortRegistration {
rx: self.tx.subscribe(),
}
}

/// Trigger cancellation (idempotent)
pub fn cancel(&self) {
let _ = self.tx.send(true);
}
}

impl AbortRegistration {
/// Check current cancellation state without blocking
pub fn is_cancelled(&self) -> bool {
*self.rx.borrow()
}

/// Wait until cancellation is triggered (returns immediately if already cancelled)
pub async fn cancelled(&mut self) {
if !*self.rx.borrow() {
let _ = self.rx.changed().await;
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
use tokio::time::{sleep, timeout};

#[tokio::test]
async fn test_abort_controller_basic() {
let (controller, mut registration) = AbortController::new();

// Initially not cancelled
assert!(!registration.is_cancelled());

// Cancel the controller
controller.cancel();

// Should be cancelled now
assert!(registration.is_cancelled());

// cancelled() should return immediately
let result = timeout(Duration::from_millis(100), registration.cancelled()).await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_abort_controller_multiple_subscribers() {
let (controller, mut reg1) = AbortController::new();
let mut reg2 = controller.subscribe();

// Both should not be cancelled initially
assert!(!reg1.is_cancelled());
assert!(!reg2.is_cancelled());

// Cancel the controller
controller.cancel();

// Both should be cancelled
assert!(reg1.is_cancelled());
assert!(reg2.is_cancelled());

// Both should return immediately from cancelled()
let result1 = timeout(Duration::from_millis(100), reg1.cancelled()).await;
let result2 = timeout(Duration::from_millis(100), reg2.cancelled()).await;
assert!(result1.is_ok());
assert!(result2.is_ok());
}

#[tokio::test]
async fn test_abort_controller_task_interruption() {
// Case 1: No cancel → should complete other branch quickly
let (_controller, mut registration) = AbortController::new();
let result = tokio::select! {
_ = registration.cancelled() => "interrupted",
_ = sleep(Duration::from_millis(20)) => "completed",
};
assert_eq!(result, "completed");

// Case 2: Cancel after short delay → should interrupt
let (controller2, mut registration2) = AbortController::new();
let controller2_clone = controller2.clone();
tokio::spawn(async move {
sleep(Duration::from_millis(30)).await;
controller2_clone.cancel();
});
let result2 = tokio::select! {
_ = registration2.cancelled() => "interrupted",
_ = sleep(Duration::from_secs(2)) => "completed",
};
assert_eq!(result2, "interrupted");
}

#[tokio::test]
async fn test_abort_controller_idempotent_cancel() {
let (controller, mut registration) = AbortController::new();

// Cancel multiple times
controller.cancel();
controller.cancel();
controller.cancel();

// Should still work correctly
assert!(registration.is_cancelled());

let result = timeout(Duration::from_millis(100), registration.cancelled()).await;
assert!(result.is_ok());
}

#[tokio::test]
async fn test_abort_controller_clone() {
let (controller, mut registration) = AbortController::new();
let controller_clone = controller.clone();

// Clone should work the same way
controller_clone.cancel();

assert!(registration.is_cancelled());

let result = timeout(Duration::from_millis(100), registration.cancelled()).await;
assert!(result.is_ok());
}
}
17 changes: 16 additions & 1 deletion core/src/agent/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ impl Default for AgentConfig {
pub struct AgentBuilder {
llm_config: crate::config::ResolvedLlmConfig,
agent_config: AgentConfig,
abort_controller: Option<super::AbortController>,
}

impl AgentBuilder {
Expand All @@ -68,6 +69,7 @@ impl AgentBuilder {
Self {
llm_config,
agent_config: AgentConfig::default(),
abort_controller: None,
}
}

Expand Down Expand Up @@ -101,12 +103,24 @@ impl AgentBuilder {
self
}

/// Inject a global AbortController for cancellation support
pub fn with_cancellation(mut self, controller: super::AbortController) -> Self {
self.abort_controller = Some(controller);
self
}

/// Build the agent with the given output handler
pub async fn build_with_output(
self,
output: Box<dyn crate::output::AgentOutput>,
) -> crate::error::Result<super::AgentCore> {
super::AgentCore::new_with_llm_config(self.agent_config, self.llm_config, output).await
super::AgentCore::new_with_llm_config(
self.agent_config,
self.llm_config,
output,
self.abort_controller,
)
.await
}

/// Build the agent with custom output handler and tool registry
Expand All @@ -120,6 +134,7 @@ impl AgentBuilder {
self.llm_config,
output,
tool_registry,
self.abort_controller,
)
.await
}
Expand Down
Loading
Loading