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
28 changes: 27 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,37 @@ export const main = task({
> [!NOTE]
> The runtime requires a task named `"main"` as the entry point. Python can define the main task itself, but it's recommended to set it manually.


When you run `capsule run main.py` (or `main.ts`), your code is compiled into a WebAssembly module and executed in a dedicated sandbox to isolate tasks.

Each task operates within its own sandbox with configurable resource limits, ensuring that failures are contained and don't cascade to other parts of your workflow. The host system controls every aspect of execution, from CPU allocation via Wasm fuel metering to memory constraints and timeout enforcement.

### Response Format

Every task returns a structured JSON envelope containing both the result and execution metadata:
```json
{
"success": true,
"result": { "processed": 5, "status": "complete" },
"error": null,
"execution": {
"task_name": "data_processor",
"duration_ms": 1523,
"retries": 0,
"fuel_consumed": 45000
}
}
```

**Response fields:**
- `success` — Boolean indicating whether the task completed successfully
- `result` — The actual return value from your task (json, string, null on failure etc..)
- `error` — Error details if the task failed (`{ error_type: string, message: string }`)
- `execution` — Performance metrics:
- `task_name` — Name of the executed task
- `duration_ms` — Execution time in milliseconds
- `retries` — Number of retry attempts that occurred
- `fuel_consumed` — CPU resources used (see [Compute Levels](#compute-levels))

## Quick Start

### Python
Expand Down
4 changes: 2 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ This document tracks the development status of Capsule.

## v0.4.0: Configuration & Observability

**Status:** In progress
**Status:** ✅ Done

**Goal:** Improve developer workflow with project configuration and richer task feedback.

- [x] **Config File:** `capsule.toml` for default task options.
- [ ] **Structured Output:** Tasks return a detailed JSON envelope with execution metadata:
- [x] **Structured Output:** Tasks return a detailed JSON envelope with execution metadata:

```json
{
Expand Down
31 changes: 30 additions & 1 deletion crates/capsule-cli/docs/README-pypi.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ Run it:
capsule run hello.py
```

> [!TIP]
> Use `--verbose` to display real-time task execution details.

## How It Works

Simply annotate your Python functions with the `@task` decorator:
Expand All @@ -53,9 +56,35 @@ def analyze_data(dataset: list) -> dict:
> [!NOTE]
> The runtime requires a task named `"main"` as the entry point. Python can define the main task itself, but it's recommended to set it manually.


When you run `capsule run main.py`, your code is compiled into a WebAssembly module and executed in a dedicated sandbox.

### Response Format

Every task returns a structured JSON envelope containing both the result and execution metadata:
```json
{
"success": true,
"result": { "processed": 5, "status": "complete" },
"error": null,
"execution": {
"task_name": "data_processor",
"duration_ms": 1523,
"retries": 0,
"fuel_consumed": 45000
}
}
```

**Response fields:**
- `success` — Boolean indicating whether the task completed successfully
- `result` — The actual return value from your task (json, string, null on failure etc..)
- `error` — Error details if the task failed (`{ error_type: string, message: string }`)
- `execution` — Performance metrics:
- `task_name` — Name of the executed task
- `duration_ms` — Execution time in milliseconds
- `retries` — Number of retry attempts that occurred
- `fuel_consumed` — CPU resources used (see [Compute Levels](#compute-levels))

## Documentation

### Task Configuration Options
Expand Down
30 changes: 30 additions & 0 deletions crates/capsule-cli/npm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ Run it:
capsule run hello.ts
```

> [!TIP]
> Use `--verbose` to display real-time task execution details.

## How It Works

Simply use a wrapper function to define your tasks:
Expand Down Expand Up @@ -73,6 +76,33 @@ export const main = task({

When you run `capsule run main.ts`, your code is compiled into a WebAssembly module and executed in a dedicated sandbox.

### Response Format

Every task returns a structured JSON envelope containing both the result and execution metadata:
```json
{
"success": true,
"result": { "processed": 5, "status": "complete" },
"error": null,
"execution": {
"task_name": "data_processor",
"duration_ms": 1523,
"retries": 0,
"fuel_consumed": 45000
}
}
```

**Response fields:**
- `success` — Boolean indicating whether the task completed successfully
- `result` — The actual return value from your task (json, string, null on failure etc..)
- `error` — Error details if the task failed (`{ error_type: string, message: string }`)
- `execution` — Performance metrics:
- `task_name` — Name of the executed task
- `duration_ms` — Execution time in milliseconds
- `retries` — Number of retry attempts that occurred
- `fuel_consumed` — CPU resources used (see [Compute Levels](#compute-levels))

## Documentation

### Task Configuration Options
Expand Down
222 changes: 155 additions & 67 deletions crates/capsule-core/src/wasm/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use crate::config::log::{InstanceState, UpdateInstanceLog};
use crate::wasm::execution_policy::ExecutionPolicy;
use crate::wasm::runtime::{Runtime, RuntimeCommand, WasmRuntimeError};
use crate::wasm::state::{CapsuleAgent, State};
use crate::wasm::utilities::task_config::{TaskError, TaskExecution, TaskResult};

pub struct RunInstance {
task_id: String,
Expand Down Expand Up @@ -51,83 +52,170 @@ impl RuntimeCommand for RunInstance {
.capsule_host_task_runner()
.call_run(&mut self.store, &self.args_json);

let result = match self.policy.timeout_duration() {
let response = match self.policy.timeout_duration() {
Some(duration) => match tokio::time::timeout(duration, wasm_future).await {
Ok(inner_result) => inner_result,
Err(_elapsed) => {
runtime
.log
.update_log(UpdateInstanceLog {
task_id: self.task_id.clone(),
state: InstanceState::TimedOut,
Ok(wasm_result) => match wasm_result {
Ok(inner_result) => match inner_result {
Ok(json_string) => {
let result_object = serde_json::from_str(&json_string)
.unwrap_or(serde_json::Value::String(json_string));

let result = result_object
.get("result")
.cloned()
.unwrap_or(result_object);

TaskResult {
success: true,
result: Some(result),
error: None,
execution: TaskExecution {
task_name: self.policy.name.clone(),
duration_ms: duration.as_millis() as u64,
retries: self.policy.max_retries,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
},
}
}
Err(error_string) => TaskResult {
success: false,
result: None,
error: Some(TaskError {
error_type: "task_error".to_string(),
message: error_string,
}),
execution: TaskExecution {
task_name: self.policy.name.clone(),
duration_ms: duration.as_millis() as u64,
retries: self.policy.max_retries,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
},
},
},
Err(e) => TaskResult {
success: false,
result: None,
error: Some(TaskError {
error_type: "Wasm_error".to_string(),
message: e.to_string(),
}),
execution: TaskExecution {
task_name: self.policy.name.clone(),
duration_ms: duration.as_millis() as u64,
retries: self.policy.max_retries,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
})
.await?;
},
},
},
Err(_elapsed) => TaskResult {
success: false,
result: None,
error: Some(TaskError {
error_type: "timeout".to_string(),
message: format!("timeout after {}ms", duration.as_millis()),
}),
execution: TaskExecution {
task_name: self.policy.name.clone(),
duration_ms: duration.as_millis() as u64,
retries: self.policy.max_retries,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
},
},
},
None => match wasm_future.await {
Ok(inner_result) => match inner_result {
Ok(json_string) => {
let result_object = serde_json::from_str(&json_string)
.unwrap_or(serde_json::Value::String(json_string));

runtime
.task_reporter
.lock()
.await
.task_failed(&self.policy.name, "Timed out");
let result = result_object
.get("result")
.cloned()
.unwrap_or(result_object);

return Ok(String::new());
}
TaskResult {
success: true,
result: Some(result),
error: None,
execution: TaskExecution {
task_name: self.policy.name.clone(),
duration_ms: 0,
retries: self.policy.max_retries,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
},
}
}
Err(error_string) => TaskResult {
success: false,
result: None,
error: Some(TaskError {
error_type: "task_error".to_string(),
message: error_string,
}),
execution: TaskExecution {
task_name: self.policy.name.clone(),
duration_ms: 0,
retries: self.policy.max_retries,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
},
},
},
Err(e) => TaskResult {
success: false,
result: None,
error: Some(TaskError {
error_type: "wasm_error".to_string(),
message: e.to_string(),
}),
execution: TaskExecution {
task_name: self.policy.name.clone(),
duration_ms: 0,
retries: self.policy.max_retries,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
},
},
},
None => wasm_future.await,
};

match result {
Ok(Ok(value)) => {
runtime
.log
.update_log(UpdateInstanceLog {
task_id: self.task_id,
state: InstanceState::Completed,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
})
.await?;
Ok(value)
}
Ok(Err(error_msg)) => {
runtime
.log
.update_log(UpdateInstanceLog {
task_id: self.task_id,
state: InstanceState::Failed,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
})
.await?;

runtime
.task_reporter
.lock()
.await
.task_failed(&self.policy.name, &error_msg);

Ok(String::new())
}
Err(e) => {
runtime
.log
.update_log(UpdateInstanceLog {
task_id: self.task_id,
state: InstanceState::Failed,
fuel_consumed: self.policy.compute.as_fuel()
- self.store.get_fuel().unwrap_or(0),
})
.await?;
let state = if response.success {
InstanceState::Completed
} else {
InstanceState::Failed
};

runtime
.log
.update_log(UpdateInstanceLog {
task_id: self.task_id.clone(),
state,
fuel_consumed: response.execution.fuel_consumed,
})
.await?;

runtime
.task_reporter
.lock()
.await
.task_failed(&self.policy.name, &e.to_string());
if !response.success {
let error_message = response
.error
.as_ref()
.map(|e| e.message.as_str())
.unwrap_or("Unknown error");

Ok(String::new())
}
runtime
.task_reporter
.lock()
.await
.task_failed(&self.policy.name, error_message);
}

let json_output = serde_json::to_string(&response)
.map_err(|e| WasmRuntimeError::SerializationError(e.to_string()))?;

Ok(json_output)
}
}
Loading