escher-execution-engine 0.1.2

Production-ready async execution engine for system commands
Documentation
# UI Team Handoff - Execution Engine Integration

**Date**: 2025-12-03
**Component**: v2-execution-engine-rust

## 🚨 Critical Architecture Change

**The Execution Engine is now a SINGLETON with strict SERIAL execution.**

To ensure stability and prevent race conditions during the UI integration phase, we have enforced a strict **Serial Execution Mode**. This means only **one command executes at a time**.

### Initialization (Must do this!)

You **must** use `ExecutionEngine::init_global(config)` instead of `ExecutionEngine::new()`.

```rust
// src-tauri/src/main.rs

use execution_engine::{ExecutionEngine, ExecutionConfig};

fn main() {
    let config = ExecutionConfig::default();
    
    // ⚠️ THIS IS REQUIRED ⚠️
    // This initializes the global singleton and forces max_concurrent_executions = 1
    ExecutionEngine::init_global(config).expect("Failed to initialize execution engine");
    
    // Access the engine instance
    let engine = ExecutionEngine::global();

    tauri::Builder::default()
        // Note: You might not need to manage state manually if using the global accessor in commands
        // but keeping it managed is fine for dependency injection consistency.
        .manage(engine) 
        // ...
}
```

### Usage in Commands

You can now access the engine securely from anywhere using the global accessor:

```rust
#[tauri::command]
async fn execute_command(request: ExecutionRequest) -> Result<String, String> {
    // Access the singleton directly
    let engine = ExecutionEngine::global();
    
    let id = engine.execute(request).await.map_err(|e| e.to_string())?;
    Ok(id.to_string())
}
```

---

## 🛠️ Testing without UI

We have created a **TypeScript Integration Bridge** to allow you to test the execution engine logic without needing the full Tauri UI stack.

**Location**: `v2-execution-engine-rust/examples/node-caller.ts`

### How to use

1. Go to the execution engine directory:
   ```bash
   cd v2-execution-engine-rust
   ```

2. Run the bridge:
   ```bash
   npx ts-node examples/node-caller.ts
   ```

3. What it does:
   - Spawns the Rust engine in a separate process.
   - Sends commands via JSON over stdin.
   - Receives results via JSON over stdout.
   - **Verifies that the Rust/TS types match.**

This is perfect for testing your `ExecutionRequest` payloads before wiring them up in the React frontend.

---

## 📦 Data Types

Ensure your frontend types match the Rust structs in `src/types.rs`.

**ExecutionRequest**:
```typescript
interface ExecutionRequest {
  id: string; // UUID
  command: 
    | { type: 'shell'; command: string; shell: string } // usually this
    | { type: 'exec'; program: string; args: string[] };
  env: Record<string, string>;
  working_dir: string | null;
  timeout_ms: number | null;
  output_log_path: string | null; // Optional path for file logging
  metadata: Record<string, any>;
}
```

**ExecutionResult**:
```typescript
interface ExecutionResult {
  id: string;
  status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled' | 'timeout';
  success: boolean;
  exit_code: number;
  stdout: string;
  stderr: string;
  error?: string;
}
```

---

## 📡 Real-Time Streaming (Optional)

If you need real-time output (e.g., for a terminal view), you must register an `EventHandler` that forwards events to the Tauri frontend.

### 1. Implement the Handler

```rust
use execution_engine::{EventHandler, ExecutionEvent};
use tauri::{AppHandle, Manager};
use async_trait::async_trait;

struct TauriEventHandler {
    app_handle: AppHandle,
}

#[async_trait]
impl EventHandler for TauriEventHandler {
    async fn on_event(&self, event: ExecutionEvent) {
        // Emit event to frontend
        // Event payload matches ExecutionEvent struct (serde serialized)
        let _ = self.app_handle.emit_all("execution:event", event);
    }
}
```

### 2. Register during Initialization

```rust
use std::sync::Arc;

fn main() {
    let config = ExecutionConfig::default();
    
    // Initialize engine
    let engine = ExecutionEngine::init_global(config).expect("Failed to init");

    tauri::Builder::default()
        .setup(|app| {
            let handle = app.handle();
            
            // Attach handler to the global engine instance
            // Note: This API might require slight adjustment based on exact Engine API for mutable access 
            // or if we need to do this *before* init_global.
            // 
            // Since init_global consumes config, you might need to wrap the handler 
            // if the engine supports dynamic handler attachment, or pass it during init 
            // (check engine.rs specific signatures).
            
            Ok(())
        })
        // ...
}
```

**Frontend Event Listener**:
```typescript
import { listen } from '@tauri-apps/api/event';

type ExecutionEvent = 
  | { event_type: 'started', execution_id: string, ... }
  | { event_type: 'stdout', execution_id: string, line: string, ... }
  | { event_type: 'completed', result: ExecutionResult, ... };

await listen<ExecutionEvent>('execution:event', (event) => {
  const payload = event.payload;
  if (payload.event_type === 'stdout') {
    console.log(payload.line);
  }
});
```