feat(sdk/script): classify signal.error/interrupt against errdefs and engine.Cause#90
Merged
Merged
Conversation
… engine.Cause
Until now `signal.error(msg)` returned a flat string, leaving hosts (scriptnode
and any future script-based engine) without a way to tell a validation failure
apart from a budget refusal or an internal crash without parsing the message.
`signal.interrupt(msg)` had the same gap on the interrupt side, always
collapsing to `engine.CauseCustom`.
Make the signal protocol natively typed:
- `script.Signal` gains `Kind` (errdefs category for errors, engine.Cause for
interrupts) and `Detail` (freeform structured metadata).
- `script.SignalToError` becomes the single point of truth that maps a signal
into either `errdefs.<Kind>(message)` or `engine.Interrupted({Cause, Detail})`.
Unknown kinds degrade safely (Internal / CauseCustom) and preserve the raw
value in the error chain for observability.
- jsrt and luart accept either a bare string (back-compat, kind stays empty) or
an object/table `{ kind, message, detail }` for both signal.error and
signal.interrupt. Parsing is identical across runtimes so behaviour does not
drift.
- scriptnode/jsnode.go drops its inline switch on sig.Type and delegates to
`script.SignalToError`, %w-wrapping with the node id so errdefs.Is… still
works through the chain.
- iteration.js forwards Kind/Detail when re-raising a child signal so the
child's classification reaches the host instead of being flattened.
Coverage:
- `sdk/script/signal_test.go` exercises every errdefs kind, every engine.Cause,
empty-kind defaults, unknown-kind degradation, back-compat bare strings.
- jsrt/luart tests gain object/table-form cases.
- scriptnode tests verify a kinded signal.error reaches the host as
errdefs.IsValidation and a kinded signal.interrupt preserves engine.Cause.
Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Script
signal.error(msg)returned a flat string, leaving hosts (scriptnode and any future script-based engine) unable to tell a validation failure apart from a budget refusal or an internal crash without parsing the message.signal.interrupt(msg)had the same gap, always collapsing toengine.CauseCustom.This PR makes the signal protocol natively typed and aligns it with
errdefs/engine.Cause:script.SignalgainsKind(errdefs category for errors,engine.Causefor interrupts) andDetail(freeform structured metadata).script.SignalToErroris the single point of truth that maps a signal into eithererrdefs.<Kind>(message)orengine.Interrupted({Cause, Detail}). Unknown kinds degrade safely (Internal/CauseCustom) and preserve the raw value in the error chain so observability surfaces the typo.jsrtandluartaccept either a bare string (back-compat, kind stays empty) or an object/table{ kind, message, detail }for bothsignal.errorandsignal.interrupt. The two implementations stay textually parallel so behaviour cannot drift.sdk/graph/node/scriptnode/jsnode.godrops its inlineswitch sig.Typeand delegates toscript.SignalToError,%w-wrapping with the node id soerrdefs.Is…anderrors.As(InterruptedError)still work through the chain.iteration.jsforwardsKind/Detailwhen re-raising a child signal so the child's classification reaches the host instead of being flattened.Script-facing API (no breaking changes)
```javascript
// Back-compat: bare string → Internal / CauseCustom
signal.error("boom");
signal.interrupt("pause");
// New: typed
signal.error({ kind: "validation", message: "model is required", detail: { field: "model" } });
signal.interrupt({ kind: "user_input", message: "barge" });
```
Exposed
ErrorKindallowlist (deliberate subset of errdefs)`validation`, `not_found`, `budget_exceeded`, `policy_denied`, `not_available`, `internal`.
Auth / rate-limit / timeout / conflict are intentionally NOT script-raisable — they are wire-level classifications that belong in the Go layer.
Test plan
Made with Cursor