Skip to content

Commit 3b83815

Browse files
authored
Merge pull request AI45Lab#84 from AI45Lab/fix/interrupt-keep-history
v4.2.3 — keep conversation on cancel (interrupt no longer drops context) + strip delegation tools from PTC
2 parents 84c57d4 + b0b5cfe commit 3b83815

9 files changed

Lines changed: 76 additions & 9 deletions

File tree

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 = "4.2.2"
3+
version = "4.2.3"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

core/src/agent/execution_state.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::AgentResult;
2-
use crate::llm::{Message, TokenUsage};
2+
use crate::llm::{ContentBlock, Message, TokenUsage};
33
use crate::verification::VerificationReport;
44
use serde_json::Value;
55
use std::time::Instant;
@@ -211,6 +211,34 @@ impl ExecutionLoopState {
211211
}
212212
}
213213

214+
/// Build a result from a turn that was cancelled mid-generation. Keeps the
215+
/// conversation accumulated so far (the user's message above all) so the next
216+
/// turn remembers it. Appends a short assistant marker when the log would
217+
/// otherwise end on a user message, so the next user turn still alternates.
218+
pub(super) fn finish_interrupted(mut self) -> AgentResult {
219+
let ends_on_user = self
220+
.messages
221+
.last()
222+
.map(|m| m.role != "assistant")
223+
.unwrap_or(false);
224+
if ends_on_user {
225+
self.messages.push(Message {
226+
role: "assistant".to_string(),
227+
content: vec![ContentBlock::Text {
228+
text: "(Response interrupted by the user.)".to_string(),
229+
}],
230+
reasoning_content: None,
231+
});
232+
}
233+
AgentResult {
234+
text: String::new(),
235+
messages: self.messages,
236+
usage: self.total_usage,
237+
tool_calls_count: self.tool_calls_count,
238+
verification_reports: self.verification_reports,
239+
}
240+
}
241+
214242
fn tool_signature(tool_name: &str, args: &Value) -> String {
215243
format!(
216244
"{}:{}",
@@ -225,6 +253,27 @@ mod tests {
225253
use super::*;
226254
use serde_json::json;
227255

256+
#[test]
257+
fn finish_interrupted_keeps_user_message_and_alternates() {
258+
// A cancelled turn must keep the user's message (so the next turn
259+
// remembers it) and end on an assistant message (so it still alternates).
260+
let mut state = ExecutionLoopState::new(&[]);
261+
state.messages.push(Message::user("what is the plan?"));
262+
let result = state.finish_interrupted();
263+
assert!(
264+
result
265+
.messages
266+
.iter()
267+
.any(|m| m.role == "user" && m.text().contains("what is the plan?")),
268+
"user message must survive the interrupt"
269+
);
270+
assert_eq!(
271+
result.messages.last().unwrap().role,
272+
"assistant",
273+
"history must end on an assistant message to alternate"
274+
);
275+
}
276+
228277
#[test]
229278
fn duplicate_tool_call_uses_recent_success_and_error_signatures() {
230279
let mut state = ExecutionLoopState::new(&[]);

core/src/agent/loop_runtime.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ impl AgentLoop {
105105
}
106106

107107
loop {
108-
let llm_turn = self
108+
let llm_turn = match self
109109
.execute_llm_turn(
110110
&mut state,
111111
&augmented_system,
@@ -114,7 +114,19 @@ impl AgentLoop {
114114
&event_tx,
115115
cancel_token,
116116
)
117-
.await?;
117+
.await
118+
{
119+
Ok(turn) => turn,
120+
// Interrupted mid-generation (Esc / cancel): keep the conversation
121+
// accumulated so far — above all the user's message — and return it
122+
// as the result so it is committed to history. Without this the
123+
// whole turn is dropped and the agent "forgets" what was just asked
124+
// when the user continues.
125+
Err(_) if cancel_token.is_cancelled() => {
126+
return Ok(state.finish_interrupted());
127+
}
128+
Err(e) => return Err(e),
129+
};
118130
let turn = llm_turn.turn;
119131
let response = llm_turn.response;
120132
let tool_calls = llm_turn.tool_calls;

core/src/tools/program_tool.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,12 @@ fn script_allowed_tools(args: &serde_json::Value, registry: &ToolRegistry) -> Ha
213213
.unwrap_or_else(|| registry.list().into_iter().collect());
214214

215215
allowed.remove("program");
216+
// Delegation tools can't run inside a PTC script: child agents need the
217+
// multi-threaded session runtime, but the script executes on a nested
218+
// single-thread runtime where they can't fan out. Force the model to call
219+
// them directly instead of `ctx.tool("parallel_task", ...)`.
220+
allowed.remove("task");
221+
allowed.remove("parallel_task");
216222
allowed
217223
}
218224

sdk/node/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-node"
3-
version = "4.2.2"
3+
version = "4.2.3"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

sdk/node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@a3s-lab/code",
3-
"version": "4.2.2",
3+
"version": "4.2.3",
44
"description": "A3S Code - Native Node.js bindings for the coding-agent runtime",
55
"main": "index.js",
66
"types": "index.d.ts",

sdk/python-bootstrap/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "a3s-code"
77
# Keep in sync with crates/code core release. The bootstrap loader fetches
88
# the matching native wheel from `https://github.com/AI45Lab/Code/releases/tag/v<version>`
99
# at import time.
10-
version = "4.2.2"
10+
version = "4.2.3"
1111
description = "A3S Code Python SDK — pure-Python bootstrap that fetches the native wheel from GitHub Releases"
1212
readme = "README.md"
1313
license = {text = "MIT"}

sdk/python/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-py"
3-
version = "4.2.2"
3+
version = "4.2.3"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"

sdk/python/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "maturin"
44

55
[project]
66
name = "a3s-code"
7-
version = "4.2.2"
7+
version = "4.2.3"
88
description = "A3S Code - Native Python bindings for the coding-agent runtime"
99
readme = "README.md"
1010
license = {text = "MIT"}

0 commit comments

Comments
 (0)