Skip to content

Commit 3eb3737

Browse files
RoyLinRoyLin
authored andcommitted
chore: release v1.3.5
## New Features ### Skill Tool Mechanism (#8) - Implement callable Skill tool with permission isolation - Skills can be invoked as `Skill("skill-name")` with temporary permission grants - Enforces skill-based access patterns - agents cannot bypass skills - RAII pattern ensures automatic permission revocation after execution - Tested with Kimi K2.5 model ### Session Cancellation API - Add `session.cancel()` method to interrupt ongoing operations - Cooperative cancellation at LLM streaming chunk boundaries - Exposed in Python and Node.js SDKs - Returns partial results when cancelled ## Documentation - Add Skill Tool documentation (English and Chinese) - Add cancellation API documentation (English and Chinese) - Add comprehensive examples and test results ## Version Updates - Core: 1.3.4 → 1.3.5 - Python SDK: 1.3.4 → 1.3.5 - Node.js SDK: 1.3.4 → 1.3.5 Closes #8
1 parent d600724 commit 3eb3737

19 files changed

Lines changed: 827 additions & 25 deletions

core/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-core"
3-
version = "1.3.4"
3+
version = "1.3.5"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

core/src/agent.rs

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1448,7 +1448,14 @@ impl AgentLoop {
14481448
.unwrap_or_default();
14491449

14501450
let result = self
1451-
.execute_loop_inner(&messages, "", &effective_prompt, session_id, event_tx, token)
1451+
.execute_loop_inner(
1452+
&messages,
1453+
"",
1454+
&effective_prompt,
1455+
session_id,
1456+
event_tx,
1457+
token,
1458+
)
14521459
.await;
14531460

14541461
match &result {
@@ -1773,7 +1780,12 @@ impl AgentLoop {
17731780
loop {
17741781
attempt += 1;
17751782
let result = self
1776-
.call_llm(&messages, augmented_system.as_deref(), &event_tx, cancel_token)
1783+
.call_llm(
1784+
&messages,
1785+
augmented_system.as_deref(),
1786+
&event_tx,
1787+
cancel_token,
1788+
)
17771789
.await;
17781790
match result {
17791791
Ok(r) => {
@@ -2586,7 +2598,9 @@ impl AgentLoop {
25862598
if let Some(queue) = command_queue {
25872599
agent = agent.with_queue(queue);
25882600
}
2589-
agent.execute_with_session(&history, &prompt, None, Some(tx), Some(&token_clone)).await
2601+
agent
2602+
.execute_with_session(&history, &prompt, None, Some(tx), Some(&token_clone))
2603+
.await
25902604
});
25912605

25922606
Ok((rx, handle, cancel_token))
@@ -2764,7 +2778,13 @@ impl AgentLoop {
27642778
);
27652779

27662780
match self
2767-
.execute_loop(&current_history, &step_prompt, None, event_tx.clone(), &tokio_util::sync::CancellationToken::new())
2781+
.execute_loop(
2782+
&current_history,
2783+
&step_prompt,
2784+
None,
2785+
event_tx.clone(),
2786+
&tokio_util::sync::CancellationToken::new(),
2787+
)
27682788
.await
27692789
{
27702790
Ok(result) => {
@@ -2852,7 +2872,13 @@ impl AgentLoop {
28522872
],
28532873
);
28542874
let result = agent_clone
2855-
.execute_loop(&base_history, &prompt, None, tx, &tokio_util::sync::CancellationToken::new())
2875+
.execute_loop(
2876+
&base_history,
2877+
&prompt,
2878+
None,
2879+
tx,
2880+
&tokio_util::sync::CancellationToken::new(),
2881+
)
28562882
.await;
28572883
(step_clone.id, sn, result)
28582884
});
@@ -3300,7 +3326,7 @@ mod tests {
33003326
let config = AgentConfig::default();
33013327

33023328
let agent = AgentLoop::new(mock_client, tool_executor, test_tool_context(), config);
3303-
let (mut rx, handle) = agent.execute_streaming(&[], "Hi").await.unwrap();
3329+
let (mut rx, handle, _cancel_token) = agent.execute_streaming(&[], "Hi").await.unwrap();
33043330

33053331
// Collect events
33063332
let mut events = Vec::new();
@@ -4309,7 +4335,7 @@ mod tests {
43094335

43104336
// Execute with session ID
43114337
let result = agent
4312-
.execute_with_session(&[], "User prompt", Some("sess-123"), None)
4338+
.execute_with_session(&[], "User prompt", Some("sess-123"), None, None)
43134339
.await
43144340
.unwrap();
43154341

core/src/agent_api.rs

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,6 +1315,7 @@ impl Agent {
13151315
cron_rx: tokio::sync::Mutex::new(cron_rx),
13161316
is_processing_cron: AtomicBool::new(false),
13171317
cron_started: AtomicBool::new(false),
1318+
cancel_token: Arc::new(tokio::sync::Mutex::new(None)),
13181319
})
13191320
}
13201321
}
@@ -1371,6 +1372,9 @@ pub struct AgentSession {
13711372
/// The ticker is started lazily on the first `send()` call so that
13721373
/// `tokio::spawn` is always called from within an async runtime context.
13731374
cron_started: AtomicBool,
1375+
/// Cancellation token for the current operation (send/stream).
1376+
/// Stored so that cancel() can abort ongoing LLM calls.
1377+
cancel_token: Arc<tokio::sync::Mutex<Option<tokio_util::sync::CancellationToken>>>,
13741378
}
13751379

13761380
impl std::fmt::Debug for AgentSession {
@@ -1515,7 +1519,19 @@ impl AgentSession {
15151519
None => read_or_recover(&self.history).clone(),
15161520
};
15171521

1518-
let result = agent_loop.execute(&effective_history, prompt, None).await?;
1522+
let cancel_token = tokio_util::sync::CancellationToken::new();
1523+
*self.cancel_token.lock().await = Some(cancel_token.clone());
1524+
let result = agent_loop
1525+
.execute_with_session(
1526+
&effective_history,
1527+
prompt,
1528+
Some(&self.session_id),
1529+
None,
1530+
Some(&cancel_token),
1531+
)
1532+
.await;
1533+
*self.cancel_token.lock().await = None;
1534+
let result = result?;
15191535

15201536
// Auto-accumulate: only update internal history when no custom
15211537
// history was provided.
@@ -1670,14 +1686,50 @@ impl AgentSession {
16701686
None => read_or_recover(&self.history).clone(),
16711687
};
16721688
let prompt = prompt.to_string();
1689+
let session_id = self.session_id.clone();
1690+
1691+
let cancel_token = tokio_util::sync::CancellationToken::new();
1692+
*self.cancel_token.lock().await = Some(cancel_token.clone());
1693+
let token_clone = cancel_token.clone();
16731694

16741695
let handle = tokio::spawn(async move {
16751696
let _ = agent_loop
1676-
.execute(&effective_history, &prompt, Some(tx))
1697+
.execute_with_session(
1698+
&effective_history,
1699+
&prompt,
1700+
Some(&session_id),
1701+
Some(tx),
1702+
Some(&token_clone),
1703+
)
16771704
.await;
16781705
});
16791706

1680-
Ok((rx, handle))
1707+
// Wrap the handle to clear the cancel token when done
1708+
let cancel_token_ref = self.cancel_token.clone();
1709+
let wrapped_handle = tokio::spawn(async move {
1710+
let _ = handle.await;
1711+
*cancel_token_ref.lock().await = None;
1712+
});
1713+
1714+
Ok((rx, wrapped_handle))
1715+
}
1716+
1717+
/// Cancel the current ongoing operation (send/stream).
1718+
///
1719+
/// If an operation is in progress, this will trigger cancellation of the LLM streaming
1720+
/// and tool execution. The operation will terminate as soon as possible.
1721+
///
1722+
/// Returns `true` if an operation was cancelled, `false` if no operation was in progress.
1723+
pub async fn cancel(&self) -> bool {
1724+
let token = self.cancel_token.lock().await.clone();
1725+
if let Some(token) = token {
1726+
token.cancel();
1727+
tracing::info!(session_id = %self.session_id, "Cancelled ongoing operation");
1728+
true
1729+
} else {
1730+
tracing::debug!(session_id = %self.session_id, "No ongoing operation to cancel");
1731+
false
1732+
}
16811733
}
16821734

16831735
/// Return a snapshot of the session's conversation history.

core/src/session/manager.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ pub struct SessionManager {
3131
/// LLM configurations for sessions (stored separately for persistence)
3232
pub(crate) llm_configs: Arc<RwLock<HashMap<String, LlmConfigData>>>,
3333
/// Ongoing operations (session_id -> CancellationToken)
34-
pub(crate) ongoing_operations: Arc<RwLock<HashMap<String, tokio_util::sync::CancellationToken>>>,
34+
pub(crate) ongoing_operations:
35+
Arc<RwLock<HashMap<String, tokio_util::sync::CancellationToken>>>,
3536
/// Skill registry for runtime skill management
3637
pub(crate) skill_registry: Arc<RwLock<Option<Arc<SkillRegistry>>>>,
3738
/// Shared memory store for agent long-term memory.
@@ -877,7 +878,8 @@ impl SessionManager {
877878
.with_tool_metrics(tool_metrics);
878879

879880
// Execute with streaming
880-
let (rx, handle, cancel_token) = agent.execute_streaming(&history, &effective_prompt).await?;
881+
let (rx, handle, cancel_token) =
882+
agent.execute_streaming(&history, &effective_prompt).await?;
881883

882884
// Store the cancellation token for cancellation support
883885
let cancel_token_clone = cancel_token.clone();

core/src/tools/skill.rs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use crate::agent::{AgentConfig, AgentLoop};
1717
use crate::llm::LlmClient;
1818
use crate::permissions::{PermissionDecision, PermissionPolicy, PermissionRule};
1919
use crate::skills::{Skill, SkillRegistry};
20-
use crate::tools::{Tool, ToolContext, ToolOutput, ToolExecutor};
20+
use crate::tools::{Tool, ToolContext, ToolExecutor, ToolOutput};
2121
use anyhow::{anyhow, Result};
2222
use async_trait::async_trait;
2323
use serde::{Deserialize, Serialize};
@@ -114,7 +114,8 @@ impl Tool for SkillTool {
114114
let args: SkillArgs = serde_json::from_value(args.clone())?;
115115

116116
// Get the skill
117-
let skill = self.skill_registry
117+
let skill = self
118+
.skill_registry
118119
.get(&args.skill_name)
119120
.ok_or_else(|| anyhow!("Skill '{}' not found", args.skill_name))?;
120121

@@ -135,9 +136,7 @@ impl Tool for SkillTool {
135136
// Build the system prompt with skill content
136137
skill_config.prompt_slots.role = Some(format!(
137138
"You are executing the '{}' skill.\n\n{}\n\n{}",
138-
skill.name,
139-
skill.description,
140-
skill.content
139+
skill.name, skill.description, skill.content
141140
));
142141

143142
// Create agent loop with skill permissions
@@ -149,9 +148,9 @@ impl Tool for SkillTool {
149148
);
150149

151150
// Execute the skill with the prompt
152-
let prompt = args.prompt.unwrap_or_else(|| {
153-
format!("Execute the '{}' skill", skill.name)
154-
});
151+
let prompt = args
152+
.prompt
153+
.unwrap_or_else(|| format!("Execute the '{}' skill", skill.name));
155154

156155
// Execute the agent loop with skill permissions
157156
let result = agent_loop.execute(&[], &prompt, None).await?;

0 commit comments

Comments
 (0)