diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f4e0e38..6728bd1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -63,17 +63,17 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.npm
- key: npm-${{ runner.os }}-${{ hashFiles('crates/zapcode-js/package.json', 'examples/typescript/package.json') }}
+ key: npm-${{ runner.os }}-${{ hashFiles('crates/zapcode-js/package.json', 'examples/typescript/basic/package.json') }}
- name: Build JS bindings
working-directory: crates/zapcode-js
run: |
npm install
npx napi build --release --platform --js index.js --dts index.d.ts
- name: Run basic example
- working-directory: examples/typescript
+ working-directory: examples/typescript/basic
run: |
npm install
- npx tsx basic.ts
+ npx tsx main.ts
# ── Python bindings — build + e2e ───────────────────────────────────
e2e-python:
@@ -101,10 +101,10 @@ jobs:
source ${{ github.workspace }}/.venv/bin/activate
maturin develop --release
- name: Run basic example
- working-directory: examples/python
+ working-directory: examples/python/basic
run: |
source ${{ github.workspace }}/.venv/bin/activate
- python basic.py
+ python main.py
# ── WASM — build + smoke test ───────────────────────────────────────
e2e-wasm:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dabc550..a729a0b 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -60,8 +60,8 @@ Key rules:
- Write tests before considering a feature done
- Core tests: `cargo test -p zapcode-core`
- Security tests: `cargo test -p zapcode-core --test security`
-- E2E JS: build bindings then run `examples/typescript/basic.ts`
-- E2E Python: build bindings then run `examples/python/basic.py`
+- E2E JS: `cd crates/zapcode-js && npm install && npx napi build --release --platform --js index.js --dts index.d.ts && cd ../../examples/typescript/basic && npm install && npx tsx main.ts`
+- E2E Python: `cd crates/zapcode-py && maturin develop --release && cd ../../examples/python/basic && python main.py`
## Reporting issues
diff --git a/Cargo.lock b/Cargo.lock
index 8de140a..e6c734e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1544,7 +1544,7 @@ dependencies = [
[[package]]
name = "zapcode-core"
-version = "1.0.0"
+version = "1.0.1"
dependencies = [
"divan",
"indexmap",
@@ -1561,7 +1561,7 @@ dependencies = [
[[package]]
name = "zapcode-js"
-version = "1.0.0"
+version = "1.0.1"
dependencies = [
"napi",
"napi-build",
@@ -1572,7 +1572,7 @@ dependencies = [
[[package]]
name = "zapcode-py"
-version = "1.0.0"
+version = "1.0.1"
dependencies = [
"indexmap",
"pyo3",
@@ -1581,7 +1581,7 @@ dependencies = [
[[package]]
name = "zapcode-wasm"
-version = "1.0.0"
+version = "1.0.1"
dependencies = [
"indexmap",
"js-sys",
diff --git a/README.md b/README.md
index 12c157e..fb88300 100644
--- a/README.md
+++ b/README.md
@@ -150,7 +150,7 @@ if (!state.completed) {
}
```
-See [`examples/typescript/basic.ts`](examples/typescript/basic.ts) for more.
+See [`examples/typescript/basic/main.ts`](examples/typescript/basic/main.ts) for more.
### Python
@@ -187,7 +187,7 @@ if state.get("suspended"):
result = restored.resume({"condition": "Clear", "temp": 26})
```
-See [`examples/python/basic.py`](examples/python/basic.py) for more.
+See [`examples/python/basic/main.py`](examples/python/basic/main.py) for more.
Rust
@@ -225,7 +225,7 @@ if let VmState::Suspended { snapshot, .. } = state {
}
```
-See [`examples/rust/basic.rs`](examples/rust/basic.rs) for more.
+See [`examples/rust/basic/basic.rs`](examples/rust/basic/basic.rs) for more.
@@ -246,7 +246,7 @@ console.log(result.output); // 120
```
-See [`examples/wasm/index.html`](examples/wasm/index.html) for a full playground.
+See [`examples/wasm/basic/index.html`](examples/wasm/basic/index.html) for a full playground.
## AI Agent Usage
@@ -300,7 +300,7 @@ const { text } = await generateText({
Under the hood: the LLM writes TypeScript code that calls your tools → Zapcode executes it in a sandbox → tool calls suspend the VM → your `execute` functions run on the host → results flow back in. All in ~2µs startup + tool execution time.
-See [`examples/typescript/ai-agent-zapcode-ai.ts`](examples/typescript/ai-agent-zapcode-ai.ts) for the full working example.
+See [`examples/typescript/ai-agent/ai-agent-zapcode-ai.ts`](examples/typescript/ai-agent/ai-agent-zapcode-ai.ts) for the full working example.
Anthropic SDK
@@ -365,7 +365,7 @@ while state.get("suspended"):
print(state["output"])
```
-See [`examples/typescript/ai-agent-anthropic.ts`](examples/typescript/ai-agent-anthropic.ts) and [`examples/python/ai_agent_anthropic.py`](examples/python/ai_agent_anthropic.py).
+See [`examples/typescript/ai-agent/ai-agent-anthropic.ts`](examples/typescript/ai-agent/ai-agent-anthropic.ts) and [`examples/python/ai-agent/ai_agent_anthropic.py`](examples/python/ai-agent/ai_agent_anthropic.py).
@@ -452,6 +452,63 @@ langchain_tool = b.custom["langchain"]
The adapter receives an `AdapterContext` with everything needed: system prompt, tool name, tool JSON schema, and a `handleToolCall` function. Return whatever shape your SDK expects.
+## Auto-Fix, Debug & Execution Tracing
+
+### Auto-fix (`autoFix`)
+
+When enabled, execution errors are returned as tool results instead of throwing — letting the LLM see the error and self-correct on the next step.
+
+**TypeScript:**
+```typescript
+const { system, tools } = zapcode({
+ autoFix: true,
+ tools: { /* ... */ },
+});
+```
+
+**Python:**
+```python
+zap = zapcode(auto_fix=True, tools={...})
+```
+
+### Execution Trace
+
+Every execution produces a trace tree with timing for each phase (parse → compile → execute). Use `printTrace()` / `print_trace()` to display the full session trace, or `getTrace()` / `get_trace()` to access the trace programmatically.
+
+**TypeScript:**
+```typescript
+const { system, tools, printTrace, getTrace } = zapcode({
+ autoFix: true,
+ tools: { /* ... */ },
+});
+
+// After running...
+printTrace();
+// ✓ zapcode.session 12.3ms
+// ✓ execute_code 8.1ms
+// ✓ parse 0.2ms
+// ✓ compile 0.1ms
+// ✓ execute 7.8ms
+
+const trace = getTrace(); // TraceSpan tree
+```
+
+**Python:**
+```python
+zap = zapcode(auto_fix=True, tools={...})
+
+# After running...
+zap.print_trace()
+trace = zap.get_trace() # TraceSpan tree
+```
+
+### Debug Logging
+
+For detailed logging of generated code, tool calls, and output, see the debug-tracing examples which show how to inspect each execution step:
+
+- [TypeScript debug-tracing example](examples/typescript/debug-tracing/main.ts)
+- [Python debug-tracing example](examples/python/debug-tracing/main.py)
+
## What Zapcode Can and Cannot Do
**Can do:**
diff --git a/crates/zapcode-core/src/lib.rs b/crates/zapcode-core/src/lib.rs
index d4f6903..1eb32c6 100644
--- a/crates/zapcode-core/src/lib.rs
+++ b/crates/zapcode-core/src/lib.rs
@@ -44,11 +44,13 @@ pub mod error;
pub mod parser;
pub mod sandbox;
pub mod snapshot;
+pub mod trace;
pub mod value;
pub mod vm;
pub use error::ZapcodeError;
pub use sandbox::ResourceLimits;
pub use snapshot::ZapcodeSnapshot;
+pub use trace::{ExecutionTrace, TraceSpan, TraceStatus};
pub use value::Value;
pub use vm::{RunResult, VmState, ZapcodeRun};
diff --git a/crates/zapcode-core/src/trace.rs b/crates/zapcode-core/src/trace.rs
new file mode 100644
index 0000000..9a4e379
--- /dev/null
+++ b/crates/zapcode-core/src/trace.rs
@@ -0,0 +1,179 @@
+//! Execution trace for debugging and observability.
+//!
+//! Captures a tree of spans covering parse → compile → execute → tool calls.
+//! The trace is lightweight and always collected (sub-microsecond overhead).
+//!
+//! The `TraceSpan` shape is designed to map cleanly to OpenTelemetry spans
+//! for future export to Jaeger, Langfuse, Datadog, etc.
+
+use std::time::{Instant, SystemTime, UNIX_EPOCH};
+
+use serde::{Deserialize, Serialize};
+
+/// A single span in the execution trace.
+///
+/// Shaped to be OTel-compatible: each span has a name, timestamps,
+/// status, key-value attributes, and children.
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct TraceSpan {
+ /// Span name (e.g. "parse", "compile", "execute", "tool_call", "suspend").
+ pub name: String,
+ /// When the span started (ms since Unix epoch).
+ pub start_time_ms: u64,
+ /// When the span ended (ms since Unix epoch). 0 if still open.
+ pub end_time_ms: u64,
+ /// Duration in microseconds.
+ pub duration_us: u64,
+ /// "ok" or "error".
+ pub status: TraceStatus,
+ /// Structured attributes. Keys use `zapcode.*` namespace.
+ pub attributes: Vec<(String, String)>,
+ /// Child spans.
+ pub children: Vec,
+}
+
+/// Span status.
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
+pub enum TraceStatus {
+ Ok,
+ Error,
+}
+
+/// Builder for constructing trace spans with proper timing.
+pub(crate) struct SpanBuilder {
+ name: String,
+ start_wall: u64,
+ start_instant: Instant,
+ attributes: Vec<(String, String)>,
+ children: Vec,
+}
+
+impl SpanBuilder {
+ pub fn new(name: &str) -> Self {
+ Self {
+ name: name.to_string(),
+ start_wall: now_ms(),
+ start_instant: Instant::now(),
+ attributes: Vec::new(),
+ children: Vec::new(),
+ }
+ }
+
+ pub fn attr(mut self, key: &str, value: impl ToString) -> Self {
+ self.attributes.push((key.to_string(), value.to_string()));
+ self
+ }
+
+ pub fn set_attr(&mut self, key: &str, value: impl ToString) {
+ self.attributes.push((key.to_string(), value.to_string()));
+ }
+
+ pub fn add_child(&mut self, child: TraceSpan) {
+ self.children.push(child);
+ }
+
+ pub fn finish(self, status: TraceStatus) -> TraceSpan {
+ let elapsed = self.start_instant.elapsed();
+ TraceSpan {
+ name: self.name,
+ start_time_ms: self.start_wall,
+ end_time_ms: self.start_wall + elapsed.as_millis() as u64,
+ duration_us: elapsed.as_micros() as u64,
+ status,
+ attributes: self.attributes,
+ children: self.children,
+ }
+ }
+
+ pub fn finish_ok(self) -> TraceSpan {
+ self.finish(TraceStatus::Ok)
+ }
+
+ pub fn finish_error(self, error: &str) -> TraceSpan {
+ self.attr("zapcode.error", error).finish(TraceStatus::Error)
+ }
+}
+
+fn now_ms() -> u64 {
+ SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .unwrap_or_default()
+ .as_millis() as u64
+}
+
+/// Execution trace covering a full run (parse + compile + execute).
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct ExecutionTrace {
+ pub root: TraceSpan,
+}
+
+impl ExecutionTrace {
+ /// Pretty-print the trace as a tree.
+ pub fn print(&self) {
+ print_span(&self.root, 0, true);
+ }
+
+ /// Format the trace as a tree string.
+ pub fn to_string_pretty(&self) -> String {
+ let mut buf = String::new();
+ format_span(&self.root, 0, true, &mut buf);
+ buf
+ }
+}
+
+fn format_duration(us: u64) -> String {
+ if us < 1000 {
+ format!("{}µs", us)
+ } else if us < 1_000_000 {
+ format!("{:.1}ms", us as f64 / 1000.0)
+ } else {
+ format!("{:.2}s", us as f64 / 1_000_000.0)
+ }
+}
+
+fn format_span(span: &TraceSpan, depth: usize, is_last: bool, buf: &mut String) {
+ let icon = match span.status {
+ TraceStatus::Ok => "✓",
+ TraceStatus::Error => "✗",
+ };
+ let duration = format_duration(span.duration_us);
+
+ // Build prefix
+ let prefix = if depth == 0 {
+ String::new()
+ } else {
+ let connector = if is_last { "└─ " } else { "├─ " };
+ let indent = "│ ".repeat(depth - 1);
+ format!("{}{}", indent, connector)
+ };
+
+ buf.push_str(&format!("{}{} {} ({})", prefix, icon, span.name, duration));
+
+ // Show key attributes inline
+ for (k, v) in &span.attributes {
+ if k == "zapcode.error" {
+ buf.push_str(&format!(" error=\"{}\"", v));
+ } else if k == "zapcode.tool.name" {
+ buf.push_str(&format!(" {}", v));
+ } else if k == "zapcode.tool.args" {
+ buf.push_str(&format!("({})", v));
+ } else if k == "zapcode.tool.result" {
+ let display = if v.len() > 60 { &v[..57] } else { v };
+ buf.push_str(&format!(" → {}", display));
+ if v.len() > 60 {
+ buf.push_str("...");
+ }
+ }
+ }
+ buf.push('\n');
+
+ for (i, child) in span.children.iter().enumerate() {
+ format_span(child, depth + 1, i == span.children.len() - 1, buf);
+ }
+}
+
+fn print_span(span: &TraceSpan, depth: usize, is_last: bool) {
+ let mut buf = String::new();
+ format_span(span, depth, is_last, &mut buf);
+ print!("{}", buf);
+}
diff --git a/crates/zapcode-core/src/vm/mod.rs b/crates/zapcode-core/src/vm/mod.rs
index e4e7337..93a516e 100644
--- a/crates/zapcode-core/src/vm/mod.rs
+++ b/crates/zapcode-core/src/vm/mod.rs
@@ -8,6 +8,7 @@ use crate::compiler::CompiledProgram;
use crate::error::{Result, ZapcodeError};
use crate::sandbox::{ResourceLimits, ResourceTracker};
use crate::snapshot::ZapcodeSnapshot;
+use crate::trace::{ExecutionTrace, SpanBuilder, TraceStatus};
use crate::value::{Closure, FunctionId, GeneratorObject, SuspendedFrame, Value};
mod builtins;
@@ -2201,20 +2202,92 @@ impl ZapcodeRun {
}
pub fn run(&self, input_values: Vec<(String, Value)>) -> Result {
- let program = crate::parser::parse(&self.source)?;
+ let mut root_span = SpanBuilder::new("zapcode.run");
+
+ // Parse
+ let parse_span = SpanBuilder::new("parse");
+ let program = match crate::parser::parse(&self.source) {
+ Ok(p) => {
+ root_span.add_child(parse_span.finish_ok());
+ p
+ }
+ Err(e) => {
+ root_span.add_child(parse_span.finish_error(&e.to_string()));
+ let _trace = ExecutionTrace {
+ root: root_span.finish(TraceStatus::Error),
+ };
+ return Err(e);
+ }
+ };
+
+ // Compile
+ let compile_span = SpanBuilder::new("compile");
let ext_set: HashSet = self.external_functions.iter().cloned().collect();
- let compiled = crate::compiler::compile_with_externals(&program, ext_set.clone())?;
+ let compiled = match crate::compiler::compile_with_externals(&program, ext_set.clone()) {
+ Ok(c) => {
+ root_span.add_child(compile_span.finish_ok());
+ c
+ }
+ Err(e) => {
+ root_span.add_child(compile_span.finish_error(&e.to_string()));
+ let _trace = ExecutionTrace {
+ root: root_span.finish(TraceStatus::Error),
+ };
+ return Err(e);
+ }
+ };
+
+ // Execute
+ let execute_span = SpanBuilder::new("execute");
let mut vm = Vm::new(compiled, self.limits.clone(), ext_set);
- // Inject inputs as globals
for (name, value) in input_values {
vm.globals.insert(name, value);
}
- let state = vm.run()?;
+ let state = match vm.run() {
+ Ok(s) => {
+ let status = match &s {
+ VmState::Complete(_) => TraceStatus::Ok,
+ VmState::Suspended {
+ function_name,
+ args,
+ ..
+ } => {
+ let mut span = execute_span;
+ span.set_attr("zapcode.suspended_on", function_name);
+ span.set_attr("zapcode.args_count", args.len());
+ root_span.add_child(span.finish(TraceStatus::Ok));
+ let trace = ExecutionTrace {
+ root: root_span.finish_ok(),
+ };
+ return Ok(RunResult {
+ state: s,
+ stdout: vm.stdout,
+ trace,
+ });
+ }
+ };
+ root_span.add_child(execute_span.finish(status));
+ s
+ }
+ Err(e) => {
+ root_span.add_child(execute_span.finish_error(&e.to_string()));
+ let _trace = ExecutionTrace {
+ root: root_span.finish(TraceStatus::Error),
+ };
+ return Err(e);
+ }
+ };
+
+ let trace = ExecutionTrace {
+ root: root_span.finish_ok(),
+ };
+
Ok(RunResult {
state,
stdout: vm.stdout,
+ trace,
})
}
@@ -2222,16 +2295,8 @@ impl ZapcodeRun {
/// instead of wrapping it in a `RunResult`. This is the primary entry point
/// for code that needs to handle suspension / snapshot / resume.
pub fn start(&self, input_values: Vec<(String, Value)>) -> Result {
- let program = crate::parser::parse(&self.source)?;
- let ext_set: HashSet = self.external_functions.iter().cloned().collect();
- let compiled = crate::compiler::compile_with_externals(&program, ext_set.clone())?;
- let mut vm = Vm::new(compiled, self.limits.clone(), ext_set);
-
- for (name, value) in input_values {
- vm.globals.insert(name, value);
- }
-
- vm.run()
+ let result = self.run(input_values)?;
+ Ok(result.state)
}
pub fn run_simple(&self) -> Result {
@@ -2250,6 +2315,8 @@ impl ZapcodeRun {
pub struct RunResult {
pub state: VmState,
pub stdout: String,
+ /// Execution trace covering parse → compile → execute.
+ pub trace: ExecutionTrace,
}
/// Quick helper to evaluate a TypeScript expression.
diff --git a/crates/zapcode-core/tests/trace.rs b/crates/zapcode-core/tests/trace.rs
new file mode 100644
index 0000000..2875e5b
--- /dev/null
+++ b/crates/zapcode-core/tests/trace.rs
@@ -0,0 +1,265 @@
+use zapcode_core::{ResourceLimits, TraceSpan, TraceStatus, Value, VmState, ZapcodeRun};
+
+// ---------------------------------------------------------------------------
+// Helpers
+// ---------------------------------------------------------------------------
+
+fn run_code(code: &str) -> zapcode_core::RunResult {
+ let runner =
+ ZapcodeRun::new(code.to_string(), vec![], vec![], ResourceLimits::default()).unwrap();
+ runner.run(vec![]).unwrap()
+}
+
+fn run_with_externals(code: &str, externals: Vec<&str>) -> zapcode_core::RunResult {
+ let runner = ZapcodeRun::new(
+ code.to_string(),
+ vec![],
+ externals.into_iter().map(|s| s.to_string()).collect(),
+ ResourceLimits::default(),
+ )
+ .unwrap();
+ runner.run(vec![]).unwrap()
+}
+
+fn assert_span_timing(span: &TraceSpan) {
+ assert!(span.start_time_ms > 0, "start_time_ms should be non-zero");
+ assert!(
+ span.end_time_ms >= span.start_time_ms,
+ "end_time_ms ({}) should be >= start_time_ms ({})",
+ span.end_time_ms,
+ span.start_time_ms
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Trace structure
+// ---------------------------------------------------------------------------
+
+#[test]
+fn trace_has_root_with_parse_compile_execute_children() {
+ let result = run_code("1 + 2");
+ let root = &result.trace.root;
+
+ assert_eq!(root.name, "zapcode.run");
+ assert_eq!(root.status, TraceStatus::Ok);
+ assert_eq!(root.children.len(), 3);
+ assert_eq!(root.children[0].name, "parse");
+ assert_eq!(root.children[1].name, "compile");
+ assert_eq!(root.children[2].name, "execute");
+}
+
+#[test]
+fn trace_all_children_have_ok_status_on_success() {
+ let result = run_code("const x = 42; x");
+ let root = &result.trace.root;
+
+ assert_eq!(root.status, TraceStatus::Ok);
+ for child in &root.children {
+ assert_eq!(
+ child.status,
+ TraceStatus::Ok,
+ "child '{}' should be Ok",
+ child.name
+ );
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Timing
+// ---------------------------------------------------------------------------
+
+#[test]
+fn trace_has_valid_timing() {
+ let result = run_code("[1, 2, 3].map(x => x * 2)");
+ let root = &result.trace.root;
+
+ assert_span_timing(root);
+ for child in &root.children {
+ assert_span_timing(child);
+ }
+}
+
+#[test]
+fn trace_root_duration_gte_children_sum() {
+ let result = run_code("let sum = 0; for (let i = 0; i < 100; i++) { sum += i; } sum");
+ let root = &result.trace.root;
+
+ let children_duration: u64 = root.children.iter().map(|c| c.duration_us).sum();
+ assert!(
+ root.duration_us >= children_duration,
+ "root duration ({}µs) should be >= sum of children ({}µs)",
+ root.duration_us,
+ children_duration
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Error traces
+// ---------------------------------------------------------------------------
+
+#[test]
+fn trace_parse_error_has_error_status() {
+ let runner = ZapcodeRun::new(
+ "{{{{".to_string(),
+ vec![],
+ vec![],
+ ResourceLimits::default(),
+ )
+ .unwrap();
+ let err = runner.run(vec![]);
+
+ // Parse errors return Err, so we can't inspect the trace from RunResult.
+ // But we verify it doesn't panic.
+ assert!(err.is_err());
+}
+
+#[test]
+fn trace_runtime_error_does_not_panic() {
+ let runner = ZapcodeRun::new(
+ "null.foo".to_string(),
+ vec![],
+ vec![],
+ ResourceLimits::default(),
+ )
+ .unwrap();
+ let err = runner.run(vec![]);
+ assert!(err.is_err());
+}
+
+// ---------------------------------------------------------------------------
+// Suspension trace
+// ---------------------------------------------------------------------------
+
+#[test]
+fn trace_on_suspension_has_execute_with_suspended_attrs() {
+ let result = run_with_externals("const x = await fetchData(); x", vec!["fetchData"]);
+ let root = &result.trace.root;
+
+ assert_eq!(root.status, TraceStatus::Ok);
+ assert_eq!(root.children.len(), 3);
+
+ let execute_span = &root.children[2];
+ assert_eq!(execute_span.name, "execute");
+ assert_eq!(execute_span.status, TraceStatus::Ok);
+
+ // Should have zapcode.suspended_on attribute
+ let suspended_attr = execute_span
+ .attributes
+ .iter()
+ .find(|(k, _)| k == "zapcode.suspended_on");
+ assert!(
+ suspended_attr.is_some(),
+ "execute span should have zapcode.suspended_on attribute"
+ );
+ assert_eq!(suspended_attr.unwrap().1, "fetchData");
+}
+
+#[test]
+fn trace_suspension_state_matches() {
+ let result = run_with_externals("const x = await myFunc(42); x", vec!["myFunc"]);
+
+ // Verify the VM actually suspended
+ match &result.state {
+ VmState::Suspended { function_name, .. } => {
+ assert_eq!(function_name, "myFunc");
+ }
+ VmState::Complete(_) => panic!("expected suspension"),
+ }
+
+ // And the trace captured it
+ let execute_span = &result.trace.root.children[2];
+ let args_count = execute_span
+ .attributes
+ .iter()
+ .find(|(k, _)| k == "zapcode.args_count");
+ assert!(args_count.is_some());
+ assert_eq!(args_count.unwrap().1, "1");
+}
+
+// ---------------------------------------------------------------------------
+// Pretty printing
+// ---------------------------------------------------------------------------
+
+#[test]
+fn trace_pretty_print_contains_span_names() {
+ let result = run_code("1 + 1");
+ let output = result.trace.to_string_pretty();
+
+ assert!(
+ output.contains("zapcode.run"),
+ "should contain root span name"
+ );
+ assert!(output.contains("parse"), "should contain parse span");
+ assert!(output.contains("compile"), "should contain compile span");
+ assert!(output.contains("execute"), "should contain execute span");
+}
+
+#[test]
+fn trace_pretty_print_contains_status_icons() {
+ let result = run_code("true");
+ let output = result.trace.to_string_pretty();
+
+ assert!(output.contains("✓"), "success trace should contain ✓ icon");
+}
+
+#[test]
+fn trace_pretty_print_contains_duration() {
+ let result = run_code("42");
+ let output = result.trace.to_string_pretty();
+
+ // Should contain at least one duration marker (µs or ms)
+ assert!(
+ output.contains("µs") || output.contains("ms"),
+ "trace output should contain duration: {}",
+ output
+ );
+}
+
+// ---------------------------------------------------------------------------
+// Multiple runs produce independent traces
+// ---------------------------------------------------------------------------
+
+#[test]
+fn trace_multiple_runs_are_independent() {
+ let runner = ZapcodeRun::new(
+ "1 + 1".to_string(),
+ vec![],
+ vec![],
+ ResourceLimits::default(),
+ )
+ .unwrap();
+
+ let result1 = runner.run(vec![]).unwrap();
+ let result2 = runner.run(vec![]).unwrap();
+
+ // Each run should produce its own trace (different start times or at least independent objects)
+ assert_eq!(result1.trace.root.children.len(), 3);
+ assert_eq!(result2.trace.root.children.len(), 3);
+}
+
+// ---------------------------------------------------------------------------
+// Trace with inputs
+// ---------------------------------------------------------------------------
+
+#[test]
+fn trace_with_inputs_still_has_full_structure() {
+ let runner = ZapcodeRun::new(
+ "x + y".to_string(),
+ vec!["x".to_string(), "y".to_string()],
+ vec![],
+ ResourceLimits::default(),
+ )
+ .unwrap();
+
+ let result = runner
+ .run(vec![
+ ("x".to_string(), Value::Int(10)),
+ ("y".to_string(), Value::Int(20)),
+ ])
+ .unwrap();
+
+ let root = &result.trace.root;
+ assert_eq!(root.status, TraceStatus::Ok);
+ assert_eq!(root.children.len(), 3);
+ assert!(matches!(result.state, VmState::Complete(Value::Int(30))));
+}
diff --git a/crates/zapcode-js/src/lib.rs b/crates/zapcode-js/src/lib.rs
index a67804f..131fc33 100644
--- a/crates/zapcode-js/src/lib.rs
+++ b/crates/zapcode-js/src/lib.rs
@@ -4,7 +4,10 @@ use std::sync::Arc;
use napi::bindgen_prelude::*;
use napi_derive::napi;
-use zapcode_core::{ResourceLimits, Value, VmState, ZapcodeRun, ZapcodeSnapshot};
+use zapcode_core::{
+ ExecutionTrace, ResourceLimits, TraceSpan, TraceStatus, Value, VmState, ZapcodeRun,
+ ZapcodeSnapshot,
+};
// ---------------------------------------------------------------------------
// Options
@@ -26,6 +29,17 @@ pub struct ZapcodeOptions {
// Result types exposed to JS
// ---------------------------------------------------------------------------
+#[napi(object)]
+pub struct JsTraceSpan {
+ pub name: String,
+ pub start_time_ms: f64,
+ pub end_time_ms: f64,
+ pub duration_us: f64,
+ pub status: String,
+ pub attributes: Vec>,
+ pub children: Vec,
+}
+
#[napi(object)]
pub struct ZapcodeResult {
/// Whether execution completed. Always true for this type.
@@ -34,6 +48,8 @@ pub struct ZapcodeResult {
pub output: serde_json::Value,
/// Captured stdout output.
pub stdout: String,
+ /// Execution trace (parse → compile → execute).
+ pub trace: JsTraceSpan,
}
#[napi(object)]
@@ -92,7 +108,19 @@ impl ZapcodeSnapshotHandle {
.clone()
.resume(value)
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
- vm_state_to_either(state, String::new())
+ // resume() doesn't produce a full trace yet — use an empty one
+ let trace = ExecutionTrace {
+ root: TraceSpan {
+ name: "resume".to_string(),
+ start_time_ms: 0,
+ end_time_ms: 0,
+ duration_us: 0,
+ status: TraceStatus::Ok,
+ attributes: Vec::new(),
+ children: Vec::new(),
+ },
+ };
+ vm_state_to_either(state, String::new(), trace)
}
}
@@ -155,6 +183,7 @@ impl Zapcode {
completed: true,
output: value_to_json(&v),
stdout: result.stdout,
+ trace: trace_to_js(&result.trace),
}),
VmState::Suspended { function_name, .. } => Err(napi::Error::from_reason(format!(
"execution suspended on external function '{}' -- use start() instead",
@@ -177,7 +206,7 @@ impl Zapcode {
.run(input_values)
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
- vm_state_to_either(result.state, result.stdout)
+ vm_state_to_either(result.state, result.stdout, result.trace)
}
}
@@ -251,16 +280,41 @@ fn value_to_json(value: &Value) -> serde_json::Value {
}
}
+fn trace_span_to_js(span: &TraceSpan) -> JsTraceSpan {
+ JsTraceSpan {
+ name: span.name.clone(),
+ start_time_ms: span.start_time_ms as f64,
+ end_time_ms: span.end_time_ms as f64,
+ duration_us: span.duration_us as f64,
+ status: match span.status {
+ TraceStatus::Ok => "ok".to_string(),
+ TraceStatus::Error => "error".to_string(),
+ },
+ attributes: span
+ .attributes
+ .iter()
+ .map(|(k, v)| vec![k.clone(), v.clone()])
+ .collect(),
+ children: span.children.iter().map(trace_span_to_js).collect(),
+ }
+}
+
+fn trace_to_js(trace: &ExecutionTrace) -> JsTraceSpan {
+ trace_span_to_js(&trace.root)
+}
+
/// Package a `VmState` into either a `ZapcodeResult` or `ZapcodeSuspension`.
fn vm_state_to_either(
state: VmState,
stdout: String,
+ trace: ExecutionTrace,
) -> napi::Result> {
match state {
VmState::Complete(v) => Ok(Either::A(ZapcodeResult {
completed: true,
output: value_to_json(&v),
stdout,
+ trace: trace_to_js(&trace),
})),
VmState::Suspended {
function_name,
diff --git a/crates/zapcode-py/src/lib.rs b/crates/zapcode-py/src/lib.rs
index 63b6894..c71bc73 100644
--- a/crates/zapcode-py/src/lib.rs
+++ b/crates/zapcode-py/src/lib.rs
@@ -4,7 +4,10 @@ use pyo3::exceptions::PyRuntimeError;
use pyo3::prelude::*;
use pyo3::types::{PyBool, PyDict, PyFloat, PyInt, PyList, PyString};
-use zapcode_core::{ResourceLimits, Value, VmState, ZapcodeError, ZapcodeSnapshot as CoreSnapshot};
+use zapcode_core::{
+ ExecutionTrace, ResourceLimits, TraceSpan as CoreTraceSpan, TraceStatus, Value, VmState,
+ ZapcodeError, ZapcodeSnapshot as CoreSnapshot,
+};
// ---------------------------------------------------------------------------
// Value conversion: zapcode_core::Value <-> Python object
@@ -157,7 +160,7 @@ impl Zapcode {
fn run(&self, py: Python<'_>, inputs: Option<&Bound<'_, PyDict>>) -> PyResult {
let input_values = extract_inputs(inputs)?;
let result = self.inner.run(input_values).map_err(zapcode_err)?;
- run_result_to_py(py, result.state, &result.stdout)
+ run_result_to_py(py, result.state, &result.stdout, Some(&result.trace))
}
/// Start execution, returning raw state (for suspension / snapshot handling).
@@ -170,13 +173,45 @@ impl Zapcode {
#[pyo3(signature = (inputs=None))]
fn start(&self, py: Python<'_>, inputs: Option<&Bound<'_, PyDict>>) -> PyResult {
let input_values = extract_inputs(inputs)?;
- let state = self.inner.start(input_values).map_err(zapcode_err)?;
- run_result_to_py(py, state, "")
+ let result = self.inner.run(input_values).map_err(zapcode_err)?;
+ run_result_to_py(py, result.state, &result.stdout, Some(&result.trace))
+ }
+}
+
+/// Convert a `TraceSpan` to a Python dict.
+fn trace_span_to_py(py: Python<'_>, span: &CoreTraceSpan) -> PyResult {
+ let dict = PyDict::new(py);
+ dict.set_item("name", &span.name)?;
+ dict.set_item("start_time_ms", span.start_time_ms)?;
+ dict.set_item("end_time_ms", span.end_time_ms)?;
+ dict.set_item("duration_us", span.duration_us)?;
+ dict.set_item(
+ "status",
+ match span.status {
+ TraceStatus::Ok => "ok",
+ TraceStatus::Error => "error",
+ },
+ )?;
+ let attrs = PyDict::new(py);
+ for (k, v) in &span.attributes {
+ attrs.set_item(k, v)?;
+ }
+ dict.set_item("attributes", attrs)?;
+ let children = PyList::empty(py);
+ for child in &span.children {
+ children.append(trace_span_to_py(py, child)?)?;
}
+ dict.set_item("children", children)?;
+ Ok(dict.into_pyobject(py)?.into_any().unbind())
}
-/// Convert a `VmState` (+ optional stdout) to a Python dict.
-fn run_result_to_py(py: Python<'_>, state: VmState, stdout: &str) -> PyResult {
+/// Convert a `VmState` (+ optional stdout + trace) to a Python dict.
+fn run_result_to_py(
+ py: Python<'_>,
+ state: VmState,
+ stdout: &str,
+ trace: Option<&ExecutionTrace>,
+) -> PyResult {
let dict = PyDict::new(py);
match state {
VmState::Complete(value) => {
@@ -199,6 +234,9 @@ fn run_result_to_py(py: Python<'_>, state: VmState, stdout: &str) -> PyResult, return_value: &Bound<'_, PyAny>) -> PyResult {
let val = py_to_value(return_value)?;
let state = self.inner.clone().resume(val).map_err(zapcode_err)?;
- run_result_to_py(py, state, "")
+ run_result_to_py(py, state, "", None)
}
}
diff --git a/crates/zapcode-wasm/src/lib.rs b/crates/zapcode-wasm/src/lib.rs
index f59c1fe..552b1e6 100644
--- a/crates/zapcode-wasm/src/lib.rs
+++ b/crates/zapcode-wasm/src/lib.rs
@@ -4,7 +4,10 @@ use js_sys::{Array, Object, Reflect};
use serde::Deserialize;
use wasm_bindgen::prelude::*;
-use zapcode_core::{ResourceLimits, Value, VmState, ZapcodeError, ZapcodeSnapshot as CoreSnapshot};
+use zapcode_core::{
+ ExecutionTrace, ResourceLimits, TraceSpan as CoreTraceSpan, TraceStatus, Value, VmState,
+ ZapcodeError, ZapcodeSnapshot as CoreSnapshot,
+};
// ---------------------------------------------------------------------------
// Value conversion: zapcode_core::Value <-> JsValue
@@ -170,7 +173,7 @@ impl Zapcode {
pub fn run(&self, inputs: JsValue) -> Result {
let input_values = extract_inputs(&inputs)?;
let result = self.inner.run(input_values).map_err(zapcode_err)?;
- vm_state_to_js(result.state, &result.stdout)
+ vm_state_to_js(result.state, &result.stdout, Some(&result.trace))
}
/// Start execution, returning raw state (for suspension / snapshot handling).
@@ -179,8 +182,8 @@ impl Zapcode {
/// @returns Same shape as `run()`.
pub fn start(&self, inputs: JsValue) -> Result {
let input_values = extract_inputs(&inputs)?;
- let state = self.inner.start(input_values).map_err(zapcode_err)?;
- vm_state_to_js(state, "")
+ let result = self.inner.run(input_values).map_err(zapcode_err)?;
+ vm_state_to_js(result.state, &result.stdout, Some(&result.trace))
}
}
@@ -204,8 +207,63 @@ fn extract_inputs(inputs: &JsValue) -> Result, JsError> {
Ok(out)
}
-/// Convert a `VmState` (+ optional stdout) to a JS object.
-fn vm_state_to_js(state: VmState, stdout: &str) -> Result {
+/// Convert a `TraceSpan` to a JS object.
+fn trace_span_to_js(span: &CoreTraceSpan) -> Result {
+ let obj = Object::new();
+ Reflect::set(&obj, &"name".into(), &JsValue::from_str(&span.name))
+ .map_err(|_| JsError::new("failed to set trace field"))?;
+ Reflect::set(
+ &obj,
+ &"startTimeMs".into(),
+ &JsValue::from(span.start_time_ms as f64),
+ )
+ .map_err(|_| JsError::new("failed to set trace field"))?;
+ Reflect::set(
+ &obj,
+ &"endTimeMs".into(),
+ &JsValue::from(span.end_time_ms as f64),
+ )
+ .map_err(|_| JsError::new("failed to set trace field"))?;
+ Reflect::set(
+ &obj,
+ &"durationUs".into(),
+ &JsValue::from(span.duration_us as f64),
+ )
+ .map_err(|_| JsError::new("failed to set trace field"))?;
+ Reflect::set(
+ &obj,
+ &"status".into(),
+ &JsValue::from_str(match span.status {
+ TraceStatus::Ok => "ok",
+ TraceStatus::Error => "error",
+ }),
+ )
+ .map_err(|_| JsError::new("failed to set trace field"))?;
+
+ let attrs = Object::new();
+ for (k, v) in &span.attributes {
+ Reflect::set(&attrs, &JsValue::from_str(k), &JsValue::from_str(v))
+ .map_err(|_| JsError::new("failed to set trace attribute"))?;
+ }
+ Reflect::set(&obj, &"attributes".into(), &attrs.into())
+ .map_err(|_| JsError::new("failed to set trace field"))?;
+
+ let children = Array::new_with_length(span.children.len() as u32);
+ for (i, child) in span.children.iter().enumerate() {
+ children.set(i as u32, trace_span_to_js(child)?);
+ }
+ Reflect::set(&obj, &"children".into(), &children.into())
+ .map_err(|_| JsError::new("failed to set trace field"))?;
+
+ Ok(obj.into())
+}
+
+/// Convert a `VmState` (+ optional stdout + trace) to a JS object.
+fn vm_state_to_js(
+ state: VmState,
+ stdout: &str,
+ trace: Option<&ExecutionTrace>,
+) -> Result {
let obj = Object::new();
match state {
VmState::Complete(value) => {
@@ -248,6 +306,10 @@ fn vm_state_to_js(state: VmState, stdout: &str) -> Result {
.map_err(|_| JsError::new("failed to set stdout"))?;
}
}
+ if let Some(t) = trace {
+ Reflect::set(&obj, &"trace".into(), &trace_span_to_js(&t.root)?)
+ .map_err(|_| JsError::new("failed to set trace"))?;
+ }
Ok(obj.into())
}
@@ -288,6 +350,6 @@ impl ZapcodeSnapshot {
pub fn resume(&self, return_value: JsValue) -> Result {
let val = js_to_value(&return_value)?;
let state = self.inner.clone().resume(val).map_err(zapcode_err)?;
- vm_state_to_js(state, "")
+ vm_state_to_js(state, "", None)
}
}
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..8d1fe03
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,39 @@
+# Examples
+
+Examples organized by language, then by topic.
+
+```text
+examples/
+├── typescript/
+│ ├── basic/ Simple expressions, inputs, snapshot/resume, classes
+│ ├── ai-agent/ AI agent with Anthropic SDK, Vercel AI SDK, zapcode-ai
+│ ├── ai-bedrock/ AWS Bedrock integration
+│ └── debug-tracing/ Debug mode, autoFix, execution tracing
+├── python/
+│ ├── basic/ Simple expressions, inputs, snapshot/resume
+│ ├── ai-agent/ AI agent with Anthropic SDK, zapcode-ai
+│ ├── ai-bedrock/ AWS Bedrock Converse API
+│ └── debug-tracing/ Debug mode, autoFix, execution tracing
+├── rust/
+│ └── basic/ Simple expressions, inputs, snapshot/resume
+└── wasm/
+ └── basic/ Browser playground (single HTML file)
+```
+
+## Quick start
+
+Each example has its own `README.md` with setup and run instructions. Pick a language and topic:
+
+```bash
+# TypeScript — basic usage (no API key needed)
+cd examples/typescript/basic && npm install && npm start
+
+# Python — basic usage (no API key needed)
+cd examples/python/basic && pip install zapcode && python main.py
+
+# Rust — basic usage
+cd examples/rust/basic && cargo run --example basic
+
+# WASM — open in browser (macOS: open, Linux: xdg-open, Windows: start)
+xdg-open examples/wasm/basic/index.html
+```
diff --git a/examples/ai-bedrock/README.md b/examples/ai-bedrock/README.md
deleted file mode 100644
index aa53d79..0000000
--- a/examples/ai-bedrock/README.md
+++ /dev/null
@@ -1,33 +0,0 @@
-# Zapcode + AWS Bedrock Example
-
-End-to-end example using Zapcode with the Vercel AI SDK and AWS Bedrock.
-
-## Prerequisites
-
-- AWS credentials configured (`~/.aws/credentials`, env vars, or IAM role)
-- Access to a Bedrock model (default: `moonshotai.kimi-k2.5` in `eu-west-2`)
-
-## TypeScript
-
-```bash
-npm install
-npm start
-```
-
-Override model/region:
-```bash
-MODEL_ID=eu.anthropic.claude-sonnet-4-20250514-v1:0 AWS_REGION=eu-west-1 npm start
-```
-
-## Python
-
-```bash
-uv venv .venv && source .venv/bin/activate
-uv pip install zapcode-ai boto3
-python main.py
-```
-
-Override model/region:
-```bash
-MODEL_ID=eu.anthropic.claude-sonnet-4-20250514-v1:0 AWS_REGION=eu-west-1 python main.py
-```
diff --git a/examples/python/README.md b/examples/python/README.md
deleted file mode 100644
index d3cc89f..0000000
--- a/examples/python/README.md
+++ /dev/null
@@ -1,56 +0,0 @@
-# Python Examples
-
-## Setup
-
-### Prerequisites
-
-- [Rust toolchain](https://rustup.rs/) (for building the native module)
-- Python 3.10+ (recommended: [pyenv](https://github.com/pyenv/pyenv))
-- [uv](https://docs.astral.sh/uv/) or pip
-
-### Create a virtualenv (recommended)
-
-```bash
-pyenv virtualenv 3.13.8 zapcode
-pyenv local zapcode
-```
-
-### Build the native module
-
-```bash
-# Install maturin
-uv pip install maturin
-
-# Build and install zapcode
-cd ../../crates/zapcode-py
-maturin develop --release
-```
-
-### With uv (alternative)
-
-```bash
-uv sync # install dependencies + build zapcode from source
-uv sync --extra ai # also install anthropic SDK for the AI agent example
-```
-
-## Run
-
-```bash
-# Basic usage (no API key needed)
-python basic.py
-
-# AI agent with zapcode-ai wrapper (requires ANTHROPIC_API_KEY)
-export ANTHROPIC_API_KEY=sk-ant-...
-python ai_agent_zapcode_ai.py
-
-# AI agent with raw Anthropic SDK
-python ai_agent_anthropic.py
-```
-
-## What's here
-
-| File | Description |
-|---|---|
-| `basic.py` | Simple expressions, inputs, data processing, snapshot/resume, serialization |
-| `ai_agent_zapcode_ai.py` | **Recommended** — uses `zapcode-ai` wrapper with Anthropic SDK |
-| `ai_agent_anthropic.py` | Raw Anthropic SDK + manual snapshot/resume loop |
diff --git a/examples/python/ai-agent/README.md b/examples/python/ai-agent/README.md
new file mode 100644
index 0000000..8053f48
--- /dev/null
+++ b/examples/python/ai-agent/README.md
@@ -0,0 +1,28 @@
+# AI Agent Examples (Python)
+
+Two ways to build AI agents with Zapcode in Python.
+
+## Setup
+
+```bash
+pip install zapcode zapcode-ai anthropic
+# or: uv pip install zapcode zapcode-ai anthropic
+export ANTHROPIC_API_KEY=sk-ant-...
+```
+
+## Run
+
+```bash
+# Recommended — zapcode-ai wrapper
+python ai_agent_zapcode_ai.py
+
+# Raw Anthropic SDK + manual snapshot/resume loop
+python ai_agent_anthropic.py
+```
+
+## What's here
+
+| File | Description |
+|---|---|
+| `ai_agent_zapcode_ai.py` | **Recommended** — uses `zapcode-ai` wrapper with Anthropic SDK |
+| `ai_agent_anthropic.py` | Raw Anthropic SDK + manual snapshot/resume loop |
diff --git a/examples/python/ai_agent_anthropic.py b/examples/python/ai-agent/ai_agent_anthropic.py
similarity index 100%
rename from examples/python/ai_agent_anthropic.py
rename to examples/python/ai-agent/ai_agent_anthropic.py
diff --git a/examples/python/ai_agent_zapcode_ai.py b/examples/python/ai-agent/ai_agent_zapcode_ai.py
similarity index 100%
rename from examples/python/ai_agent_zapcode_ai.py
rename to examples/python/ai-agent/ai_agent_zapcode_ai.py
diff --git a/examples/python/pyproject.toml b/examples/python/ai-agent/pyproject.toml
similarity index 51%
rename from examples/python/pyproject.toml
rename to examples/python/ai-agent/pyproject.toml
index 0e1f335..4065651 100644
--- a/examples/python/pyproject.toml
+++ b/examples/python/ai-agent/pyproject.toml
@@ -1,14 +1,10 @@
[project]
-name = "zapcode-examples"
+name = "zapcode-ai-agent-example"
version = "0.0.1"
-description = "Example usage of Zapcode with AI agents"
+description = "Zapcode AI agent examples (Python)"
requires-python = ">=3.10"
dependencies = [
"zapcode",
"zapcode-ai",
-]
-
-[project.optional-dependencies]
-ai = [
"anthropic>=0.39.0",
]
diff --git a/examples/python/ai-bedrock/README.md b/examples/python/ai-bedrock/README.md
new file mode 100644
index 0000000..7d11aaf
--- /dev/null
+++ b/examples/python/ai-bedrock/README.md
@@ -0,0 +1,23 @@
+# AWS Bedrock Example (Python)
+
+Zapcode + AWS Bedrock Converse API.
+
+## Prerequisites
+
+AWS credentials must be configured (env vars, `~/.aws/credentials`, or IAM role) with access to the Bedrock model specified by `MODEL_ID` in your target `AWS_REGION`.
+
+## Setup
+
+```bash
+pip install zapcode-ai boto3
+# or: uv pip install zapcode-ai boto3
+```
+
+## Run
+
+```bash
+python main.py
+
+# Override model/region
+MODEL_ID=eu.anthropic.claude-sonnet-4-20250514-v1:0 AWS_REGION=eu-west-1 python main.py
+```
diff --git a/examples/ai-bedrock/main.py b/examples/python/ai-bedrock/main.py
similarity index 100%
rename from examples/ai-bedrock/main.py
rename to examples/python/ai-bedrock/main.py
diff --git a/examples/ai-bedrock/pyproject.toml b/examples/python/ai-bedrock/pyproject.toml
similarity index 100%
rename from examples/ai-bedrock/pyproject.toml
rename to examples/python/ai-bedrock/pyproject.toml
diff --git a/examples/python/basic/README.md b/examples/python/basic/README.md
new file mode 100644
index 0000000..8831bf0
--- /dev/null
+++ b/examples/python/basic/README.md
@@ -0,0 +1,16 @@
+# Basic Python Example
+
+Simple expressions, inputs, data processing, snapshot/resume, and serialization.
+
+## Setup
+
+```bash
+pip install zapcode
+# or: uv pip install zapcode
+```
+
+## Run
+
+```bash
+python main.py
+```
diff --git a/examples/python/basic.py b/examples/python/basic/main.py
similarity index 96%
rename from examples/python/basic.py
rename to examples/python/basic/main.py
index 5198ae9..d1a2f44 100644
--- a/examples/python/basic.py
+++ b/examples/python/basic/main.py
@@ -1,8 +1,8 @@
"""
Basic Zapcode example — execute TypeScript from Python.
-Prerequisites: build zapcode-py (see README)
-Run with: python examples/python/basic.py
+Prerequisites: pip install zapcode
+Run with: python main.py
"""
from zapcode import Zapcode, ZapcodeSnapshot
diff --git a/examples/python/basic/pyproject.toml b/examples/python/basic/pyproject.toml
new file mode 100644
index 0000000..f1b718f
--- /dev/null
+++ b/examples/python/basic/pyproject.toml
@@ -0,0 +1,8 @@
+[project]
+name = "zapcode-basic-example"
+version = "0.0.1"
+description = "Basic Zapcode usage from Python"
+requires-python = ">=3.10"
+dependencies = [
+ "zapcode",
+]
diff --git a/examples/python/debug-tracing/README.md b/examples/python/debug-tracing/README.md
new file mode 100644
index 0000000..1b0b5a5
--- /dev/null
+++ b/examples/python/debug-tracing/README.md
@@ -0,0 +1,29 @@
+# Debug & Tracing Example (Python)
+
+Demonstrates Zapcode's debug mode, auto-fix error recovery, and execution tracing.
+
+## Features
+
+- **`debug=True`** — Prints the LLM-generated code, external tool calls, and output for each execution
+- **`auto_fix=True`** — When the LLM generates code that fails, the error is returned as a tool result instead of raising, letting the LLM self-correct on the next step
+- **`print_trace()`** — Displays the full execution trace tree (parse -> compile -> execute) with timing
+
+## Prerequisites
+
+AWS credentials must be configured (env vars, `~/.aws/credentials`, or IAM role) with access to the Bedrock model specified by `MODEL_ID` in your target `AWS_REGION`.
+
+## Setup
+
+```bash
+pip install zapcode-ai boto3
+# or: uv pip install zapcode-ai boto3
+```
+
+## Run
+
+```bash
+python main.py
+
+# With a specific model
+MODEL_ID=anthropic.claude-sonnet-4-20250514 python main.py
+```
diff --git a/examples/python/debug-tracing/main.py b/examples/python/debug-tracing/main.py
new file mode 100644
index 0000000..cf7e8fa
--- /dev/null
+++ b/examples/python/debug-tracing/main.py
@@ -0,0 +1,200 @@
+"""
+Zapcode debug & tracing example (Python).
+
+Demonstrates:
+ - Logging LLM-generated code, tool calls, and output
+ - auto_fix=True — catches execution errors and feeds them back to the LLM
+ - print_trace() — displays the execution trace tree with timing
+
+Prerequisites:
+ pip install zapcode-ai boto3
+ AWS credentials configured (env vars, ~/.aws/credentials, or IAM role)
+
+Run: python main.py
+"""
+
+import json
+import os
+import time
+
+import boto3
+from zapcode_ai import zapcode, ToolDefinition, ParamDef
+
+
+# --- Bedrock setup ---
+REGION = os.environ.get("AWS_REGION", "eu-west-1")
+MODEL_ID = os.environ.get("MODEL_ID", "global.amazon.nova-2-lite-v1:0")
+
+bedrock = boto3.client("bedrock-runtime", region_name=REGION)
+
+
+# --- Tools ---
+def get_weather(args):
+ data = {
+ "London": {"condition": "Overcast", "temp": 12},
+ "Tokyo": {"condition": "Clear", "temp": 26},
+ "Paris": {"condition": "Sunny", "temp": 22},
+ "New York": {"condition": "Rain", "temp": 14},
+ }
+ return data.get(args["city"], {"condition": "Unknown", "temp": 0})
+
+
+def search_flights(args):
+ origin = args["from"]
+ destination = args["to"]
+ return [
+ {"from": origin, "to": destination, "airline": "BA", "flight": "BA123", "price": 450, "departure": "08:00"},
+ {"from": origin, "to": destination, "airline": "AF", "flight": "AF456", "price": 380, "departure": "14:30"},
+ ]
+
+
+# --- Zapcode setup with auto_fix ---
+zap = zapcode(
+ auto_fix=True,
+ system="You are a helpful assistant that can look up weather and do math.",
+ tools={
+ "getWeather": ToolDefinition(
+ description="Get current weather for a city. Returns { condition: string, temp: number }",
+ parameters={"city": ParamDef(type="string", description="City name")},
+ execute=get_weather,
+ ),
+ "searchFlights": ToolDefinition(
+ description="Search flights between two cities. Returns Array<{ from, to, airline, flight, price, departure }>",
+ parameters={
+ "from": ParamDef(type="string", description="Departure city"),
+ "to": ParamDef(type="string", description="Arrival city"),
+ },
+ execute=search_flights,
+ ),
+ },
+)
+
+
+# --- Debug: log generated code, tool calls, and output ---
+def log_execution(result):
+ # Print the generated code
+ indented = "\n".join(" " + line for line in result.code.split("\n"))
+ print(f"\n[zapcode] Code:\n{indented}")
+
+ # Print each tool call
+ for tc in result.tool_calls:
+ args_str = ", ".join(json.dumps(a, default=str) for a in tc["args"])
+ print(f"[zapcode] Tool call: {tc['name']}({args_str}) → {json.dumps(tc['result'], default=str)}")
+
+ # Print output or error
+ if result.error:
+ print(f"[zapcode] Error: {result.error}")
+ else:
+ print(f"[zapcode] Output: {json.dumps(result.output, default=str)}")
+
+
+def main():
+ print(f"Model: {MODEL_ID} | Region: {REGION}")
+ print("Debug: ON | AutoFix: ON")
+
+ t0 = time.perf_counter()
+
+ messages = [
+ {"role": "user", "content": [{"text": "What's the weather in Tokyo and Paris? Find flights from the colder city to the warmer one."}]}
+ ]
+
+ tool_config = {
+ "tools": [
+ {
+ "toolSpec": {
+ "name": "execute_code",
+ "description": "Execute TypeScript code in a secure sandbox. The code can call the available tool functions using await. The last expression is the return value.",
+ "inputSchema": {
+ "json": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "string",
+ "description": "TypeScript code to execute in the sandbox",
+ }
+ },
+ "required": ["code"],
+ }
+ },
+ }
+ }
+ ]
+ }
+
+ max_steps = 10
+ steps = 0
+ total_tokens = 0
+
+ while steps < max_steps:
+ steps += 1
+ response = bedrock.converse(
+ modelId=MODEL_ID,
+ messages=messages,
+ system=[{"text": zap.system}],
+ toolConfig=tool_config,
+ )
+
+ total_tokens += response["usage"]["inputTokens"] + response["usage"]["outputTokens"]
+ stop_reason = response["stopReason"]
+
+ if stop_reason == "tool_use":
+ assistant_content = response["output"]["message"]["content"]
+ messages.append({"role": "assistant", "content": assistant_content})
+
+ tool_results = []
+ for block in assistant_content:
+ if "toolUse" in block:
+ tool_use = block["toolUse"]
+ code = tool_use["input"]["code"]
+ result = zap.handle_tool_call(code)
+
+ # Debug: log the execution
+ log_execution(result)
+
+ if result.error:
+ tool_results.append({
+ "toolResult": {
+ "toolUseId": tool_use["toolUseId"],
+ "content": [{"text": result.error}],
+ "status": "error",
+ }
+ })
+ else:
+ tool_results.append({
+ "toolResult": {
+ "toolUseId": tool_use["toolUseId"],
+ "content": [{"json": {"output": result.output, "stdout": result.stdout}}],
+ }
+ })
+
+ messages.append({"role": "user", "content": tool_results})
+ elif stop_reason in ("end_turn", "stop_sequence"):
+ text = ""
+ for block in response["output"]["message"]["content"]:
+ if "text" in block:
+ text += block["text"]
+
+ total_ms = (time.perf_counter() - t0) * 1000
+
+ print(f"\nAnswer: {text}")
+ print("\n--- Timing ---")
+ print(f"Total (LLM + Zapcode): {total_ms:.0f}ms")
+ print(f"Steps: {steps}")
+ print(f"Tokens: {total_tokens}")
+
+ # Print the full execution trace tree
+ print("\n--- Execution Trace ---")
+ zap.print_trace()
+ return
+ else:
+ raise RuntimeError(
+ f"Bedrock Converse returned unexpected stop reason: {stop_reason}"
+ )
+
+ raise RuntimeError(
+ f"Model did not produce a final answer within {max_steps} steps"
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/examples/python/debug-tracing/pyproject.toml b/examples/python/debug-tracing/pyproject.toml
new file mode 100644
index 0000000..4411220
--- /dev/null
+++ b/examples/python/debug-tracing/pyproject.toml
@@ -0,0 +1,9 @@
+[project]
+name = "zapcode-debug-tracing-example"
+version = "0.0.1"
+description = "Zapcode debug & tracing example (Python)"
+requires-python = ">=3.10"
+dependencies = [
+ "zapcode-ai",
+ "boto3",
+]
diff --git a/examples/rust/README.md b/examples/rust/README.md
deleted file mode 100644
index 34aea19..0000000
--- a/examples/rust/README.md
+++ /dev/null
@@ -1,19 +0,0 @@
-# Rust Examples
-
-## Prerequisites
-
-- [Rust toolchain](https://rustup.rs/)
-
-## Run
-
-```bash
-cargo run --example basic
-```
-
-> **Note:** The examples crate is excluded from the workspace. It has its own `Cargo.toml` that depends on `zapcode-core` via path.
-
-## What's here
-
-| File | Description |
-|---|---|
-| `basic.rs` | Simple expressions, inputs, external functions (snapshot/resume), snapshot serialization |
diff --git a/examples/rust/basic/Cargo.lock b/examples/rust/basic/Cargo.lock
new file mode 100644
index 0000000..89a2364
--- /dev/null
+++ b/examples/rust/basic/Cargo.lock
@@ -0,0 +1,704 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "atomic-polyfill"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
+dependencies = [
+ "critical-section",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+[[package]]
+name = "bitflags"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "castaway"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a"
+dependencies = [
+ "rustversion",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cobs"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
+dependencies = [
+ "thiserror",
+]
+
+[[package]]
+name = "compact_str"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
+dependencies = [
+ "castaway",
+ "cfg-if",
+ "itoa",
+ "rustversion",
+ "ryu",
+ "static_assertions",
+]
+
+[[package]]
+name = "cow-utils"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79"
+
+[[package]]
+name = "critical-section"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
+
+[[package]]
+name = "dragonbox_ecma"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf"
+
+[[package]]
+name = "embedded-io"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
+
+[[package]]
+name = "embedded-io"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "hash32"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "allocator-api2",
+]
+
+[[package]]
+name = "heapless"
+version = "0.7.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
+dependencies = [
+ "atomic-polyfill",
+ "hash32",
+ "rustc_version",
+ "serde",
+ "spin",
+ "stable_deref_trait",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
+dependencies = [
+ "equivalent",
+ "hashbrown",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+
+[[package]]
+name = "nonmax"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51"
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "owo-colors"
+version = "4.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
+
+[[package]]
+name = "oxc-miette"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a7ba54c704edefead1f44e9ef09c43e5cfae666bdc33516b066011f0e6ebf7"
+dependencies = [
+ "cfg-if",
+ "owo-colors",
+ "oxc-miette-derive",
+ "textwrap",
+ "thiserror",
+ "unicode-segmentation",
+ "unicode-width",
+]
+
+[[package]]
+name = "oxc-miette-derive"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4faecb54d0971f948fbc1918df69b26007e6f279a204793669542e1e8b75eb3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "oxc_allocator"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b44277218c002c09167474648a478d3d29a29095ef8950ec9f1fac016c62d7"
+dependencies = [
+ "allocator-api2",
+ "hashbrown",
+ "oxc_data_structures",
+ "rustc-hash",
+]
+
+[[package]]
+name = "oxc_ast"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4222e4e7a1ab01b2a20420a5a65798377a748ea37ee7ece4d7a6b733f86eb61"
+dependencies = [
+ "bitflags",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_estree",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_ast_macros"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e65a38ae589e284dd45a85008024f04aa680e9ddf1321c163cf7f187c805e91"
+dependencies = [
+ "phf",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "oxc_data_structures"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f53bed71cad192596aee8f87f6d6bc2a38a4f898255a69b1d41da1968b9b2c6f"
+
+[[package]]
+name = "oxc_diagnostics"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a2d2491c0a1ea29a83abe645424f85c64b5c825f60e5304a453e4314a8b6d88"
+dependencies = [
+ "cow-utils",
+ "oxc-miette",
+ "percent-encoding",
+]
+
+[[package]]
+name = "oxc_ecmascript"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "71b23b64fa8c4a84b1406de383c4666366c9f54ffb9cb11a63b8d7433950460a"
+dependencies = [
+ "cow-utils",
+ "num-bigint",
+ "num-traits",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_syntax",
+]
+
+[[package]]
+name = "oxc_estree"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47515ead44bc8beec1ae1514f10ecca63cde043da167c0395dc914f098ea5d2"
+
+[[package]]
+name = "oxc_index"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb3e6120999627ec9703025eab7c9f410ebb7e95557632a8902ca48210416c2b"
+dependencies = [
+ "nonmax",
+ "serde",
+]
+
+[[package]]
+name = "oxc_parser"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3278d4f34d01cdaf85a2391d7b12daba1d95c20c1ff2ac9316d3c28f36353e4e"
+dependencies = [
+ "bitflags",
+ "cow-utils",
+ "memchr",
+ "num-bigint",
+ "num-traits",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_data_structures",
+ "oxc_diagnostics",
+ "oxc_ecmascript",
+ "oxc_regular_expression",
+ "oxc_span",
+ "oxc_syntax",
+ "rustc-hash",
+ "seq-macro",
+]
+
+[[package]]
+name = "oxc_regular_expression"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3d680252672b22c24abbaf6e401eace0be9f53072a03411936204625ff349d0"
+dependencies = [
+ "bitflags",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_diagnostics",
+ "oxc_span",
+ "phf",
+ "rustc-hash",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "oxc_span"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6eb1bd62de89fb0c646bfb053b72370750fab43a84ebe09ad97cfa020712314"
+dependencies = [
+ "compact_str",
+ "oxc-miette",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_estree",
+ "oxc_str",
+]
+
+[[package]]
+name = "oxc_str"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e65cbfb06ecbae07e0da931815b6b03ade886d016302c400bda7dc0a2f600d3"
+dependencies = [
+ "compact_str",
+ "hashbrown",
+ "oxc_allocator",
+ "oxc_estree",
+]
+
+[[package]]
+name = "oxc_syntax"
+version = "0.117.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0f1617f0aa890517fb61ffa1d2d73a8497aca52e84ef6f027fad1e93250eccc"
+dependencies = [
+ "bitflags",
+ "cow-utils",
+ "dragonbox_ecma",
+ "nonmax",
+ "oxc_allocator",
+ "oxc_ast_macros",
+ "oxc_estree",
+ "oxc_index",
+ "oxc_span",
+ "phf",
+ "unicode-id-start",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "serde",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher",
+]
+
+[[package]]
+name = "postcard"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
+dependencies = [
+ "cobs",
+ "embedded-io 0.4.0",
+ "embedded-io 0.6.1",
+ "heapless",
+ "serde",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "semver"
+version = "1.0.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
+
+[[package]]
+name = "seq-macro"
+version = "0.3.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "siphasher"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
+
+[[package]]
+name = "smawk"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "unicode-id-start"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "zapcode-core"
+version = "1.0.1"
+dependencies = [
+ "indexmap",
+ "oxc_allocator",
+ "oxc_ast",
+ "oxc_parser",
+ "oxc_span",
+ "oxc_syntax",
+ "postcard",
+ "serde",
+ "thiserror",
+]
+
+[[package]]
+name = "zapcode-examples"
+version = "0.0.1"
+dependencies = [
+ "indexmap",
+ "zapcode-core",
+]
diff --git a/examples/rust/Cargo.toml b/examples/rust/basic/Cargo.toml
similarity index 74%
rename from examples/rust/Cargo.toml
rename to examples/rust/basic/Cargo.toml
index 5e7424f..f22d9a0 100644
--- a/examples/rust/Cargo.toml
+++ b/examples/rust/basic/Cargo.toml
@@ -9,5 +9,5 @@ name = "basic"
path = "basic.rs"
[dependencies]
-zapcode-core = { path = "../../crates/zapcode-core" }
+zapcode-core = { path = "../../../crates/zapcode-core" }
indexmap = "2"
diff --git a/examples/rust/basic/README.md b/examples/rust/basic/README.md
new file mode 100644
index 0000000..52c5486
--- /dev/null
+++ b/examples/rust/basic/README.md
@@ -0,0 +1,10 @@
+# Basic Rust Example
+
+Simple expressions, inputs, external functions (snapshot/resume), and snapshot serialization.
+
+## Run
+
+```bash
+# From this directory (examples/rust/basic/)
+cargo run --example basic
+```
diff --git a/examples/rust/basic.rs b/examples/rust/basic/basic.rs
similarity index 100%
rename from examples/rust/basic.rs
rename to examples/rust/basic/basic.rs
diff --git a/examples/typescript/README.md b/examples/typescript/README.md
deleted file mode 100644
index c788686..0000000
--- a/examples/typescript/README.md
+++ /dev/null
@@ -1,48 +0,0 @@
-# TypeScript Examples
-
-## Setup
-
-### Prerequisites
-
-- [Rust toolchain](https://rustup.rs/) (for building the native addon)
-- [Node.js](https://nodejs.org/) (v18+)
-
-### Build the native addon
-
-```bash
-cd ../../crates/zapcode-js
-npm install @napi-rs/cli --save-dev
-npx napi build --release --platform --js index.js --dts index.d.ts
-```
-
-### Install dependencies
-
-```bash
-npm install
-```
-
-## Run
-
-```bash
-# Basic usage (no API key needed)
-npm run basic
-
-# AI agent with @unchartedfr/zapcode-ai wrapper (requires ANTHROPIC_API_KEY)
-export ANTHROPIC_API_KEY=sk-ant-...
-npm run agent
-
-# AI agent with raw Anthropic SDK
-npm run agent:anthropic
-
-# AI agent with Vercel AI SDK
-npm run agent:vercel
-```
-
-## What's here
-
-| File | Description |
-|---|---|
-| `basic.ts` | Simple expressions, inputs, data processing, classes, resource limits |
-| `ai-agent-zapcode-ai.ts` | **Recommended** — uses `@unchartedfr/zapcode-ai` wrapper with Vercel AI SDK |
-| `ai-agent-anthropic.ts` | Raw Anthropic SDK + manual snapshot/resume loop |
-| `ai-agent-vercel-ai.ts` | Vercel AI SDK with manual code generation |
diff --git a/examples/typescript/ai-agent/README.md b/examples/typescript/ai-agent/README.md
new file mode 100644
index 0000000..e63d077
--- /dev/null
+++ b/examples/typescript/ai-agent/README.md
@@ -0,0 +1,31 @@
+# AI Agent Examples (TypeScript)
+
+Three ways to build AI agents with Zapcode, from high-level to low-level.
+
+## Setup
+
+```bash
+npm install
+export ANTHROPIC_API_KEY=sk-ant-...
+```
+
+## Run
+
+```bash
+# Recommended — zapcode-ai wrapper
+npm run agent
+
+# Vercel AI SDK with streamText
+npm run agent:vercel
+
+# Raw Anthropic SDK + manual snapshot/resume loop
+npm run agent:anthropic
+```
+
+## What's here
+
+| File | Description |
+|---|---|
+| `ai-agent-zapcode-ai.ts` | **Recommended** — uses `@unchartedfr/zapcode-ai` wrapper with Vercel AI SDK |
+| `ai-agent-vercel-ai.ts` | Vercel AI SDK with `generateText` and `streamText` |
+| `ai-agent-anthropic.ts` | Raw Anthropic SDK + manual snapshot/resume loop |
diff --git a/examples/typescript/ai-agent-anthropic.ts b/examples/typescript/ai-agent/ai-agent-anthropic.ts
similarity index 97%
rename from examples/typescript/ai-agent-anthropic.ts
rename to examples/typescript/ai-agent/ai-agent-anthropic.ts
index 055a425..6114ad4 100644
--- a/examples/typescript/ai-agent-anthropic.ts
+++ b/examples/typescript/ai-agent/ai-agent-anthropic.ts
@@ -12,10 +12,10 @@
* 4. Your app resolves the tool call, then resumes Zapcode with the result
*
* Prerequisites:
- * npm install @anthropic-ai/sdk @unchartedfr/zapcode
+ * npm install
* export ANTHROPIC_API_KEY=sk-...
*
- * Run with: npx tsx ai-agent-anthropic.ts
+ * Run with: npm run agent:anthropic
*/
import Anthropic from "@anthropic-ai/sdk";
diff --git a/examples/typescript/ai-agent-vercel-ai.ts b/examples/typescript/ai-agent/ai-agent-vercel-ai.ts
similarity index 96%
rename from examples/typescript/ai-agent-vercel-ai.ts
rename to examples/typescript/ai-agent/ai-agent-vercel-ai.ts
index b62d14e..7e4ce0c 100644
--- a/examples/typescript/ai-agent-vercel-ai.ts
+++ b/examples/typescript/ai-agent/ai-agent-vercel-ai.ts
@@ -8,10 +8,10 @@
* Works with any AI SDK provider: Anthropic, OpenAI, Google, etc.
*
* Prerequisites:
- * npm install @unchartedfr/zapcode-ai ai @ai-sdk/anthropic
+ * npm install
* export ANTHROPIC_API_KEY=sk-...
*
- * Run with: npx tsx ai-agent-vercel-ai.ts
+ * Run with: npm run agent:vercel
*/
import { zapcode } from "@unchartedfr/zapcode-ai";
diff --git a/examples/typescript/ai-agent-zapcode-ai.ts b/examples/typescript/ai-agent/ai-agent-zapcode-ai.ts
similarity index 96%
rename from examples/typescript/ai-agent-zapcode-ai.ts
rename to examples/typescript/ai-agent/ai-agent-zapcode-ai.ts
index 2d193e2..c587205 100644
--- a/examples/typescript/ai-agent-zapcode-ai.ts
+++ b/examples/typescript/ai-agent/ai-agent-zapcode-ai.ts
@@ -6,10 +6,10 @@
* directly into Vercel AI SDK's `generateText` / `streamText`.
*
* Prerequisites:
- * npm install @unchartedfr/zapcode-ai ai @ai-sdk/anthropic
+ * npm install
* export ANTHROPIC_API_KEY=sk-...
*
- * Run with: npx tsx ai-agent-zapcode-ai.ts
+ * Run with: npm run agent
*/
import { zapcode } from "@unchartedfr/zapcode-ai";
diff --git a/examples/typescript/ai-agent/package.json b/examples/typescript/ai-agent/package.json
new file mode 100644
index 0000000..da3afcd
--- /dev/null
+++ b/examples/typescript/ai-agent/package.json
@@ -0,0 +1,21 @@
+{
+ "name": "zapcode-ai-agent-example",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "agent": "tsx ai-agent-zapcode-ai.ts",
+ "agent:anthropic": "tsx ai-agent-anthropic.ts",
+ "agent:vercel": "tsx ai-agent-vercel-ai.ts"
+ },
+ "dependencies": {
+ "@unchartedfr/zapcode": "file:../../../crates/zapcode-js",
+ "@unchartedfr/zapcode-ai": "file:../../../packages/zapcode-ai",
+ "@anthropic-ai/sdk": "^0.39.0",
+ "@ai-sdk/anthropic": "^1.1.0",
+ "ai": "^4.1.0"
+ },
+ "devDependencies": {
+ "tsx": "^4.0.0",
+ "typescript": "^5.0.0"
+ }
+}
diff --git a/examples/typescript/ai-bedrock/README.md b/examples/typescript/ai-bedrock/README.md
new file mode 100644
index 0000000..c42df4b
--- /dev/null
+++ b/examples/typescript/ai-bedrock/README.md
@@ -0,0 +1,22 @@
+# AWS Bedrock Example (TypeScript)
+
+Zapcode + Vercel AI SDK + AWS Bedrock.
+
+## Prerequisites
+
+AWS credentials must be configured (env vars, `~/.aws/credentials`, or IAM role) with access to the Bedrock model specified by `MODEL_ID` in your target `AWS_REGION`.
+
+## Setup
+
+```bash
+npm install
+```
+
+## Run
+
+```bash
+npm start
+
+# Override model/region
+MODEL_ID=eu.anthropic.claude-sonnet-4-20250514-v1:0 AWS_REGION=eu-west-1 npm start
+```
diff --git a/examples/ai-bedrock/main.ts b/examples/typescript/ai-bedrock/main.ts
similarity index 100%
rename from examples/ai-bedrock/main.ts
rename to examples/typescript/ai-bedrock/main.ts
diff --git a/examples/ai-bedrock/package.json b/examples/typescript/ai-bedrock/package.json
similarity index 72%
rename from examples/ai-bedrock/package.json
rename to examples/typescript/ai-bedrock/package.json
index 185cb03..c7af8f9 100644
--- a/examples/ai-bedrock/package.json
+++ b/examples/typescript/ai-bedrock/package.json
@@ -8,8 +8,8 @@
"dependencies": {
"@ai-sdk/amazon-bedrock": "^2.0.0",
"@aws-sdk/credential-providers": "^3.0.0",
- "@unchartedfr/zapcode": "latest",
- "@unchartedfr/zapcode-ai": "latest",
+ "@unchartedfr/zapcode": "file:../../../crates/zapcode-js",
+ "@unchartedfr/zapcode-ai": "file:../../../packages/zapcode-ai",
"ai": "^4.0.0"
},
"devDependencies": {
diff --git a/examples/typescript/basic/README.md b/examples/typescript/basic/README.md
new file mode 100644
index 0000000..c288195
--- /dev/null
+++ b/examples/typescript/basic/README.md
@@ -0,0 +1,15 @@
+# Basic TypeScript Example
+
+Simple expressions, inputs, data processing, snapshot/resume, classes, and resource limits.
+
+## Setup
+
+```bash
+npm install
+```
+
+## Run
+
+```bash
+npm start
+```
diff --git a/examples/typescript/basic.ts b/examples/typescript/basic/main.ts
similarity index 96%
rename from examples/typescript/basic.ts
rename to examples/typescript/basic/main.ts
index 5f3aecc..6637977 100644
--- a/examples/typescript/basic.ts
+++ b/examples/typescript/basic/main.ts
@@ -1,8 +1,8 @@
/**
* Basic Zapcode example — execute TypeScript from Node.js.
*
- * Prerequisites: build zapcode-js (see README)
- * Run with: npx ts-node examples/typescript/basic.ts
+ * Prerequisites: npm install
+ * Run with: npx tsx main.ts
*/
import { Zapcode, ZapcodeSnapshotHandle } from "@unchartedfr/zapcode";
diff --git a/examples/typescript/basic/package.json b/examples/typescript/basic/package.json
new file mode 100644
index 0000000..0a4ad4c
--- /dev/null
+++ b/examples/typescript/basic/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "zapcode-basic-example",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "npx tsx main.ts"
+ },
+ "dependencies": {
+ "@unchartedfr/zapcode": "file:../../../crates/zapcode-js"
+ },
+ "devDependencies": {
+ "tsx": "^4.0.0",
+ "typescript": "^5.0.0"
+ }
+}
diff --git a/examples/typescript/debug-tracing/README.md b/examples/typescript/debug-tracing/README.md
new file mode 100644
index 0000000..afcc71e
--- /dev/null
+++ b/examples/typescript/debug-tracing/README.md
@@ -0,0 +1,56 @@
+# Debug & Tracing Example
+
+Demonstrates Zapcode's debug mode, auto-fix error recovery, and execution tracing.
+
+## Features
+
+- **`debug: true`** — Prints the LLM-generated code, external tool calls, and output for each execution
+- **`autoFix: true`** — When the LLM generates code that fails, the error is returned as a tool result instead of throwing, letting the LLM self-correct on the next step
+- **`printTrace()`** — Displays the full execution trace tree (parse -> compile -> execute) with timing
+
+## Prerequisites
+
+AWS credentials must be configured (env vars, `~/.aws/credentials`, or IAM role) with access to the Bedrock model specified by `MODEL_ID` in your target `AWS_REGION`.
+
+## Setup
+
+```bash
+npm install
+```
+
+## Run
+
+```bash
+# Default model (Amazon Nova)
+npm start
+
+# With a specific model
+MODEL_ID=anthropic.claude-sonnet-4-20250514 npm start
+```
+
+## Example output
+
+```text
+Model: global.amazon.nova-2-lite-v1:0 | Region: eu-west-1
+Debug: ON | AutoFix: ON
+
+[zapcode] Code:
+ const tokyo = await getWeather("Tokyo");
+ const paris = await getWeather("Paris");
+ const colder = tokyo.temp < paris.temp ? "Tokyo" : "Paris";
+ const warmer = tokyo.temp < paris.temp ? "Paris" : "Tokyo";
+ const flights = await searchFlights(colder, warmer);
+ flights;
+
+[zapcode] Tool call: getWeather("Tokyo") -> {"condition":"Clear","temp":26}
+[zapcode] Tool call: getWeather("Paris") -> {"condition":"Sunny","temp":22}
+[zapcode] Tool call: searchFlights("Paris", "Tokyo") -> [...]
+[zapcode] Output: [{"from":"Paris","to":"Tokyo",...}]
+
+--- Execution Trace ---
+session [zapcode.tools: getWeather, searchFlights] 12.4ms
+ attempt_1 8.2ms
+ parse 0.1ms
+ compile 0.0ms
+ execute 8.1ms
+```
diff --git a/examples/typescript/debug-tracing/main.ts b/examples/typescript/debug-tracing/main.ts
new file mode 100644
index 0000000..3f1761b
--- /dev/null
+++ b/examples/typescript/debug-tracing/main.ts
@@ -0,0 +1,139 @@
+/**
+ * Zapcode debug & tracing example.
+ *
+ * Demonstrates:
+ * - Logging LLM-generated code, tool calls, and output
+ * - `autoFix: true` — catches execution errors and feeds them back to the LLM
+ * - `printTrace()` — displays the execution trace tree with timing
+ *
+ * Prerequisites:
+ * npm install
+ * AWS credentials configured (env vars, ~/.aws/credentials, or IAM role)
+ *
+ * Run: npm start
+ */
+
+import { zapcode, type ExecutionResult } from "@unchartedfr/zapcode-ai";
+import { generateText } from "ai";
+import { createAmazonBedrock } from "@ai-sdk/amazon-bedrock";
+import { fromNodeProviderChain } from "@aws-sdk/credential-providers";
+
+// --- Bedrock setup ---
+const REGION = process.env.AWS_REGION ?? "eu-west-1";
+
+const bedrock = createAmazonBedrock({
+ credentialProvider: fromNodeProviderChain(),
+ region: REGION,
+});
+
+const MODEL_ID = process.env.MODEL_ID ?? "global.amazon.nova-2-lite-v1:0";
+const model = bedrock(MODEL_ID);
+
+// --- Zapcode setup with autoFix ---
+const { system, tools, printTrace } = zapcode({
+ autoFix: true,
+ system: "You are a helpful assistant that can look up weather and do math.",
+ tools: {
+ getWeather: {
+ description:
+ "Get current weather for a city. Returns { condition: string, temp: number }",
+ parameters: {
+ city: { type: "string", description: "City name" },
+ },
+ execute: async ({ city }) => {
+ const data: Record = {
+ London: { condition: "Overcast", temp: 12 },
+ Tokyo: { condition: "Clear", temp: 26 },
+ Paris: { condition: "Sunny", temp: 22 },
+ "New York": { condition: "Rain", temp: 14 },
+ };
+ return data[city as string] ?? { condition: "Unknown", temp: 0 };
+ },
+ },
+ searchFlights: {
+ description:
+ "Search flights between two cities. Returns Array<{ from, to, airline, flight, price, departure }>",
+ parameters: {
+ from: { type: "string", description: "Departure city" },
+ to: { type: "string", description: "Arrival city" },
+ },
+ execute: async ({ from, to }) => {
+ return [
+ { from, to, airline: "BA", flight: "BA123", price: 450, departure: "08:00" },
+ { from, to, airline: "AF", flight: "AF456", price: 380, departure: "14:30" },
+ ];
+ },
+ },
+ },
+});
+
+// --- Debug: log each step's generated code, tool calls, and output ---
+function logExecution(result: ExecutionResult) {
+ // Print the generated code
+ const indented = result.code.split("\n").map((l) => " " + l).join("\n");
+ console.log(`\n[zapcode] Code:\n${indented}`);
+
+ // Print each tool call
+ for (const tc of result.toolCalls) {
+ const argsStr = (tc.args as unknown[]).map((a) => JSON.stringify(a)).join(", ");
+ console.log(`[zapcode] Tool call: ${tc.name}(${argsStr}) → ${JSON.stringify(tc.result)}`);
+ }
+
+ // Print output or error
+ if (result.error) {
+ console.log(`[zapcode] Error: ${result.error}`);
+ } else {
+ console.log(`[zapcode] Output: ${JSON.stringify(result.output)}`);
+ }
+}
+
+// --- Run ---
+async function main() {
+ console.log(`Model: ${MODEL_ID} | Region: ${REGION}`);
+ console.log(`Debug: ON | AutoFix: ON`);
+
+ const t0 = performance.now();
+
+ const result = await generateText({
+ model,
+ system,
+ tools,
+ maxSteps: 10,
+ messages: [
+ {
+ role: "user",
+ content:
+ "What's the weather in Tokyo and Paris? Find flights from the colder city to the warmer one.",
+ },
+ ],
+ onStepFinish: (step) => {
+ // Log every execute_code tool call result
+ for (const toolResult of step.toolResults) {
+ if (toolResult.toolName === "execute_code") {
+ logExecution(toolResult.result as ExecutionResult);
+ }
+ }
+ },
+ });
+
+ const totalMs = (performance.now() - t0).toFixed(0);
+
+ console.log("\nAnswer:", result.text);
+ console.log(`\n--- Timing ---`);
+ console.log(`Total (LLM + Zapcode): ${totalMs}ms`);
+ console.log(`Steps: ${result.steps.length}`);
+ const toolCallCount = result.steps.reduce(
+ (count, step) => count + step.toolCalls.length,
+ 0,
+ );
+ console.log(`Tool calls: ${toolCallCount}`);
+ console.log(
+ `Usage: ${result.usage.promptTokens} prompt + ${result.usage.completionTokens} completion = ${result.usage.totalTokens} tokens`,
+ );
+
+ // Print the full execution trace tree
+ console.log(`\n--- Execution Trace ---`);
+ printTrace();
+}
+
+main().catch(console.error);
diff --git a/examples/typescript/debug-tracing/package.json b/examples/typescript/debug-tracing/package.json
new file mode 100644
index 0000000..db4ba76
--- /dev/null
+++ b/examples/typescript/debug-tracing/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "zapcode-debug-tracing-example",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "start": "npx tsx main.ts"
+ },
+ "dependencies": {
+ "@ai-sdk/amazon-bedrock": "^2.0.0",
+ "@aws-sdk/credential-providers": "^3.0.0",
+ "@unchartedfr/zapcode": "file:../../../crates/zapcode-js",
+ "@unchartedfr/zapcode-ai": "file:../../../packages/zapcode-ai",
+ "ai": "^4.0.0"
+ },
+ "devDependencies": {
+ "tsx": "^4.0.0",
+ "typescript": "^5.0.0"
+ }
+}
diff --git a/examples/typescript/package.json b/examples/typescript/package.json
deleted file mode 100644
index 885739e..0000000
--- a/examples/typescript/package.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "name": "zapcode-examples",
- "version": "0.0.1",
- "private": true,
- "type": "module",
- "description": "Example usage of Zapcode with AI agents",
- "scripts": {
- "basic": "tsx basic.ts",
- "agent": "tsx ai-agent-zapcode-ai.ts",
- "agent:anthropic": "tsx ai-agent-anthropic.ts",
- "agent:vercel": "tsx ai-agent-vercel-ai.ts"
- },
- "dependencies": {
- "@unchartedfr/zapcode": "latest",
- "@unchartedfr/zapcode-ai": "latest"
- },
- "devDependencies": {
- "@types/node": "^22.0.0",
- "tsx": "^4.19.0",
- "typescript": "^5.7.0"
- },
- "optionalDependencies": {
- "@ai-sdk/amazon-bedrock": "^2.0.0",
- "@ai-sdk/anthropic": "^1.1.0",
- "@ai-sdk/google": "^1.1.0",
- "@ai-sdk/openai": "^1.1.0",
- "@anthropic-ai/sdk": "^0.39.0",
- "ai": "^4.1.0"
- }
-}
diff --git a/examples/typescript/tsconfig.json b/examples/typescript/tsconfig.json
deleted file mode 100644
index 18993d9..0000000
--- a/examples/typescript/tsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2022",
- "module": "ESNext",
- "moduleResolution": "bundler",
- "strict": true,
- "esModuleInterop": true,
- "skipLibCheck": true
- },
- "include": ["*.ts"]
-}
diff --git a/examples/wasm/index.html b/examples/wasm/basic/index.html
similarity index 100%
rename from examples/wasm/index.html
rename to examples/wasm/basic/index.html
diff --git a/packages/zapcode-ai-python/README.md b/packages/zapcode-ai-python/README.md
new file mode 100644
index 0000000..e4e6e4c
--- /dev/null
+++ b/packages/zapcode-ai-python/README.md
@@ -0,0 +1,5 @@
+# zapcode-ai
+
+AI SDK integration for Zapcode — let LLMs write and execute TypeScript safely.
+
+See the [main README](https://github.com/TheUncharted/zapcode) for full documentation.
diff --git a/packages/zapcode-ai-python/src/zapcode_ai/__init__.py b/packages/zapcode-ai-python/src/zapcode_ai/__init__.py
index b602487..53f7fec 100644
--- a/packages/zapcode-ai-python/src/zapcode_ai/__init__.py
+++ b/packages/zapcode-ai-python/src/zapcode_ai/__init__.py
@@ -29,6 +29,8 @@ def adapt(self, ctx):
from __future__ import annotations
+import json
+import time
from dataclasses import dataclass, field
from typing import Any, Callable, Awaitable
@@ -55,12 +57,27 @@ class ToolDefinition:
execute: Callable[..., Any] # (args: dict) -> Any or awaitable
+@dataclass
+class TraceSpan:
+ """A single span in the execution trace. OTel-compatible shape."""
+ name: str
+ start_time: float # ms since epoch
+ end_time: float = 0.0
+ duration_ms: float = 0.0
+ status: str = "ok" # "ok" or "error"
+ attributes: dict[str, Any] = field(default_factory=dict)
+ children: list[TraceSpan] = field(default_factory=list)
+
+
@dataclass
class ExecutionResult:
"""Result of executing guest code."""
+ code: str
output: Any
stdout: str
tool_calls: list[dict[str, Any]]
+ error: str | None = None
+ trace: TraceSpan | None = None
# ---------------------------------------------------------------------------
@@ -142,6 +159,39 @@ def _build_system_prompt(
return "\n\n".join(parts)
+# ---------------------------------------------------------------------------
+# Trace helpers
+# ---------------------------------------------------------------------------
+
+def _create_span(name: str, attributes: dict[str, Any] | None = None) -> TraceSpan:
+ return TraceSpan(
+ name=name,
+ start_time=time.time() * 1000,
+ attributes=attributes or {},
+ )
+
+
+def _end_span(span: TraceSpan, status: str | None = None) -> TraceSpan:
+ span.end_time = time.time() * 1000
+ span.duration_ms = span.end_time - span.start_time
+ if status:
+ span.status = status
+ return span
+
+
+def _print_trace(span: TraceSpan, indent: int = 0) -> None:
+ prefix = "" if indent == 0 else "│ " * (indent - 1) + "├─ "
+ icon = "✗" if span.status == "error" else "✓"
+ duration = "<1ms" if span.duration_ms < 1 else f"{span.duration_ms:.0f}ms"
+ attrs = " ".join(
+ f"{k}={str(v)[:80]}" for k, v in span.attributes.items()
+ if not k.startswith("zapcode.code") # don't dump full code in trace
+ )
+ print(f"{prefix}{icon} {span.name} ({duration}){' ' + attrs if attrs else ''}")
+ for child in span.children:
+ _print_trace(child, indent + 1)
+
+
# ---------------------------------------------------------------------------
# Execution engine
# ---------------------------------------------------------------------------
@@ -152,48 +202,100 @@ def _execute_code(
*,
memory_limit_bytes: int | None = None,
time_limit_ms: int | None = None,
+ debug: bool = False,
+ auto_fix: bool = False,
) -> ExecutionResult:
tool_names = list(tool_defs.keys())
tool_calls: list[dict[str, Any]] = []
+ tracing = debug or auto_fix
- kwargs: dict[str, Any] = {"external_functions": tool_names}
- if time_limit_ms is not None:
- kwargs["time_limit_ms"] = time_limit_ms
- if memory_limit_bytes is not None:
- kwargs["memory_limit_bytes"] = memory_limit_bytes
-
- sandbox = Zapcode(code, **kwargs)
- state = sandbox.start()
-
- while state.get("suspended"):
- fn_name = state["function_name"]
- args = state["args"]
-
- tool_def = tool_defs.get(fn_name)
- if not tool_def:
- raise ValueError(
- f"Guest code called unknown function '{fn_name}'. "
- f"Available: {', '.join(tool_names)}"
- )
-
- # Build named args from positional args
- param_names = list(tool_def.parameters.keys())
- named_args = {
- param_names[i]: args[i]
- for i in range(min(len(param_names), len(args)))
- }
+ exec_span = _create_span("execute", {"zapcode.code": code}) if tracing else None
- result = tool_def.execute(named_args)
- tool_calls.append({"name": fn_name, "args": args, "result": result})
+ try:
+ kwargs: dict[str, Any] = {"external_functions": tool_names}
+ if time_limit_ms is not None:
+ kwargs["time_limit_ms"] = time_limit_ms
+ if memory_limit_bytes is not None:
+ kwargs["memory_limit_bytes"] = memory_limit_bytes
- snapshot: ZapcodeSnapshot = state["snapshot"]
- state = snapshot.resume(result)
+ sandbox = Zapcode(code, **kwargs)
+ state = sandbox.start()
- return ExecutionResult(
- output=state.get("output"),
- stdout=state.get("stdout", ""),
- tool_calls=tool_calls,
- )
+ while state.get("suspended"):
+ fn_name = state["function_name"]
+ args = state["args"]
+
+ tool_def = tool_defs.get(fn_name)
+ if not tool_def:
+ raise ValueError(
+ f"Guest code called unknown function '{fn_name}'. "
+ f"Available: {', '.join(tool_names)}"
+ )
+
+ # Build named args from positional args
+ param_names = list(tool_def.parameters.keys())
+ named_args = {
+ param_names[i]: args[i]
+ for i in range(min(len(param_names), len(args)))
+ }
+
+ tool_span = _create_span("tool_call", {
+ "zapcode.tool.name": fn_name,
+ "zapcode.tool.args": json.dumps(args, default=str),
+ }) if tracing else None
+
+ result = tool_def.execute(named_args)
+ tool_calls.append({"name": fn_name, "args": args, "result": result})
+
+ if tool_span:
+ tool_span.attributes["zapcode.tool.result"] = json.dumps(result, default=str)
+ _end_span(tool_span)
+ exec_span.children.append(tool_span)
+
+ snapshot: ZapcodeSnapshot = state["snapshot"]
+ state = snapshot.resume(result)
+
+ stdout = state.get("stdout", "")
+
+ if exec_span:
+ exec_span.attributes["zapcode.output"] = json.dumps(state.get("output"), default=str)
+ if stdout:
+ exec_span.attributes["zapcode.stdout"] = stdout
+ _end_span(exec_span)
+
+ if debug and exec_span:
+ _print_trace(exec_span)
+
+ return ExecutionResult(
+ code=code,
+ output=state.get("output"),
+ stdout=stdout,
+ tool_calls=tool_calls,
+ trace=exec_span,
+ )
+ except Exception as err:
+ error_msg = str(err)
+
+ if exec_span:
+ exec_span.attributes["zapcode.error"] = error_msg
+ _end_span(exec_span, "error")
+
+ if not auto_fix:
+ if debug and exec_span:
+ _print_trace(exec_span)
+ raise
+
+ if debug and exec_span:
+ _print_trace(exec_span)
+
+ return ExecutionResult(
+ code=code,
+ output=None,
+ stdout="",
+ tool_calls=tool_calls,
+ error=f"Execution failed: {error_msg}. Please fix your code and try again.",
+ trace=exec_span,
+ )
# ---------------------------------------------------------------------------
@@ -241,6 +343,12 @@ class ZapcodeAI:
custom: dict[str, Any] = field(default_factory=dict)
"""Output from custom adapters, keyed by adapter name."""
+ get_trace: Callable[[], TraceSpan | None] = field(default=lambda: None)
+ """Get the full session trace tree. Available when debug or auto_fix is enabled."""
+
+ print_trace: Callable[[], None] = field(default=lambda: None)
+ """Print the full session trace tree to the console."""
+
# ---------------------------------------------------------------------------
# Main entry point
@@ -252,6 +360,8 @@ def zapcode(
system: str | None = None,
memory_limit_bytes: int | None = None,
time_limit_ms: int = 10_000,
+ debug: bool = False,
+ auto_fix: bool = False,
adapters: list[Adapter] | None = None,
) -> ZapcodeAI:
"""
@@ -263,6 +373,11 @@ def zapcode(
- `handle_tool_call(code)` → Universal handler for any SDK
- `custom` → Output from custom adapters
+ Args:
+ debug: Log generated code, tool calls, and output to the console.
+ auto_fix: When True, execution errors are returned as tool results
+ instead of raising. The LLM sees the error and can self-correct.
+
Example with Anthropic SDK::
from zapcode_ai import zapcode, ToolDefinition, ParamDef
@@ -293,13 +408,30 @@ def zapcode(
print(result.output)
"""
system_prompt = _build_system_prompt(tools, system)
+ tracing = debug or auto_fix
+
+ # Session-level trace collects all attempts
+ session_trace: TraceSpan | None = (
+ _create_span("session", {"zapcode.tools": ", ".join(tools.keys())})
+ if tracing else None
+ )
+ attempt_count = 0
def handle_tool_call(code: str) -> ExecutionResult:
- return _execute_code(
+ nonlocal attempt_count
+ attempt_count += 1
+ result = _execute_code(
code, tools,
memory_limit_bytes=memory_limit_bytes,
time_limit_ms=time_limit_ms,
+ debug=debug,
+ auto_fix=auto_fix,
)
+ if session_trace and result.trace:
+ result.trace.name = f"attempt_{attempt_count}"
+ result.trace.attributes["zapcode.attempt"] = attempt_count
+ session_trace.children.append(result.trace)
+ return result
# Anthropic SDK format
anthropic_tools = [
@@ -335,12 +467,28 @@ def handle_tool_call(code: str) -> ExecutionResult:
for adapter in adapters:
custom[adapter.name] = adapter.adapt(ctx)
+ def get_trace() -> TraceSpan | None:
+ if not session_trace:
+ return None
+ status = "ok" if any(c.status == "ok" for c in session_trace.children) else "error"
+ _end_span(session_trace, status)
+ return session_trace
+
+ def print_session_trace() -> None:
+ trace = get_trace()
+ if trace:
+ print("\n─── Zapcode Trace ───")
+ _print_trace(trace)
+ print("─────────────────────\n")
+
return ZapcodeAI(
system=system_prompt,
anthropic_tools=anthropic_tools,
openai_tools=openai_tools,
handle_tool_call=handle_tool_call,
custom=custom,
+ get_trace=get_trace,
+ print_trace=print_session_trace,
)
@@ -350,6 +498,8 @@ def execute(
*,
memory_limit_bytes: int | None = None,
time_limit_ms: int | None = None,
+ debug: bool = False,
+ auto_fix: bool = False,
) -> ExecutionResult:
"""
Execute TypeScript code directly in a Zapcode sandbox with tool resolution.
@@ -374,4 +524,6 @@ def execute(
code, tools,
memory_limit_bytes=memory_limit_bytes,
time_limit_ms=time_limit_ms,
+ debug=debug,
+ auto_fix=auto_fix,
)
diff --git a/packages/zapcode-ai/src/index.ts b/packages/zapcode-ai/src/index.ts
index ac22cc3..5a7c153 100644
--- a/packages/zapcode-ai/src/index.ts
+++ b/packages/zapcode-ai/src/index.ts
@@ -62,13 +62,48 @@ export interface ZapcodeAIOptions {
timeLimitMs?: number;
/** Custom adapters for additional AI SDKs. */
adapters?: ZapcodeAdapter[];
+ /**
+ * Log generated code, tool calls, and output to the console.
+ * Useful for understanding what the LLM generates.
+ */
+ debug?: boolean;
+ /**
+ * When true, execution errors are returned as tool results instead of
+ * throwing. The LLM sees the error and can self-correct on the next step.
+ * Works with `maxSteps` in the Vercel AI SDK. Default: false.
+ */
+ autoFix?: boolean;
+}
+
+/** A single span in the execution trace. OTel-compatible shape. */
+export interface TraceSpan {
+ /** Span name (e.g. "execute", "tool_call", "error", "retry"). */
+ name: string;
+ /** When the span started (ms since epoch). */
+ startTime: number;
+ /** When the span ended (ms since epoch). */
+ endTime: number;
+ /** Duration in ms. */
+ durationMs: number;
+ /** "ok" or "error". */
+ status: "ok" | "error";
+ /** Structured attributes — keys map to OTel attribute naming. */
+ attributes: Record;
+ /** Child spans. */
+ children: TraceSpan[];
}
/** Result of executing guest code. */
export interface ExecutionResult {
+ /** The TypeScript code that the LLM generated. */
+ code: string;
output: unknown;
stdout: string;
toolCalls: Array<{ name: string; args: unknown[]; result: unknown }>;
+ /** Present when autoFix is enabled and execution failed. */
+ error?: string;
+ /** Execution trace. Present when debug or autoFix is enabled. */
+ trace?: TraceSpan;
}
/** What `zapcode()` returns — adapters for every major AI SDK. */
@@ -106,6 +141,19 @@ export interface ZapcodeAIResult {
* Access with `result.custom["my-adapter-name"]`.
*/
custom: Record;
+
+ /**
+ * Get the full session trace tree (all attempts).
+ * Available when debug or autoFix is enabled.
+ * Call after generateText/streamText completes.
+ */
+ getTrace: () => TraceSpan | undefined;
+
+ /**
+ * Print the full session trace tree to the console.
+ * Available when debug or autoFix is enabled.
+ */
+ printTrace: () => void;
}
// ---------------------------------------------------------------------------
@@ -212,6 +260,46 @@ const CODE_TOOL_DESCRIPTION =
"The code can call the available tool functions using await. " +
"The last expression is the return value.";
+// ---------------------------------------------------------------------------
+// Trace helpers
+// ---------------------------------------------------------------------------
+
+function createSpan(name: string, attributes: Record = {}): TraceSpan {
+ return {
+ name,
+ startTime: Date.now(),
+ endTime: 0,
+ durationMs: 0,
+ status: "ok",
+ attributes,
+ children: [],
+ };
+}
+
+function endSpan(span: TraceSpan, status?: "ok" | "error"): TraceSpan {
+ span.endTime = Date.now();
+ span.durationMs = span.endTime - span.startTime;
+ if (status) span.status = status;
+ return span;
+}
+
+function printTrace(span: TraceSpan, indent = 0): void {
+ const prefix = indent === 0 ? "" : "│ ".repeat(indent - 1) + "├─ ";
+ const icon = span.status === "error" ? "✗" : "✓";
+ const duration = span.durationMs < 1 ? "<1ms" : `${span.durationMs}ms`;
+ const attrs = Object.entries(span.attributes)
+ .map(([k, v]) => {
+ const str = typeof v === "string" && v.length > 80 ? v.slice(0, 77) + "..." : String(v);
+ return `${k}=${str}`;
+ })
+ .join(" ");
+
+ console.log(`${prefix}${icon} ${span.name} (${duration})${attrs ? " " + attrs : ""}`);
+ for (const child of span.children) {
+ printTrace(child, indent + 1);
+ }
+}
+
// ---------------------------------------------------------------------------
// Execution engine
// ---------------------------------------------------------------------------
@@ -219,56 +307,111 @@ const CODE_TOOL_DESCRIPTION =
async function executeCode(
code: string,
toolDefs: Record,
- options: { memoryLimitMb?: number; timeLimitMs?: number }
+ options: { memoryLimitMb?: number; timeLimitMs?: number; debug?: boolean; autoFix?: boolean }
): Promise {
const toolNames = Object.keys(toolDefs);
const toolCalls: ExecutionResult["toolCalls"] = [];
+ const debug = options.debug ?? false;
+ const autoFix = options.autoFix ?? false;
+ const tracing = debug || autoFix;
+
+ const execSpan = tracing ? createSpan("execute", { "zapcode.code": code }) : undefined;
+
+ try {
+ const sandbox = new Zapcode(code, {
+ externalFunctions: toolNames,
+ timeLimitMs: options.timeLimitMs ?? 10_000,
+ memoryLimitMb: options.memoryLimitMb ?? 32,
+ });
+
+ let state = sandbox.start();
+ let stdout = "";
+
+ // Snapshot/resume loop — resolve each tool call as the VM suspends
+ while (!state.completed) {
+ const { functionName, args } = state;
+
+ const toolDef = toolDefs[functionName];
+ if (!toolDef) {
+ throw new Error(
+ `Guest code called unknown function '${functionName}'. ` +
+ `Available: ${toolNames.join(", ")}`
+ );
+ }
+
+ // Build named args from positional args using the parameter schema
+ const paramNames = Object.keys(toolDef.parameters);
+ const namedArgs: Record = {};
+ for (let i = 0; i < paramNames.length && i < args.length; i++) {
+ namedArgs[paramNames[i]] = args[i];
+ }
+
+ const toolSpan = tracing ? createSpan("tool_call", {
+ "zapcode.tool.name": functionName,
+ "zapcode.tool.args": JSON.stringify(args),
+ }) : undefined;
+
+ const result = await toolDef.execute(namedArgs);
+ toolCalls.push({ name: functionName, args, result });
+
+ if (toolSpan) {
+ toolSpan.attributes["zapcode.tool.result"] = JSON.stringify(result);
+ endSpan(toolSpan);
+ execSpan!.children.push(toolSpan);
+ }
+
+ // Resume the VM with the tool's return value
+ const snapshot = ZapcodeSnapshotHandle.load(state.snapshot);
+ state = snapshot.resume(result);
+ }
- const sandbox = new Zapcode(code, {
- externalFunctions: toolNames,
- timeLimitMs: options.timeLimitMs ?? 10_000,
- memoryLimitMb: options.memoryLimitMb ?? 32,
- });
-
- let state = sandbox.start();
- let stdout = "";
-
- // Snapshot/resume loop — resolve each tool call as the VM suspends
- while (!state.completed) {
- const { functionName, args } = state;
-
- const toolDef = toolDefs[functionName];
- if (!toolDef) {
- throw new Error(
- `Guest code called unknown function '${functionName}'. ` +
- `Available: ${toolNames.join(", ")}`
- );
+ if (state.stdout) {
+ stdout = state.stdout;
}
- // Build named args from positional args using the parameter schema
- const paramNames = Object.keys(toolDef.parameters);
- const namedArgs: Record = {};
- for (let i = 0; i < paramNames.length && i < args.length; i++) {
- namedArgs[paramNames[i]] = args[i];
+ if (execSpan) {
+ execSpan.attributes["zapcode.output"] = JSON.stringify(state.output);
+ if (stdout) execSpan.attributes["zapcode.stdout"] = stdout;
+ endSpan(execSpan);
}
- const result = await toolDef.execute(namedArgs);
- toolCalls.push({ name: functionName, args, result });
+ if (debug && execSpan) {
+ printTrace(execSpan);
+ }
- // Resume the VM with the tool's return value
- const snapshot = ZapcodeSnapshotHandle.load(state.snapshot);
- state = snapshot.resume(result);
- }
+ return {
+ code,
+ output: state.output,
+ stdout,
+ toolCalls,
+ ...(execSpan ? { trace: execSpan } : {}),
+ };
+ } catch (err: any) {
+ const errorMsg = err.message ?? String(err);
- if (state.stdout) {
- stdout = state.stdout;
- }
+ if (execSpan) {
+ execSpan.attributes["zapcode.error"] = errorMsg;
+ endSpan(execSpan, "error");
+ }
- return {
- output: state.output,
- stdout,
- toolCalls,
- };
+ if (!autoFix) {
+ if (debug && execSpan) printTrace(execSpan);
+ throw err;
+ }
+
+ if (debug && execSpan) {
+ printTrace(execSpan);
+ }
+
+ return {
+ code,
+ output: null,
+ stdout: "",
+ toolCalls,
+ error: `Execution failed: ${errorMsg}. Please fix your code and try again.`,
+ ...(execSpan ? { trace: execSpan } : {}),
+ };
+ }
}
// ---------------------------------------------------------------------------
@@ -309,15 +452,31 @@ async function executeCode(
* ```
*/
export function zapcode(options: ZapcodeAIOptions): ZapcodeAIResult {
- const { tools: toolDefs, system: userSystem, memoryLimitMb, timeLimitMs, adapters } = options;
+ const { tools: toolDefs, system: userSystem, memoryLimitMb, timeLimitMs, adapters, debug, autoFix } = options;
const system = buildSystemPrompt(toolDefs, userSystem);
- const execOptions = { memoryLimitMb, timeLimitMs };
+ const execOptions = { memoryLimitMb, timeLimitMs, debug, autoFix };
+ const tracing = debug || autoFix;
+
+ // Session-level trace collects all attempts
+ const sessionTrace: TraceSpan | undefined = tracing
+ ? createSpan("session", { "zapcode.tools": Object.keys(toolDefs).join(", ") })
+ : undefined;
+ let attemptCount = 0;
// Universal handler
const handleToolCall = async (code: string): Promise => {
- return executeCode(code, toolDefs, execOptions);
+ attemptCount++;
+ const result = await executeCode(code, toolDefs, execOptions);
+
+ if (sessionTrace && result.trace) {
+ result.trace.name = `attempt_${attemptCount}`;
+ result.trace.attributes["zapcode.attempt"] = attemptCount;
+ sessionTrace.children.push(result.trace);
+ }
+
+ return result;
};
// Vercel AI SDK format — use tool() + jsonSchema() for proper integration
@@ -365,7 +524,22 @@ export function zapcode(options: ZapcodeAIOptions): ZapcodeAIResult {
}
}
- return { system, tools, openaiTools, anthropicTools, handleToolCall, custom };
+ const getTrace = (): TraceSpan | undefined => {
+ if (!sessionTrace) return undefined;
+ endSpan(sessionTrace, sessionTrace.children.some(c => c.status === "ok") ? "ok" : "error");
+ return sessionTrace;
+ };
+
+ const printSessionTrace = (): void => {
+ const trace = getTrace();
+ if (trace) {
+ console.log(`\n─── Zapcode Trace ───`);
+ printTrace(trace);
+ console.log(`─────────────────────\n`);
+ }
+ };
+
+ return { system, tools, openaiTools, anthropicTools, handleToolCall, custom, getTrace, printTrace: printSessionTrace };
}
// ---------------------------------------------------------------------------
@@ -483,7 +657,7 @@ export function createAdapter(
export async function execute(
code: string,
tools: Record,
- options?: { memoryLimitMb?: number; timeLimitMs?: number }
+ options?: { memoryLimitMb?: number; timeLimitMs?: number; debug?: boolean; autoFix?: boolean }
): Promise {
return executeCode(code, tools, options ?? {});
}