lib/host/ra.sx — a PURE-SX seam runner (advertises {effect,branch,each,suspend}) with an INJECTED
erl-eval (real = er-to-sx-deep ∘ erlang-eval-ast; mock in unit tests), so it loads in the plain host
(Erlang refs resolve lazily inside lambdas) and is unit-testable without the Erlang runtime.
host/ra--{atom,bin,erl-src,start-expr,resume-expr,parse,make-runner,resume,real-eval}: marshals our
canonical activity → Erlang source (CID as <<"…">> binary, atoms single-quoted), starts a named
next/ flow via flow_store, parses (ok Id (flow_done V))→{:status done :effects V :flow-id} /
(ok Id (flow_suspended T))→{:status suspended :resume {:id :tag}}.
DUAL-RUNNER ROUTING (flows.sx): host/flow--required-caps now handles a {:erl-flow :needs} DAG
(declared caps, since a foreign flow can't be introspected); host/flow--select-runner picks the
cheapest runner whose capabilities cover the DAG's needs. The capability model is now REAL with two
runners — an {effect,branch} composition lands on exec-runner; a {suspend} DAG routes to RA.
Verified: ra 9/9 (mock erl-eval) + plans/ra-integration.sh 4/4 (the REAL module driving live
flow_store: urgent→done, newsletter→suspended with a resume handle, digest_sent effect-as-data).
Full host conformance 607/607; next/tests/triggers_e2e.sh 10/10 baseline intact.
FINDING → RA-LIVE deferred: gen_servers don't persist across separate erlang-eval-ast calls (flow
README), so true cross-call suspend/resume needs a PERSISTENT next/ kernel process. The runner +
marshalling + suspend/resume mechanics are proven; RA-live is process lifecycle + wiring, documented.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
3.7 KiB
Plaintext
64 lines
3.7 KiB
Plaintext
;; lib/host/ra.sx — RA: the ERLANG (durable) RUNNER ADAPTER (plans/business-logic-fed-flows.md).
|
|
;; A seam runner (behavior.sx) that runs a behavior as a DURABLE next/flow (flow_store), so a flow
|
|
;; can SUSPEND (wait-until-morning) and RESUME — the capability {suspend} the execute-fold runner
|
|
;; lacks. Spike-proven (plans/ra-spike.sh, 4/4): our canonical activity marshals to Erlang, drives
|
|
;; blog_publish_digest, and completes a suspend→resume cycle with no er-scheduler deadlock.
|
|
;;
|
|
;; PURE SX + INJECTED erl-eval. host/ra--make-runner takes an `erl-eval` fn (src -> clean SX value).
|
|
;; The REAL one (host/ra--real-eval) wraps (er-to-sx-deep (erlang-eval-ast src)) and only works in a
|
|
;; process with the Erlang runtime + next/flow loaded; unit tests inject a MOCK. So this module loads
|
|
;; in the plain host (the Erlang refs sit inside a lambda, resolved lazily at call time) and is
|
|
;; testable without the Erlang substrate. A durable behavior DAG is {:erl-flow <name> :needs <caps>}.
|
|
|
|
(define host/ra--atom (fn (s) (str "'" s "'"))) ;; single-quoted Erlang atom (special-char safe)
|
|
(define host/ra--bin (fn (s) (str "<<\"" s "\">>"))) ;; Erlang binary (no /utf8 — unsupported)
|
|
|
|
;; our canonical activity dict -> the Erlang activity-proplist SOURCE the flows consume.
|
|
(define host/ra--erl-src
|
|
(fn (a)
|
|
(str "[{type, " (host/ra--atom (get a :verb)) "}, {actor, " (host/ra--atom (get a :actor)) "}, "
|
|
"{id, " (host/ra--bin (get a :id)) "}, "
|
|
"{object, [{type, " (host/ra--atom (get a :object-type)) "}, "
|
|
"{category, " (host/ra--atom (get a :category)) "}]}]")))
|
|
|
|
;; the Erlang expression that starts a named durable flow with the marshalled activity env.
|
|
(define host/ra--start-expr
|
|
(fn (flow-name activity)
|
|
(str "flow_store:start(" flow-name ", [{activity, " (host/ra--erl-src activity)
|
|
"}, {actor, " (host/ra--atom (get activity :actor)) "}])")))
|
|
;; the Erlang expression that resumes a suspended instance with an effect result (async re-entry).
|
|
(define host/ra--resume-expr
|
|
(fn (id result) (str "flow_store:resume(" id ", " (host/ra--atom result) ")")))
|
|
|
|
;; map flow_store's result (er-to-sx-deep'd) onto the seam runner contract.
|
|
;; (ok Id (flow_done V…)) -> {:status "done" :effects (V…) :flow-id Id}
|
|
;; (ok Id (flow_suspended T)) -> {:status "suspended" :resume {:id Id :tag T}}
|
|
(define host/ra--parse
|
|
(fn (r)
|
|
(if (or (not (= (type-of r) "list")) (not (= (str (first r)) "ok")))
|
|
{:status "failed" :error r}
|
|
(let ((id (first (rest r))) (outcome (first (rest (rest r)))))
|
|
(let ((kind (str (first outcome))))
|
|
(cond
|
|
((= kind "flow_done")
|
|
{:status "done" :effects (rest outcome) :flow-id id})
|
|
((= kind "flow_suspended")
|
|
{:status "suspended" :resume {:id id :tag (str (first (rest outcome)))}})
|
|
(else {:status "failed" :error r})))))))
|
|
|
|
;; the RA runner — a seam {:capabilities :run}. Advertises {effect, branch, each, suspend}. :run
|
|
;; marshals the activity, starts the DAG's named flow via the injected erl-eval, parses the result.
|
|
(define host/ra--make-runner
|
|
(fn (erl-eval)
|
|
{:capabilities (list "effect" "branch" "each" "suspend")
|
|
:run (fn (dag env)
|
|
(host/ra--parse (erl-eval (host/ra--start-expr (get dag :erl-flow) (get env :activity)))))}))
|
|
|
|
;; resume a suspended instance out-of-band (the async re-entry path) — re-drive + parse.
|
|
(define host/ra--resume
|
|
(fn (erl-eval id result) (host/ra--parse (erl-eval (host/ra--resume-expr id result)))))
|
|
|
|
;; the REAL erl-eval — ONLY works where the Erlang runtime + next/flow are loaded (refs resolved
|
|
;; lazily at call time, so defining it here is harmless in the plain host).
|
|
(define host/ra--real-eval (fn (src) (er-to-sx-deep (erlang-eval-ast src))))
|