Skip to content

Commit d600724

Browse files
RoyLinRoyLin
authored andcommitted
fix(code): skip circuit breaker retry when cancelled
When cancel_token is cancelled, bail immediately instead of retrying. This prevents the 2-attempt delay before the interrupt takes effect.
1 parent 2abd168 commit d600724

6 files changed

Lines changed: 736 additions & 2 deletions

File tree

core/src/agent.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -962,7 +962,7 @@ impl AgentLoop {
962962
loop {
963963
tokio::select! {
964964
_ = cancel_token.cancelled() => {
965-
tracing::info!("LLM streaming cancelled by user");
965+
tracing::info!("🛑 LLM streaming cancelled by CancellationToken");
966966
anyhow::bail!("Operation cancelled by user");
967967
}
968968
event = stream_rx.recv() => {
@@ -1779,6 +1779,10 @@ impl AgentLoop {
17791779
Ok(r) => {
17801780
break r;
17811781
}
1782+
// Never retry if cancelled
1783+
Err(e) if cancel_token.is_cancelled() => {
1784+
anyhow::bail!(e);
1785+
}
17821786
// Retry when: non-streaming under threshold, OR first streaming attempt
17831787
Err(e) if attempt < threshold && (event_tx.is_none() || attempt == 1) => {
17841788
tracing::warn!(

core/src/agent_api.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1134,7 +1134,7 @@ impl Agent {
11341134
context_providers: opts.context_providers.clone(),
11351135
planning_enabled: opts.planning_enabled,
11361136
goal_tracking: opts.goal_tracking,
1137-
skill_registry: Some(effective_registry),
1137+
skill_registry: Some(Arc::clone(&effective_registry)),
11381138
max_parse_retries: opts.max_parse_retries.unwrap_or(base.max_parse_retries),
11391139
tool_timeout_ms: opts.tool_timeout_ms.or(base.tool_timeout_ms),
11401140
circuit_breaker_threshold: opts
@@ -1157,6 +1157,20 @@ impl Agent {
11571157
..base
11581158
};
11591159

1160+
// Register Skill tool — enables skills to be invoked as first-class tools
1161+
// with temporary permission grants. Must be registered after effective_registry
1162+
// and config are built so the Skill tool has access to both.
1163+
{
1164+
use crate::tools::register_skill;
1165+
register_skill(
1166+
tool_executor.registry(),
1167+
Arc::clone(&llm_client),
1168+
Arc::clone(&effective_registry),
1169+
Arc::clone(&tool_executor),
1170+
config.clone(),
1171+
);
1172+
}
1173+
11601174
// Create lane queue if configured
11611175
// A shared broadcast channel is used for both queue events and subagent events.
11621176
let (agent_event_tx, _) = broadcast::channel::<crate::agent::AgentEvent>(256);

examples/SDK_SKILL_TOOL_USAGE.md

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# Skill Tool SDK Usage
2+
3+
## Overview
4+
5+
The Skill tool is automatically registered in all agent sessions. It allows the LLM to invoke skills as first-class tools with temporary permission grants.
6+
7+
## Python SDK
8+
9+
### Basic Usage
10+
11+
```python
12+
from a3s_code import Agent
13+
14+
# Create agent
15+
agent = Agent.create("agent.hcl")
16+
17+
# Create session with skills
18+
session = agent.session(
19+
".",
20+
builtin_skills=True, # Enable built-in skills
21+
skill_dirs=["./skills"] # Load custom skills
22+
)
23+
24+
# The LLM can now invoke skills as tools
25+
# Example: LLM calls Skill("data-processor", prompt="Read README.md")
26+
response = await session.send("Use the data-processor skill to analyze README.md")
27+
print(response.text)
28+
```
29+
30+
### Permission Isolation
31+
32+
```python
33+
from a3s_code import Agent, SessionOptions, PermissionPolicy, PermissionRule
34+
35+
# Create agent with restricted permissions
36+
config = AgentConfig(
37+
permission_policy=PermissionPolicy(
38+
allow=[PermissionRule("Skill(*)")], # Only allow Skill tool
39+
deny=[PermissionRule("read(*)")], # Deny direct read access
40+
default_decision="deny"
41+
)
42+
)
43+
44+
agent = Agent(config=config)
45+
46+
# Register a skill with specific allowed-tools
47+
agent.register_skill(
48+
name="data-processor",
49+
description="Process and analyze data files",
50+
allowed_tools="read(*), grep(*)", # Skill can use read and grep
51+
content="You are a data processing specialist..."
52+
)
53+
54+
session = agent.session(".")
55+
56+
# Agent can only access read/grep through the skill
57+
# Direct read calls will be denied
58+
response = await session.send("Use data-processor to read file.txt")
59+
```
60+
61+
## Node.js SDK
62+
63+
### Basic Usage
64+
65+
```typescript
66+
import { Agent } from 'a3s-code';
67+
68+
// Create agent
69+
const agent = Agent.create('agent.hcl');
70+
71+
// Create session with skills
72+
const session = agent.session('.', {
73+
builtinSkills: true, // Enable built-in skills
74+
skillDirs: ['./skills'] // Load custom skills
75+
});
76+
77+
// The LLM can now invoke skills as tools
78+
const response = await session.send(
79+
'Use the data-processor skill to analyze README.md'
80+
);
81+
console.log(response.text);
82+
```
83+
84+
### Permission Isolation
85+
86+
```typescript
87+
import { Agent, SessionOptions, PermissionPolicy, PermissionRule } from 'a3s-code';
88+
89+
// Create agent with restricted permissions
90+
const config = {
91+
permissionPolicy: new PermissionPolicy({
92+
allow: [new PermissionRule('Skill(*)')], // Only allow Skill tool
93+
deny: [new PermissionRule('read(*)')], // Deny direct read access
94+
defaultDecision: 'deny'
95+
})
96+
};
97+
98+
const agent = new Agent(config);
99+
100+
// Register a skill with specific allowed-tools
101+
agent.registerSkill({
102+
name: 'data-processor',
103+
description: 'Process and analyze data files',
104+
allowedTools: 'read(*), grep(*)', // Skill can use read and grep
105+
content: 'You are a data processing specialist...'
106+
});
107+
108+
const session = agent.session('.');
109+
110+
// Agent can only access read/grep through the skill
111+
const response = await session.send('Use data-processor to read file.txt');
112+
```
113+
114+
## How It Works
115+
116+
1. **Automatic Registration**: The Skill tool is automatically registered when you create a session
117+
2. **LLM Invocation**: The LLM can call `Skill("skill-name", prompt="...")` as a tool
118+
3. **Temporary Permissions**: The skill's `allowed-tools` are granted during execution
119+
4. **Automatic Revocation**: Permissions are revoked after the skill completes
120+
5. **Permission Isolation**: Parent agent cannot bypass skills to access underlying tools
121+
122+
## Skill Definition Format
123+
124+
Skills are defined with YAML frontmatter in `.md` files:
125+
126+
```markdown
127+
---
128+
name: data-processor
129+
description: Process and analyze data files
130+
allowed-tools: read(*), grep(*)
131+
---
132+
133+
# Data Processor Skill
134+
135+
You are a data processing specialist. You can:
136+
- Read files to analyze data
137+
- Search for patterns using grep
138+
- Process and summarize information
139+
140+
You CANNOT:
141+
- Write files
142+
- Execute bash commands
143+
- Access the network
144+
```
145+
146+
## Permission Patterns
147+
148+
### Pattern 1: Skill-Only Access
149+
150+
Agent can only use tools through skills:
151+
152+
```python
153+
PermissionPolicy(
154+
allow=[PermissionRule("Skill(*)")],
155+
deny=[PermissionRule("*")], # Deny all direct tool access
156+
default_decision="deny"
157+
)
158+
```
159+
160+
### Pattern 2: Mixed Access
161+
162+
Agent can use some tools directly, others through skills:
163+
164+
```python
165+
PermissionPolicy(
166+
allow=[
167+
PermissionRule("Skill(*)"),
168+
PermissionRule("bash(*)"), # Direct bash access
169+
],
170+
deny=[PermissionRule("read(*)")], # Must use skill for read
171+
default_decision="deny"
172+
)
173+
```
174+
175+
### Pattern 3: Skill-Specific Restrictions
176+
177+
Different skills have different permissions:
178+
179+
```python
180+
# Skill 1: Read-only
181+
agent.register_skill(
182+
name="reader",
183+
allowed_tools="read(*), grep(*)"
184+
)
185+
186+
# Skill 2: Write access
187+
agent.register_skill(
188+
name="writer",
189+
allowed_tools="read(*), write(*), edit(*)"
190+
)
191+
```
192+
193+
## Best Practices
194+
195+
1. **Principle of Least Privilege**: Only grant skills the minimum tools they need
196+
2. **Skill Composition**: Break complex tasks into multiple focused skills
197+
3. **Clear Descriptions**: Write clear skill descriptions so the LLM knows when to use them
198+
4. **Permission Boundaries**: Use skills to enforce security boundaries
199+
5. **Audit Logging**: Monitor which skills are invoked and what tools they use
200+
201+
## Troubleshooting
202+
203+
### Skill Not Found
204+
205+
```
206+
Error: Skill 'data-processor' not found
207+
```
208+
209+
**Solution**: Ensure the skill is registered or loaded from a skill directory:
210+
211+
```python
212+
session = agent.session(".", skill_dirs=["./skills"])
213+
```
214+
215+
### Permission Denied
216+
217+
```
218+
Error: Tool 'read' is blocked by permission policy
219+
```
220+
221+
**Solution**: Check that the skill's `allowed-tools` includes the tool:
222+
223+
```yaml
224+
allowed-tools: read(*), grep(*)
225+
```
226+
227+
### Nested Skill Invocation
228+
229+
Currently, skills cannot invoke other skills. If you need this, structure your skills to be independent or use the Task tool for delegation.
230+
231+
## See Also
232+
233+
- [Skill Tool Implementation](./SKILL_TOOL_IMPLEMENTATION.md)
234+
- [GitHub Issue #8](https://github.com/A3S-Lab/Code/issues/8)
235+
- [Permission System Documentation](../docs/permissions.md)

0 commit comments

Comments
 (0)