@whalent/agent 0.3.263 → 0.3.265
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.
- package/dist/core.cjs +7 -7
- package/dist/jupyter_sidecar.py +55 -21
- package/package.json +2 -2
package/dist/jupyter_sidecar.py
CHANGED
|
@@ -19,6 +19,7 @@ import traceback
|
|
|
19
19
|
from typing import Any
|
|
20
20
|
|
|
21
21
|
from jupyter_client import KernelManager
|
|
22
|
+
from jupyter_client.kernelspec import KernelSpec
|
|
22
23
|
|
|
23
24
|
|
|
24
25
|
WRITE_LOCK = threading.Lock()
|
|
@@ -75,10 +76,19 @@ def start_kernel() -> dict[str, Any]:
|
|
|
75
76
|
return safe_status()
|
|
76
77
|
STATE = "starting"
|
|
77
78
|
manager = KernelManager()
|
|
78
|
-
# The sidecar itself runs under the selected interpreter.
|
|
79
|
-
#
|
|
80
|
-
manager.kernel_cmd
|
|
81
|
-
|
|
79
|
+
# The sidecar itself runs under the selected interpreter. Pin ipykernel
|
|
80
|
+
# to the same executable via an explicit KernelSpec: assigning
|
|
81
|
+
# `manager.kernel_cmd` is a silent no-op on jupyter_client >= 7 (the
|
|
82
|
+
# trait is gone), while `_kernel_spec` is the cache slot behind the
|
|
83
|
+
# KernelManager.kernel_spec property and keeps its meaning on 5.x-8.x.
|
|
84
|
+
manager._kernel_spec = KernelSpec(
|
|
85
|
+
argv=[sys.executable, "-m", "ipykernel_launcher", "-f", "{connection_file}"],
|
|
86
|
+
display_name="Whalent (pinned)",
|
|
87
|
+
language="python",
|
|
88
|
+
)
|
|
89
|
+
# Kernel stdout/stderr go to the sidecar's stderr so raw fd writes from
|
|
90
|
+
# user code can never corrupt the NDJSON protocol on stdout.
|
|
91
|
+
manager.start_kernel(stdout=sys.stderr, stderr=sys.stderr)
|
|
82
92
|
client = manager.client()
|
|
83
93
|
client.start_channels()
|
|
84
94
|
client.wait_for_ready(timeout=30)
|
|
@@ -127,16 +137,21 @@ def output_from_message(message: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
127
137
|
|
|
128
138
|
def execute_worker(request_id: str, cell_id: str, code: str, timeout_seconds: float) -> None:
|
|
129
139
|
global STATE, CURRENT_CELL
|
|
140
|
+
generation = GENERATION
|
|
130
141
|
try:
|
|
131
142
|
start_kernel()
|
|
132
143
|
with KERNEL_LOCK:
|
|
144
|
+
# Capture the generation this execute belongs to (start_kernel may
|
|
145
|
+
# have revived a dead kernel and bumped it); every frame below is
|
|
146
|
+
# stamped with it so the daemon can fence stale generations.
|
|
147
|
+
generation = GENERATION
|
|
133
148
|
client = KC
|
|
134
149
|
if client is None:
|
|
135
150
|
raise RuntimeError("kernel client unavailable")
|
|
136
151
|
msg_id = client.execute(code, stop_on_error=True, allow_stdin=False)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
emit({"type": "execution.started", "request_id": request_id, "cell_id": cell_id, **safe_status()})
|
|
152
|
+
STATE = "busy"
|
|
153
|
+
CURRENT_CELL = cell_id
|
|
154
|
+
emit({"type": "execution.started", "request_id": request_id, "cell_id": cell_id, **safe_status(), "generation": generation})
|
|
140
155
|
deadline = time.monotonic() + timeout_seconds
|
|
141
156
|
execution_count = None
|
|
142
157
|
shell_done = False
|
|
@@ -151,9 +166,9 @@ def execute_worker(request_id: str, cell_id: str, code: str, timeout_seconds: fl
|
|
|
151
166
|
output = output_from_message(message)
|
|
152
167
|
if output:
|
|
153
168
|
if output.get("outputType") == "clear_output":
|
|
154
|
-
emit({"type": "output.clear", "cell_id": cell_id, "wait": output.get("wait", False)})
|
|
169
|
+
emit({"type": "output.clear", "cell_id": cell_id, "generation": generation, "wait": output.get("wait", False)})
|
|
155
170
|
else:
|
|
156
|
-
emit({"type": "output", "cell_id": cell_id, "output": output})
|
|
171
|
+
emit({"type": "output", "cell_id": cell_id, "generation": generation, "output": output})
|
|
157
172
|
except queue.Empty:
|
|
158
173
|
pass
|
|
159
174
|
if not shell_done:
|
|
@@ -172,8 +187,10 @@ def execute_worker(request_id: str, cell_id: str, code: str, timeout_seconds: fl
|
|
|
172
187
|
except Exception:
|
|
173
188
|
pass
|
|
174
189
|
raise TimeoutError(f"cell execution exceeded {int(timeout_seconds)} seconds")
|
|
175
|
-
|
|
176
|
-
|
|
190
|
+
with KERNEL_LOCK:
|
|
191
|
+
if CURRENT_CELL == cell_id:
|
|
192
|
+
STATE = "idle"
|
|
193
|
+
CURRENT_CELL = None
|
|
177
194
|
emit({
|
|
178
195
|
"type": "execution.reply",
|
|
179
196
|
"request_id": request_id,
|
|
@@ -181,10 +198,15 @@ def execute_worker(request_id: str, cell_id: str, code: str, timeout_seconds: fl
|
|
|
181
198
|
"cell_id": cell_id,
|
|
182
199
|
"execution_count": execution_count,
|
|
183
200
|
**safe_status(),
|
|
201
|
+
"generation": generation,
|
|
184
202
|
})
|
|
185
203
|
except Exception as exc:
|
|
186
|
-
|
|
187
|
-
|
|
204
|
+
# Only release the claim if this worker still owns it: a restart may
|
|
205
|
+
# already have handed the kernel to a newer execute.
|
|
206
|
+
with KERNEL_LOCK:
|
|
207
|
+
if CURRENT_CELL == cell_id:
|
|
208
|
+
STATE = "idle" if KM is not None and KM.is_alive() else "dead"
|
|
209
|
+
CURRENT_CELL = None
|
|
188
210
|
emit({
|
|
189
211
|
"type": "execution.reply",
|
|
190
212
|
"request_id": request_id,
|
|
@@ -192,9 +214,8 @@ def execute_worker(request_id: str, cell_id: str, code: str, timeout_seconds: fl
|
|
|
192
214
|
"cell_id": cell_id,
|
|
193
215
|
"error": {"message": str(exc)},
|
|
194
216
|
**safe_status(),
|
|
217
|
+
"generation": generation,
|
|
195
218
|
})
|
|
196
|
-
finally:
|
|
197
|
-
CURRENT_CELL = None
|
|
198
219
|
|
|
199
220
|
|
|
200
221
|
def interrupt_kernel() -> dict[str, Any]:
|
|
@@ -234,7 +255,7 @@ def shutdown_kernel() -> dict[str, Any]:
|
|
|
234
255
|
|
|
235
256
|
|
|
236
257
|
def handle(message: dict[str, Any]) -> None:
|
|
237
|
-
global
|
|
258
|
+
global STATE, CURRENT_CELL
|
|
238
259
|
request_id = str(message.get("id", ""))
|
|
239
260
|
method = str(message.get("method", ""))
|
|
240
261
|
params = message.get("params") if isinstance(message.get("params"), dict) else {}
|
|
@@ -246,11 +267,24 @@ def handle(message: dict[str, Any]) -> None:
|
|
|
246
267
|
code = str(params.get("code", ""))
|
|
247
268
|
if not cell_id:
|
|
248
269
|
raise ValueError("cell_id required")
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
270
|
+
with KERNEL_LOCK:
|
|
271
|
+
if CURRENT_CELL is not None:
|
|
272
|
+
raise ValueError(f"kernel busy: cell {CURRENT_CELL} still running")
|
|
273
|
+
# Claim the kernel before the worker thread exists so two
|
|
274
|
+
# racing execute requests can never drain IOPub concurrently.
|
|
275
|
+
STATE = "busy"
|
|
276
|
+
CURRENT_CELL = cell_id
|
|
277
|
+
try:
|
|
278
|
+
threading.Thread(
|
|
279
|
+
target=execute_worker,
|
|
280
|
+
args=(request_id, cell_id, code, max(1.0, min(float(params.get("timeout_seconds", 1800)), 3600.0))),
|
|
281
|
+
daemon=True,
|
|
282
|
+
).start()
|
|
283
|
+
except BaseException:
|
|
284
|
+
with KERNEL_LOCK:
|
|
285
|
+
STATE = "idle" if KM is not None and KM.is_alive() else "dead"
|
|
286
|
+
CURRENT_CELL = None
|
|
287
|
+
raise
|
|
254
288
|
elif method == "interrupt":
|
|
255
289
|
reply(request_id, True, interrupt_kernel())
|
|
256
290
|
elif method == "restart":
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@whalent/agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.265",
|
|
4
4
|
"description": "Stable wrapper for Whalent Agent core runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"yaml": "^2.4.0"
|
|
55
55
|
},
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@whalent/agent-core": "0.3.
|
|
57
|
+
"@whalent/agent-core": "0.3.265",
|
|
58
58
|
"node-pty": "^1.1.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|