A3S Code is an embeddable Rust coding agent framework. In 20 lines of code, you can build a terminal assistant with Claude Code's core capabilities.
import { Agent } from '@a3s-lab/code';
import * as readline from 'readline';
const agent = await Agent.create('agent.hcl');
const session = agent.session(process.cwd());
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
console.log('A3S Code — type your question, Ctrl+C to exit\n');
while (true) {
const input = await new Promise<string>((resolve) => rl.question('> ', resolve));
if (!input.trim()) continue;
const events = await session.stream(input);
for await (const event of events) {
if (event.type === 'text_delta') process.stdout.write(event.text);
else if (event.type === 'tool_use') console.log(`\n[${event.tool}]`);
else if (event.type === 'end') console.log('\n');
}
}That's all the core code. It already has: multi-turn conversation, streaming output, tool execution (read files, write files, run commands), and codebase context understanding.
Why so simple? Because Claude Code is a terminal tool with no programming API. OpenCode is also a CLI — it can't be embedded. A3S Code is a library — callable directly from your code, embeddable in any product.
Why is the coding agent the core of Agentic AI? True "autonomy" requires one condition: the Agent can judge whether its results are correct without relying on humans to tell it. A customer service agent needs humans to evaluate response quality. A writing agent needs humans to judge content. They are fundamentally "executors," not "autonomous agents." The coding agent is different: the compiler either passes or errors, tests are either green or red. This objective, automatable feedback loop enables genuine autonomy — execute, verify, find errors, self-correct, repeat — without human intervention. Once this capability is established, it transfers to any structured environment with objective feedback: data pipelines, infrastructure configuration, API integration. That's why the coding agent isn't just a vertical application of AI — it's the core form of the entire Agentic AI field.
| Claude Code | OpenCode | OpenClaw | A3S Code | |
|---|---|---|---|---|
| Core positioning | Developer terminal tool | Open ecosystem terminal tool | Messaging platform personal assistant | Embeddable Agent infrastructure |
| Usage | CLI interaction | CLI interaction | Messaging platform conversation | Code invocation (library) |
| Embeddability | No programming API | No programming API | No programming API | Core design goal |
| Extension | MCP + Skills + Hooks | MCP + plugins | Plugins | Trait extension points + MCP |
| Multi-tenancy | Not supported | Not supported | Not supported | Supported |
| Parallel tasks | Sub-agent parallelism | Limited | Not supported | Lane queue + multi-machine dispatch |
| Open source | No | Yes | Yes | Yes |
This isn't a comparison of feature quality — it's a comparison of design goals. If you're using a coding agent, Claude Code is the most mature choice. If you're building a product that includes coding agent capability, A3S Code provides the infrastructure you need.
I. Why Does 20 Lines Suffice? Minimal Core + Tool Bootstrapping
This is the most important architectural principle for building Agentic AI systems, and the design foundation of A3S Code.
What is a minimal core?
A minimal core means: the non-replaceable parts of the system should be as small as possible.
The true core of a coding Agent is only five components:
Agent ← Configuration loading, session lifecycle management
AgentSession ← Workspace-bound execution context
AgentLoop ← Execution engine driving LLM turns
ToolExecutor ← Tool registration, dispatch, execution
LlmClient ← Unified abstraction over LLM providersThese five components are the skeleton of the system. Their relationships are fixed: Agent creates AgentSession, AgentSession holds AgentLoop, AgentLoop calls LlmClient for decisions each turn, then executes tools through ToolExecutor.
This skeleton should not change with business requirements. It is stable, testable, and independently reasoned about.
What is tool bootstrapping?
Tool bootstrapping means: all of the Agent's extended capabilities are implemented through tools, not by modifying the core.
This principle has two layers:
Layer one: built-in tools cover foundational capabilities. File read/write, code search, command execution, network requests — these are the basic actions of a coding Agent, available as built-in tools, ready out of the box.
Layer two: external tools extend specialized capabilities. Database operations, code review, deployment pipelines, third-party APIs — these are integrated via MCP (Model Context Protocol) or custom tools, without touching core code.
The key insight of tool bootstrapping is: the LLM itself is the best tool router. You don't need to write complex intent-recognition logic. Just give the LLM clear enough tool descriptions, and it will decide when to use which tool.
The result of this design: the system's capability boundary is determined by the tool set, and the tool set can be dynamically extended at runtime, while core code stays unchanged.
Why does this principle matter?
Consider the opposite: what happens if you bake permission control, memory management, skills systems, and MCP integration into the core?
The core becomes bloated. Every new requirement requires modifying the core. Different features become coupled. Testing becomes difficult. Maintenance cost grows exponentially.
Minimal core + tool bootstrapping dissolves all of these problems: the core does one thing (drive LLM turns), everything else is a pluggable extension.
In A3S Code, this principle manifests as 19 trait extension points, each with a default implementation:
// Not happy with the default permission system? Implement your own.
class MyPermissionChecker implements PermissionChecker {
async check(tool: string, args: unknown): Promise<Permission> {
// your logic
}
}
const session = agent.session('.', {
permissionChecker: new MyPermissionChecker(),
});Works out of the box. Any part is replaceable. Core stays stable.
Another implication of tool bootstrapping: no pre-processing of knowledge required. Traditional RAG thinking is "pre-vectorize or graph-index knowledge, then retrieve and inject." But tool bootstrapping reveals another path: the Agent doesn't need to know everything upfront — it can read what it needs through tools when it needs it. A codebase doesn't need to be vectorized in advance — the Agent reads it directly with read_file and search_code tools on demand. This isn't just saving the engineering cost of pre-processing. More importantly: tool calls are the most precise form of context retrieval, because the Agent itself decides what to read, rather than a retrieval system guessing what's relevant.
II. A3S Code's Full Capability Boundary
Minimal core + tool bootstrapping is the architectural principle, but A3S Code's actual capability boundary goes much further. Here's a systematic overview of all core features — memory system, context retrieval, hooks, security layer, planner, and more.
2.1 Memory System: Long-Term Memory Across Sessions
MemoryStore lets the Agent retain key information across multiple sessions. Unlike SessionStore (which saves conversation history), MemoryStore stores distilled knowledge — user preferences, project context, important decisions.
import { MemoryStore, Memory } from '@a3s-lab/code';
class VectorMemoryStore implements MemoryStore {
async save(memory: Memory): Promise<void> {
// Store in a vector database (e.g. Pinecone, Qdrant)
await vectorDB.upsert({
id: memory.id,
vector: await embed(memory.content),
metadata: { timestamp: memory.timestamp, tags: memory.tags },
});
}
async search(query: string, limit: number): Promise<Memory[]> {
// Semantic search for relevant memories
const results = await vectorDB.query(await embed(query), limit);
return results.map((r) => ({
id: r.id,
content: r.metadata.content,
timestamp: r.metadata.timestamp,
tags: r.metadata.tags,
}));
}
}
const session = agent.session('.', {
memoryStore: new VectorMemoryStore(),
});
// Agent automatically retrieves relevant context from memory
await session.stream('Continue the refactoring task from last time');
// → Agent retrieves: "Last refactoring goal: migrate auth module to JWT"2.2 Context Retrieval (RAG): Dynamically Inject External Knowledge
ContextProvider automatically retrieves relevant documents before each turn and injects them into the LLM context. This is the standard RAG (Retrieval-Augmented Generation) implementation.
import { ContextProvider, ContextChunk } from '@a3s-lab/code';
class CodebaseContextProvider implements ContextProvider {
async retrieve(query: string, maxChunks: number): Promise<ContextChunk[]> {
// Retrieve relevant code snippets from the codebase index
const results = await codeSearch.search(query, maxChunks);
return results.map((r) => ({
content: r.code,
source: r.filePath,
score: r.relevance,
}));
}
}
const session = agent.session('.', {
contextProvider: new CodebaseContextProvider(),
});
// User question triggers automatic retrieval
await session.stream('Where is the JWT validation logic in the auth module?');
// → ContextProvider retrieves relevant code from src/auth/jwt.ts
// → LLM answers based on the retrieved code2.3 Hooks System: Event-Driven Automation
Hooks execute automatically before and after tool calls, for logging, auditing, and automated workflows.
import { HookHandler, ToolCallEvent, ToolResultEvent } from '@a3s-lab/code';
class AuditHookHandler implements HookHandler {
async onToolCall(event: ToolCallEvent): Promise<void> {
// Before tool call: write audit log
await auditLog.write({
timestamp: Date.now(),
tool: event.tool,
args: event.args,
sessionId: event.sessionId,
});
// High-risk operations: send notification
if (['bash', 'write', 'delete'].includes(event.tool)) {
await slack.notify(`⚠️ Agent is executing ${event.tool}`);
}
}
async onToolResult(event: ToolResultEvent): Promise<void> {
// After tool call: record result
await auditLog.write({
timestamp: Date.now(),
tool: event.tool,
success: !event.result.isError,
duration: event.duration,
});
}
}
const session = agent.session('.', {
hookHandler: new AuditHookHandler(),
});2.4 Security Layer: Input Taint Analysis and Output Sanitization
SecurityProvider detects malicious content in inputs before tool execution, and sanitizes sensitive information from outputs.
import { SecurityProvider, TaintAnalysis, SanitizeResult } from '@a3s-lab/code';
class ProductionSecurityProvider implements SecurityProvider {
async analyzeTaint(input: string): Promise<TaintAnalysis> {
// Detect command injection, path traversal, SQL injection
const threats = [];
if (/;\s*(rm|curl|wget|nc)\s/.test(input)) threats.push('command_injection');
if (/\.\.[\\/\\]/.test(input)) threats.push('path_traversal');
if (/(union|select|drop|insert)\s+/i.test(input)) threats.push('sql_injection');
return {
isTainted: threats.length > 0,
threats,
riskLevel: threats.length > 0 ? 'high' : 'low',
};
}
async sanitizeOutput(output: string): Promise<SanitizeResult> {
// Remove sensitive data: API keys, passwords, tokens
let sanitized = output;
sanitized = sanitized.replace(/sk-[a-zA-Z0-9]{48}/g, '[REDACTED_API_KEY]');
sanitized = sanitized.replace(/password["\s:=]+[^\s"]+/gi, 'password=[REDACTED]');
sanitized = sanitized.replace(/Bearer\s+[^\s]+/g, 'Bearer [REDACTED]');
return { sanitized, redacted: sanitized !== output };
}
}
const session = agent.session('.', {
securityProvider: new ProductionSecurityProvider(),
});2.5 Planner: Automatic Decomposition of Complex Tasks
Planner breaks complex tasks into a sequence of sub-tasks that the Agent executes step by step.
import { Planner, Task, Plan } from '@a3s-lab/code';
class HierarchicalPlanner implements Planner {
async plan(goal: string, context: string): Promise<Plan> {
// Use LLM to generate task decomposition
const response = await llm.complete({
prompt: `Goal: ${goal}\nContext: ${context}\n\nDecompose into an executable sequence of sub-tasks.`,
});
const tasks: Task[] = parseTasksFromResponse(response);
return { goal, tasks, estimatedSteps: tasks.length };
}
}
const session = agent.session('.', {
planner: new HierarchicalPlanner(),
});
// Complex task is automatically decomposed
await session.stream('Refactor the entire auth module to use JWT with refresh tokens, update all tests');
// → Planner decomposes into:
// 1. Read existing auth code
// 2. Design JWT + refresh token approach
// 3. Implement new auth logic
// 4. Update tests
// 5. Verify all tests pass2.6 Context Compaction: Automatic Token Budget Management
When conversation history exceeds a threshold, ContextCompactor automatically compresses old turns while preserving key information.
# agent.hcl
context_compaction {
enabled = true
trigger_threshold = 100000 # trigger at 100k tokens
target_size = 50000 # compress to 50k tokens
strategy = "semantic" # semantic compression (preserve key info)
}Compaction strategies:
- Truncate: drop the oldest turns directly
- Summarize: use LLM to summarize old turns, keep the summary
- Semantic: retain the most relevant turns based on semantic similarity
2.7 Multi-Language SDKs: Rust / Node.js / Python
A3S Code is a Rust core library with Node.js and Python native bindings via FFI.
Rust API (zero overhead, maximum performance):
use a3s_code::{Agent, Event};
use futures::StreamExt;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let agent = Agent::new("agent.hcl").await?;
let session = agent.session("/project", None)?;
let mut stream = session.stream("Refactor the auth module").await?;
while let Some(event) = stream.next().await {
match event {
Event::TextDelta(text) => print!("{}", text),
Event::ToolUse(tool) => println!("\n[{}]", tool.name),
Event::End => println!(),
_ => {}
}
}
Ok(())
}Python SDK (async-first, type hints):
from a3s_code import Agent, SessionLane
import asyncio
async def main():
agent = await Agent.create("agent.hcl")
session = agent.session(".", tools=finance_tools)
# Parallel research
tasks = [
{"prompt": f"Analyze {sym}", "lane": SessionLane.GENERATE}
for sym in ["AAPL", "MSFT", "NVDA"]
]
results = await session.submit_batch(tasks)
print(results)
asyncio.run(main())2.8 Feature Summary
| Feature | Purpose | Extension Point |
|---|---|---|
| Memory system | Long-term memory across sessions | MemoryStore |
| Context retrieval | RAG dynamic knowledge injection | ContextProvider |
| Hooks | Event-driven automation | HookHandler |
| Security layer | Input taint analysis, output sanitization | SecurityProvider |
| Planner | Complex task decomposition | Planner |
| Context compaction | Automatic token budget management | Config-driven |
| Multi-language SDKs | Rust / Node.js / Python | Native bindings |
All of these features are optional extensions — the system ships with default implementations and works out of the box. To customize, implement the corresponding trait. Core code stays unchanged.
2.9 Context Engineering: Progressive Disclosure
The core question of context engineering is: how do you give the Agent exactly enough information at each turn within a limited token budget?
The answer is progressive disclosure: the Agent doesn't stuff all knowledge into context at once — it acquires information on demand, in layers.
Take understanding an unfamiliar codebase. The Agent's natural behavior is:
- Read directory structure → understand global layout (low cost, high coverage)
- Read top-level comments of key files → understand module responsibilities (medium cost)
- Locate specific functions → read implementation details (high cost, precise)
Each step discloses only what's needed, keeping context lean. This is more accurate than "vectorize the whole codebase and retrieve" because the Agent itself knows what it needs.
How A3S Code implements this:
// ContextProvider: automatically retrieves relevant context before each turn
class ProgressiveContextProvider implements ContextProvider {
async retrieve(query: string, maxChunks: number): Promise<ContextChunk[]> {
// Layer 1: directory structure (always injected)
const structure = await getProjectStructure();
// Layer 2: semantically relevant file summaries
const summaries = await searchFileSummaries(query, maxChunks - 1);
// Layer 3: precise code snippets (on demand)
const snippets = await searchCodeSnippets(query, 2);
return [structure, ...summaries, ...snippets];
}
}
const session = agent.session('.', {
contextProvider: new ProgressiveContextProvider(),
// MemoryStore retains key findings across sessions, avoiding re-exploration
memoryStore: new FileMemoryStore('.a3s/memory'),
});ContextProvider controls what gets injected each turn, MemoryStore retains key knowledge across sessions, and ContextCompactor automatically compresses old turns when the token budget is exceeded — together these three form A3S Code's context engineering system.
III. A3S Code + A3S Box: Providing a Sandbox Environment for Agents
When Agents execute code, they run on the host machine by default — meaning risks like malicious code, accidental deletion, and resource exhaustion are directly exposed to your system. A3S Box is a lightweight MicroVM sandbox that provides a fully isolated environment for Agent code execution.
3.1 What is A3S Box?
A3S Box is an embedded MicroVM sandbox based on libkrun, with these characteristics:
- Lightweight: startup time < 100ms, memory footprint < 50MB
- No daemon: embeds directly in your application, no Docker daemon required
- Complete isolation: independent filesystem, network, process space
- TEE support: optional Trusted Execution Environment (AMD SEV-SNP)
State machine: Created → Ready → Busy → Stopped
3.2 Why Do Agents Need a Sandbox?
Consider this scenario: a user asks the Agent to "clean up temporary files in the project," and the Agent executes rm -rf /tmp/* — but due to a path resolution error, it actually runs rm -rf /*.
Without sandbox: host filesystem destroyed.
With sandbox: only the Box filesystem is affected; host is safe.
Other risk scenarios:
- Agent downloads and executes a malicious script
- Agent starts a process that consumes all CPU
- Agent accidentally exposes sensitive files (like
.env)
A3S Box isolates all these risks inside the sandbox.
3.3 Integration: Route Agent Code Execution to Box
import { Agent } from '@a3s-lab/code';
import { BoxSdk } from '@a3s-lab/box';
async function main() {
// Create Box SDK instance
const boxSdk = new BoxSdk();
// Create sandbox: Alpine Linux, 512MB memory, mount current directory
const sandbox = boxSdk.create({
image: 'alpine:latest',
memoryMb: 512,
mounts: [{ hostPath: process.cwd(), guestPath: '/workspace', readonly: false }],
workdir: '/workspace',
});
// Define Box tools: execute commands inside the sandbox
const boxTools = [
{
name: 'bash',
description: 'Execute shell commands in an isolated sandbox environment',
parameters: {
type: 'object',
properties: {
command: { type: 'string', description: 'Shell command' },
},
required: ['command'],
},
execute: async (args: { command: string }) => {
const result = await sandbox.exec('sh', ['-c', args.command]);
return {
content: result.stdout,
isError: result.exitCode !== 0,
};
},
},
];
// Agent's bash tool now routes to Box
const agent = await Agent.create('agent.hcl');
const session = agent.session('.', { tools: boxTools });
// User requests a dangerous operation, but only affects the sandbox
await session.stream('Delete all .tmp files');
// → Agent calls bash tool
// → Command executes inside Box, host is safe
// Stop sandbox after task completes
sandbox.stop();
}3.4 Extended Box Tools: File Upload/Download, Snapshots
Beyond command execution, Box also supports file transfer and snapshot management:
const boxTools = [
{
name: 'box_exec',
description: 'Execute command in sandbox',
execute: async (args: { command: string }) => {
const result = await sandbox.exec('sh', ['-c', args.command]);
return { content: result.stdout, isError: result.exitCode !== 0 };
},
},
{
name: 'box_upload',
description: 'Upload file to sandbox',
parameters: {
type: 'object',
properties: {
localPath: { type: 'string' },
guestPath: { type: 'string' },
},
required: ['localPath', 'guestPath'],
},
execute: async (args: { localPath: string; guestPath: string }) => {
const data = await fs.readFile(args.localPath);
await sandbox.upload(data, args.guestPath);
return { content: `Uploaded ${args.localPath} → ${args.guestPath}` };
},
},
{
name: 'box_download',
description: 'Download file from sandbox',
parameters: {
type: 'object',
properties: {
guestPath: { type: 'string' },
localPath: { type: 'string' },
},
required: ['guestPath', 'localPath'],
},
execute: async (args: { guestPath: string; localPath: string }) => {
const data = await sandbox.download(args.guestPath);
await fs.writeFile(args.localPath, data);
return { content: `Downloaded ${args.guestPath} → ${args.localPath}` };
},
},
];3.5 Skill Mechanism: Teaching the Agent How to Operate Box
Through Skill files, you can teach the Agent when to use Box and how to safely operate the sandbox.
<!-- skills/box_operator.md -->
# Box Sandbox Operator
## Available Tools
- `box_exec`: Execute command in sandbox
- `box_upload`: Upload file to sandbox
- `box_download`: Download file from sandbox
## Operating Guidelines
### When to Use Sandbox
The following operations **must** execute in the sandbox, not on the host:
- Execute user-provided scripts or commands
- Install third-party dependencies (npm install, pip install)
- Run tests (may contain malicious test cases)
- Compile code from unknown sources
- Any file operations involving `rm`, `mv`, `chmod`
### Safety Checklist
Before executing any command, check:
1. Does the command contain dangerous patterns like `rm -rf /`, `:(){ :|:& };:`
2. Does it require root privileges (no root in sandbox)
3. Does it need network access (isolated by default)
### File Transfer Guidelines
- Validate file size before upload (< 100MB)
- Validate path before download (must not contain `..`)
- Sensitive files (.env, id_rsa) must not be uploaded to sandbox
## Example Workflow
User request: "Run the project's test suite"
1. Check test command (e.g. `npm test`)
2. Upload project files to sandbox `/workspace`
3. Execute `npm install && npm test` in sandbox
4. Download test report to host
5. Stop sandbox to release resourcesAfter loading the Skill, the Agent automatically follows these guidelines:
const session = agent.session('.', {
tools: boxTools,
skills: ['box_operator'],
});
// Agent automatically determines sandbox is needed
await session.stream('Run the tests, then clean up all temporary files');
// → Agent reads box_operator skill
// → Recognizes "run tests" and "clean files" as dangerous operations
// → Automatically uses box_exec to execute in sandbox
// → Host is safe3.6 Complete Example: Sandboxed Coding Agent
import { Agent } from '@a3s-lab/code';
import { BoxSdk } from '@a3s-lab/box';
import * as fs from 'fs/promises';
async function main() {
const boxSdk = new BoxSdk();
const sandbox = boxSdk.create({
image: 'node:20-alpine',
memoryMb: 1024,
mounts: [{ hostPath: process.cwd(), guestPath: '/workspace' }],
workdir: '/workspace',
network: true, // Allow network access (npm install)
});
const boxTools = [
{
name: 'bash',
description: 'Execute command in isolated sandbox',
execute: async (args: { command: string }) => {
const result = await sandbox.exec('sh', ['-c', args.command]);
return { content: result.stdout, isError: result.exitCode !== 0 };
},
},
];
const agent = await Agent.create('agent.hcl');
const session = agent.session('.', {
tools: boxTools,
skills: ['box_operator'],
});
console.log('Sandboxed coding Agent ready. All code execution happens in an isolated environment.\n');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.on('close', () => {
sandbox.stop();
process.exit(0);
});
while (true) {
const input = await new Promise<string>((resolve) => rl.question('> ', resolve));
if (!input.trim()) continue;
const events = await session.stream(input);
for await (const event of events) {
if (event.type === 'text_delta') process.stdout.write(event.text);
else if (event.type === 'tool_use') process.stdout.write(`\n[sandbox] ${event.tool} `);
else if (event.type === 'end') console.log('\n');
}
}
}
main().catch(console.error);3.7 Other Box Capabilities
Persistent workspace: share the same sandbox state across multiple sessions
const sandbox = boxSdk.create({
image: 'ubuntu:22.04',
workspace: { name: 'my-project', guestPath: '/workspace' },
});
// Workspace content persists after sandbox stops, restored on next startPort forwarding: access services inside the sandbox
const sandbox = boxSdk.create({
image: 'node:20',
portForwards: [{ guestPort: 3000, hostPort: 3000 }],
});
// Port 3000 inside sandbox maps to host port 3000TEE mode: Trusted Execution Environment (requires hardware support)
const sandbox = boxSdk.create({
image: 'alpine:latest',
tee: true, // Enable AMD SEV-SNP
});
// Sandbox memory is encrypted, host cannot readIV. Building a Claude Code-Like Product with the TypeScript SDK
With the architectural principles understood, let's put them into practice. We'll use the A3S Code TypeScript SDK to build a terminal coding assistant with Claude Code's core capabilities.
Target product features:
- Multi-turn conversation with codebase context understanding
- Streaming output showing the Agent's thinking in real time
- Tool execution (read files, write files, run commands)
- Permission control requiring user confirmation for sensitive operations
- MCP server support for dynamically extending the tool set
- Session persistence supporting resumption of previous conversations
4.1 Installation and Configuration
npm install @a3s-lab/codeCreate an agent.hcl config file:
# Supports any OpenAI-compatible endpoint
default_model = "anthropic/claude-sonnet-4-20250514"
providers {
name = "anthropic"
api_key = env("ANTHROPIC_API_KEY")
}
# Optional: connect MCP servers
mcp_servers {
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
}4.2 Core Session Loop
This is the skeleton of the entire product. The upgraded version shows complete event handling: thinking process, tool result display, token usage stats, and abort/resume:
import { Agent, AgentSession, StreamEvent } from '@a3s-lab/code';
import * as readline from 'readline';
const agent = await Agent.create('agent.hcl');
const session = agent.session(process.cwd());
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
// Graceful exit: wait for current turn to finish on Ctrl+C
let currentAbort: AbortController | null = null;
rl.on('close', async () => {
currentAbort?.abort();
await session.flush(); // wait for persistence to complete
process.exit(0);
});
console.log('A3S Code — type your question, Ctrl+C to exit\n');
while (true) {
const input = await new Promise<string>((resolve) => rl.question('> ', resolve));
if (!input.trim()) continue;
currentAbort = new AbortController();
const startTime = Date.now();
try {
const events = await session.stream(input, { signal: currentAbort.signal });
let toolCount = 0;
let inputTokens = 0;
let outputTokens = 0;
for await (const event of events) {
switch (event.type) {
case 'thinking':
// Agent's reasoning process (only on models with extended thinking)
process.stdout.write(`\x1b[2m${event.thinking}\x1b[0m`);
break;
case 'text_delta':
process.stdout.write(event.text);
break;
case 'tool_use':
toolCount++;
process.stdout.write(`\n\x1b[36m[${event.tool}]\x1b[0m `);
break;
case 'tool_result':
// Tool execution complete: show result preview
const preview = typeof event.result.content === 'string'
? event.result.content.slice(0, 80).replace(/\n/g, ' ')
: '[binary]';
process.stdout.write(
event.result.isError
? `\x1b[31m✗ ${preview}\x1b[0m\n`
: `\x1b[32m✓\x1b[0m ${preview}…\n`
);
break;
case 'usage':
// Token usage (fires at end of each turn)
inputTokens = event.inputTokens;
outputTokens = event.outputTokens;
break;
case 'end':
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(
`\n\x1b[2m[${toolCount} tools · ${inputTokens}↑ ${outputTokens}↓ tokens · ${elapsed}s]\x1b[0m\n`
);
break;
}
}
} catch (err) {
if ((err as Error).name === 'AbortError') {
console.log('\n[aborted]\n');
} else {
console.error(`\n\x1b[31mError: ${(err as Error).message}\x1b[0m\n`);
}
} finally {
currentAbort = null;
}
}4.3 Adding Permission Control
A core design of Claude Code is that dangerous operations require user confirmation. We implement the same effect with A3S Code's permission system:
import { Agent, PermissionChecker, Permission } from '@a3s-lab/code';
import * as readline from 'readline';
// High-risk tools that require confirmation
const DANGEROUS_TOOLS = new Set(['bash', 'write', 'edit', 'patch']);
class InteractivePermissionChecker implements PermissionChecker {
private rl: readline.Interface;
constructor(rl: readline.Interface) {
this.rl = rl;
}
async check(tool: string, args: unknown): Promise<Permission> {
// Read-only tools pass through immediately
if (!DANGEROUS_TOOLS.has(tool)) {
return Permission.Allow;
}
// Show the operation about to be executed
const preview = this.formatPreview(tool, args);
console.log(`\n⚠️ Agent wants to execute:\n${preview}`);
const answer = await new Promise<string>((resolve) => {
this.rl.question('Allow? [y/N] ', resolve);
});
return answer.toLowerCase() === 'y' ? Permission.Allow : Permission.Deny;
}
private formatPreview(tool: string, args: unknown): string {
const a = args as Record<string, unknown>;
switch (tool) {
case 'bash':
return ` $ ${a.command}`;
case 'write':
return ` Write file: ${a.file_path}`;
case 'edit':
return ` Edit file: ${a.file_path}`;
default:
return ` ${tool}: ${JSON.stringify(args, null, 2)}`;
}
}
}
// Integrate into session
const session = agent.session(process.cwd(), {
permissionChecker: new InteractivePermissionChecker(rl),
});4.4 Dynamic MCP Server Registration
Claude Code supports declaring MCP servers in config files. A3S Code also supports runtime dynamic registration, letting you load tools on demand based on the user's project type:
// Detect project type and load MCP servers on demand
async function loadProjectMcpServers(session: AgentSession, projectPath: string) {
const fs = await import('fs/promises');
// Detected package.json → load Node.js-related tools
try {
await fs.access(`${projectPath}/package.json`);
const count = await session.addMcpServer(
'nodejs-tools',
'npx',
['-y', '@modelcontextprotocol/server-filesystem', projectPath],
);
console.log(`[MCP] Loaded Node.js toolset, ${count} tools available`);
} catch {}
// Detected Cargo.toml → load Rust-related tools
try {
await fs.access(`${projectPath}/Cargo.toml`);
const count = await session.addMcpServer(
'rust-tools',
'npx',
['-y', '@modelcontextprotocol/server-filesystem', projectPath],
);
console.log(`[MCP] Loaded Rust toolset, ${count} tools available`);
} catch {}
}4.5 Session Persistence
Claude Code supports resuming previous sessions. A3S Code implements the same capability through the SessionStore trait:
import { SessionStore, SessionData } from '@a3s-lab/code';
import * as fs from 'fs/promises';
import * as path from 'path';
class FileSessionStore implements SessionStore {
private storePath: string;
constructor(storePath: string) {
this.storePath = storePath;
}
async save(sessionId: string, data: SessionData): Promise<void> {
const filePath = path.join(this.storePath, `${sessionId}.json`);
await fs.mkdir(this.storePath, { recursive: true });
await fs.writeFile(filePath, JSON.stringify(data, null, 2));
}
async load(sessionId: string): Promise<SessionData | null> {
const filePath = path.join(this.storePath, `${sessionId}.json`);
try {
const content = await fs.readFile(filePath, 'utf-8');
return JSON.parse(content);
} catch {
return null;
}
}
async delete(sessionId: string): Promise<void> {
const filePath = path.join(this.storePath, `${sessionId}.json`);
await fs.unlink(filePath).catch(() => {});
}
}
// Use the project directory as session ID for "one session per project"
const projectId = Buffer.from(process.cwd()).toString('base64url');
const session = agent.session(process.cwd(), {
sessionId: projectId,
sessionStore: new FileSessionStore(path.join(process.env.HOME!, '.a3s/sessions')),
permissionChecker: new InteractivePermissionChecker(rl),
});4.6 Slash Commands
Claude Code has built-in commands like /help, /clear, /cost. A3S Code's SlashCommand interface lets you implement the same:
import { SlashCommand, CommandContext, CommandOutput } from '@a3s-lab/code';
// /cost command: show token usage for this session
class CostCommand implements SlashCommand {
name = 'cost';
description = 'Show token usage and estimated cost for this session';
execute(_args: string, ctx: CommandContext): CommandOutput {
const usage = ctx.tokenUsage;
// Claude Sonnet 4 pricing (for reference)
const inputCost = (usage.promptTokens / 1_000_000) * 3.0;
const outputCost = (usage.completionTokens / 1_000_000) * 15.0;
const total = inputCost + outputCost;
return CommandOutput.text(
`Token usage:\n` +
` Input: ${usage.promptTokens.toLocaleString()} tokens ($${inputCost.toFixed(4)})\n` +
` Output: ${usage.completionTokens.toLocaleString()} tokens ($${outputCost.toFixed(4)})\n` +
` Total: $${total.toFixed(4)}`
);
}
}
// /clear command: clear conversation history
class ClearCommand implements SlashCommand {
name = 'clear';
description = 'Clear conversation history and start a new session';
execute(_args: string, ctx: CommandContext): CommandOutput {
ctx.clearHistory();
return CommandOutput.text('Conversation history cleared');
}
}
session.registerCommand(new CostCommand());
session.registerCommand(new ClearCommand());4.7 Full Product Assembly
Putting it all together:
import { Agent } from '@a3s-lab/code';
import * as readline from 'readline';
import * as path from 'path';
async function main() {
const agent = await Agent.create('agent.hcl');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: true,
});
// Handle Ctrl+C
rl.on('close', () => {
console.log('\nGoodbye!');
process.exit(0);
});
const projectId = Buffer.from(process.cwd()).toString('base64url');
const session = agent.session(process.cwd(), {
sessionId: projectId,
sessionStore: new FileSessionStore(
path.join(process.env.HOME!, '.a3s/sessions')
),
permissionChecker: new InteractivePermissionChecker(rl),
});
// Register commands
session.registerCommand(new CostCommand());
session.registerCommand(new ClearCommand());
// Load MCP servers on demand
await loadProjectMcpServers(session, process.cwd());
// Show welcome message
const isResumed = await session.hasHistory();
if (isResumed) {
console.log('✓ Previous session resumed. Type /clear to start fresh.\n');
} else {
console.log('A3S Code — type your question, /help for commands, Ctrl+C to exit\n');
}
// Main loop
while (true) {
const input = await new Promise<string>((resolve) => {
rl.question('> ', resolve);
});
if (!input.trim()) continue;
// Slash commands
if (input.startsWith('/')) {
const output = await session.executeCommand(input);
console.log(output.text);
continue;
}
// Agent streaming response
try {
const events = await session.stream(input);
for await (const event of events) {
if (event.type === 'text_delta') {
process.stdout.write(event.text);
} else if (event.type === 'tool_use') {
process.stdout.write(`\n[${event.tool}] `);
} else if (event.type === 'end') {
console.log('\n');
}
}
} catch (err) {
console.error(`\nError: ${err instanceof Error ? err.message : err}\n`);
}
}
}
main().catch(console.error);This product already has Claude Code's core capabilities: multi-turn conversation, streaming output, tool execution, permission control, MCP support, session persistence, and slash commands. Under 200 lines of code.
V. Building an Agentic Finance Application
The coding agent isn't just a tool for developers — it's the infrastructure for building Agentic applications in any domain. The difference between a real Agentic application and an ordinary chatbot is this: it can autonomously plan, execute in parallel, and coordinate multiple specialized Agents — rather than passively responding to one instruction at a time.
Here we use the A3S Code TypeScript SDK to build a financial research system that demonstrates four core Agentic features: skills system, auto-planning, Agent Team, and parallel research analysis.
5.1 Skills System: Define Professional Behavior
A skill is a Markdown behavior specification file that tells the Agent what process to follow in a given scenario. It's not code — it's structured professional knowledge.
<!-- skills/portfolio_analyst.md -->
# Portfolio Analyst
## Analysis Process
When receiving a portfolio analysis request, **you must** execute the following steps in order
without skipping any:
1. **Data collection**: call get_portfolio for all holdings, call get_quote for live prices
2. **Parallel research**: launch independent analysis tasks for each holding simultaneously
(use submit_batch)
3. **Risk assessment**: calculate overall portfolio volatility, concentration risk, correlation
4. **Generate report**: output a structured report with holdings overview, risk ratings,
and rebalancing recommendations
## Report Format
\`\`\`
## Portfolio Analysis Report
**Time**: {timestamp}
**Total Value**: ${total_value} **Total P&L**: ${total_pnl} ({pnl_pct}%)
### Holdings
| Symbol | Shares | Cost | Price | P&L | Risk |
...
### Risk Assessment
- Concentration: {concentration}
- Highest-risk position: {symbol}
- Recommendation: {recommendation}
\`\`\`
## Notes
- All recommendations must be data-driven, no speculation
- High-risk positions must be explicitly flagged
- Rebalancing suggestions must include rationaleLoad the skill in HCL config:
# agent.hcl
default_model = "anthropic/claude-sonnet-4-20250514"
providers {
name = "anthropic"
api_key = env("ANTHROPIC_API_KEY")
}
skills_dir = "./skills"The value of the skills system is reusable professional knowledge: the same analysis process can be loaded by different Agent instances, ensuring behavioral consistency and making it easy for teams to maintain collaboratively.
5.2 Auto-Planning: One Sentence Triggers the Full Flow
When the user says "give me a complete portfolio analysis," the Agent doesn't answer directly — it plans first, then executes.
import { Agent, SessionLane } from '@a3s-lab/code';
const agent = await Agent.create('agent.hcl');
// Load the portfolio analyst skill
const session = agent.session('.', {
tools: financeTools,
skills: ['portfolio_analyst'], // inject professional behavior spec
});
// The user provides one sentence; the Agent automatically plans and executes:
// 1. call get_portfolio to fetch holdings
// 2. call get_quote to batch-fetch prices
// 3. launch parallel research tasks (one per holding)
// 4. aggregate results and generate structured report
const events = await session.stream(
'Give me a complete portfolio analysis, focusing on risk exposure and rebalancing recommendations'
);
for await (const event of events) {
if (event.type === 'text_delta') process.stdout.write(event.text);
else if (event.type === 'tool_use') console.log(`\n→ [${event.tool}]`);
else if (event.type === 'end') console.log('\n');
}The key here: the skill file defines the planning logic, and the Agent autonomously decides the call order and tool combinations. You don't write any orchestration code.
5.3 Parallel Research: Analyze Multiple Stocks Simultaneously
Deep research on multiple stocks is slow when done serially. submitBatch runs tasks in parallel with progress tracking and partial failure handling:
import { Agent, SessionLane, BatchResult } from '@a3s-lab/code';
async function parallelStockResearch(symbols: string[]) {
const agent = await Agent.create('agent.hcl');
const session = agent.session('.', { tools: financeTools });
const researchTasks = symbols.map((sym) => ({
id: sym, // task ID for progress tracking
prompt: `Deep analysis of ${sym}:
1. Fetch live quote and volume
2. Assess current risk level (low/medium/high)
3. Give a data-driven buy/hold/sell recommendation
Output structured JSON: { symbol, price, volume, risk, recommendation, rationale }`,
lane: SessionLane.Generate,
}));
console.log(`Launching ${symbols.length} parallel research tasks...\n`);
// Progress callback: print each result as it completes
const results = await session.submitBatch(researchTasks, {
onProgress: (completed, total, result: BatchResult) => {
const icon = result.error ? '✗' : '✓';
const summary = result.error
? result.error.message
: JSON.parse(result.output).recommendation ?? 'done';
console.log(`[${completed}/${total}] ${icon} ${result.id}: ${summary}`);
},
// Partial failure: collect all results before handling errors
continueOnError: true,
});
const succeeded = results.filter((r) => !r.error);
const failed = results.filter((r) => r.error);
if (failed.length > 0) {
console.warn(`\n⚠ ${failed.length} tasks failed: ${failed.map((r) => r.id).join(', ')}`);
}
// Pass successful results to a summary Agent
const summarySession = agent.session('.', { tools: financeTools });
const summaryEvents = await summarySession.stream(
`Here are the research results (${succeeded.length}/${symbols.length} succeeded):\n` +
`${JSON.stringify(succeeded.map((r) => JSON.parse(r.output)), null, 2)}\n\n` +
`Synthesize the above into a portfolio optimization report with risk diversification ` +
`and position sizing recommendations.` +
(failed.length > 0 ? `\nNote: data missing for ${failed.map((r) => r.id).join(', ')} — treat conservatively.` : '')
);
for await (const event of summaryEvents) {
if (event.type === 'text_delta') process.stdout.write(event.text);
else if (event.type === 'end') console.log('\n');
}
}
await parallelStockResearch(['AAPL', 'MSFT', 'NVDA', 'TSLA']);5.4 Agent Team: Specialized Collaboration
Complex financial analysis requires different areas of expertise working together. Agent Team lets you assign tasks to specialized Worker Agents, coordinated by a Lead Agent.
import { Agent, AgentTeam } from '@a3s-lab/code';
async function buildFinanceTeam() {
// Lead Agent: receives user requests, plans tasks, delegates to Workers, aggregates results
const lead = await Agent.create('agent.hcl');
const leadSession = lead.session('.', {
tools: financeTools,
skills: ['portfolio_analyst'],
role: 'lead',
});
// Worker 1: Market Analyst — focused on price data and technical indicators
const marketAnalyst = await Agent.create('agent.hcl');
const marketSession = marketAnalyst.session('.', {
tools: [financeTools.find((t) => t.name === 'get_quote')!],
systemPrompt: 'You are a market analyst specializing in price data, volume analysis, and price trends.',
role: 'worker',
workerId: 'market_analyst',
});
// Worker 2: Risk Analyst — focused on risk assessment and stress testing
const riskAnalyst = await Agent.create('agent.hcl');
const riskSession = riskAnalyst.session('.', {
tools: [financeTools.find((t) => t.name === 'risk_assessment')!],
systemPrompt: 'You are a risk analyst specializing in volatility calculation, risk exposure, and stress testing.',
role: 'worker',
workerId: 'risk_analyst',
});
// Form the team — Lead can delegate sub-tasks to Workers
const team = AgentTeam.create({
lead: leadSession,
workers: [marketSession, riskSession],
});
// Lead receives the user request and automatically decides which sub-tasks go to which Worker.
// Market data → market_analyst, risk calculations → risk_analyst.
// Both Workers run in parallel; Lead aggregates and replies.
const events = await team.stream(
'Analyze my portfolio: market analyst handles price data, risk analyst handles risk assessment, then give me combined recommendations'
);
for await (const event of events) {
if (event.type === 'text_delta') process.stdout.write(event.text);
else if (event.type === 'worker_started') console.log(`\n[Team] ${event.workerId} started`);
else if (event.type === 'worker_completed') console.log(`\n[Team] ${event.workerId} done`);
else if (event.type === 'end') console.log('\n');
}
}
buildFinanceTeam().catch(console.error);5.5 Full System: All Four Features Working Together
Combine the above into a complete financial research terminal:
import { Agent, AgentTeam, SessionLane } from '@a3s-lab/code';
import * as readline from 'readline';
async function main() {
const agent = await Agent.create('agent.hcl');
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
rl.on('close', () => process.exit(0));
// Main session: skills loaded, auto-planning enabled
const session = agent.session('.', {
tools: financeTools,
skills: ['portfolio_analyst'],
role: 'lead',
queueConfig: { queryMaxConcurrency: 4 }, // up to 4 parallel Query lane tasks
});
// Register slash command for parallel research
session.registerCommand({
name: 'research',
description: 'Research multiple stocks in parallel. Usage: /research AAPL MSFT NVDA',
execute: async (args) => {
const symbols = args.trim().split(/\s+/).filter(Boolean);
if (symbols.length === 0) return { text: 'Usage: /research AAPL MSFT NVDA' };
console.log(`\nLaunching ${symbols.length} parallel research tasks...\n`);
const tasks = symbols.map((sym) => ({
prompt: `Analyze ${sym}: quote, risk level, investment recommendation`,
lane: SessionLane.Generate,
}));
const results = await session.submitBatch(tasks);
return { text: JSON.stringify(results, null, 2) };
},
});
console.log('Financial research system ready.\n');
console.log(' > Give me a complete portfolio analysis (auto-planning + skills)');
console.log(' > /research AAPL MSFT NVDA TSLA (parallel research)');
console.log(' > Build a team to deep-dive the tech sector (Agent Team)\n');
while (true) {
const input = await new Promise<string>((resolve) => rl.question('> ', resolve));
if (!input.trim()) continue;
if (input.startsWith('/')) {
const output = await session.executeCommand(input);
console.log(output.text + '\n');
continue;
}
const events = await session.stream(input);
for await (const event of events) {
if (event.type === 'text_delta') process.stdout.write(event.text);
else if (event.type === 'tool_use') process.stdout.write(`\n→ [${event.tool}] `);
else if (event.type === 'end') console.log('\n');
}
}
}
main().catch(console.error);The fundamental difference between this system and an ordinary chatbot: when the user says "do a full analysis," the system autonomously plans the steps, executes research in parallel, coordinates multiple specialized Agents, and aggregates everything into a structured report — without the user needing to guide each step. That's the core value of an Agentic application.
VI. From Single Machine to Multi-Machine: The Value of Lane Queues
The above builds a single-machine version. When your product needs to handle more complex scenarios — serving multiple users simultaneously, or distributing compute-intensive tasks to remote machines — the lane queue provides the solution.
The lane queue divides tool calls into four priority channels:
Control (P0) ← Control instructions, highest priority, sequential execution
Query (P1) ← Read files, search, parallel execution
Execute (P2) ← Write files, run commands, sequential execution
Generate(P3) ← LLM calls, lowest priorityParallel execution in the Query channel is the key optimization: when the LLM returns multiple file-read requests in a single turn, they execute in parallel rather than waiting serially.
import { SessionQueueConfig } from '@a3s-lab/code';
const session = agent.session(process.cwd(), {
queueConfig: {
enableAllFeatures: true,
queryMaxConcurrency: 8, // up to 8 read operations in parallel
},
});When you need to dispatch execution tasks to remote machines:
// Switch Execute channel to external mode
await session.setLaneHandler('execute', {
mode: 'external',
timeoutMs: 120_000,
});
// Listen for external tasks and dispatch to Workers
const events = await session.stream('Run the full test suite and fix all failures');
for await (const event of events) {
if (event.type === 'external_task_pending') {
const tasks = await session.pendingExternalTasks();
for (const task of tasks) {
// Send to remote Worker (your transport layer: gRPC, HTTP, message queue, etc.)
const result = await dispatchToWorker(task);
await session.completeExternalTask(task.task_id, result);
}
}
}This pattern lets you transparently distribute compute-intensive tasks to any number of remote machines without modifying Agent logic.
VII. Why A3S Code Is the Best Choice for Building Agentic Applications
Building applications with genuine Agentic characteristics requires three conditions to be met simultaneously.
First: an autonomous execution loop. The Agent must be able to perceive, decide, act, and verify — forming a closed loop without relying on humans to guide each step. This requires the underlying framework to provide a stable AgentLoop driving mechanism, plus reliable tool execution and concurrency control. A3S Code's minimal core (5 components) is designed exactly for this — the Lane queue ensures execution order, the Query lane reads in parallel, the Execute lane writes sequentially, and the Generate lane manages LLM calls.
Second: complete context engineering capability. The Agent needs exactly enough information at each turn — no more, no less. This isn't a single-point problem; it requires multiple mechanisms working together:
ContextProviderimplements progressive disclosure — retrieves on demand, no need to pre-vectorize an entire knowledge baseMemoryStoreretains key findings across sessions, avoiding redundant explorationContextCompactorautomatically compresses when the token budget is exceeded, preserving semantically critical information
Together these three form a complete context engineering system. No single RAG library or memory system can replace this coordinated mechanism.
Third: embeddable, extensible, production-ready. Agentic capability must be embeddable as infrastructure in your product — not locked inside a CLI tool. A3S Code is a Rust core library with native Node.js and Python bindings:
- 19 trait extension points — any part is replaceable, core stays stable
- Multi-tenancy support — a single Agent instance can serve multiple users
- Multi-machine task dispatch — the Execute Lane can transparently route to remote Workers
- A3S Box integration — code execution is naturally isolated in a MicroVM sandbox
These three conditions: A3S Code is currently the only open-source framework that satisfies all of them simultaneously. From the 20 lines of code at the start of this article to a multi-machine distributed execution system — the same API throughout.
VIII. Conclusion
The core of Agentic AI is the perceive–decide–act loop, and coding tasks are the highest-density test of this loop.
Building reliable Agentic AI systems requires two core principles:
Minimal core — the non-replaceable parts of the system should be as small as possible, containing only the components necessary to drive LLM turns.
Tool bootstrapping — all extended capabilities are implemented through tools. The LLM is the best tool router. Core code stays unchanged.
From the 20 lines of code at the start of this article to a multi-machine distributed execution system, A3S Code's API stays the same throughout. That's the value of a minimal core: stable foundation, unlimited capability expansion.
A3S Code is an open-source Rust coding agent framework with Rust, Node.js, and Python SDKs. View Docs · GitHub