Skip to content
Open
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
23 changes: 12 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ npm install @a3s-lab/code

## Quick Start

**1. Create an agent config** (`agent.hcl`):
**1. Create an agent config** (`agent.acl`; legacy `.hcl` filenames still work):

```hcl
default_model = "anthropic/claude-sonnet-4-20250514"

providers {
name = "anthropic"
api_key = env("ANTHROPIC_API_KEY")
providers "anthropic" {
apiKey = env("ANTHROPIC_API_KEY")
}
```

Expand All @@ -39,7 +38,7 @@ providers {
```python
from a3s_code import Agent

agent = Agent.create("agent.hcl")
agent = Agent.create("agent.acl")
session = agent.session("/my-project")

result = session.send("Find all places where we handle authentication errors")
Expand All @@ -49,7 +48,7 @@ print(result.text)
```typescript
import { Agent } from '@a3s-lab/code';

const agent = await Agent.create('agent.hcl');
const agent = await Agent.create('agent.acl');
const session = agent.session('/my-project');

const result = await session.send('Find all places where we handle authentication errors');
Expand Down Expand Up @@ -300,14 +299,16 @@ Sessions intercept slash commands:

## Configuration

**HCL format:**
The config language is ACL (Agent Configuration Language). It is HCL-like and
the loader still accepts existing `.hcl` filenames and HCL-style
`providers { name = "..." }` blocks, but new configs should use `.acl` and
labeled provider/model blocks.

```hcl
default_model = "anthropic/claude-sonnet-4-20250514"

providers {
name = "anthropic"
api_key = env("ANTHROPIC_API_KEY")
providers "anthropic" {
apiKey = env("ANTHROPIC_API_KEY")
}

mcp_servers = []
Expand All @@ -333,7 +334,7 @@ ahp = {
```
Agent (facade — config-driven, workspace-independent)
├── LlmClient (Anthropic / OpenAI / compatible)
├── CodeConfig (HCL / JSON)
├── CodeConfig (ACL-compatible config; legacy .hcl filenames accepted)
├── SessionManager (multi-session support)
│ └── AgentSession (workspace-bound)
│ └── AgentLoop (core execution engine)
Expand Down
42 changes: 16 additions & 26 deletions core/src/agent_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -630,9 +630,9 @@ impl std::fmt::Debug for Agent {
}

impl Agent {
/// Create from a config file path or inline HCL string.
/// Create from a config file path or inline ACL-compatible string.
///
/// Auto-detects: `.hcl` file path vs inline HCL.
/// Auto-detects: `.acl`/legacy `.hcl` file path vs inline ACL-compatible config.
pub async fn new(config_source: impl Into<String>) -> Result<Self> {
let source = config_source.into();

Expand All @@ -652,7 +652,7 @@ impl Agent {

let config = if matches!(
path.extension().and_then(|ext| ext.to_str()),
Some("hcl" | "json")
Some("acl" | "hcl")
) {
if !path.exists() {
return Err(CodeError::Config(format!(
Expand All @@ -663,34 +663,24 @@ impl Agent {

CodeConfig::from_file(path)
.with_context(|| format!("Failed to load config: {}", path.display()))?
} else if matches!(path.extension().and_then(|ext| ext.to_str()), Some("acl")) {
// Load .acl file
if !path.exists() {
return Err(CodeError::Config(format!(
"Config file not found: {}",
path.display()
)));
}
let content = std::fs::read_to_string(path)
.map_err(|e| CodeError::Config(format!("Failed to read ACL file: {}", e)))?;
CodeConfig::from_acl(&content)
.with_context(|| format!("Failed to parse ACL config: {}", path.display()))?
} else if source.trim().starts_with('{') {
// Try to parse as JSON string
serde_json::from_str(&source)
.map_err(|e| CodeError::Config(format!("Failed to parse JSON config: {}", e)))?
} else if source.trim().starts_with("providers \"") {
// ACL string (starts with ACL labeled block like providers "openai" { })
CodeConfig::from_acl(&source).context("Failed to parse config as ACL string")?
return Err(CodeError::Config(
"JSON config is not supported; use ACL-compatible .acl/.hcl config".into(),
)
.into());
} else if matches!(path.extension().and_then(|ext| ext.to_str()), Some("json")) {
return Err(CodeError::Config(
"JSON config files are not supported; use .acl or legacy .hcl".into(),
)
.into());
} else {
// Try to parse as ACL string (legacy format without quotes)
CodeConfig::from_acl(&source).context("Failed to parse config as ACL string")?
};

Self::from_config(config).await
}

/// Create from a config file path or inline HCL string.
/// Create from a config file path or inline ACL-compatible string.
///
/// Alias for [`Agent::new()`] — provides a consistent API with
/// the Python and Node.js SDKs.
Expand Down Expand Up @@ -3382,7 +3372,7 @@ dir content
async fn test_new_with_existing_hcl_file_uses_file_loading() {
let temp_dir = tempfile::tempdir().unwrap();
let config_path = temp_dir.path().join("agent.hcl");
std::fs::write(&config_path, "this is not valid hcl").unwrap();
std::fs::write(&config_path, "this is not valid acl").unwrap();

let err = Agent::new(config_path.display().to_string())
.await
Expand All @@ -3391,7 +3381,7 @@ dir content

assert!(msg.contains("Failed to load config"));
assert!(msg.contains("agent.hcl"));
assert!(!msg.contains("Failed to parse config as HCL string"));
assert!(!msg.contains("Failed to parse config as ACL string"));
}

#[tokio::test]
Expand All @@ -3406,7 +3396,7 @@ dir content

assert!(msg.contains("Config file not found"));
assert!(msg.contains("agent.hcl"));
assert!(!msg.contains("Failed to parse config as HCL string"));
assert!(!msg.contains("Failed to parse config as ACL string"));
}

#[test]
Expand Down
Loading
Loading