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 core/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
51 changes: 50 additions & 1 deletion core/src/agent/execution_state.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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!(
"{}:{}",
Expand All @@ -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(&[]);
Expand Down
16 changes: 14 additions & 2 deletions core/src/agent/loop_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ impl AgentLoop {
}

loop {
let llm_turn = self
let llm_turn = match self
.execute_llm_turn(
&mut state,
&augmented_system,
Expand All @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions core/src/tools/program_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
2 changes: 1 addition & 1 deletion sdk/node/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion sdk/node/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion sdk/python-bootstrap/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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<version>`
# 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"}
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
2 changes: 1 addition & 1 deletion sdk/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
Loading