Skip to content

Commit 3f09dfb

Browse files
RoyLinRoyLin
authored andcommitted
feat(ptc): run host tool calls on the outer runtime so delegation fans out
The program/PTC VM runs on a nested single-thread runtime, so parallel_task's child agents couldn't actually run in parallel — delegation in a script was broken (and was blocked in 4.2.3). Now execute_host_tool_json captures the OUTER multi-threaded session runtime Handle and spawns the tool there, so ctx.tool('parallel_task', …) fans out child agents in parallel. Re-allow task/parallel_task in script_allowed_tools. Bumps to 4.2.6.
1 parent 6947904 commit 3f09dfb

11 files changed

Lines changed: 67 additions & 40 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

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

core/src/tools/program_tool.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -213,12 +213,9 @@ 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");
216+
// `task`/`parallel_task` ARE allowed in PTC scripts now: host tool calls run
217+
// on the outer multi-threaded runtime (see execute_host_tool_json), so
218+
// `ctx.tool("parallel_task", …)` fans out child agents in parallel.
222219
allowed
223220
}
224221

@@ -277,6 +274,9 @@ async fn run_quickjs_script(
277274
.max_output_bytes
278275
.unwrap_or(DEFAULT_SCRIPT_MAX_OUTPUT_BYTES);
279276
let executable_source = script_source_with_host_entrypoint(source)?;
277+
// Captured on the outer multi-threaded runtime (we're async here, before the
278+
// VM's nested single-thread runtime is built) so host tool calls fan out.
279+
let outer = tokio::runtime::Handle::current();
280280
let state = Arc::new(Mutex::new(ScriptVmState {
281281
registry,
282282
ctx,
@@ -285,6 +285,7 @@ async fn run_quickjs_script(
285285
max_output_bytes,
286286
tool_calls: 0,
287287
records: Vec::new(),
288+
outer,
288289
}));
289290

290291
let vm_state = Arc::clone(&state);
@@ -407,6 +408,10 @@ struct ScriptVmState {
407408
max_output_bytes: usize,
408409
tool_calls: usize,
409410
records: Vec<ScriptCallRecord>,
411+
/// Handle to the OUTER multi-threaded session runtime. The script VM runs on
412+
/// a nested single-thread runtime; host tool calls are dispatched here so
413+
/// delegation tools (`parallel_task`/`task`) can actually fan out children.
414+
outer: tokio::runtime::Handle,
410415
}
411416

412417
fn embedded_script_bootstrap(inputs_json: &str) -> String {
@@ -447,7 +452,7 @@ async fn execute_host_tool_json(
447452
let args = serde_json::from_str(&args_json).map_err(|err| {
448453
JsError::new_from_js_message("string", "object", format!("invalid tool args JSON: {err}"))
449454
})?;
450-
let (registry, ctx, max_output_bytes) = {
455+
let (registry, ctx, max_output_bytes, outer) = {
451456
let mut script = state.lock().await;
452457
if !script.allowed_tools.contains(&tool) {
453458
return Err(JsError::new_from_js_message(
@@ -468,12 +473,18 @@ async fn execute_host_tool_json(
468473
Arc::clone(&script.registry),
469474
script.ctx.clone(),
470475
script.max_output_bytes,
476+
script.outer.clone(),
471477
)
472478
};
473479

474-
let result = registry
475-
.execute_with_context(&tool, &args, &ctx)
480+
// Run the tool on the OUTER multi-threaded runtime (not this nested
481+
// single-thread VM runtime) so delegation tools can spawn child agents that
482+
// actually run in parallel — `ctx.tool("parallel_task", …)` now fans out.
483+
let tool_for_spawn = tool.clone();
484+
let result = outer
485+
.spawn(async move { registry.execute_with_context(&tool_for_spawn, &args, &ctx).await })
476486
.await
487+
.map_err(|err| JsError::new_from_js_message("tool", "spawn", err.to_string()))?
477488
.map_err(|err| JsError::new_from_js_message("tool", "result", err.to_string()))?;
478489
let mut output = result.output;
479490
if output.len() > max_output_bytes {
@@ -676,6 +687,22 @@ mod tests {
676687
assert!(!allowed.contains("program"));
677688
}
678689

690+
#[test]
691+
fn program_tool_allows_delegation_tools_in_scripts() {
692+
// Delegation tools are allowed in PTC scripts again (host tool calls run
693+
// on the outer multi-threaded runtime, so they fan out). Only `program`
694+
// stays stripped (no nested PTC recursion).
695+
let registry = ToolRegistry::new(PathBuf::from("/tmp"));
696+
let args = serde_json::json!({
697+
"allowed_tools": ["parallel_task", "task", "program", "echo"]
698+
});
699+
let allowed = script_allowed_tools(&args, &registry);
700+
assert!(allowed.contains("parallel_task"));
701+
assert!(allowed.contains("task"));
702+
assert!(allowed.contains("echo"));
703+
assert!(!allowed.contains("program"));
704+
}
705+
679706
#[tokio::test]
680707
async fn program_tool_source_uses_default_all_registered_tools() {
681708
let registry = Arc::new(ToolRegistry::new(PathBuf::from("/tmp")));

sdk/node/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-node"
3-
version = "4.2.5"
3+
version = "4.2.6"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"
@@ -11,7 +11,7 @@ description = "A3S Code Node.js bindings - Native addon via napi-rs"
1111
crate-type = ["cdylib"]
1212

1313
[dependencies]
14-
a3s-code-core = { version = "4.2.5", path = "../../core", features = ["ahp", "s3", "serve"] }
14+
a3s-code-core = { version = "4.2.6", path = "../../core", features = ["ahp", "s3", "serve"] }
1515
napi = { version = "2", features = ["async", "napi6", "serde-json"] }
1616
napi-derive = "2"
1717
tokio = { version = "1.35", features = ["full"] }

sdk/node/examples/package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/node/package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

sdk/node/package.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@a3s-lab/code",
3-
"version": "4.2.5",
3+
"version": "4.2.6",
44
"description": "A3S Code - Native Node.js bindings for the coding-agent runtime",
55
"main": "index.js",
66
"types": "index.d.ts",
@@ -43,11 +43,11 @@
4343
"test:helpers": "node test-helpers.mjs"
4444
},
4545
"optionalDependencies": {
46-
"@a3s-lab/code-darwin-arm64": "4.2.5",
47-
"@a3s-lab/code-linux-x64-gnu": "4.2.5",
48-
"@a3s-lab/code-linux-x64-musl": "4.2.5",
49-
"@a3s-lab/code-linux-arm64-gnu": "4.2.5",
50-
"@a3s-lab/code-linux-arm64-musl": "4.2.5",
51-
"@a3s-lab/code-win32-x64-msvc": "4.2.5"
46+
"@a3s-lab/code-darwin-arm64": "4.2.6",
47+
"@a3s-lab/code-linux-x64-gnu": "4.2.6",
48+
"@a3s-lab/code-linux-x64-musl": "4.2.6",
49+
"@a3s-lab/code-linux-arm64-gnu": "4.2.6",
50+
"@a3s-lab/code-linux-arm64-musl": "4.2.6",
51+
"@a3s-lab/code-win32-x64-msvc": "4.2.6"
5252
}
5353
}

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.5"
10+
version = "4.2.6"
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-bootstrap/src/a3s_code/_bootstrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131

3232
# Version is the bootstrap's own version, which equals the matching native
3333
# wheel version on GH Releases. Bumped by the release workflow.
34-
__version__ = "4.2.5"
34+
__version__ = "4.2.6"
3535

3636
_DEFAULT_BASE_URL = "https://github.com/AI45Lab/Code/releases/download"
3737
_REQUEST_TIMEOUT_S = 120

sdk/python/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-py"
3-
version = "4.2.5"
3+
version = "4.2.6"
44
edition = "2021"
55
authors = ["A3S Lab Team"]
66
license = "MIT"
@@ -12,7 +12,7 @@ name = "a3s_code"
1212
crate-type = ["cdylib"]
1313

1414
[dependencies]
15-
a3s-code-core = { version = "4.2.5", path = "../../core", features = ["ahp", "s3", "serve"] }
15+
a3s-code-core = { version = "4.2.6", path = "../../core", features = ["ahp", "s3", "serve"] }
1616
pyo3 = "0.23"
1717
tokio = { version = "1.35", features = ["full"] }
1818
serde_json = "1.0"

0 commit comments

Comments
 (0)