Skip to content

feat(sdk/script): classify signal.error/interrupt against errdefs and engine.Cause#90

Merged
lIang70 merged 1 commit into
mainfrom
feat/script-signal-errdefs-kind
May 11, 2026
Merged

feat(sdk/script): classify signal.error/interrupt against errdefs and engine.Cause#90
lIang70 merged 1 commit into
mainfrom
feat/script-signal-errdefs-kind

Conversation

@lIang70

@lIang70 lIang70 commented May 11, 2026

Copy link
Copy Markdown
Collaborator

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 to engine.CauseCustom.

This PR makes the signal protocol natively typed and aligns it with errdefs / engine.Cause:

  • script.Signal gains Kind (errdefs category for errors, engine.Cause for interrupts) and Detail (freeform structured metadata).
  • New script.SignalToError is 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 so observability surfaces the typo.
  • 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. The two implementations stay textually parallel so behaviour cannot drift.
  • sdk/graph/node/scriptnode/jsnode.go drops its inline switch sig.Type and delegates to script.SignalToError, %w-wrapping with the node id so errdefs.Is… and errors.As(InterruptedError) still work 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.

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 ErrorKind allowlist (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

  • `go vet ./sdk/...`
  • `go test ./sdk/...` — all packages pass (script, script/jsrt, script/luart, script/bindings, graph/node/scriptnode, etc.)
  • `go test ./sdkx/... ./vessel/... ./voice/... ./cmd/vesseld/...` — no consumers broke
  • New `sdk/script/signal_test.go` covers every errdefs kind, every `engine.Cause`, empty-kind defaults, unknown-kind degradation, back-compat bare strings
  • `jsrt` / `luart` tests cover the object/table form for both signal.error and signal.interrupt and assert Kind / Detail flow through
  • scriptnode integration tests assert a kinded `signal.error` reaches the host as `errdefs.IsValidation` and a kinded `signal.interrupt` preserves `engine.Cause=user_input`

Made with Cursor

… 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>
@lIang70
lIang70 merged commit 6900111 into main May 11, 2026
19 checks passed
@lIang70
lIang70 deleted the feat/script-signal-errdefs-kind branch May 11, 2026 07:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant