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
3 changes: 2 additions & 1 deletion .github/workflows/publish-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ name: Publish Node SDK

on:
workflow_call:
workflow_dispatch:

jobs:
build:
if: startsWith(github.ref, 'refs/tags/v')
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -90,6 +90,7 @@ jobs:

publish:
name: Publish to npm
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs: build
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-python-bootstrap.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ name: Publish Python Bootstrap

on:
workflow_call:
workflow_dispatch:

jobs:
publish:
name: Publish Python Bootstrap to PyPI
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
Expand Down
8 changes: 2 additions & 6 deletions .github/workflows/publish-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,6 @@ on:
required: false
type: string
description: "Specific target to build (e.g., x86_64-unknown-linux-gnu). If empty, builds all targets."
workflow_dispatch:
inputs:
target:
required: false
type: string
description: "Specific target to build (e.g., x86_64-unknown-linux-gnu). If empty, builds all targets."

permissions:
contents: write
Expand All @@ -28,6 +22,7 @@ jobs:
# These are uploaded to GitHub Releases, NOT to PyPI
# ─────────────────────────────────────────────────────────────────
build:
if: startsWith(github.ref, 'refs/tags/v')
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -101,6 +96,7 @@ jobs:
# ─────────────────────────────────────────────────────────────────
publish:
name: Publish Python SDK
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs: build
steps:
Expand Down
31 changes: 26 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
tags:
- "v*"
workflow_dispatch:

permissions:
contents: write
Expand All @@ -21,6 +20,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Setup workspace context
run: bash .github/setup-workspace.sh
Expand All @@ -39,14 +40,34 @@ jobs:
- name: Clippy
run: cargo clippy --workspace --lib --bins -- -D warnings

- name: Check version alignment
run: bash check-version.sh
- name: Validate release tag and version alignment
shell: bash
run: |
set -euo pipefail
case "$GITHUB_REF" in
refs/tags/v*) ;;
*) echo "Release publishing requires a v* tag" >&2; exit 1 ;;
esac
VERSION="${GITHUB_REF_NAME#v}"
test -n "$VERSION"
bash check-version.sh "$VERSION"

- name: Check public API compatibility with v5.2.8
run: |
cargo install cargo-semver-checks --version 0.48.0 --locked
bash scripts/check_semver.sh 5.2.8

- name: Check SDK protocol and API alignment
run: |
node scripts/generate_event_protocol_artifacts.mjs --check
node scripts/sdk_api_alignment_check.mjs

- name: Check Node SDK
run: cargo check --manifest-path sdk/node/Cargo.toml

- name: Check Python SDK
run: cargo check --manifest-path sdk/python/Cargo.toml

- name: Default tests
run: cargo test --workspace

Expand Down Expand Up @@ -122,7 +143,7 @@ jobs:
# ───────────────────────────────────────────────
publish-node:
name: Node SDK
needs: [ci, ci-windows]
needs: [publish-crate]
uses: ./.github/workflows/publish-node.yml
secrets: inherit

Expand All @@ -133,7 +154,7 @@ jobs:
# ───────────────────────────────────────────────
publish-python:
name: Python SDK (native wheels → GH Release)
needs: [ci, ci-windows]
needs: [publish-crate]
uses: ./.github/workflows/publish-python.yml
secrets: inherit

Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [5.3.0] - 2026-07-15

### Fixed

- Made language-server initialization and post-initialization settling
cooperatively cancellable so session shutdown does not wait for a cold
semantic runtime.
- Stabilized the first navigation query for each saved document revision after
language-server initialization, preventing cold empty or partial reference
results without treating a legitimate empty result as an error.

## [5.2.8] - 2026-07-16

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ context providers, policies, stores, and active child work. The runtime can:
- resume complete snapshots from a `SessionStore`;
- atomically replace an idle persisted session with new runtime options;
- list and close live sessions owned by an `Agent`;
- cancel and settle an active operation before safely reusing its session;
- add, replace, list, and remove lifecycle-owned Skills while restoring an
exactly shadowed registration on removal;
- add and remove session-local MCP servers without disturbing inherited or
Expand Down Expand Up @@ -302,9 +303,11 @@ npm install --global typescript typescript-language-server
Queries use saved files only. Public lines and characters are zero-based, and
characters count UTF-16 code units. Document results include the saved-content
revision and hash plus a stale flag when the file changes while a query is in
flight. The semantic tools return metadata and workspace-relative locations;
agents continue to use `read`, `grep`, `edit`, and `patch` for source text and
mutations.
flight. The first navigation request for a saved revision includes a bounded,
cancellable stabilization pass so a protocol-ready but still-indexing server
does not leak a cold partial result. The semantic tools return metadata and
workspace-relative locations; agents continue to use `read`, `grep`, `edit`,
and `patch` for source text and mutations.

See the [Code Intelligence design](manual/CODE_INTELLIGENCE_DESIGN.md) for the
capability boundary, shared runtime architecture, lifecycle, and verification
Expand Down
2 changes: 1 addition & 1 deletion check-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"

scripts/check_release_versions.sh
scripts/check_release_versions.sh "${1:-}"
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 = "5.2.8"
version = "5.3.0"
edition = "2021"
authors = ["A3S Lab Team"]
license = "MIT"
Expand Down
5 changes: 4 additions & 1 deletion core/src/agent/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4088,11 +4088,14 @@ async function run(ctx, inputs) {
.await
});

// This deadline protects test setup only. QuickJS initialization can
// take several seconds on a loaded Windows runner; cancellation latency
// remains independently bounded below.
tokio::time::timeout(std::time::Duration::from_secs(10), started.notified())
.await
.expect("nested tool must start before cancellation");
cancellation.cancel();
let result = tokio::time::timeout(std::time::Duration::from_secs(10), run)
let result = tokio::time::timeout(std::time::Duration::from_secs(2), run)
.await
.expect("cancellation must stop the dynamic workflow")
.unwrap()
Expand Down
3 changes: 2 additions & 1 deletion core/src/agent_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,8 @@ pub struct SessionOptions {
/// Per-session tool round limit override.
///
/// When set, overrides the agent-level `max_tool_rounds` for this session only.
/// Maps directly from [`AgentDefinition::max_steps`] when creating sessions
/// Maps directly from [`AgentDefinition::max_steps`](crate::subagent::AgentDefinition::max_steps)
/// when creating sessions
/// via [`Agent::session_for_agent`].
pub max_tool_rounds: Option<usize>,
/// Per-session parallel fan-out limit override.
Expand Down
3 changes: 2 additions & 1 deletion core/src/agent_api/agent_facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ impl Agent {
agent_sessions::create_session(self, workspace, options)
}

/// Create a session pre-configured from an [`AgentDefinition`].
/// Create a session pre-configured from an
/// [`AgentDefinition`](crate::subagent::AgentDefinition).
///
/// Maps the definition's `permissions`, `prompt`, `model`, and `max_steps`
/// directly into [`SessionOptions`], so markdown/YAML-defined subagents can
Expand Down
4 changes: 2 additions & 2 deletions core/src/agent_api/session_facade.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,8 +258,8 @@ impl AgentSession {
/// Return the names of all tools currently registered on this session.
///
/// Equivalent to `tool_definitions().into_iter().map(|t| t.name).collect()`.
/// Tools added via [`add_mcp_server`] appear immediately; tools removed via
/// [`remove_mcp_server`] disappear immediately.
/// Tools added via [`Self::add_mcp_server`] appear immediately; tools
/// removed via [`Self::remove_mcp_server`] disappear immediately.
pub fn tool_names(&self) -> Vec<String> {
DirectToolRuntime::from_session(self).names()
}
Expand Down
3 changes: 2 additions & 1 deletion core/src/agent_api/session_options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@ impl SessionOptions {
}

/// Add skill directories to scan for skill files (*.md).
/// Merged with any global `skill_dirs` from [`CodeConfig`] at session build time.
/// Merged with any global `skill_dirs` from
/// [`CodeConfig`](crate::config::CodeConfig) at session build time.
pub fn with_skill_dirs(mut self, dirs: impl IntoIterator<Item = impl Into<PathBuf>>) -> Self {
self.skill_dirs.extend(dirs.into_iter().map(Into::into));
self
Expand Down
19 changes: 19 additions & 0 deletions core/src/agent_api/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5538,6 +5538,19 @@ async function run(ctx, inputs) {
async fn test_dynamic_workflow_parallel_deep_research_inherits_parent_permissions() {
let dir = tempfile::tempdir().unwrap();
let agent = Agent::from_config(test_config()).await.unwrap();
let workspace_fs: Arc<dyn crate::workspace::WorkspaceFileSystem> =
Arc::new(TestWorkspaceFs::default());
let runner = Arc::new(TestWorkspaceRunner::default());
let runner_backend: Arc<dyn crate::workspace::WorkspaceCommandRunner> = runner.clone();
let services = crate::workspace::WorkspaceServices::builder(
crate::workspace::WorkspaceRef::new(
"deep-research-permission-inheritance",
dir.path().to_string_lossy(),
),
workspace_fs,
)
.command_runner(runner_backend)
.build();
let client = Arc::new(ScriptedStreamingClient::new(vec![
scripted_tool_call_response(
"bash-1",
Expand All @@ -5550,6 +5563,7 @@ async fn test_dynamic_workflow_parallel_deep_research_inherits_parent_permission
let opts = SessionOptions::new()
.with_llm_client(client)
.with_permission_policy(policy)
.with_workspace_backend(services)
.with_max_parallel_tasks(2)
.with_manual_delegation_enabled(true);
let session = agent
Expand Down Expand Up @@ -5620,6 +5634,11 @@ async function run(ctx, inputs) {
"deep-research must inherit the parent confirmation context: {}",
result.output
);
assert_eq!(
runner.commands.read().unwrap().as_slice(),
["echo inherited-dynamic-workflow-deep-research"],
"the child must execute through the parent workspace runner"
);
}

#[tokio::test]
Expand Down
4 changes: 2 additions & 2 deletions core/src/budget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
//! decision points and emits structured events. The host
//! implements [`BudgetGuard`] with whatever backend it likes
//! (per-tenant counters in Redis, per-day USD caps in Postgres, etc.)
//! and plugs it into [`SessionOptions::with_budget_guard`].
//! and plugs it into [`SessionOptions::with_budget_guard`](crate::SessionOptions::with_budget_guard).
//!
//! Decision points wired today:
//!
//! 1. **Before each LLM call** — [`BudgetGuard::check_before_llm`].
//! A `Deny` aborts the call; a `SoftLimit` lets the call proceed but
//! triggers an [`AgentEvent::BudgetThresholdHit`] so in-session
//! triggers an [`AgentEvent::BudgetThresholdHit`](crate::AgentEvent::BudgetThresholdHit) so in-session
//! policy (hooks, custom prompts) can react.
//! 2. **After each LLM call** — [`BudgetGuard::record_after_llm`].
//! The host updates its running spend total with the actual usage.
Expand Down
36 changes: 32 additions & 4 deletions core/src/code_intelligence/language_profile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ use serde_json::{json, Value};
use std::collections::{BTreeMap, BTreeSet};
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::time::Duration;

const DEFAULT_INITIALIZATION_SETTLE_DELAY: Duration = Duration::from_millis(750);
const DEFAULT_NAVIGATION_SETTLE_DELAY: Duration = Duration::from_millis(250);

/// Process topology used by a language server profile.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
Expand Down Expand Up @@ -41,6 +45,8 @@ pub(crate) struct LanguageServerProfile {
id: ProjectLanguageProfile,
topology: ServerTopology,
command: LanguageServerCommand,
initialization_settle_delay: Duration,
navigation_settle_delay: Duration,
}

impl LanguageServerProfile {
Expand All @@ -49,6 +55,8 @@ impl LanguageServerProfile {
id: ProjectLanguageProfile::Rust,
topology: ServerTopology::MultiFolder,
command: LanguageServerCommand::new(program, std::iter::empty::<OsString>()),
initialization_settle_delay: DEFAULT_INITIALIZATION_SETTLE_DELAY,
navigation_settle_delay: DEFAULT_NAVIGATION_SETTLE_DELAY,
}
}

Expand All @@ -57,6 +65,8 @@ impl LanguageServerProfile {
id: ProjectLanguageProfile::TypeScriptJavaScript,
topology: ServerTopology::MultiFolder,
command: LanguageServerCommand::new(program, ["--stdio"]),
initialization_settle_delay: DEFAULT_INITIALIZATION_SETTLE_DELAY,
navigation_settle_delay: DEFAULT_NAVIGATION_SETTLE_DELAY,
}
}

Expand All @@ -80,6 +90,25 @@ impl LanguageServerProfile {
&self.command
}

pub(crate) fn initialization_settle_delay(&self) -> Duration {
self.initialization_settle_delay
}

pub(crate) fn navigation_settle_delay(&self) -> Duration {
self.navigation_settle_delay
}

#[cfg(test)]
pub(crate) fn with_settle_delays(
mut self,
initialization: Duration,
navigation: Duration,
) -> Self {
self.initialization_settle_delay = initialization;
self.navigation_settle_delay = navigation;
self
}

pub(crate) fn language_ids(&self) -> &'static [&'static str] {
match self.id {
ProjectLanguageProfile::Rust => &["rust"],
Expand Down Expand Up @@ -256,11 +285,10 @@ mod tests {
let profile = LanguageServerProfile::rust("server");
let layout = layout(&["crates/a/Cargo.toml", "crates/b/Cargo.toml"]);
let root = Path::new("/workspace");
let linked_projects = ["crates/a/Cargo.toml", "crates/b/Cargo.toml"]
.map(|manifest| root.join(manifest).to_string_lossy().replace('\\', "/"));
let expected = json!({
"linkedProjects": [
"/workspace/crates/a/Cargo.toml",
"/workspace/crates/b/Cargo.toml"
],
"linkedProjects": linked_projects,
"cargo": {
"buildScripts": {
"enable": false,
Expand Down
Loading
Loading