Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "CONTRIBUTING.md" -type f

Repository: TheUncharted/zapcode

Length of output: 81


🏁 Script executed:

head -n 70 CONTRIBUTING.md | tail -n 20

Repository: TheUncharted/zapcode

Length of output: 1138


🏁 Script executed:

# Check if there's a Python environment setup already documented
rg -A 5 -B 5 "E2E Python" CONTRIBUTING.md

Repository: TheUncharted/zapcode

Length of output: 663


🏁 Script executed:

# Check for any existing venv or setup instructions
rg "venv|virtualenv|maturin" CONTRIBUTING.md

Repository: TheUncharted/zapcode

Length of output: 181


🌐 Web query:

maturin develop virtualenv requirements documentation

💡 Result:

  • Create/activate a virtualenv (recommended for local dev): maturin’s own tutorial starts by creating a virtual environment, then installing maturin. [1]
  • Use maturin develop to install into the current environment: maturin develop builds your Rust crate and installs it directly into the currently active virtualenv (skipping wheel generation). This is the standard workflow for fast local iteration. [1][2][3]
  • Docs (Sphinx) “requirements” setup: maturin’s Sphinx integration docs explicitly suggest putting documentation dependencies in docs/requirements.txt (i.e., Python packages needed to build the docs). [4]
  • If you’re packaging (pyproject.toml): the tutorial notes you typically declare maturin as a requirement (with a pinned version) and set it as the build-backend per PEP 517. [1]

Sources: [1] Maturin User Guide – Tutorial, [2] Maturin User Guide – Local Development, [3] PyPI maturin (current release page), [4] Maturin User Guide – Sphinx Integration.


🌐 Web query:

maturin installation requirements Python binding

💡 Result:

Maturin (Python binding / Rust-extension builds): installation requirements

  • Python environment: a working Python install (for PyO3-based projects, Python ≥ 3.7 is required). [3]
  • Rust toolchain: rustc/cargo installed (for PyO3, the minimum Rust version is 1.83). [3]
  • Maturin itself (recommended install): pipx install maturin (or pip install maturin). [1][3]

Optional / scenario-specific dependencies

  • zig: optional, used as a linker to simplify cross-compiling and help with manylinux compliance. [1]
  • patchelf: optional, used to repair wheels that link to other shared libraries (install via maturin[patchelf]). [1]
  • If using cffi bindings: ensure cffi is installed (maturin may auto-install it only in some virtualenv cases; otherwise you must pip install cffi). [2]

Sources: [1] Maturin Installation docs, [2] Maturin Bindings docs, [3] PyO3 “Getting started / Installation” docs.


Document the Python env/bootstrap step before maturin develop.

maturin develop installs into the current virtualenv, and maturin itself must be installed separately. A fresh contributor will hit a failure before the example runs if neither is set up. Please add the venv creation and maturin installation step.

♻️ Suggested doc update
-- E2E Python: `cd crates/zapcode-py && maturin develop --release && cd ../../examples/python/basic && python main.py`
+- E2E Python: `python -m venv .venv && . .venv/bin/activate && pip install maturin && cd crates/zapcode-py && maturin develop --release && cd ../../examples/python/basic && python main.py`
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- E2E Python: `cd crates/zapcode-py && maturin develop --release && cd ../../examples/python/basic && python main.py`
- E2E Python: `python -m venv .venv && . .venv/bin/activate && pip install maturin && cd crates/zapcode-py && maturin develop --release && cd ../../examples/python/basic && python main.py`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` at line 64, Update the E2E Python instruction that currently
runs "cd crates/zapcode-py && maturin develop --release && cd
../../examples/python/basic && python main.py" to first create/activate a Python
virtualenv and install maturin into it; specifically, add steps to create a venv
(python -m venv .venv), activate it (.venv\\Scripts\\activate on Windows or
source .venv/bin/activate on Unix), and run pip install --upgrade pip maturin
before running maturin develop so that maturin installs into the active
environment.


## Reporting issues

Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

69 changes: 63 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.

<details>
<summary><strong>Rust</strong></summary>
Expand Down Expand Up @@ -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.
</details>

<details>
Expand All @@ -246,7 +246,7 @@ console.log(result.output); // 120
</script>
```

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.
</details>

## AI Agent Usage
Expand Down Expand Up @@ -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.

<details>
<summary><strong>Anthropic SDK</strong></summary>
Expand Down Expand Up @@ -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).
</details>

<details>
Expand Down Expand Up @@ -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.
</details>

## 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:**
Expand Down
2 changes: 2 additions & 0 deletions crates/zapcode-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
179 changes: 179 additions & 0 deletions crates/zapcode-core/src/trace.rs
Original file line number Diff line number Diff line change
@@ -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<TraceSpan>,
}

/// 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<TraceSpan>,
}

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);
}
Loading
Loading