Skip to content

Commit eb7798d

Browse files
RoyLinRoyLin
authored andcommitted
feat(python-sdk): v1.4.5 - Add tool skill support and fix SubAgentConfig
- Add support for kind: tool in skill system - Fix SubAgentConfig.skill_dirs parameter passing to Rust layer - Add complete PyO3 bindings for all SubAgentConfig parameters - Reorganize tests: unit tests in tests/unit/, integration in tests/integration/ - Remove hardcoded API keys, use environment variables - Add comprehensive test suite with real LLM execution - Add CHANGELOG.md for version tracking Security: - All credentials now use environment variables - Added .gitignore to prevent credential leaks
1 parent 898f484 commit eb7798d

36 files changed

Lines changed: 4580 additions & 5 deletions

core/src/skills/mod.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,18 @@
1010
//! name: my-skill
1111
//! description: What the skill does
1212
//! allowed-tools: "read(*), grep(*)"
13-
//! kind: instruction
13+
//! kind: instruction # or "persona" or "tool"
1414
//! ---
1515
//! # Skill Instructions
1616
//!
1717
//! You are a specialized assistant that...
1818
//! ```
19+
//!
20+
//! ## Skill Kinds
21+
//!
22+
//! - `instruction` (default): Injected into system prompt when matched
23+
//! - `persona`: Session-level system prompt (bound at session creation)
24+
//! - `tool`: Tool-like skill with specialized functionality (treated like instruction)
1925
2026
mod builtin;
2127
pub mod feedback;
@@ -40,12 +46,14 @@ use std::path::Path;
4046
/// Determines how the skill is used:
4147
/// - `Instruction`: Prompt/instruction content injected into system prompt
4248
/// - `Persona`: Session-level system prompt (bound at session creation, not injected globally)
49+
/// - `Tool`: Tool-like skill that provides specialized functionality (treated as instruction)
4350
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
4451
#[serde(rename_all = "lowercase")]
4552
pub enum SkillKind {
4653
#[default]
4754
Instruction,
4855
Persona,
56+
Tool,
4957
}
5058

5159
/// Tool permission pattern

core/src/skills/registry.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,10 @@ impl SkillRegistry {
235235

236236
let instruction_skills: Vec<_> = skills
237237
.values()
238-
.filter(|s| s.kind == super::SkillKind::Instruction)
238+
.filter(|s| {
239+
// Include both Instruction and Tool kinds in system prompt
240+
s.kind == super::SkillKind::Instruction || s.kind == super::SkillKind::Tool
241+
})
239242
.filter(|s| match scorer.as_ref() {
240243
Some(sc) => !sc.should_disable(&s.name),
241244
None => true,
@@ -265,7 +268,10 @@ impl SkillRegistry {
265268

266269
let matched: Vec<_> = skills
267270
.values()
268-
.filter(|s| s.kind == super::SkillKind::Instruction)
271+
.filter(|s| {
272+
// Include both Instruction and Tool kinds in matching
273+
s.kind == super::SkillKind::Instruction || s.kind == super::SkillKind::Tool
274+
})
269275
.filter(|s| match scorer.as_ref() {
270276
Some(sc) => !sc.should_disable(&s.name),
271277
None => true,

core/tests/test_subagent_permissions.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ async fn test_subagent_config_permissive_deny() {
100100
parent_id: None,
101101
metadata: serde_json::json!({}),
102102
agent_dirs: vec![],
103+
skill_dirs: vec![],
103104
lane_config: None,
104105
};
105106

sdk/python/CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Changelog
2+
3+
All notable changes to the A3S Code Python SDK will be documented in this file.
4+
5+
## [1.4.5] - 2026-03-16
6+
7+
### Added
8+
- Support for `kind: tool` in skill system (in addition to `instruction` and `persona`)
9+
- Complete PyO3 bindings for `SubAgentConfig` with all 12 parameters exposed
10+
- Comprehensive integration tests with real LLM execution
11+
12+
### Fixed
13+
- `SubAgentConfig.skill_dirs` parameter now properly passed from Python to Rust layer
14+
- Skill registry now correctly filters and loads tool-type skills
15+
- All SubAgentConfig fields now have proper getter/setter methods
16+
17+
### Changed
18+
- Reorganized test structure: `tests/unit/` for mock tests, `tests/integration/` for real API tests
19+
- Removed hardcoded API keys from test files (now use environment variables)
20+
21+
### Security
22+
- All test files now use environment variables for API credentials
23+
- Added `.gitignore` to prevent accidental credential commits
24+
25+
## [1.4.4] - 2026-03-15
26+
27+
### Added
28+
- Initial Python SDK release with PyO3 bindings
29+
- Basic agent and session management
30+
- Skill system support
31+
- Orchestrator and sub-agent execution

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

sdk/python/diagnose_skill_dirs.py

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Diagnostic script for SubAgentConfig skill_dirs issue.
4+
5+
This script helps diagnose why skills are not being found by sub-agents.
6+
"""
7+
8+
import os
9+
import sys
10+
from pathlib import Path
11+
12+
print("=" * 70)
13+
print("SubAgentConfig skill_dirs Diagnostic Tool")
14+
print("=" * 70)
15+
16+
# Check 1: Verify skill file format
17+
print("\n1. Checking skill file format...")
18+
print("-" * 70)
19+
20+
skill_file = input("Enter the full path to your skill file: ").strip()
21+
22+
if not os.path.exists(skill_file):
23+
print(f"❌ File not found: {skill_file}")
24+
sys.exit(1)
25+
26+
print(f"✓ File exists: {skill_file}")
27+
28+
with open(skill_file, 'r') as f:
29+
content = f.read()
30+
31+
print("\nFile content (first 500 chars):")
32+
print("-" * 70)
33+
print(content[:500])
34+
print("-" * 70)
35+
36+
# Check frontmatter
37+
if not content.startswith("---"):
38+
print("❌ ERROR: File must start with '---' (YAML frontmatter)")
39+
print("\nExpected format:")
40+
print("""---
41+
name: skill-name
42+
description: What the skill does
43+
allowed-tools: "read(*), grep(*)"
44+
kind: instruction
45+
---
46+
# Skill Instructions
47+
48+
Your skill content here...
49+
""")
50+
sys.exit(1)
51+
52+
parts = content.split("---")
53+
if len(parts) < 3:
54+
print("❌ ERROR: Invalid frontmatter format")
55+
print(" Frontmatter must be enclosed between two '---' markers")
56+
sys.exit(1)
57+
58+
frontmatter = parts[1].strip()
59+
body = parts[2].strip()
60+
61+
print("\n✓ Frontmatter structure is valid")
62+
print("\nFrontmatter content:")
63+
print("-" * 70)
64+
print(frontmatter)
65+
print("-" * 70)
66+
67+
# Check for required fields
68+
import re
69+
70+
name_match = re.search(r'^name:\s*(.+)$', frontmatter, re.MULTILINE)
71+
if not name_match:
72+
print("❌ ERROR: 'name' field is required in frontmatter")
73+
sys.exit(1)
74+
75+
skill_name = name_match.group(1).strip()
76+
print(f"\n✓ Skill name found: '{skill_name}'")
77+
78+
# Check for common formatting issues
79+
issues = []
80+
81+
# Check for unclosed quotes
82+
if frontmatter.count('"') % 2 != 0:
83+
issues.append("Unclosed double quotes in frontmatter")
84+
85+
if frontmatter.count("'") % 2 != 0:
86+
issues.append("Unclosed single quotes in frontmatter")
87+
88+
# Check for invalid fields
89+
valid_fields = ['name', 'description', 'allowed-tools', 'disable-model-invocation',
90+
'kind', 'tags', 'version']
91+
for line in frontmatter.split('\n'):
92+
line = line.strip()
93+
if ':' in line and not line.startswith('#'):
94+
field = line.split(':')[0].strip()
95+
if field and field not in valid_fields:
96+
issues.append(f"Unknown field '{field}' (valid: {', '.join(valid_fields)})")
97+
98+
if issues:
99+
print("\n⚠️ Potential issues found:")
100+
for issue in issues:
101+
print(f" - {issue}")
102+
else:
103+
print("\n✓ No obvious formatting issues detected")
104+
105+
# Check 2: Test with a3s_code
106+
print("\n2. Testing with a3s_code SDK...")
107+
print("-" * 70)
108+
109+
try:
110+
from a3s_code import SubAgentConfig
111+
112+
skill_dir = str(Path(skill_file).parent)
113+
print(f"Skill directory: {skill_dir}")
114+
115+
cfg = SubAgentConfig(
116+
agent_type="test",
117+
prompt=f"Call Skill('{skill_name}')",
118+
workspace=".",
119+
skill_dirs=[skill_dir]
120+
)
121+
122+
print(f"\n✓ SubAgentConfig created successfully")
123+
print(f" - skill_dirs: {cfg.skill_dirs}")
124+
print(f" - hasattr(cfg, 'skill_dirs'): {hasattr(cfg, 'skill_dirs')}")
125+
126+
except ImportError as e:
127+
print(f"❌ Cannot import a3s_code: {e}")
128+
print(" Make sure the SDK is installed: pip install a3s-code")
129+
sys.exit(1)
130+
except Exception as e:
131+
print(f"❌ Error creating SubAgentConfig: {e}")
132+
sys.exit(1)
133+
134+
# Check 3: Verify path is absolute
135+
print("\n3. Checking path format...")
136+
print("-" * 70)
137+
138+
if not os.path.isabs(skill_dir):
139+
print(f"⚠️ WARNING: Path is relative: {skill_dir}")
140+
print(f" Absolute path: {os.path.abspath(skill_dir)}")
141+
print(" Recommendation: Use absolute paths for skill_dirs")
142+
else:
143+
print(f"✓ Path is absolute: {skill_dir}")
144+
145+
# Check 4: Verify file extension
146+
print("\n4. Checking file extension...")
147+
print("-" * 70)
148+
149+
if not skill_file.endswith('.md'):
150+
print(f"❌ ERROR: Skill file must have .md extension")
151+
print(f" Current: {skill_file}")
152+
sys.exit(1)
153+
else:
154+
print(f"✓ File has .md extension")
155+
156+
# Summary
157+
print("\n" + "=" * 70)
158+
print("DIAGNOSTIC SUMMARY")
159+
print("=" * 70)
160+
161+
print(f"""
162+
Skill Information:
163+
- Name: {skill_name}
164+
- File: {skill_file}
165+
- Directory: {skill_dir}
166+
- File size: {len(content)} bytes
167+
168+
Configuration to use:
169+
SubAgentConfig(
170+
agent_type="your-agent-type",
171+
prompt="Call Skill('{skill_name}')",
172+
workspace="/your/workspace",
173+
skill_dirs=["{skill_dir}"],
174+
permissive=True
175+
)
176+
177+
Next Steps:
178+
1. If issues were found above, fix them in your skill file
179+
2. Make sure you're using the absolute path for skill_dirs
180+
3. Enable debug logging to see skill loading messages:
181+
182+
import logging
183+
logging.basicConfig(level=logging.DEBUG)
184+
185+
4. Check for warning messages like:
186+
- "Failed to load session skill dir"
187+
- "Skill validation failed"
188+
""")
189+
190+
print("=" * 70)

0 commit comments

Comments
 (0)