Hactar Engine — Script Executor
Status: production (v2.0). Hactar is the pure script-execution engine in Vancetope. Authoring of JavaScript orchestrators happens in Slartibartfast via
outputSchemaType=SCRIPT_JS; Hactar just loads, validates, and runs the persisted body in a sandboxed GraalJS context.Historical note: pre-v2.0 Hactar was a script-architect engine with FRAMING / DRAFTING / REVIEWING / VALIDATING phases. The authoring/executor split (
planning/script-architect-executor-split.md) moved the authoring half to Slartibartfast and reduced Hactar to the executor pipeline below.
1. Role and Boundary
Hactar’s responsibilities:
- Load a script body from a project document path.
- Validate the body for pre-flight safety — parse, JSDoc header, tool-allowlist intersect.
- Execute the body in a sandboxed GraalJS context.
- Optionally deep-validate (LLM semantic review) when the caller opts in.
Hactar’s non-responsibilities:
- No LLM-authoring. Hactar makes zero LLM calls during a run.
All validation calls happen via
HactarService(deep-validate usesLightLlmServicewith thescript-reviewrecipe — Hactar itself doesn’t drive the LLM dialog). - No recovery loop. Any failure (load miss, validation reject,
executor exception, script throw) is terminal. The caller’s next
step is a fresh Slart spawn with
mode=UPDATEand the capturedfailureReason(manual, perplanning/script-architect-executor-split.md§6.3). - No script generation. Use Slart for that.
2. Lifecycle
READY → LOADING → [VALIDATING] → EXECUTING → DONE
│ │ │
└───────────┴────────────┴→ FAILED
(any failure is terminal — no recovery loop)
VALIDATING is opt-in via engineParams.validateBeforeRun=true.
Otherwise LOADING transitions directly to EXECUTING.
2.1 LOADING
LoadingPhase (vance-brain/.../hactar/phases/LoadingPhase.java):
- Read
state.scriptRef(mirror of the engine-param). documentService.lookupCascade(...)resolves the body.HactarService.validate(...)runs the minimal gate (parse +ScriptHeaderParser+ tool-allowlist intersect).- On any ERROR-severity issue → FAILED with the aggregated
issues recorded on
state.validationIssuesand the first 5 surfaced instate.failureReason. - On pass → VALIDATING if
validateBeforeRun=true, else EXECUTING.
2.2 VALIDATING (opt-in)
ValidatingPhase calls HactarService.deepValidate(...) — runs
a LightLlmService call with the bundled script-review recipe.
The LLM reviews the script for semantic / logic / API-misuse
issues. On reject → FAILED with the LLM issues merged into
state.validationIssues.
Reasons to opt in:
- Cortex “Test Run” button (user wants quick semantic sanity-check before side-effects).
- Untrusted source — hand-edited script in production, or scheduler firing an externally-supplied path.
Reasons to skip (the default):
- Scheduler-driven cron run of a proven script (Mail-Bot, daily- briefing).
- Slart-self-execute (script just survived Slart’s VALIDATING loop, redundant).
2.3 EXECUTING
ExecutingPhase calls ScriptExecutor.run(...) (see
script-engine.md §5a). The script’s tool
surface is narrowed to engineParams.scriptAllowedTools via
ContextToolsApi. On exception → FAILED with
state.executionError / state.executionErrorClass recorded.
On success → DONE with state.executionResult carrying the JS
return value.
Long-running scripts (e.g. Mail-Bot iterating 200 mails) keep the
Hactar process RUNNING throughout — GraalJS runs in a Watchdog-
Future. Heartbeats via vance.process.progress(...) are planned
(planning/script-architect-executor-split.md §6.5 — pending,
not yet implemented).
3. Engine Parameters
vance-brain/.../hactar/HactarEngine.java exposes the following
keys (re-exported from the phase classes where appropriate):
| Param | Type | Default | Effect |
|---|---|---|---|
scriptRef |
string (path) | required | Document path of the script body |
language |
string | js |
Only "js" accepted in v2; reserved for future Python |
validateBeforeRun |
bool | false |
Opt-in HactarService.deepValidate before EXECUTING |
scriptAllowedTools |
list |
[] |
Tool names the script may call via vance.tools.call(...) |
scriptParams |
map | {} |
Bindings the script sees as vance.params.* |
timeout |
int (seconds) or "30s"/"5m"/"1h" |
5 minutes | Wall-clock cap (clamps against header @timeout) |
buildInitialState validates inputs: missing scriptRef
or language != "js" → IllegalStateException at spawn time.
4. State
HactarState (vance-api/.../hactar/HactarState.java),
persisted on engineParams.deepThoughtState (legacy key name kept
for Mongo-document backwards compatibility).
| Field | Type | Effect |
|---|---|---|
scriptRef |
string | mirror of the engine-param |
language |
string | "js" |
scriptBody |
string | loaded body (set by LOADING) |
validateBeforeRun |
bool | controls the LOADING → VALIDATING vs. LOADING → EXECUTING branch |
validationIssues |
list |
accumulated from LOADING + (if enabled) VALIDATING |
executionResult |
object | JS return value on success |
executionError |
string | ScriptExecutionException.getMessage() on failure |
executionErrorClass |
string | ScriptExecutionException.ErrorClass enum name on failure |
executionDurationMs |
long | wall-clock duration of the EXECUTING pass |
status |
HactarStatus | one of READY/LOADING/VALIDATING/EXECUTING/DONE/FAILED |
failureReason |
string | human-readable summary for the parent / Cortex run-panel |
5. Triggers
Hactar runs are spawnable from multiple surfaces:
| Trigger | Path |
|---|---|
Direct process_create |
process_create(recipe="hactar-run", params={scriptRef, …}) |
hactar_run tool |
convenience wrapper around the process_create call above |
| Scheduler | _vance/scheduler/<name>.yaml with recipe: hactar-run + params.scriptRef: "<path>" |
| Slart self-execute | slart-and-run recipe → Slart writes SCRIPT_JS → Slart’s EXECUTING spawns Hactar via JsScriptArchitect.directExecutionSpawn |
Cortex Run-Button (▶ Run JS) |
bypasses Hactar (uses ScriptCortexExecutionService directly) — Hactar’s role here is covered by the same ScriptExecutor; the Cortex path skips the LOADING/VALIDATING ceremony because the body is already in the editor |
6. HactarService — Shared Validation Surface
HactarService (vance-brain/.../hactar/HactarService.java) is
the single owner of “is this script valid?”. Two methods:
validate(code, language, sourceName, callerAllowedTools, …)→ minimal gate (parse + header + allow-set intersect). Local, no LLM. ~ms. Used by Hactar’s LOADING, Slart’sJsScriptArchitect.validateDraftShape, and Cortex’sPOST /scripts/validate.deepValidate(...)→ LLM semantic review viaLightLlmService+ bundledscript-reviewrecipe. Used by Hactar’s optional VALIDATING phase and Cortex’sPOST /scripts/validate-deep.
Rationale for the split (script-execution being the owner): the
executor is the trust boundary. If a “valid” script crashes at
runtime, the validation logic is broken — that’s a Hactar issue.
Slart and Cortex are consumers of this truth, not parallel
implementations. See
planning/script-architect-executor-split.md §5.6.
7. Bundled Recipes
| Recipe | Engine | Use case |
|---|---|---|
hactar |
hactar | Engine-default; selector hits this on triggers like “run script” |
hactar-run |
hactar | Pass-through wrapper used by the scheduler, the hactar_run tool, and Slart-self-execute |
slart-and-run |
slartibartfast | Generate + execute in one step (default routing.fallback.recipe) |
script-review |
jeltz (internal LightLlm config) | Used by HactarService.deepValidate — not spawnable |
8. Reference
planning/script-architect-executor-split.md— full design rationale of the v2 refactor (Phase 1–5 + open points).- script-engine.md — GraalJS runtime layer Hactar’s EXECUTING phase calls into.
- slartibartfast-engine.md — the authoring engine that produces scripts Hactar executes.
- recipe-routing.md — how the routing-
fallback hits
slart-and-runby default.