Skip to content

Commit 138c3af

Browse files
committed
fix(sdk): add permissive policy and missing session options
Root cause: default permission decision is `Ask`, which requires a HITL confirmation manager. Without one, all tool calls are rejected with "Tool requires confirmation but no HITL confirmation manager configured". Changes: - Add `SessionOptions::with_permissive_policy()` convenience method - Python SDK: expose permissive, planning, goal_tracking, max_parse_retries, tool_timeout_ms, circuit_breaker_threshold kwargs on session() - Node SDK: expose permissive, planning, goalTracking, maxParseRetries, toolTimeoutMs, circuitBreakerThreshold on SessionOptions - All 3 agentic_loop_demo examples now use permissive mode
1 parent ca8b76d commit 138c3af

6 files changed

Lines changed: 125 additions & 17 deletions

File tree

core/examples/agentic_loop_demo.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,15 @@
2828
use a3s_code_core::{Agent, AgentEvent, SessionOptions};
2929
use std::path::PathBuf;
3030

31+
/// Create a permissive session options that allows all tool execution.
32+
///
33+
/// Without this, the default permission decision is `Ask`, which requires
34+
/// a HITL confirmation manager. For automated demos we use `with_permissive_policy()`
35+
/// to allow all tools without confirmation.
36+
fn permissive_options() -> SessionOptions {
37+
SessionOptions::new().with_permissive_policy()
38+
}
39+
3140
fn resolve_config() -> PathBuf {
3241
if let Ok(env_path) = std::env::var("A3S_CONFIG") {
3342
return PathBuf::from(env_path);
@@ -91,7 +100,7 @@ async fn demo_1_autonomous_coding(agent: &Agent) -> anyhow::Result<()> {
91100

92101
let tmp = tempfile::tempdir()?;
93102
let workspace = tmp.path().display().to_string();
94-
let session = agent.session(&workspace, None)?;
103+
let session = agent.session(&workspace, Some(permissive_options()))?;
95104

96105
println!(" Workspace: {}", workspace);
97106
println!(" Prompt: Create a Rust file, then improve it\n");
@@ -146,7 +155,7 @@ async fn demo_2_streaming_events(agent: &Agent) -> anyhow::Result<()> {
146155
r#"{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]}"#,
147156
)?;
148157

149-
let session = agent.session(&workspace, None)?;
158+
let session = agent.session(&workspace, Some(permissive_options()))?;
150159

151160
println!(" Workspace: {}", workspace);
152161
println!(" Prompt: Read data.json and create a summary\n");
@@ -228,7 +237,7 @@ async fn demo_3_planning_mode(agent: &Agent) -> anyhow::Result<()> {
228237
let session = agent.session(
229238
&workspace,
230239
Some(
231-
SessionOptions::new()
240+
permissive_options()
232241
.with_planning(true)
233242
.with_goal_tracking(true),
234243
),
@@ -278,7 +287,7 @@ async fn demo_4_multi_turn(agent: &Agent) -> anyhow::Result<()> {
278287

279288
let tmp = tempfile::tempdir()?;
280289
let workspace = tmp.path().display().to_string();
281-
let session = agent.session(&workspace, None)?;
290+
let session = agent.session(&workspace, Some(permissive_options()))?;
282291

283292
println!(" Workspace: {}\n", workspace);
284293

@@ -365,7 +374,7 @@ API_KEY = "sk-1234567890abcdef"
365374

366375
let session = agent.session(
367376
&workspace,
368-
Some(SessionOptions::new().with_builtin_skills()),
377+
Some(permissive_options().with_builtin_skills()),
369378
)?;
370379

371380
println!(" Workspace: {}", workspace);
@@ -415,7 +424,7 @@ async fn demo_6_resilient_session(agent: &Agent) -> anyhow::Result<()> {
415424
let session = agent.session(
416425
&workspace,
417426
Some(
418-
SessionOptions::new()
427+
permissive_options()
419428
.with_resilience_defaults() // parse_retries=2, timeout=120s, circuit_breaker=3
420429
.with_builtin_skills(),
421430
),

core/src/agent_api.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,18 @@ impl SessionOptions {
207207
self
208208
}
209209

210+
/// Allow all tool execution without confirmation (permissive mode).
211+
///
212+
/// Use this for automated scripts, demos, and CI environments where
213+
/// human-in-the-loop confirmation is not needed. Without this (or a
214+
/// custom permission checker), the default is `Ask`, which requires a
215+
/// HITL confirmation manager to be configured.
216+
pub fn with_permissive_policy(self) -> Self {
217+
self.with_permission_checker(Arc::new(
218+
crate::permissions::PermissionPolicy::permissive(),
219+
))
220+
}
221+
210222
/// Enable planning
211223
pub fn with_planning(mut self, enabled: bool) -> Self {
212224
self.planning_enabled = enabled;

sdk/node/examples/agentic_loop_demo.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ async function demo1AutonomousCoding(agent) {
8282

8383
const workspace = makeTempDir();
8484
try {
85-
const session = agent.session(workspace);
85+
const session = agent.session(workspace, { permissive: true });
8686

8787
console.log(` Workspace: ${workspace}`);
8888
console.log(' Prompt: Create a Rust file, then improve it\n');
@@ -135,7 +135,7 @@ async function demo2StreamingEvents(agent) {
135135
'{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]}'
136136
);
137137

138-
const session = agent.session(workspace);
138+
const session = agent.session(workspace, { permissive: true });
139139

140140
console.log(` Workspace: ${workspace}`);
141141
console.log(' Prompt: Read data.json and create a summary\n');
@@ -208,6 +208,7 @@ async function demo3PlanningMode(agent) {
208208
const session = agent.session(workspace, {
209209
planning: true,
210210
goalTracking: true,
211+
permissive: true,
211212
});
212213

213214
console.log(` Workspace: ${workspace}`);
@@ -250,7 +251,7 @@ async function demo4MultiTurn(agent) {
250251

251252
const workspace = makeTempDir();
252253
try {
253-
const session = agent.session(workspace);
254+
const session = agent.session(workspace, { permissive: true });
254255

255256
console.log(` Workspace: ${workspace}\n`);
256257

@@ -327,7 +328,7 @@ async function demo5SkillsAugmented(agent) {
327328
);
328329

329330
const skills = builtinSkills();
330-
const session = agent.session(workspace, { builtinSkills: true });
331+
const session = agent.session(workspace, { builtinSkills: true, permissive: true });
331332

332333
console.log(` Workspace: ${workspace}`);
333334
console.log(` Skills: built-in (${skills.length} skills active)\n`);
@@ -373,6 +374,7 @@ async function demo6ResilientSession(agent) {
373374
try {
374375
const session = agent.session(workspace, {
375376
builtinSkills: true,
377+
permissive: true,
376378
maxParseRetries: 2,
377379
toolTimeoutMs: 120_000,
378380
circuitBreakerThreshold: 3,

sdk/node/src/lib.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,18 @@ pub struct SessionOptions {
258258
pub agent_dirs: Option<Vec<String>>,
259259
/// Optional queue configuration for lane-based tool execution.
260260
pub queue_config: Option<SessionQueueConfig>,
261+
/// Allow all tools without HITL confirmation (default: false).
262+
pub permissive: Option<bool>,
263+
/// Enable planning mode (default: false).
264+
pub planning: Option<bool>,
265+
/// Enable goal tracking (default: false).
266+
pub goal_tracking: Option<bool>,
267+
/// Max consecutive parse errors before abort.
268+
pub max_parse_retries: Option<u32>,
269+
/// Per-tool execution timeout in milliseconds.
270+
pub tool_timeout_ms: Option<f64>,
271+
/// Max LLM API failures before abort.
272+
pub circuit_breaker_threshold: Option<u32>,
261273
}
262274

263275
/// A single message in conversation history.
@@ -460,6 +472,24 @@ impl Agent {
460472
if let Some(qc) = o.queue_config {
461473
opts = opts.with_queue_config(js_queue_config_to_rust(&qc));
462474
}
475+
if o.permissive.unwrap_or(false) {
476+
opts = opts.with_permissive_policy();
477+
}
478+
if o.planning.unwrap_or(false) {
479+
opts = opts.with_planning(true);
480+
}
481+
if o.goal_tracking.unwrap_or(false) {
482+
opts = opts.with_goal_tracking(true);
483+
}
484+
if let Some(n) = o.max_parse_retries {
485+
opts = opts.with_parse_retries(n);
486+
}
487+
if let Some(ms) = o.tool_timeout_ms {
488+
opts = opts.with_tool_timeout(ms as u64);
489+
}
490+
if let Some(n) = o.circuit_breaker_threshold {
491+
opts = opts.with_circuit_breaker(n);
492+
}
463493
opts
464494
});
465495

sdk/python/examples/agentic_loop_demo.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def demo_1_autonomous_coding(agent):
7777
separator("Demo 1: Autonomous Multi-Step Coding")
7878

7979
with tempfile.TemporaryDirectory() as workspace:
80-
session = agent.session(workspace)
80+
session = agent.session(workspace, permissive=True)
8181

8282
print(f" Workspace: {workspace}")
8383
print(" Prompt: Create a Rust file, then improve it\n")
@@ -123,7 +123,7 @@ def demo_2_streaming_events(agent):
123123
'{"users": [{"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}]}'
124124
)
125125

126-
session = agent.session(workspace)
126+
session = agent.session(workspace, permissive=True)
127127

128128
print(f" Workspace: {workspace}")
129129
print(" Prompt: Read data.json and create a summary\n")
@@ -173,7 +173,7 @@ def demo_3_planning_mode(agent):
173173
separator("Demo 3: Planning Mode (Task Decomposition)")
174174

175175
with tempfile.TemporaryDirectory() as workspace:
176-
session = agent.session(workspace, planning=True, goal_tracking=True)
176+
session = agent.session(workspace, planning=True, goal_tracking=True, permissive=True)
177177

178178
print(f" Workspace: {workspace}")
179179
print(" Planning: enabled")
@@ -206,7 +206,7 @@ def demo_4_multi_turn(agent):
206206
separator("Demo 4: Multi-Turn Conversation (Context Preservation)")
207207

208208
with tempfile.TemporaryDirectory() as workspace:
209-
session = agent.session(workspace)
209+
session = agent.session(workspace, permissive=True)
210210

211211
print(f" Workspace: {workspace}\n")
212212

@@ -276,7 +276,7 @@ def demo_5_skills_augmented(agent):
276276

277277
# List available built-in skills
278278
skills = builtin_skills()
279-
session = agent.session(workspace, builtin_skills=True)
279+
session = agent.session(workspace, builtin_skills=True, permissive=True)
280280

281281
print(f" Workspace: {workspace}")
282282
print(f" Skills: built-in ({len(skills)} skills active)\n")
@@ -314,6 +314,7 @@ def demo_6_resilient_session(agent):
314314
session = agent.session(
315315
workspace,
316316
builtin_skills=True,
317+
permissive=True,
317318
max_parse_retries=2,
318319
tool_timeout_ms=120_000,
319320
circuit_breaker_threshold=3,

sdk/python/src/lib.rs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,13 @@ impl PyAgent {
426426
/// skill_dirs: Optional list of directories to scan for skill files
427427
/// agent_dirs: Optional list of directories to scan for agent files
428428
/// queue_config: Optional SessionQueueConfig for lane-based tool execution
429-
#[pyo3(signature = (workspace, options=None, model=None, builtin_skills=None, skill_dirs=None, agent_dirs=None, queue_config=None))]
429+
/// permissive: Optional bool to allow all tools without HITL confirmation (default: False)
430+
/// planning: Optional bool to enable planning mode (default: False)
431+
/// goal_tracking: Optional bool to enable goal tracking (default: False)
432+
/// max_parse_retries: Optional max consecutive parse errors before abort
433+
/// tool_timeout_ms: Optional per-tool execution timeout in milliseconds
434+
/// circuit_breaker_threshold: Optional max LLM API failures before abort
435+
#[pyo3(signature = (workspace, options=None, model=None, builtin_skills=None, skill_dirs=None, agent_dirs=None, queue_config=None, permissive=None, planning=None, goal_tracking=None, max_parse_retries=None, tool_timeout_ms=None, circuit_breaker_threshold=None))]
430436
fn session(
431437
&self,
432438
workspace: String,
@@ -436,6 +442,12 @@ impl PyAgent {
436442
skill_dirs: Option<Vec<String>>,
437443
agent_dirs: Option<Vec<String>>,
438444
queue_config: Option<PySessionQueueConfig>,
445+
permissive: Option<bool>,
446+
planning: Option<bool>,
447+
goal_tracking: Option<bool>,
448+
max_parse_retries: Option<u32>,
449+
tool_timeout_ms: Option<u64>,
450+
circuit_breaker_threshold: Option<u32>,
439451
) -> PyResult<PySession> {
440452
// If a SessionOptions object is provided, use it directly
441453
let opts = if let Some(so) = options {
@@ -455,14 +467,38 @@ impl PyAgent {
455467
if let Some(qc) = so.queue_config {
456468
o = o.with_queue_config(qc.inner);
457469
}
470+
if permissive.unwrap_or(false) {
471+
o = o.with_permissive_policy();
472+
}
473+
if planning.unwrap_or(false) {
474+
o = o.with_planning(true);
475+
}
476+
if goal_tracking.unwrap_or(false) {
477+
o = o.with_goal_tracking(true);
478+
}
479+
if let Some(n) = max_parse_retries {
480+
o = o.with_parse_retries(n);
481+
}
482+
if let Some(ms) = tool_timeout_ms {
483+
o = o.with_tool_timeout(ms);
484+
}
485+
if let Some(n) = circuit_breaker_threshold {
486+
o = o.with_circuit_breaker(n);
487+
}
458488
Some(o)
459489
} else {
460490
// Fall back to individual keyword arguments
461491
let has_overrides = model.is_some()
462492
|| builtin_skills.is_some()
463493
|| skill_dirs.is_some()
464494
|| agent_dirs.is_some()
465-
|| queue_config.is_some();
495+
|| queue_config.is_some()
496+
|| permissive.is_some()
497+
|| planning.is_some()
498+
|| goal_tracking.is_some()
499+
|| max_parse_retries.is_some()
500+
|| tool_timeout_ms.is_some()
501+
|| circuit_breaker_threshold.is_some();
466502

467503
if has_overrides {
468504
let mut o = RustSessionOptions::new();
@@ -485,6 +521,24 @@ impl PyAgent {
485521
if let Some(qc) = queue_config {
486522
o = o.with_queue_config(qc.inner);
487523
}
524+
if permissive.unwrap_or(false) {
525+
o = o.with_permissive_policy();
526+
}
527+
if planning.unwrap_or(false) {
528+
o = o.with_planning(true);
529+
}
530+
if goal_tracking.unwrap_or(false) {
531+
o = o.with_goal_tracking(true);
532+
}
533+
if let Some(n) = max_parse_retries {
534+
o = o.with_parse_retries(n);
535+
}
536+
if let Some(ms) = tool_timeout_ms {
537+
o = o.with_tool_timeout(ms);
538+
}
539+
if let Some(n) = circuit_breaker_threshold {
540+
o = o.with_circuit_breaker(n);
541+
}
488542
Some(o)
489543
} else {
490544
None

0 commit comments

Comments
 (0)