@damurka/jovian 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +175 -0
  3. package/docs/api/README.md +55 -0
  4. package/docs/api/session.md +143 -0
  5. package/docs/api/types.md +120 -0
  6. package/docs/architecture/overview.md +218 -0
  7. package/docs/cpp-usage.md +60 -0
  8. package/docs/development.md +105 -0
  9. package/docs/getting-started.md +127 -0
  10. package/docs/guides/comms.md +70 -0
  11. package/docs/guides/environments.md +80 -0
  12. package/docs/guides/history.md +44 -0
  13. package/docs/guides/interactive-input.md +48 -0
  14. package/docs/guides/interrupting.md +45 -0
  15. package/docs/guides/playground.md +33 -0
  16. package/docs/guides/sessions-lifecycle.md +70 -0
  17. package/docs/kernels.md +117 -0
  18. package/docs/protocol.md +135 -0
  19. package/docs/releasing.md +73 -0
  20. package/docs/troubleshooting.md +110 -0
  21. package/lib/execution/execution-queue.d.ts +20 -0
  22. package/lib/execution/execution-queue.js +256 -0
  23. package/lib/handlers/display-handler.d.ts +7 -0
  24. package/lib/handlers/display-handler.js +10 -0
  25. package/lib/handlers/error-handler.d.ts +7 -0
  26. package/lib/handlers/error-handler.js +8 -0
  27. package/lib/handlers/result-handler.d.ts +7 -0
  28. package/lib/handlers/result-handler.js +10 -0
  29. package/lib/handlers/stream-handler.d.ts +7 -0
  30. package/lib/handlers/stream-handler.js +8 -0
  31. package/lib/index.d.ts +7 -0
  32. package/lib/index.js +5 -0
  33. package/lib/messaging/message-parser.d.ts +6 -0
  34. package/lib/messaging/message-parser.js +33 -0
  35. package/lib/messaging/message-router.d.ts +14 -0
  36. package/lib/messaging/message-router.js +41 -0
  37. package/lib/middleware/index.d.ts +5 -0
  38. package/lib/middleware/index.js +5 -0
  39. package/lib/middleware/middleware-chain.d.ts +7 -0
  40. package/lib/middleware/middleware-chain.js +14 -0
  41. package/lib/middleware/middleware.d.ts +5 -0
  42. package/lib/middleware/middleware.js +2 -0
  43. package/lib/middleware/plugins/logging-plugin.d.ts +6 -0
  44. package/lib/middleware/plugins/logging-plugin.js +9 -0
  45. package/lib/middleware/plugins/metrics-plugin.d.ts +8 -0
  46. package/lib/middleware/plugins/metrics-plugin.js +13 -0
  47. package/lib/session/comm.d.ts +39 -0
  48. package/lib/session/comm.js +58 -0
  49. package/lib/session/native-paths.d.ts +48 -0
  50. package/lib/session/native-paths.js +108 -0
  51. package/lib/session/session-manager.d.ts +229 -0
  52. package/lib/session/session-manager.js +842 -0
  53. package/lib/session/supervisor-client.d.ts +36 -0
  54. package/lib/session/supervisor-client.js +147 -0
  55. package/lib/types/engine.d.ts +269 -0
  56. package/lib/types/engine.js +2 -0
  57. package/lib/types/index.d.ts +3 -0
  58. package/lib/types/index.js +3 -0
  59. package/lib/types/messages.d.ts +68 -0
  60. package/lib/types/messages.js +2 -0
  61. package/lib/utils/logger.d.ts +12 -0
  62. package/lib/utils/logger.js +58 -0
  63. package/lib/utils/network.d.ts +11 -0
  64. package/lib/utils/network.js +50 -0
  65. package/package.json +57 -0
  66. package/packages/hera/DESCRIPTION +29 -0
  67. package/packages/hera/LICENSE +2 -0
  68. package/packages/hera/LICENSE.md +21 -0
  69. package/packages/hera/NAMESPACE +32 -0
  70. package/packages/hera/NEWS.md +7 -0
  71. package/packages/hera/R/cell_options.R +13 -0
  72. package/packages/hera/R/comm.R +228 -0
  73. package/packages/hera/R/completion.R +54 -0
  74. package/packages/hera/R/execute.R +199 -0
  75. package/packages/hera/R/inspect.R +73 -0
  76. package/packages/hera/R/log.R +14 -0
  77. package/packages/hera/R/mime_bundle.R +65 -0
  78. package/packages/hera/R/routines.R +86 -0
  79. package/packages/hera/R/utils.R +32 -0
  80. package/packages/hera/R/zzz.R +128 -0
  81. package/packages/hera/man/Comm.Rd +179 -0
  82. package/packages/hera/man/CommManager.Rd +215 -0
  83. package/packages/hera/man/View.Rd +22 -0
  84. package/packages/hera/man/cell_options.Rd +20 -0
  85. package/packages/hera/man/clear_output.Rd +23 -0
  86. package/packages/hera/man/complete.Rd +23 -0
  87. package/packages/hera/man/display_data.Rd +22 -0
  88. package/packages/hera/man/is_elara.Rd +18 -0
  89. package/packages/hera/man/mime_bundle.Rd +25 -0
  90. package/packages/hera/man/mime_types.Rd +22 -0
  91. package/packages/hera/man/reexports.Rd +16 -0
@@ -0,0 +1,218 @@
1
+ # Jovian Architecture Overview
2
+
3
+ Jovian runs language kernels as supervised Jupyter kernels that a Node.js or Electron process can drive. It ships two kernels — R (**Elara**) and Python (**Carpo**) — selected per session with `EngineOptions.kernelType` (`'r'` | `'python'`, default `'r'`).
4
+
5
+ ## Components
6
+
7
+ The parts are named after moons of Jupiter:
8
+
9
+ | Name | Role | Where |
10
+ |---|---|---|
11
+ | **Jovian** | The umbrella product and npm package (`jovian`): a TypeScript client over the native binaries below | `lib/` |
12
+ | **Adrastea** | Language-neutral Jupyter kernel framework: wire protocol, ZMQ transport, kernel request loop, the abstract interpreter interface (`adrastea::`). Built as a **static library** shared by Elara, Carpo and Themisto | `native/src/adrastea`, `native/include/adrastea` |
13
+ | **Elara** | The R kernel: embeds R on top of Adrastea, loading R's shared library at runtime (`elara::`, the `elara` executable) | `native/src/elara` |
14
+ | **Carpo** | The Python kernel: embeds CPython on top of Adrastea the same way (`carpo::`, the `carpo` executable) | `native/src/carpo` |
15
+ | **Themisto** | The kernel supervisor: spawns and monitors one kernel process per session, speaks ZMQ to each, and re-exposes sessions over HTTP + WebSocket (`themisto::`, the `themisto` executable) | `native/src/themisto` |
16
+ | [hera](../../packages/hera) | The R companion package loaded inside every Elara session | `packages/hera` |
17
+
18
+ There is no Node-API addon and no in-process engine. `lib/` talks to Themisto over plain HTTP (session lifecycle) and WebSocket (execute, requests, message streaming), and Themisto spawns one Elara or Carpo process per session. That is deliberate: a session blocked in a long call (a Shiny app, a long Python loop) cannot starve another, because they are different processes with different interpreters, and only Themisto ever links a native ZMQ binding — the process that embeds `lib/` (an Electron main process, a VS Code extension host) needs no native dependency at all.
19
+
20
+ ## Process model
21
+
22
+ ```
23
+ ┌─────────────────────────────────────────────────────────────┐
24
+ │ Your Node.js / Electron process │
25
+ │ lib/ SessionManager ─ Session ─ ExecutionQueue ─ Comm │
26
+ └───────────────┬──────────────────────────┬──────────────────┘
27
+ HTTP (create / stop / restart) WebSocket, one per Session
28
+ │ │ (127.0.0.1 only)
29
+ ┌───────────────┴──────────────────────────┴──────────────────┐
30
+ │ themisto (one per SessionManager, spawned on first session)│
31
+ │ HttpApi · WsRelay · SessionRegistry │
32
+ │ per session: KernelProcess, ClientZmq, poll thread │
33
+ └───────┬─────────────────────────────────────────┬───────────┘
34
+ │ ZMQ: shell, control, stdin, │ ZMQ (same, other session)
35
+ │ iopub, heartbeat (127.0.0.1) │
36
+ ┌───────┴─────────────────┐ ┌────────┴─────────────────┐
37
+ │ elara (R session) │ │ carpo (Python session) │
38
+ │ Adrastea + RInterpreter │ │ Adrastea + PyInterpreter │
39
+ └───────┬─────────────────┘ └────────┬─────────────────┘
40
+ │ R C API (dlopen / LoadLibrary) │ Python C API (dynamic)
41
+ R + packages/hera CPython + bootstrap module
42
+ ```
43
+
44
+ - **Session = process.** `POST /sessions` makes Themisto spawn `elara` or `carpo` (whichever `kernelType` names), wait for it to register, and connect a ZMQ client to it.
45
+ - **Registration handshake.** Themisto binds one registration `ROUTER` socket and passes its address and a shared HMAC key on the kernel's command line (`--registration-ip`, `--registration-port`, `--key`). The kernel binds its five sockets on free ports and sends the port numbers to that registration socket (signed with the key). Themisto waits up to 60 s, aborting early if the kernel process dies first.
46
+ - **Authentication.** Every ZMQ message is signed (`hmac-sha256`) with that key; the key is shared by all kernels one Themisto spawns.
47
+ - **Loopback only.** Themisto's HTTP and WebSocket servers bind `127.0.0.1` on OS-assigned ports; kernel ports are `127.0.0.1` too.
48
+ - **Lifetime.** On Windows every kernel is assigned to a job object with `KILL_ON_JOB_CLOSE`, so kernels die with Themisto however Themisto exits. `SupervisorClient.kill()` (called by `SessionManager.stopAll()/killAll()` and an `exit` handler) ends Themisto.
49
+
50
+ ## Channels and threads
51
+
52
+ ### Jupyter channels
53
+
54
+ | Channel | ZMQ pattern (kernel side) | Used for |
55
+ |---|---|---|
56
+ | shell | ROUTER | `execute`, `complete`, `inspect`, `is_complete`, `kernel_info`, `history`, `comm_*` requests and their replies |
57
+ | control | ROUTER | `interrupt_request`, `shutdown_request` and their replies |
58
+ | stdin | ROUTER | `input_request` from the kernel, `input_reply` from the client |
59
+ | iopub | XPUB, owned by a publisher thread (the main thread hands it messages over an in-process PUB) | `status`, `execute_input`, `stream`, `display_data`, `update_display_data`, `clear_output`, `execute_result`, `error`, kernel-initiated `comm_*` |
60
+ | heartbeat | ROUTER (the supervisor's client uses REQ) | liveness pings |
61
+
62
+ Themisto's client side uses DEALER sockets for shell/control/stdin, a SUB for iopub and a REQ for the heartbeat. The three DEALERs share one explicit ZMQ routing id: the kernel addresses an `input_request` on the stdin ROUTER using the identity it captured from the *shell* message that started the execution, which only reaches the client if its stdin socket presents the same identity.
63
+
64
+ ### Inside a kernel process (Elara / Carpo)
65
+
66
+ | Thread | Job |
67
+ |---|---|
68
+ | **Main thread** | Runs the ZMQ poll loop (shell + control) **and** the interpreter: R or Python executes on the process's own main thread (R's C-stack-bounds detection assumes it; Python's `KeyboardInterrupt` is raised on the thread that initialised it). |
69
+ | **Publisher** | Owns the iopub XPUB socket; the main thread hands it messages. |
70
+ | **Heartbeat** | Answers ping/pong, on its own thread — so it keeps answering while the main thread is busy running code. |
71
+ | **Control watcher** | Started lazily; alive for the kernel's lifetime but only *active* while code is executing (see below). |
72
+
73
+ Because the interpreter runs on the same thread that reads sockets, a control message sent mid-execution would normally sit unread until the execution finished — useless for interrupt. The **control watcher** fixes that:
74
+
75
+ - `KernelCore::executeRequest` brackets the interpreter call with `Server::beginExecution()` / `endExecution()`.
76
+ - Between those calls, the watcher thread polls the control socket (every 5 ms). An `interrupt_request` is dispatched immediately, on the watcher thread: the interpreter's `interruptRequestImpl()` flags the interpreter (R: sets `R_interrupts_pending` on POSIX / `UserBreak` on Windows; Python: delivers a real SIGINT — `raise(SIGINT)` on Windows, `pthread_kill` of the interpreter thread on POSIX — after the bootstrap installed `signal.default_int_handler`), and replies on the control channel. Any *other* control message (a `shutdown_request`, say) is queued and delivered by the main loop after the execution ends, in order, exactly as before.
77
+ - Interrupting an idle kernel is a no-op — the flag is only set while an execution is actually running, so a stale break can never abort the *next* execution.
78
+ - Locking: the control `ROUTER` is guarded by a recursive mutex (the interrupt handler, running on the watcher thread, replies through the same send path); iopub publishing is guarded by a mutex (the watcher publishes `status`/`interrupt` messages while the main thread streams output through the same publishing socket).
79
+ - **Stream flusher.** `Interpreter::publishStream` coalesces stdout/stderr text (one message per ~50 ms or 16 KB) and lazily starts a small flusher thread that publishes stale buffered text while the interpreter is busy computing and not writing; everything else that is published (results, display data, errors, input prompts, the `execute_reply`) flushes the buffer first, so ordering is unchanged. Publishing from that thread goes through the same iopub mutex.
80
+
81
+ `interruptRequestImpl()` therefore runs on a thread other than the interpreter's. Implementations must only do thread-safe things (see [cpp-usage.md](../cpp-usage.md)).
82
+
83
+ ### Inside Themisto
84
+
85
+ | Thread | Job |
86
+ |---|---|
87
+ | HTTP server | cpp-httplib listener (plus its worker threads) handling session create/list/get/delete/restart |
88
+ | WebSocket server | ixwebsocket server threads, one connection per `Session` object on the client |
89
+ | **Poll thread, one per session** | Every ~5 ms: checks whether the kernel OS process is still alive; drains the client's iopub queue and the shell, control and stdin channels; relays each message as a JSON frame to the attached WebSocket |
90
+ | Client iopub / heartbeat threads | Inside each `ClientZmq`: receive iopub into a queue; run the heartbeat — a ping roughly every 100 ms, up to 20 s to wait for each answer, 3 retries. Every answer updates a `HeartbeatStatus` (round trip, age of the last pong, consecutive misses) that `sessionToJson()` publishes as the session's `heartbeat` |
91
+ | Kernel output pump, one per kernel | Reads the kernel's stdout/stderr pipe and re-prints it on Themisto's stderr with an `[elara]` / `[carpo]` prefix |
92
+
93
+ Every operation targeting a session id (`sendExecute`, `sendRequest`, `stopSession`, `restartSession`, …) takes a per-id recursive mutex so a request that arrives mid-restart waits and then addresses whichever kernel is live afterwards. `DealerChannel` additionally serialises each DEALER socket (ZMQ sockets are not thread-safe, and the poll thread receives while HTTP/WS threads send); a "blocking" receive is a loop of short non-blocking attempts so it never starves a sender.
94
+
95
+ ### Crash detection
96
+
97
+ Two independent mechanisms, fastest first:
98
+
99
+ 1. **OS liveness** — the poll thread checks the kernel's process handle every iteration (~5 ms). A kernel killed from outside (Task Manager, a segfault, `os._exit()`) is reported as `kernelExit` almost at once, with the exit code decoded when recognised (e.g. `STATUS_ACCESS_VIOLATION`). The final messages the kernel published are drained first.
100
+ 2. **Heartbeat** — for a kernel that is alive but stuck (deadlocked, in a native call): 3 missed pings at 20 s each ≈ 60–80 s, reported as `kernelExit` with a `heartbeat gave up waiting for a response` reason. The same channel doubles as a health readout: every answered ping records its round trip, exposed through `GET /sessions/:id` and `Session.status()`. Because the kernel replies from a dedicated thread, a kernel that is merely *busy* (a long cell) still answers — the heartbeat separates "busy" from "stuck".
101
+
102
+ A session whose stop/restart was *requested* sets an "expecting exit" flag first, so the orderly exit that follows is not reported as a crash.
103
+
104
+ ## Dynamic loading of R and Python
105
+
106
+ Neither interpreter is linked at build time.
107
+
108
+ - **R** (`native/src/elara/r/r_dynlib.{hpp,cpp}`): `R.dll` / `libR.so` / `libR.dylib` is loaded with `LoadLibrary` / `dlopen` when the kernel starts (after `R_HOME` and `PATH` are set up). Switching R installations is a runtime `rHome` decision needing no rebuild, and a missing R surfaces as a clean, catchable error inside `elara` (exit code 1, an actionable message) instead of the OS refusing to start the process. On Windows, `R.dll` exports no hookable console pointers, so `RInterpreter` starts R with the documented embedding sequence (`R_DefParamsEx`, `R_SetParams`, `setup_Rmainloop`, …) and its own `ReadConsole` callback; older R (< 4.2) falls back to `Rf_initEmbeddedR`.
109
+ - **Python** (`native/src/carpo/py/py_dynlib.{hpp,cpp}`): the shared library is found by scanning `pythonHome` for the newest `python3NN.dll` / `libpython3.*.so*` / `libpython3.*.dylib` (the name embeds the version, unlike R's). Only the documented stable-ABI subset of the C API is used and `PyObject` stays opaque; reference counting goes through the real `Py_IncRef` / `Py_DecRef` functions.
110
+ - **POSIX library path.** A kernel's own interpreter library is `dlopen`ed by full path, but R's base packages and Python's extension modules have the shared library as an unqualified dependency. Themisto therefore sets `LD_LIBRARY_PATH` / `DYLD_LIBRARY_PATH` (`<R_HOME>/lib`, `<PYTHONHOME>/lib`) in the child *between `fork()` and `exec()`* — setting it from inside the running kernel would be too late, since the dynamic linker reads it at process start.
111
+
112
+ ## Message flows
113
+
114
+ ### Execute
115
+
116
+ ```
117
+ Session.execute(code)
118
+ └ ExecutionQueue (single-flight, timeout) { type:"execute", id, code, options }
119
+ └ WebSocket ─────────────────────────────────────────────────────────► WsRelay
120
+ SessionRegistry::sendExecute
121
+ └ shell: execute_request ─► kernel
122
+ kernel main thread: KernelCore::executeRequest
123
+ status busy (iopub) · execute_input · [beginExecution: control watcher on]
124
+ RInterpreter → hera:::hera_call("execute", …) | PyInterpreter → __carpo_run(code, globals)
125
+ stream (coalesced ≤ every ~50 ms / 16 KB) / display_data / execute_result / error (iopub)
126
+ [endExecution] execute_reply (shell) · status idle (iopub)
127
+ poll thread ── relays every message as {type:"message", channel, msg_type, parent_msg_id, content}
128
+ Session ── MessageRouter emits events; ExecutionQueue collects output for this id;
129
+ execute_reply resolves the promise (ExecutionResult)
130
+ ```
131
+
132
+ ### Request / reply (`complete`, `inspect`, `is_complete`, `kernel_info`, `history`, `comm_info`)
133
+
134
+ ```
135
+ Session.complete(code, pos)
136
+ └ request(): id = uuid; pending[id] = {replyType:"complete_reply", timer}
137
+ └ WS {type:"request", id, channel:"shell", msgType:"complete_request", content}
138
+ └ WsRelay → SessionRegistry::sendRequest (whitelist!) → shell: complete_request ─► kernel
139
+ kernel (between executions, on the main thread) → complete_reply (shell)
140
+ poll thread → {type:"message", channel:"shell", msg_type:"complete_reply", parent_msg_id:id, …}
141
+ Session.settleRequest → resolves with the reply `content`; status "error"/"aborted" rejects
142
+ If the supervisor refuses: {type:"requestError", id, error} → rejects at once
143
+ ```
144
+
145
+ Requests are answered on the kernel's main thread, so they wait for a running execution to finish. Only `interrupt_request` (control) is serviced during one.
146
+
147
+ ### Interactive input
148
+
149
+ ```
150
+ kernel: R readline() / Python input() ──► adrastea::blockingInputRequest()
151
+ (only if the execute_request had allow_stdin; otherwise fail fast — Python raises
152
+ RuntimeError, R reports on stderr and reads no input)
153
+ kernel main thread sends input_request on the stdin ROUTER and BLOCKS on the reply
154
+ poll thread relays it: {type:"message", channel:"stdin", msg_type:"input_request", …}
155
+ Session emits 'input_request' {prompt, password}; the execution's timeout is cleared
156
+ caller: session.sendInputReply(value) → WS {type:"inputReply", value}
157
+ → SessionRegistry::sendInputReply → stdin: input_reply → the kernel unblocks
158
+ ```
159
+
160
+ ### Interrupt
161
+
162
+ ```
163
+ Session.interrupt() → request('interrupt_request') on channel "control"
164
+ → control DEALER → kernel control ROUTER
165
+ kernel control-watcher thread (active only while code runs):
166
+ interrupt_request → interpreter flags R / raises SIGINT for Python
167
+ status busy/idle (iopub, parented to the interrupt) · interrupt_reply (control)
168
+ interpreter thread: R notices the flag at its next R_CheckUserInterrupt() /
169
+ Python's handler raises KeyboardInterrupt → the execution ends with an error;
170
+ execute_reply arrives as usual, the kernel stays alive
171
+ poll thread relays interrupt_reply (channel "control"); interrupt() resolves true
172
+ ```
173
+
174
+ ### Stop and restart
175
+
176
+ ```
177
+ Session.stop() Session.restart(options?)
178
+ DELETE /sessions/:id POST /sessions/:id/restart (optional new options)
179
+ └ SessionRegistry::stopSession(id, false) └ restartSession → stopSession(id, true) → spawn new kernel
180
+ under the SAME session id
181
+ stopSession:
182
+ 1. expectingExit = true (an orderly exit is not a crash)
183
+ 2. control: shutdown_request {restart} (if the process is alive)
184
+ 3. poll thread keeps running ≤ 2 s and relays iopub "shutdown" + control shutdown_reply
185
+ 4. stop poll thread, tear down the ZMQ client, force-kill if the process is still alive
186
+ (normal for a session blocked inside shiny::runApp())
187
+ 5. status = stopped; the session is removed from the registry
188
+ Session emits 'shutdown_reply' ({status, restart}); stop() then emits 'stopped',
189
+ restart() reconnects its WebSocket to the same URL and emits 'restarted'.
190
+ ```
191
+
192
+ ## Repository layout
193
+
194
+ ```
195
+ native/
196
+ ├── include/{adrastea,elara,carpo}/ public headers
197
+ ├── src/
198
+ │ ├── adrastea/ core/ (kernel, execution, messaging, history) · transport/ (server, client, common)
199
+ │ │ · platform/ · utils/
200
+ │ ├── elara/ r/ (routine.cpp, r_dynlib, interpreter_r.cpp) · bridge/ · elara.cpp
201
+ │ ├── carpo/ py/ (py_dynlib) · interpreter_py.cpp · bridge/ · carpo.cpp
202
+ │ └── themisto/ main.cpp · session_registry · kernel_process · http_api · ws_relay
203
+ └── test/ adrastea/ · elara/ · carpo/ · themisto/ (GoogleTest, one CTest entry per feature)
204
+ lib/ session/ (SessionManager, Session, Comm, SupervisorClient) · messaging/ · handlers/
205
+ · execution/ (ExecutionQueue) · middleware/ · utils/ · types/
206
+ packages/hera/ R companion package
207
+ test/ unit/lib (TypeScript, no processes) · integration (real themisto + kernels)
208
+ tools/ playground/ (Next.js) · jupyter-kernelspec/
209
+ examples/ basic/ · advanced/
210
+ scripts/ build.js · test.js · clean.js · dev.js · format.js · coverage.js
211
+ docs/ this documentation
212
+ ```
213
+
214
+ `adrastea` is a static library, so `elara`, `carpo` and `themisto` are standalone executables with no companion library to ship. There is no installable CMake package for it; see [C++ usage](../cpp-usage.md).
215
+
216
+ ## Further reading
217
+
218
+ [Protocol](../protocol.md) · [Kernels](../kernels.md) · [API reference](../api/README.md) · [Development](../development.md) · [Jupyter messaging protocol](https://jupyter-client.readthedocs.io/en/stable/messaging.html) · [ZeroMQ Guide](https://zguide.zeromq.org/)
@@ -0,0 +1,60 @@
1
+ # Using Jovian from C++
2
+
3
+ Jovian's native layer is four CMake targets, defined in [`native/CMakeLists.txt`](../native/CMakeLists.txt):
4
+
5
+ - **`adrastea`** — a static library: the language-neutral Jupyter kernel framework (transport, messaging, the kernel request loop, the abstract `Interpreter` interface). Public headers under [`native/include/adrastea/`](../native/include/adrastea).
6
+ - **`elara`** — an executable: embeds R on top of `adrastea`. Public headers under [`native/include/elara/`](../native/include/elara).
7
+ - **`carpo`** — an executable: embeds Python (CPython) on top of `adrastea` the same way. Public headers under [`native/include/carpo/`](../native/include/carpo).
8
+ - **`themisto`** — an executable: the supervisor that spawns/monitors `elara`/`carpo` processes, one per session, keyed by that session's `kernelType`.
9
+
10
+ ## Current state: in-tree only
11
+
12
+ There is no installed/exported CMake package today — no `install()`, no `<Package>Config.cmake`, no pkg-config file. `adrastea` is only ever consumed via `add_subdirectory` from within this same repository (see how `elara`/`themisto` link it in `native/CMakeLists.txt`). If you want to link against `adrastea` from a separate CMake project right now, the only supported path is:
13
+
14
+ ```cmake
15
+ add_subdirectory(path/to/jovian/native adrastea-build)
16
+ target_link_libraries(your_target PRIVATE adrastea)
17
+ ```
18
+
19
+ This pulls in `adrastea`'s `PUBLIC` include directories, compile definitions (`ADRASTEA_STATIC_LIB`), and link libraries (nlohmann_json, cppzmq, OpenSSL::Crypto) automatically, the same way `elara`/`themisto` get them.
20
+
21
+ Building a real `find_package(adrastea)`-style exported package (an `install(TARGETS ... EXPORT ...)` + generated config/version files) is a reasonable follow-up if an external consumer actually needs one — it hasn't been built yet because nothing outside this repo currently needs it.
22
+
23
+ ## Writing a new interpreter (a new language kernel)
24
+
25
+ `adrastea::Interpreter` ([`native/include/adrastea/interpreter.hpp`](../native/include/adrastea/interpreter.hpp)) is the extension point. A new language kernel:
26
+
27
+ 1. Subclasses `Interpreter` and implements its `*Impl()` virtual methods (`configureImpl`, `executeRequestImpl`, `completeRequestImpl`, `kernelInfoRequestImpl`, etc.) — see [`native/src/elara/r/interpreter_r.cpp`](../native/src/elara/r/interpreter_r.cpp)'s `RInterpreter` for a complete example.
28
+ 2. Calls `adrastea::registerInterpreter(this)` once constructed, before anything calls `adrastea::getInterpreter()`.
29
+ 3. Gets embedded into its own executable the way `elara.cpp` does — `main()` parses CLI args, builds an `adrastea::KernelConfiguration`, constructs the interpreter, and runs `adrastea::Kernel::start()` (see [`native/src/elara/bridge/engine.cpp`](../native/src/elara/bridge/engine.cpp)'s `Server::start()`).
30
+
31
+ This is exactly the shape both Elara and Carpo have (`native/src/carpo/`, built by default via `JOVIAN_BUILD_CARPO`) -- two concrete, working examples proving the extension point genuinely generalizes, not just in theory. `PyInterpreter` (`native/src/carpo/interpreter_py.cpp`) implements the full `Interpreter` interface for real: `executeRequestImpl`/`isCompleteRequestImpl`/`completeRequestImpl`/`inspectRequestImpl` all embed and drive a real CPython interpreter, the same way `RInterpreter` drives R. Its own C API is loaded dynamically at runtime (`native/src/carpo/py/py_dynlib.hpp`, mirroring `native/src/elara/r/r_dynlib.hpp`) rather than linked at build time, for the same version-switching/clean-failure reasons Elara does it for R. A third language kernel would follow the exact same three steps above; nothing about the surrounding scaffold (registration, the executable, the CMake target, its own test suite) needs to change shape to add one.
32
+
33
+ ### Threading contract for interpreter authors
34
+
35
+ The kernel executes code and reads its sockets on **one thread** (the process's main thread), and every `Interpreter` virtual is called from it — with **one exception**:
36
+
37
+ - **`interruptRequestImpl()` is called from a different thread**, the *control watcher*, while `executeRequestImpl()` is still running (`KernelCore::executeRequest` brackets the interpreter call with `Server::beginExecution()` / `endExecution()`; between them a watcher thread services `interrupt_request` on the control channel — see [Architecture](architecture/overview.md#inside-a-kernel-process-elara--carpo)). It must therefore only do thread-safe things: flag the runtime to break out of what it is doing, and return `createInterruptReply()`. Do not touch the interpreter's state or call its API from it.
38
+ - Only flag an interrupt **while an execution is actually running** (keep an `std::atomic<bool>` set by `executeRequestImpl`, as `RInterpreter` and `PyInterpreter` do). An interrupt with nothing to interrupt would otherwise linger and abort the *next* execution.
39
+ - Make the runtime check that flag *and* wake blocking calls. R: set `R_interrupts_pending` (POSIX) / `UserBreak` (Windows); R polls it. Python: a real SIGINT to the interpreter thread (`raise(SIGINT)` on Windows, `pthread_kill` on POSIX) with `signal.default_int_handler` installed, because `PyErr_SetInterrupt()` alone does not wake `time.sleep()`.
40
+ - Publishing from the watcher is safe (`ServerZmqImpl` serialises iopub publishing and control replies); everything else, including anything reached through `getInterpreter()`, is not.
41
+ - All other control messages (`shutdown_request`, …) sent during an execution are queued and delivered on the main thread afterwards.
42
+
43
+ Also part of the contract: `executeRequestImpl` must call its reply callback exactly once, honour `ExecuteRequestConfig::allow_stdin` for blocking reads (`adrastea::blockingInputRequest`, which throws when stdin is not allowed), evaluate `user_expressions` after a successful execution and return them in `createSuccessfulReply(payload, user_expressions)`, and report `restart` back in `shutdownRequestImpl`'s `createShutdownReply(restart)`.
44
+
45
+ ## Building and testing just the C++ side
46
+
47
+ ```sh
48
+ cmake -S . -B dist/native -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
49
+ cmake --build dist/native --config Release
50
+
51
+ # Tests (a separate build tree, so JOVIAN_BUILD_TESTS=ON doesn't stick around
52
+ # in dist/native's cache for future plain builds):
53
+ cmake -S . -B dist/native-test -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" -DJOVIAN_BUILD_TESTS=ON
54
+ cmake --build dist/native-test --config Release
55
+ ctest --test-dir dist/native-test -C Release --output-on-failure --timeout 180
56
+ ```
57
+
58
+ See [Development](development.md) for what each CTest entry covers and the platform prerequisites ([README](../README.md#requirements)).
59
+
60
+ Requires an R installation (only its headers, at build time — `elara` loads R's shared library dynamically at *runtime*, see [`native/src/elara/r/r_dynlib.hpp`](../native/src/elara/r/r_dynlib.hpp)) and [vcpkg](https://github.com/microsoft/vcpkg) with `VCPKG_ROOT` set. No Python installation is needed to *build* `carpo` at all -- unlike R, Carpo doesn't even include Python's headers at compile time (see `py_dynlib.hpp`'s file comment for why); a Python install is only needed at runtime, and only to actually run a Python session (`CarpoTest` also needs one, to embed and exercise for real -- it skips itself via `GTEST_SKIP` if `native/test/CMakeLists.txt`'s `find_package(Python3)` doesn't find one at configure time).
@@ -0,0 +1,105 @@
1
+ # Development
2
+
3
+ ## Repository layout
4
+
5
+ ```
6
+ CMakeLists.txt, native/CMakeLists.txt, native/test/CMakeLists.txt the build (see below)
7
+ cmake/ FindR.cmake, FindLibUUID.cmake
8
+ vcpkg.json native dependencies (manifest, pinned baseline)
9
+ native/ C++: adrastea (static lib), elara, carpo, themisto — see architecture/overview.md
10
+ lib/ the TypeScript package
11
+ packages/hera/ the R companion package
12
+ test/unit/lib/ TypeScript unit tests (Session against a fake WebSocket, ExecutionQueue, …)
13
+ test/integration/ end-to-end tests: real themisto + real kernels
14
+ tools/playground/ Next.js playground (own package.json)
15
+ tools/jupyter-kernelspec/ kernel.json generator
16
+ examples/ runnable examples
17
+ scripts/ build.js, test.js, clean.js, dev.js, format.js, coverage.js
18
+ docs/ documentation
19
+ dist/ ALL build output (gitignored): dist/lib, dist/native, dist/native-test, dist/ide/*
20
+ ```
21
+
22
+ ## Build system
23
+
24
+ **CMake ≥ 3.24, C++23**, dependencies from vcpkg (`vcpkg.json`; the toolchain file is `$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake`). The root `CMakeLists.txt` resolves `nlohmann_json`, `cppzmq`, `zeromq`, `OpenSSL`, `R` (**headers only** — `cmake/FindR.cmake` runs `R RHOME`), `httplib` and `ixwebsocket`, then adds `native/`.
25
+
26
+ | Option | Default | Effect |
27
+ |---|---|---|
28
+ | `JOVIAN_BUILD_ELARA` | ON | Build the `elara` executable |
29
+ | `JOVIAN_BUILD_THEMISTO` | ON | Build the `themisto` executable |
30
+ | `JOVIAN_BUILD_CARPO` | ON | Build the `carpo` executable |
31
+ | `JOVIAN_BUILD_TESTS` | OFF | Build the GoogleTest executables and register them with CTest |
32
+ | `JOVIAN_SANITIZE_ADDRESS` | OFF | Address sanitizer |
33
+
34
+ Outputs go to `dist/native/<config>/` (`CMAKE_RUNTIME_OUTPUT_DIRECTORY`) — executables, and on Windows the vcpkg DLLs next to them. Tests use a **separate build tree** (`dist/native-test`), so `JOVIAN_BUILD_TESTS=ON` does not stick in the main cache — but the output directory is set from the source root, so the test executables land in `dist/native/<config>/` too (which is why `package.json`'s `files` excludes `*_test.exe`).
35
+
36
+ ```sh
37
+ # what `npm run build` does (native part):
38
+ cmake -S . -B dist/native -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
39
+ cmake --build dist/native --config Release
40
+ # TypeScript:
41
+ npx tsc --build # lib/ -> dist/lib
42
+ ```
43
+
44
+ Platform notes baked into the build: Windows links the MSVC dynamic runtime, gives `elara` a 128 MiB stack (`/STACK:134217728`, added after a real `STATUS_STACK_OVERFLOW`) and links `bcrypt` for Themisto; Linux links `libuuid` (`find_package(LibUUID)`) and `dl`; macOS links CoreFoundation.
45
+
46
+ `CMakePresets.json` has Ninja + `cl.exe` presets for Windows IDE use (output under `dist/ide/<preset>`); `npm` scripts never use them.
47
+
48
+ ## Test layers
49
+
50
+ | Layer | Command | What it needs | What it covers |
51
+ |---|---|---|---|
52
+ | **Native (GoogleTest / CTest)** | `npm test` (first stage), or by hand: configure `dist/native-test` with `-DJOVIAN_BUILD_TESTS=ON`, build, `ctest --test-dir dist/native-test -C Release --output-on-failure --timeout 180` | vcpkg deps; R + `hera` for `SessionRegistryTest`; Python for `CarpoTest` | 12 CTest entries, below |
53
+ | **TypeScript unit** | `npm run test:unit` | built `dist/lib` | `Session` against a fake WebSocket, `ExecutionQueue`, router, middleware, option bodies. No processes. |
54
+ | **Integration** | `npm run test:integration` | built native binaries + `dist/lib`, R + `hera`; Python + `carpo` for the Python tests | Real `SessionManager` → `themisto` → `elara`/`carpo`: execute, streaming, stdin (R and Python), history, complete/inspect/is_complete/kernel_info, user expressions, `stopOnError`, interrupt (R and Python), working directory, stderr, comms (client- and kernel-initiated), `shutdown_reply` on stop/restart. Skips itself if `themisto` is not built; the Python tests skip without Python or `carpo`. |
55
+
56
+ CTest entries: `MessageTest`, `MiddlewareTest`, `AuthenticationTest`, `ZmqSerializerTest`, `KernelConfigurationTest`, `ClientZmqTest`, `ClientHeartbeatTest`, `ClientHandshakeZmqTest` (transport, no R); `ElaraTest` (spawns `elara` for its start-up failure paths); `KernelProcessTest` (spawn/liveness/kill against a dummy helper); `SessionRegistryTest` (drives a real `elara` through `SessionRegistry` — create/execute/restart/stop/interrupt/requests/comms/stop-on-error/user-expressions and concurrency races; ~20 kernel starts, ~25 s locally, 180 s CTest timeout); `CarpoTest` (embeds real Python: execution, streaming, complete/inspect, venv activation).
57
+
58
+ Playground tests are separate: `npm --prefix tools/playground test`.
59
+
60
+ The `hera` R package is installed into your R library, not run from the repo: after changing `packages/hera/R/*`, run `npm run hera:install` (CI installs it from source on every run). Features that live in `hera` — such as live streaming of one long-running expression — only work with the updated copy installed.
61
+
62
+ `npm test` (`scripts/test.js`) runs the native stage (with OpenCppCoverage if installed on Windows, otherwise plain `ctest`), then the two Node stages (`--test-force-exit`) through a wrapper that force-kills `node --test` after **20 minutes** — a backstop against a hung run, far above the couple of minutes the integration suite takes, so `npm test` is a valid one-shot check. The stages can also be run on their own: `npm run test:unit` and `npm run test:integration`.
63
+
64
+ ### Gotchas
65
+
66
+ - **The TypeScript tests import the compiled library from `dist/lib`.** After changing `lib/`, run `npm run build:lib` first, or you are testing stale code.
67
+ - **Windows will not overwrite a running `.exe`.** A leftover `themisto.exe` / `elara.exe` / `carpo.exe` (a crashed test run, a playground left open) makes the next native build fail at link time (`LNK1104`) and can silently slow or hang later runs. Kernels are put in a job object that dies with Themisto, but a supervisor that outlives its test runner still keeps them. Check with `tasklist | findstr /i "themisto elara carpo"` and kill strays before building. To run something while rebuilding, point `JOVIAN_NATIVE_DIR` at a *copy* of `dist/native/Release`.
68
+ - **Linux (Ubuntu/Debian).** The suites were run on Ubuntu 26.04 under WSL (native 12/12, unit, integration). What that setup needed: `cmake ninja-build uuid-dev r-base-dev python3-venv` from apt (`python3-venv` is for CarpoTest's venv test); the **official Node** from nodejs.org, because the distribution's Node has no TypeScript type stripping (`ERR_UNKNOWN_FILE_EXTENSION` on `.ts`); and `hera`'s dependencies from CRAN in a private `R_LIBS_USER` with `R_LIBS_SITE=/nonexistent`, since the apt `r-cran-*` packages fail to load (`undefined symbol: SETLENGTH`, built for a different R ABI). Export those two variables in the shell that runs `ctest` and the Node tests so the kernel processes inherit them. Build in the WSL filesystem (rsync the tree), not under `/mnt/c`. macOS is covered only by CI.
69
+ - **A test that holds a mutex its own `onMessage` callback needs, while calling `stopSession()`, deadlocks:** the supervisor's poll thread now keeps relaying (the kernel's `shutdown_reply`) during the stop. Scope such locks.
70
+ - **Every real-kernel test starts a process** (~0.7 s locally, more on cold CI runners) — keep the number of sessions per test small.
71
+ - `execute()` rejections and `'error'` events: tests that create a `Session` must attach an `'error'` listener or the Node process dies (see [API](api/README.md#events)).
72
+
73
+ ## CI
74
+
75
+ `.github/workflows/ci.yml` runs on every push to `main`, on pull requests, and manually, on **Windows, Ubuntu and macOS** (`fail-fast: false`):
76
+
77
+ 1. Checkout, Node (`lts/*`), R (`release`, without Rtools on Windows), Python (`3.x`).
78
+ 2. `r-lib/actions/setup-r-dependencies` with `packages: local::packages/hera` — installs `hera` and all its CRAN imports (real code execution needs it).
79
+ 3. `uuid-dev` on Linux.
80
+ 4. Clone and bootstrap vcpkg (`VCPKG_ROOT`), with the vcpkg binary cache keyed on `vcpkg.json`.
81
+ 5. Configure + build native (Release): elara, themisto, carpo.
82
+ 6. Configure + build the native tests (`dist/native-test`, `-DJOVIAN_BUILD_TESTS=ON`) and run `ctest -C Release --output-on-failure --timeout 180`.
83
+ 7. `npm ci --legacy-peer-deps` (a known peer-dependency conflict between TypeScript 7 and the `@typescript-eslint` plugin), `npx tsc --build`, unit tests, integration tests.
84
+
85
+ ## Releasing
86
+
87
+ The npm packages are built and published by `.github/workflows/release.yml` from a version tag; see [releasing.md](releasing.md).
88
+
89
+ ## Debugging
90
+
91
+ - **Kernel logs.** Everything a kernel prints (`[R Interpreter] …`, `[carpo] …`, and anything R/Python writes outside an execution) appears on the *supervisor's* stderr, which `lib/` forwards to your process's stderr, prefixed `[elara]` / `[carpo]`. That is the first place to look when a session fails to start.
92
+ - **Library logs.** Pass `logger` to `createSession()` (or read the default console output): `trace` shows queueing, request ids and timeouts.
93
+ - **Run a kernel by hand.** Generate a kernelspec (`npm run jupyter:kernelspec`) and start it from `jupyter console --kernel elara`, or run `elara -f <connection-file> --r-home …` yourself — no supervisor involved.
94
+ - **Talk to the supervisor directly.** Start `themisto` (it prints `{"type":"supervisorReady","httpPort":…,"wsPort":…}`) and use `curl` against [its HTTP API](protocol.md#2-themistos-http-api) and any WebSocket client against `ws://127.0.0.1:<wsPort>/sessions/<id>/messages`.
95
+ - **One native test.** `dist/native/Release/session_registry_test.exe --gtest_filter=*Interrupt*` (kernel log noise goes to the same stdout; filter with `grep -v "^\[elara\]"`).
96
+ - **Crashes.** A kernel crash is reported with its decoded exit code (`0xc0000005` = access violation). The Windows Event Viewer's Application log usually has the faulting module.
97
+ - **Hangs in native tests** are almost always a leaked lock or a socket used from two threads: ZMQ sockets are not thread-safe, which is why `DealerChannel`, the control socket and iopub publishing carry mutexes.
98
+
99
+ ## Style and tooling
100
+
101
+ `npm run format` / `npm run format:check` (clang-format) and `npm run lint` (`eslint lib/**/*.ts`, `clang-tidy native/src/**/*.cpp`) are wired in `package.json`; no ESLint or clang-format configuration file is currently checked in, so they use defaults (or your own config). Code comments in this repo explain *why* (constraints, past bugs), not what.
102
+
103
+ ## Adding a kernel
104
+
105
+ See [C++ usage](cpp-usage.md#writing-a-new-interpreter-a-new-language-kernel): implement `adrastea::Interpreter`, register it, write a `main()`, add a CMake target, and add its executable to Themisto's `kernelExePaths` map (`native/src/themisto/main.cpp`) under a new `kernelType`.
@@ -0,0 +1,127 @@
1
+ # Getting started
2
+
3
+ This walks from a clean machine to a running R session and a running Python session, **building Jovian from source**. If you only want to use it from Node.js, `npm install @damurka/jovian` ships prebuilt binaries and you can skip to [your first R session](#4-your-first-r-session) — see [Install](../README.md#install) for what R needs. The [README](../README.md#requirements) has the precise requirements per platform; this page is the sequence.
4
+
5
+ ## 1. Install the prerequisites
6
+
7
+ | Platform | Install |
8
+ |---|---|
9
+ | Windows | **Visual Studio** with the *Desktop development with C++* workload (the repo is built with Visual Studio 2026 / MSVC v145), **Git**, **Node.js** (recent LTS), **R** (4.2 or newer), optionally **Python 3**. |
10
+ | Linux | A C++23 compiler, `cmake`, Git, Node.js (the official build from nodejs.org — see the note below), R built with a shared library (`--enable-R-shlib`; distribution packages are), `uuid-dev`, optionally Python 3. On Ubuntu/Debian: `sudo apt install cmake ninja-build uuid-dev r-base-dev python3 python3-venv`. |
11
+ | macOS | Xcode Command Line Tools, CMake, Git, Node.js, R, optionally Python 3. |
12
+
13
+ On Ubuntu/Debian (verified on Ubuntu 26.04 under WSL): Debian's split R headers are found automatically (`cmake/FindR.cmake` asks `R CMD config --cppflags`), and Carpo finds a distribution Python's `libpython` in `lib/<arch>-linux-gnu/`, so `PYTHONHOME=/usr` works. The distribution's packaged Node.js has no TypeScript type stripping — install the official Node from nodejs.org to run the `.ts` tests. macOS is covered only by CI.
14
+
15
+ Then install **vcpkg** and point `VCPKG_ROOT` at it:
16
+
17
+ ```sh
18
+ git clone https://github.com/microsoft/vcpkg.git
19
+ cd vcpkg
20
+ ./bootstrap-vcpkg.sh # Windows: .\bootstrap-vcpkg.bat
21
+ export VCPKG_ROOT=$PWD # PowerShell: $env:VCPKG_ROOT = (Get-Location).Path
22
+ ```
23
+
24
+ ## 2. Install the R side
25
+
26
+ Elara needs the R package `hera` (in `packages/hera`) and its dependencies in the R library the session will use:
27
+
28
+ ```r
29
+ install.packages(c("cli", "evaluate", "glue", "IRdisplay", "jsonlite", "R6", "repr", "rlang"))
30
+ ```
31
+ ```sh
32
+ npm run hera:install # = R CMD INSTALL packages/hera, from the repo root; re-run it after pulling to pick up hera changes
33
+ ```
34
+
35
+ On Debian/Ubuntu, if `hera` fails to install with `undefined symbol: SETLENGTH`, the apt `r-cran-*` packages were built for a different R ABI. Install the dependencies from CRAN into a private library instead: `export R_LIBS_SITE=/nonexistent R_LIBS_USER=$HOME/Rlib`, `mkdir -p $R_LIBS_USER`, then `install.packages(c("cli", "evaluate", "glue", "IRdisplay", "jsonlite", "R6", "repr", "rlang"), lib = Sys.getenv("R_LIBS_USER"))` and `npm run hera:install` in that same shell (keep both variables set when running sessions).
36
+
37
+ (Alternatively pass `heraSrcPath` when creating a session and let Elara install it — see [Environments](guides/environments.md#the-hera-package-required).)
38
+
39
+ ## 3. Build
40
+
41
+ ```sh
42
+ npm install --legacy-peer-deps
43
+ npm run build # native (elara, carpo, themisto) -> dist/native/Release, then TypeScript -> dist/lib
44
+ ```
45
+
46
+ `npm run build` uses `VCPKG_ROOT`'s toolchain file; the first build compiles ZeroMQ, OpenSSL and the other vcpkg dependencies, which takes a while (vcpkg caches them afterwards). If it succeeds you have `dist/native/Release/{themisto,elara,carpo}[.exe]` and `dist/lib/`.
47
+
48
+ ## 4. Your first R session
49
+
50
+ Save as `first-r.mjs` in the repo root and run `node first-r.mjs`:
51
+
52
+ ```javascript
53
+ import { SessionManager } from './dist/lib/index.js';
54
+
55
+ const manager = new SessionManager();
56
+ const session = await manager.createSession({
57
+ kernelType: 'r',
58
+ rHome: process.env.R_HOME, // e.g. "C:/Program Files/R/R-4.6.0" or the output of `R RHOME`
59
+ rPath: process.env.R_PATH, // Windows only: e.g. "C:/Program Files/R/R-4.6.0/bin/x64"
60
+ workingDirectory: process.cwd(), // where getwd() will point
61
+ });
62
+
63
+ session.on('error', () => {}); // required: see docs/api/README.md#events
64
+ session.on('stdout', (text) => process.stdout.write(text));
65
+
66
+ const result = await session.execute('print("hello from R"); x <- 1:10; mean(x)');
67
+ console.log('success:', result.success);
68
+ console.log(result.output.map((m) => `${m.msgType}: ${JSON.stringify(m.content).slice(0, 80)}`));
69
+
70
+ console.log(await session.kernelInfo()); // language_info.name === 'R'
71
+ console.log((await session.complete('pri')).matches); // [ 'print', … ]
72
+
73
+ await manager.stopAll(); // always: it also ends the supervisor
74
+ ```
75
+
76
+ ## 5. Your first Python session
77
+
78
+ ```javascript
79
+ const py = await manager.createSession({
80
+ kernelType: 'python',
81
+ pythonHome: process.env.PYTHONHOME, // the prefix that contains libpython — python -c "import sys; print(sys.base_prefix)"
82
+ workingDirectory: process.cwd(),
83
+ });
84
+ py.on('error', () => {});
85
+ py.on('stdout', (t) => process.stdout.write(t));
86
+ await py.execute('import os\nprint(os.getcwd())\nsum(range(1, 11))');
87
+ ```
88
+
89
+ Create both in one script, side by side — they are separate processes.
90
+
91
+ ## 6. Things worth trying next
92
+
93
+ ```javascript
94
+ // Interactive input
95
+ session.on('input_request', ({ prompt }) => session.sendInputReply('World'));
96
+ await session.execute('name <- readline("name? "); cat("hello", name, "\\n")', { allowStdin: true });
97
+
98
+ // Interrupt a long call
99
+ const running = session.execute('Sys.sleep(60)', { timeout: 0 });
100
+ setTimeout(() => session.interrupt(), 500);
101
+ console.log((await running).success); // false — interrupted
102
+
103
+ // Evaluate an expression after the code
104
+ const r = await session.execute('x <- 21', { userExpressions: { double: 'x * 2' } });
105
+ console.log(r.userExpressions.double); // { status: 'ok', data: { 'text/plain': '[1] 42' }, … }
106
+ ```
107
+
108
+ More: [interactive input](guides/interactive-input.md), [interrupting](guides/interrupting.md), [comms](guides/comms.md), [session lifecycle](guides/sessions-lifecycle.md), and the runnable [`examples/`](../examples).
109
+
110
+ ## 7. The playground
111
+
112
+ A browser UI on top of the same API, handy for trying an installation:
113
+
114
+ ```sh
115
+ npm run playground:install # once
116
+ npm run playground # http://127.0.0.1:4173
117
+ ```
118
+
119
+ It pre-fills R and Python from auto-discovery, shows PID / memory / working directory per session, and completes as you type (`Tab` accepts) and inspects a word when you rest the mouse or caret on it (`Shift+Tab` asks explicitly). See [the playground guide](guides/playground.md).
120
+
121
+ ## 8. Run the tests
122
+
123
+ ```sh
124
+ npm test # native ctest + TypeScript unit + integration
125
+ ```
126
+
127
+ If something fails to start, see [Troubleshooting](troubleshooting.md).
@@ -0,0 +1,70 @@
1
+ # Comms
2
+
3
+ A **comm** is a named, bidirectional message stream between your application and a *target* registered inside the kernel — the mechanism Jupyter widgets are built on. Jovian exposes it as `Comm` objects. Comms are an **R feature today**: Carpo has no API for registering comm targets, so every `comm_open` sent to a Python session is answered with a `comm_close`.
4
+
5
+ ## Client → kernel: `openComm`
6
+
7
+ First register a target in the kernel (R, via `hera`), then open a comm to it:
8
+
9
+ ```typescript
10
+ await session.execute(`
11
+ hera::CommManager$register_comm_target("echo", function(comm, message) {
12
+ comm$on_message(function(msg) {
13
+ comm$send(list(echo = msg$content$data$text))
14
+ })
15
+ })
16
+ `);
17
+
18
+ const comm = await session.openComm('echo', { hello: 'kernel' }); // data becomes the comm_open payload
19
+ comm.on('message', (data) => console.log(data)); // { echo: 'ping' }
20
+ await comm.send({ text: 'ping' });
21
+ await comm.close();
22
+ ```
23
+
24
+ The callback receives the new `comm` and the `comm_open` `message`; `comm$on_message()` registers a handler for the client's `comm_msg`s, `comm$send()` / `comm$open()` / `comm$close()` send to the client (data is serialised with `jsonlite`).
25
+
26
+ If the kernel has no such target, it answers with a `comm_close` and the returned `Comm` emits `'close'`. (You can also check with `await session.commInfo('echo')`.)
27
+
28
+ ## Kernel → client: the `'comm'` event
29
+
30
+ A comm the kernel creates and opens arrives as a `'comm'` event:
31
+
32
+ ```typescript
33
+ session.on('comm', (comm, data) => {
34
+ console.log(comm.targetName, 'opened with', data);
35
+ comm.on('message', (msg) => console.log('kernel says', msg));
36
+ comm.send({ text: 'hi' });
37
+ });
38
+
39
+ await session.execute(`
40
+ hera::CommManager$register_comm_target("kernel_side") # a target must exist before new_comm()
41
+ comm <- hera::CommManager$new_comm("kernel_side")
42
+ comm$on_message(function(msg) comm$send(list(echo = msg$content$data$text)))
43
+ comm$open(list(greeting = "from R"))
44
+ `);
45
+ ```
46
+
47
+ Attach the `'comm'` listener **before** running the code that opens it. `new_comm()` returns `NULL` for an unregistered target, so register the target first.
48
+
49
+ ## The `Comm` object
50
+
51
+ | | |
52
+ |---|---|
53
+ | `comm.id`, `comm.targetName`, `comm.closed` | Identity and state. |
54
+ | `comm.send(data)` | Sends a `comm_msg`; rejects if the comm is closed. |
55
+ | `comm.close(data?)` | Sends `comm_close`; emits `'close'`. |
56
+ | `'message'` `(data)` | The kernel sent a `comm_msg`. |
57
+ | `'close'` `(data)` | Closed by either side; or the kernel restarted / exited / the session stopped / the connection dropped, in which case `data.reason` says which. |
58
+
59
+ ## Listing comms
60
+
61
+ `await session.commInfo()` returns `{ comms: { <commId>: { target_name } } }` for every open comm in the kernel; pass a target name to filter.
62
+
63
+ ## Low-level API
64
+
65
+ `commOpen(targetName, data?, commId?)`, `commMsg(commId, data?)` and `commClose(commId, data?)` send the raw messages and resolve with the message id they were sent under (useful for correlating iopub replies — e.g. the `comm_close` for an unknown target has that id as its `parentMsgId`). The kernel's `comm_open` / `comm_msg` / `comm_close` are also available as plain events of those names (`session.on('comm_msg', (content) => …)` with `content.comm_id`). `openComm()` and the `'comm'` event are built on them.
66
+
67
+ ## Notes
68
+
69
+ - Comm requests are handled on the kernel's main thread between executions, so a comm message sent while code is running waits for it to finish.
70
+ - Comms do not survive a restart; open `Comm`s emit `'close'` with `reason: 'kernel restarted'`.