Skip to content

Commit bfba9f4

Browse files
RoyLinRoyLin
authored andcommitted
feat(skills): support session-local registries
1 parent ccc232e commit bfba9f4

File tree

8 files changed

+86
-20
lines changed

8 files changed

+86
-20
lines changed

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

core/src/session/manager.rs

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ pub struct SessionManager {
3535
Arc<RwLock<HashMap<String, tokio_util::sync::CancellationToken>>>,
3636
/// Skill registry for runtime skill management
3737
pub(crate) skill_registry: Arc<RwLock<Option<Arc<SkillRegistry>>>>,
38+
/// Per-session skill registries layered on top of the shared registry.
39+
pub(crate) session_skill_registries: Arc<RwLock<HashMap<String, Arc<SkillRegistry>>>>,
3840
/// Shared memory store for agent long-term memory.
3941
/// When set, each `generate`/`generate_streaming` call wraps this in
4042
/// `AgentMemory` and injects it into `AgentConfig.memory`.
@@ -56,6 +58,7 @@ impl SessionManager {
5658
llm_configs: Arc::new(RwLock::new(HashMap::new())),
5759
ongoing_operations: Arc::new(RwLock::new(HashMap::new())),
5860
skill_registry: Arc::new(RwLock::new(None)),
61+
session_skill_registries: Arc::new(RwLock::new(HashMap::new())),
5962
memory_store: Arc::new(RwLock::new(None)),
6063
mcp_manager: Arc::new(RwLock::new(None)),
6164
}
@@ -85,6 +88,7 @@ impl SessionManager {
8588
llm_configs: Arc::new(RwLock::new(HashMap::new())),
8689
ongoing_operations: Arc::new(RwLock::new(HashMap::new())),
8790
skill_registry: Arc::new(RwLock::new(None)),
91+
session_skill_registries: Arc::new(RwLock::new(HashMap::new())),
8892
memory_store: Arc::new(RwLock::new(None)),
8993
mcp_manager: Arc::new(RwLock::new(None)),
9094
};
@@ -114,6 +118,7 @@ impl SessionManager {
114118
llm_configs: Arc::new(RwLock::new(HashMap::new())),
115119
ongoing_operations: Arc::new(RwLock::new(HashMap::new())),
116120
skill_registry: Arc::new(RwLock::new(None)),
121+
session_skill_registries: Arc::new(RwLock::new(HashMap::new())),
117122
memory_store: Arc::new(RwLock::new(None)),
118123
mcp_manager: Arc::new(RwLock::new(None)),
119124
}
@@ -151,6 +156,27 @@ impl SessionManager {
151156
self.skill_registry.read().await.clone()
152157
}
153158

159+
/// Override the active skill registry for a single session.
160+
pub async fn set_session_skill_registry(
161+
&self,
162+
session_id: impl Into<String>,
163+
registry: Arc<SkillRegistry>,
164+
) {
165+
self.session_skill_registries
166+
.write()
167+
.await
168+
.insert(session_id.into(), registry);
169+
}
170+
171+
/// Get the active skill registry for a single session, if one was set.
172+
pub async fn session_skill_registry(&self, session_id: &str) -> Option<Arc<SkillRegistry>> {
173+
self.session_skill_registries
174+
.read()
175+
.await
176+
.get(session_id)
177+
.cloned()
178+
}
179+
154180
/// Set the shared memory store for agent long-term memory.
155181
///
156182
/// When set, every `generate`/`generate_streaming` call wraps this store
@@ -480,6 +506,11 @@ impl SessionManager {
480506
configs.remove(id);
481507
}
482508

509+
{
510+
let mut registries = self.session_skill_registries.write().await;
511+
registries.remove(id);
512+
}
513+
483514
// Remove storage type tracking
484515
{
485516
let mut storage_types = self.session_storage_types.write().await;
@@ -663,7 +694,10 @@ impl SessionManager {
663694
};
664695

665696
// Inject skill registry into system prompt and agent config
666-
let skill_registry = self.skill_registry.read().await.clone();
697+
let skill_registry = match self.session_skill_registry(session_id).await {
698+
Some(registry) => Some(registry),
699+
None => self.skill_registry.read().await.clone(),
700+
};
667701
let system = if let Some(ref registry) = skill_registry {
668702
let skill_prompt = registry.to_system_prompt();
669703
if skill_prompt.is_empty() {
@@ -820,7 +854,10 @@ impl SessionManager {
820854
};
821855

822856
// Inject skill registry into system prompt and agent config
823-
let skill_registry = self.skill_registry.read().await.clone();
857+
let skill_registry = match self.session_skill_registry(session_id).await {
858+
Some(registry) => Some(registry),
859+
None => self.skill_registry.read().await.clone(),
860+
};
824861
let system = if let Some(ref registry) = skill_registry {
825862
let skill_prompt = registry.to_system_prompt();
826863
if skill_prompt.is_empty() {
@@ -1162,6 +1199,22 @@ impl SessionManager {
11621199
Ok(())
11631200
}
11641201

1202+
/// Update a session's system prompt in place.
1203+
pub async fn set_system_prompt(
1204+
&self,
1205+
session_id: &str,
1206+
system_prompt: Option<String>,
1207+
) -> Result<()> {
1208+
{
1209+
let session_lock = self.get_session(session_id).await?;
1210+
let mut session = session_lock.write().await;
1211+
session.config.system_prompt = system_prompt;
1212+
}
1213+
1214+
self.persist_in_background(session_id, "set_system_prompt");
1215+
Ok(())
1216+
}
1217+
11651218
/// Get session count
11661219
pub async fn session_count(&self) -> usize {
11671220
let sessions = self.sessions.read().await;

core/src/skills/registry.rs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,25 +134,39 @@ impl SkillRegistry {
134134
let entry = entry?;
135135
let path = entry.path();
136136

137-
// Only process .md files
138-
if path.extension().and_then(|s| s.to_str()) != Some("md") {
137+
let candidate = if path.is_dir() {
138+
let skill_md = path.join("SKILL.md");
139+
if skill_md.is_file() {
140+
Some(skill_md)
141+
} else {
142+
None
143+
}
144+
} else if path.extension().and_then(|s| s.to_str()) == Some("md") {
145+
Some(path.clone())
146+
} else {
147+
None
148+
};
149+
150+
let Some(candidate) = candidate else {
139151
continue;
140-
}
152+
};
141153

142-
// Try to load the skill
143-
match Skill::from_file(&path) {
154+
match Skill::from_file(&candidate) {
144155
Ok(skill) => {
145156
let skill = Arc::new(skill);
146157
match self.register(skill) {
147158
Ok(()) => loaded += 1,
148159
Err(e) => {
149-
tracing::warn!("Skill validation failed for {}: {}", path.display(), e);
160+
tracing::warn!(
161+
"Skill validation failed for {}: {}",
162+
candidate.display(),
163+
e
164+
);
150165
}
151166
}
152167
}
153168
Err(e) => {
154-
// Log but don't fail - some .md files might not be skills
155-
tracing::debug!("Skipped {}: {}", path.display(), e);
169+
tracing::debug!("Skipped {}: {}", candidate.display(), e);
156170
}
157171
}
158172
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
2-
name: chart-generator
2+
name: vis-chart
33
description: Generate interactive charts using vis-chart markdown syntax
44
allowed-tools: read(*), grep(*), bash(*)
55
---
66

7-
# Chart Generator Skill
7+
# Vis Chart Skill
88

99
You are a data visualization specialist. Your job is to generate interactive charts using the `vis-chart` markdown code block format.
1010

sdk/node/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "a3s-code-node"
3-
version = "1.5.1"
3+
version = "1.5.2"
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 = "1.4", path = "../../core", features = ["ahp"] }
14+
a3s-code-core = { version = "1.5.2", path = "../../core", features = ["ahp"] }
1515
a3s-ahp = { version = "2.0", features = ["all-transports"] }
1616
napi = { version = "2", features = ["async", "napi6", "serde-json"] }
1717
napi-derive = "2"
@@ -26,4 +26,3 @@ napi-build = "2"
2626
[features]
2727
default = ["ahp"]
2828
ahp = []
29-

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": "1.5.1",
3+
"version": "1.5.2",
44
"description": "A3S Code - Native AI coding agent library for Node.js",
55
"main": "index.js",
66
"types": "index.d.ts",

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 = "1.5.0"
3+
version = "1.5.2"
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 = "1.4", path = "../../core", features = ["ahp"] }
15+
a3s-code-core = { version = "1.5.2", path = "../../core", features = ["ahp"] }
1616
a3s-ahp = { version = "2.0", features = ["all-transports"] }
1717
pyo3 = { version = "0.23", features = ["extension-module"] }
1818
tokio = { version = "1.35", features = ["full"] }

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 = "1.5.1"
7+
version = "1.5.2"
88
description = "A3S Code - Native Python bindings for the AI coding agent"
99
readme = "README.md"
1010
license = {text = "MIT"}

0 commit comments

Comments
 (0)