From d96de178660848babf1fea8c0ae01741dd201ab1 Mon Sep 17 00:00:00 2001 From: RoyLin Date: Fri, 26 Jun 2026 09:31:14 +0800 Subject: [PATCH 1/2] fix(agent): keep conversation on cancel; strip delegation tools from PTC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Interrupting a turn (cancel) dropped it entirely: the internal history only updates on a COMPLETED stream, so the next turn forgot what was just asked. Now a cancelled llm-turn returns Ok(state.finish_interrupted()) — the accumulated conversation (the user's message above all) is committed, ending on an assistant marker so the next user turn still alternates. Also strip task/parallel_task from the PTC script_allowed_tools: child agents can't fan out on the program tool's single-thread runtime, so force direct calls. Bumps to 4.2.3. --- core/Cargo.toml | 2 +- core/src/agent/execution_state.rs | 51 ++++++++++++++++++++++++++++++- core/src/agent/loop_runtime.rs | 16 ++++++++-- core/src/tools/program_tool.rs | 6 ++++ 4 files changed, 71 insertions(+), 4 deletions(-) diff --git a/core/Cargo.toml b/core/Cargo.toml index e9da14a..70890c2 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "a3s-code-core" -version = "4.2.2" +version = "4.2.3" edition = "2021" authors = ["A3S Lab Team"] license = "MIT" diff --git a/core/src/agent/execution_state.rs b/core/src/agent/execution_state.rs index d71f23e..6d50455 100644 --- a/core/src/agent/execution_state.rs +++ b/core/src/agent/execution_state.rs @@ -1,5 +1,5 @@ use super::AgentResult; -use crate::llm::{Message, TokenUsage}; +use crate::llm::{ContentBlock, Message, TokenUsage}; use crate::verification::VerificationReport; use serde_json::Value; use std::time::Instant; @@ -211,6 +211,34 @@ impl ExecutionLoopState { } } + /// Build a result from a turn that was cancelled mid-generation. Keeps the + /// conversation accumulated so far (the user's message above all) so the next + /// turn remembers it. Appends a short assistant marker when the log would + /// otherwise end on a user message, so the next user turn still alternates. + pub(super) fn finish_interrupted(mut self) -> AgentResult { + let ends_on_user = self + .messages + .last() + .map(|m| m.role != "assistant") + .unwrap_or(false); + if ends_on_user { + self.messages.push(Message { + role: "assistant".to_string(), + content: vec![ContentBlock::Text { + text: "(Response interrupted by the user.)".to_string(), + }], + reasoning_content: None, + }); + } + AgentResult { + text: String::new(), + messages: self.messages, + usage: self.total_usage, + tool_calls_count: self.tool_calls_count, + verification_reports: self.verification_reports, + } + } + fn tool_signature(tool_name: &str, args: &Value) -> String { format!( "{}:{}", @@ -225,6 +253,27 @@ mod tests { use super::*; use serde_json::json; + #[test] + fn finish_interrupted_keeps_user_message_and_alternates() { + // A cancelled turn must keep the user's message (so the next turn + // remembers it) and end on an assistant message (so it still alternates). + let mut state = ExecutionLoopState::new(&[]); + state.messages.push(Message::user("what is the plan?")); + let result = state.finish_interrupted(); + assert!( + result + .messages + .iter() + .any(|m| m.role == "user" && m.text().contains("what is the plan?")), + "user message must survive the interrupt" + ); + assert_eq!( + result.messages.last().unwrap().role, + "assistant", + "history must end on an assistant message to alternate" + ); + } + #[test] fn duplicate_tool_call_uses_recent_success_and_error_signatures() { let mut state = ExecutionLoopState::new(&[]); diff --git a/core/src/agent/loop_runtime.rs b/core/src/agent/loop_runtime.rs index 5e48fee..22ee770 100644 --- a/core/src/agent/loop_runtime.rs +++ b/core/src/agent/loop_runtime.rs @@ -105,7 +105,7 @@ impl AgentLoop { } loop { - let llm_turn = self + let llm_turn = match self .execute_llm_turn( &mut state, &augmented_system, @@ -114,7 +114,19 @@ impl AgentLoop { &event_tx, cancel_token, ) - .await?; + .await + { + Ok(turn) => turn, + // Interrupted mid-generation (Esc / cancel): keep the conversation + // accumulated so far — above all the user's message — and return it + // as the result so it is committed to history. Without this the + // whole turn is dropped and the agent "forgets" what was just asked + // when the user continues. + Err(_) if cancel_token.is_cancelled() => { + return Ok(state.finish_interrupted()); + } + Err(e) => return Err(e), + }; let turn = llm_turn.turn; let response = llm_turn.response; let tool_calls = llm_turn.tool_calls; diff --git a/core/src/tools/program_tool.rs b/core/src/tools/program_tool.rs index eeeeac7..562c0a7 100644 --- a/core/src/tools/program_tool.rs +++ b/core/src/tools/program_tool.rs @@ -213,6 +213,12 @@ fn script_allowed_tools(args: &serde_json::Value, registry: &ToolRegistry) -> Ha .unwrap_or_else(|| registry.list().into_iter().collect()); allowed.remove("program"); + // Delegation tools can't run inside a PTC script: child agents need the + // multi-threaded session runtime, but the script executes on a nested + // single-thread runtime where they can't fan out. Force the model to call + // them directly instead of `ctx.tool("parallel_task", ...)`. + allowed.remove("task"); + allowed.remove("parallel_task"); allowed } From b0b5cfeb8b63791e56bc0810c2c6ec5072a8679f Mon Sep 17 00:00:00 2001 From: RoyLin Date: Fri, 26 Jun 2026 09:37:18 +0800 Subject: [PATCH 2/2] chore(release): bump SDK package versions to 4.2.3 --- sdk/node/Cargo.toml | 2 +- sdk/node/package.json | 2 +- sdk/python-bootstrap/pyproject.toml | 2 +- sdk/python/Cargo.toml | 2 +- sdk/python/pyproject.toml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sdk/node/Cargo.toml b/sdk/node/Cargo.toml index 4cac6a4..6506b24 100644 --- a/sdk/node/Cargo.toml +++ b/sdk/node/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "a3s-code-node" -version = "4.2.2" +version = "4.2.3" edition = "2021" authors = ["A3S Lab Team"] license = "MIT" diff --git a/sdk/node/package.json b/sdk/node/package.json index f77d058..a7a9101 100644 --- a/sdk/node/package.json +++ b/sdk/node/package.json @@ -1,6 +1,6 @@ { "name": "@a3s-lab/code", - "version": "4.2.2", + "version": "4.2.3", "description": "A3S Code - Native Node.js bindings for the coding-agent runtime", "main": "index.js", "types": "index.d.ts", diff --git a/sdk/python-bootstrap/pyproject.toml b/sdk/python-bootstrap/pyproject.toml index f605ebc..e6e71a0 100644 --- a/sdk/python-bootstrap/pyproject.toml +++ b/sdk/python-bootstrap/pyproject.toml @@ -7,7 +7,7 @@ name = "a3s-code" # Keep in sync with crates/code core release. The bootstrap loader fetches # the matching native wheel from `https://github.com/AI45Lab/Code/releases/tag/v` # at import time. -version = "4.2.2" +version = "4.2.3" description = "A3S Code Python SDK — pure-Python bootstrap that fetches the native wheel from GitHub Releases" readme = "README.md" license = {text = "MIT"} diff --git a/sdk/python/Cargo.toml b/sdk/python/Cargo.toml index e49825c..da8a6e7 100644 --- a/sdk/python/Cargo.toml +++ b/sdk/python/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "a3s-code-py" -version = "4.2.2" +version = "4.2.3" edition = "2021" authors = ["A3S Lab Team"] license = "MIT" diff --git a/sdk/python/pyproject.toml b/sdk/python/pyproject.toml index 7e2e412..103fb75 100644 --- a/sdk/python/pyproject.toml +++ b/sdk/python/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "a3s-code" -version = "4.2.2" +version = "4.2.3" description = "A3S Code - Native Python bindings for the coding-agent runtime" readme = "README.md" license = {text = "MIT"}