Skip to content
Open
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
1 change: 1 addition & 0 deletions .agents/skills/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ This directory contains specialized skills (recipes) to guide contributors and A
- [Add Native Extension](./add-native-extension/SKILL.md) — C++ operations and JSI bindings.
- [Add Task Pipeline](./add-task-pipeline/SKILL.md) — TypeScript task pipelines and React hooks.
- [Model Schema Validation](./model-schema-validation/SKILL.md) — Model specs, dynamic shapes, and schema validation.
- [Error Handling](./error-handling/SKILL.md) — Error codes, throwing across worklet and JSI boundaries, and catching.
- [Verify and Build](./verify-and-build/SKILL.md) — TypeScript typechecking, native rebuilding, and troubleshooting.
- [Skills Maintenance](./skills-maintenance/SKILL.md) — Keeping skills synchronized with core primitives.
17 changes: 15 additions & 2 deletions .agents/skills/add-native-extension/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Before writing any C++ code, ensure you adhere to the following principles:
- **Do NOT return implicitly allocated JSI Tensors:** Never return newly created `TensorHostObject` instances from C++. This forces the JavaScript layer to reason about their garbage collection and manual lifetimes, leading to native memory leaks.
- **Do NOT define default parameters in C++:** Native C++ functions must never define default argument values (e.g. `axis = -1`). Define all default values explicitly in the TypeScript wrapper layer instead.
- **Do NOT perform in-place mutation without safety checks:** Never allow inputs and outputs to share the same underlying instance.
- **Do NOT throw raw `jsi::JSError`, `std::runtime_error`, or `std::invalid_argument`:** They reach JavaScript with no error code. Throw `RnExecuTorchException` and register through `error::guarded(...)`. See the [Error Handling Skill](../error-handling/SKILL.md).

---

Expand Down Expand Up @@ -61,10 +62,12 @@ namespace rnexecutorch::extensions::<domain>
- Convert primitive parameters using `conversions::asType<T>`.
- Prevent in-place mutations (aliasing) using `tensor::checkNotSameTensor`.
- Lock tensors for thread-safe access using `tensor::tryLockShared` (for inputs) and `tensor::tryLockUnique` (for outputs), which also ensure the underlying memory buffer has not been disposed.
- Raise every failure as an `RnExecuTorchException` and wrap the registration in `error::guarded(...)`, so the error reaches JavaScript with a `code`. See the [Error Handling Skill](../error-handling/SKILL.md).

```cpp
#include "operations.h"
#include "core/conversions.h"
#include "core/error.h"
#include "core/tensor_helpers.h"
#include <algorithm>

Expand All @@ -73,6 +76,11 @@ namespace rnexecutorch::extensions::<domain>
namespace jsi = facebook::jsi;
namespace conversions = rnexecutorch::core::conversions;
namespace tensor = rnexecutorch::core::tensor;
// Required under extensions::*; OMIT inside rnexecutorch::core::*, where
// unqualified `error` already resolves to the sibling namespace.
namespace error = rnexecutorch::core::error;
using rnexecutorch::core::error::RnExecuTorchErrorCode;
using rnexecutorch::core::error::RnExecuTorchException;
using rnexecutorch::core::types::DType;

void install_customOp(jsi::Runtime &rt, jsi::Object &module)
Expand All @@ -83,7 +91,7 @@ namespace rnexecutorch::extensions::<domain>
// 1. Strict argument count validation (No default values here!)
if (count != 3)
{
throw jsi::JSError(rt, "Usage: customOp(src, dst, factor)");
throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument, "Usage: customOp(src, dst, factor)");
}

// 2. Validate, extract input/output tensors and check DType/Shape constraints using fromJs
Expand Down Expand Up @@ -114,7 +122,9 @@ namespace rnexecutorch::extensions::<domain>
return jsi::Value(rt, args[1]);
};

module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, fnBody));
// error::guarded turns any RnExecuTorchException raised in the native stack into a
// JS Error carrying `code`. Never register a bare fnBody.
module.setProperty(rt, name, jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3, error::guarded(fnBody)));
}
}
```
Expand Down Expand Up @@ -192,6 +202,9 @@ When adding a native extension, verify that:
- [ ] In-place mutation is explicitly prevented using `tensor::checkNotSameTensor`.
- [ ] Input and output tensors are locked using `tensor::tryLockShared` and `tensor::tryLockUnique` respectively.
- [ ] No default parameter values are defined in the C++ header/source files.
- [ ] Every failure is raised as `RnExecuTorchException(RnExecuTorchErrorCode::X, ...)`; no raw `jsi::JSError` / `std::runtime_error` / `std::invalid_argument` was introduced.
- [ ] The host function is registered through `error::guarded(fnBody)`, not a bare `fnBody`.
- [ ] `#include "core/error.h"` and its `using` declarations sit at file scope, outside any `#if defined(__ANDROID__)` / `#elif defined(__APPLE__)` branch.
- [ ] The custom operation install function is registered in both the domain `install` function and core [cpp/RnExecutorch.cpp](../../../packages/react-native-executorch/cpp/RnExecutorch.cpp).
- [ ] The TypeScript wrapper lives in the right place: domain-general ops in the shared `ops.ts`, model-/task-specific ops under `src/extensions/<domain>/utils/<name>.ts`.
- [ ] The TypeScript wrapper imports and uses `rnexecutorchJsi` instead of the global `__rnexecutorch_jsi__`.
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/add-task-pipeline/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ When implementing task constructors like `create<Task>` (e.g. `createClassifier`

- **Do NOT access tensors by index:** Avoid using `tensors[0]` or `tensors[1]` throughout the function body. Always destructure and name them explicitly.
- **Do NOT define extra inner helper functions:** You must define **exactly two** inner functions inside the `create<Task>` constructor: the `dispose` function and the task `worklet` executor function. **Push back hard against implementing any other helper closures inside the constructor scope.** Placing other helper functions (especially those that are called from inside the worklet and use the `create<Task>` scope variables) inside `create<Task>` creates implicit dependencies and closures that capture variables, making the code extremely difficult to reason about and debug.
- **Do NOT throw bare `Error`:** Every failure needs a code. Use `RnExecuTorchError('CODE', msg)`, which works both in the `create<Task>` body and inside the worklet executor. See the [Error Handling Skill](../error-handling/SKILL.md).
- **Do NOT leak raw Tensors to consumers:** The returned methods must never return raw `Tensor` objects to the API consumer. Always convert output data to standard JavaScript values/objects before returning.
- **Do NOT cross thread boundaries unnecessarily:** Minimize passing heavy objects between JS and the Worklet thread to avoid serialization overhead.
- **Do NOT treat the `.pte` model as an unchangeable black box:** Reshape the model's inputs and outputs during the PyTorch export phase to make the mobile client pipeline as lightweight as possible. Do not make input/output contracts so specific that they break extensibility.
Expand Down Expand Up @@ -229,6 +230,7 @@ When adding a task pipeline or React hook, verify that:
- [ ] The constructor contains exactly two inner functions (the `dispose` function and the worklet executor).
- [ ] Auxiliary helpers are defined outside the constructor and marked with the `'worklet';` directive if run on the worklet runtime.
- [ ] Raw `Tensor` objects are never returned to the consumer.
- [ ] Every throw uses `RnExecuTorchError('CODE', message)`.
- [ ] Data configurations that genuinely vary across models (e.g. thresholds, labels) are configurable dynamically via the TypeScript task options.
- [ ] Every parameter is bucketed per Principle 6: varies across variants → option; fixed by the export → `const` in the task file; per-call choice → executor argument.
- [ ] No exposed option has exactly one valid value, and no two `models.ts` variants pass an identical options object.
Expand Down
2 changes: 2 additions & 0 deletions .agents/skills/core-guidelines/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,15 @@ Use the following index to locate the specific procedural guides for your task:
| **Create a task pipeline or hook** | [SKILL.md](../add-task-pipeline/SKILL.md) | Guide to building end-to-end TS pipelines (e.g. object detection) and exposing them via React hooks. |
| **Verify, rebuild, or troubleshoot changes** | [SKILL.md](../verify-and-build/SKILL.md) | Workflows for rebuilding TS/C++ and resolving common JSI runtime errors. |
| **Validate model constraints & schemas** | [SKILL.md](../model-schema-validation/SKILL.md) | Guide on specifying model specs, dynamic shapes, and runtime constraints for model validation. |
| **Throw, catch, or classify an error** | [SKILL.md](../error-handling/SKILL.md) | The error code set, `RnExecuTorchError`, C++ `RnExecuTorchException`/`guarded`, and adding a code. |
| **Maintain or refactor codebase patterns** | [SKILL.md](../skills-maintenance/SKILL.md) | Guide to keeping workspace skills in sync with codebase state to prevent documentation decay. |

---

## 💡 Key Coding Conventions

- **Worklets**: Ensure all TypeScript functions directly wrapping native JSI calls start with the `"worklet";` directive so they are compatible with worklet-based libraries (e.g., React Native Reanimated).
- **Errors**: Every failure the library raises carries a string `code`. Use `RnExecuTorchError('CODE', msg)` in TypeScript (a function, not a class, so it survives worklet boundaries) and `RnExecuTorchException` + `error::guarded(...)` in C++. Never branch on message text. See the [Error Handling Skill](../error-handling/SKILL.md).
- **Memory Management**: When writing native C++ code with JSI, pay close attention to JSI reference management and handle ExecuTorch lifecycle states safely.
- **Keep Core Clean**: Always build on top of core primitives. Do not modify files in `cpp/core/` or `src/core/` unless you are fixing a bug in the foundational runtime.

Expand Down
177 changes: 177 additions & 0 deletions .agents/skills/error-handling/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
---
name: error-handling
description: Use when throwing, catching, or classifying errors anywhere in the library (TypeScript, worklets, C++/JSI), or when adding a new error code.
metadata:
id: error_handling
scope: src/core/error.ts, cpp/core/error.*
---

# Skill: Error Handling

Every failure the library raises carries a machine-readable `code`. Consumers branch on
the code; the message is for humans and may be reworded in any release.

**Never make control flow depend on message text.** A regex over `e.message` is the exact
failure this design exists to prevent.

`src/core/error.ts` is the source of truth. `cpp/core/error.h` mirrors it **by hand**, the
same way the rest of the TS/JSI interface is mirrored (DType definitions, schema types, host object
interfaces). Nothing here is generated. If you add a code, add it in both files.

---

## 🚦 Throwing

One factory, usable everywhere including inside worklets:

```typescript
import { RnExecuTorchError } from '../../../core/error';

throw RnExecuTorchError('INVALID_ARGUMENT', `topk must be non-negative, got ${topk}`);
```

`RnExecuTorchError` is a function, not a class. Worklet runtimes are separate JavaScript
runtimes and a value thrown on one does not keep its class identity or prototype chain
when it travels to another, so a class would only survive some of the paths that throw.
The factory returns a plain `Error` with `name`, `code`, and optionally
`etRuntimeErrorCode` attached, which is exactly what crosses every boundary intact.

Write messages the way the rest of the codebase does: prefix with the function name and
include the offending values (`` `execute: Unknown method '${methodName}'` ``). The code
says what class of problem it is; the message says which values caused it.

---

## 🎣 Catching

`isRnExecuTorchError(err, code?)` narrows, and takes an optional code so you rarely need a
second comparison:

```typescript
import { isRnExecuTorchError } from '../core/error';

try {
await classifier.classify(image);
} catch (e) {
if (isRnExecuTorchError(e, 'RESOURCE_BUSY')) return; // a run is already in flight
throw e;
}
```

It is duck-typed and marked `'worklet'`, so it works on both runtimes and on values that
crossed the JSI boundary.

---

## 🔢 The Code Set

```text
LOAD_FAILED EXECUTION_FAILED SCHEMA_MISMATCH INVALID_ARGUMENT INVALID_STATE
RESOURCE_DISPOSED RESOURCE_BUSY DOWNLOAD_FAILED DOWNLOAD_ABORTED UNKNOWN
```

The set is deliberately coarse. **A distinct code earns its place only when an app can
genuinely recover differently from it** (retry a download, wait for a busy resource,
re-create a disposed one). Everything else is a category that exists so crash reporters
can group failures, and the detail belongs in the message.

Concretely: do not add a code for a specific subsystem (a tokenizer failure is
`EXECUTION_FAILED` or `LOAD_FAILED`) or for a variant of "the caller got it wrong"
(`INVALID_ARGUMENT` already covers unsupported languages, bad ranges, and wrong types).
Subdividing non-recoverable failures buys nothing over the message.

Adding one means editing `VALID_ERROR_CODES` in `src/core/error.ts` **and** both the
`RnExecuTorchErrorCode` enum and `errorCodeToString` in `cpp/core/error.h`.

---

## ⚙️ C++ / JSI

Two rules, both mandatory:

1. **Never throw `jsi::JSError` from library code.** Throw `RnExecuTorchException`.
`guarded` is the only place that turns an exception into a JavaScript value, so a code
cannot be lost on the way out.
2. **Wrap every host function registration in `error::guarded(...)`.** Without it the code
is lost, including on the synchronous worklet path that VisionCamera frame processors
use, which has no `wrapAsync` fallback.

```cpp
#include "core/error.h"

namespace rnexecutorch::extensions::<domain> {
namespace jsi = facebook::jsi;
// Required in extensions::*, where sibling lookup does not reach core.
// OMIT this alias inside rnexecutorch::core::*, where unqualified `error`
// already resolves to rnexecutorch::core::error (clang-tidy fails on a dead alias).
namespace error = rnexecutorch::core::error;
using rnexecutorch::core::error::RnExecuTorchErrorCode;
using rnexecutorch::core::error::RnExecuTorchException;

void install_customOp(jsi::Runtime &rt, jsi::Object &module) {
const auto *name = "customOp";
auto fnBody = [](jsi::Runtime &rt, const jsi::Value & /*thisVal*/, const jsi::Value *args, size_t count) -> jsi::Value {
if (count != 3) {
throw RnExecuTorchException(RnExecuTorchErrorCode::InvalidArgument,
"Usage: customOp(src, dst, factor)");
}
// ...
return jsi::Value(rt, args[1]);
};

module.setProperty(rt, name,
jsi::Function::createFromHostFunction(rt, jsi::PropNameID::forAscii(rt, name), 3,
error::guarded(fnBody)));
}
} // namespace
```

`RnExecuTorchException` has a second constructor taking an `executorch::runtime::Error`,
which travels to JS as `etRuntimeErrorCode` for diagnostics. Unwrapping an ExecuTorch
`Result` is done with a small file-local `unwrap` helper (see `cpp/core/model.cpp`), not a
shared utility.

`guard` deliberately lets an existing `jsi::JSError` pass through untouched, so an error
thrown by an app's own callback (for example inside `tensor.through(fn)`) is never
rewritten with our name and code.

**Placement warning:** put `#include "core/error.h"` and the `using` declarations at file
scope, never inside an `#if defined(__ANDROID__)` / `#elif defined(__APPLE__)` branch. Code
inside a platform branch compiles on your machine and breaks on every other target, and a
macOS-only local syntax check will not catch it.

---

## 📱 Example Apps

Leave error handling in `apps/` alone. They are a testing ground, so failures should be
surfaced raw (`e.message`, `String(e)`) rather than translated into friendly copy. See
issue #1288 for the separate discussion about splitting user-facing examples out.

---

## 🚫 Avoid / Anti-Patterns

- **Do NOT branch on message text.** No `/disposed/i.test(msg)`, no `msg.includes(...)`.
Use the code.
- **Do NOT throw bare `Error` or `jsi::JSError`** from library code.
- **Do NOT make `RnExecuTorchError` a class.** It has to survive worklet boundaries.
- **Do NOT add a code for a failure an app cannot recover from differently.** Enrich the
message instead.
- **Do NOT generate the codes.** They are mirrored by hand, like the rest of the TS/JSI
interface.

---

## 📋 Verification Checklist

When adding or changing error handling, verify that:

- [ ] Every new throw site uses `RnExecuTorchError('CODE', message)` (TS) or `RnExecuTorchException` (C++).
- [ ] No raw `jsi::JSError` / `std::runtime_error` / `std::invalid_argument` was introduced.
- [ ] Every new `createFromHostFunction` registration is wrapped in `error::guarded(...)`.
- [ ] The `namespace error = ...` alias is present in `extensions::*` and absent in `core::*`.
- [ ] `#include "core/error.h"` and its `using` declarations sit at file scope, outside any preprocessor branch.
- [ ] Catch sites use `isRnExecuTorchError(e, 'CODE')` rather than matching on the message.
- [ ] Any new code was justified by a distinct recovery path, and added to `src/core/error.ts` **and** `cpp/core/error.h` (enum + `errorCodeToString`).
- [ ] Error handling in `apps/` was left surfacing raw errors.
10 changes: 10 additions & 0 deletions .agents/skills/model-schema-validation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,15 @@ const { dims } = validateSpec(model.schema, {

---

## ⚠️ Failure Codes

`validateSpec` and the native spec validation raise `SCHEMA_MISMATCH` (the spec-builder
helpers `ConstantDim` / `RangeDim` / `EnumDim` raise `INVALID_ARGUMENT` for their own
arguments). A runtime constraint violated by the tensors actually passed to `execute`
raises `INVALID_ARGUMENT`, not `SCHEMA_MISMATCH`: the model is fine, the call is not. See the [Error Handling Skill](../error-handling/SKILL.md).

---

## 📋 Verification Checklist

When specifying model schema validations, verify that:
Expand All @@ -229,3 +238,4 @@ When specifying model schema validations, verify that:
- [ ] Symbol values are extracted using `dims.constant(...)`, `dims.range(...)`, or `dims.enum(...)`.
- [ ] Multiple shape variants (e.g. `batched` vs `unbatched`) are provided when supported.
- [ ] Input and output constraints map accurately to model specifications.
- [ ] New validation failures throw `SCHEMA_MISMATCH` (or `INVALID_ARGUMENT` when the caller's own arguments are at fault), never a bare `Error`.
Loading