@hy-sde-org/dsh-code-runtime-kernels 0.1.0-rc.8
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/LICENSE +32 -0
- package/README.md +85 -0
- package/README.zh.md +85 -0
- package/cordis.patch.yml +34 -0
- package/dist/core/invariant.d.ts +16 -0
- package/dist/core/invariant.d.ts.map +1 -0
- package/dist/core/invariant.js +23 -0
- package/dist/core/invariant.js.map +1 -0
- package/dist/core/kernel.d.ts +133 -0
- package/dist/core/kernel.d.ts.map +1 -0
- package/dist/core/kernel.js +565 -0
- package/dist/core/kernel.js.map +1 -0
- package/dist/core/protocol.d.ts +115 -0
- package/dist/core/protocol.d.ts.map +1 -0
- package/dist/core/protocol.js +12 -0
- package/dist/core/protocol.js.map +1 -0
- package/dist/core/session.d.ts +49 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/session.js +153 -0
- package/dist/core/session.js.map +1 -0
- package/dist/index.d.ts +135 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +478 -0
- package/dist/index.js.map +1 -0
- package/dist/nodejs/runner.d.ts +26 -0
- package/dist/nodejs/runner.d.ts.map +1 -0
- package/dist/nodejs/runner.js +298 -0
- package/dist/nodejs/runner.js.map +1 -0
- package/dist/python/runner.d.ts +26 -0
- package/dist/python/runner.d.ts.map +1 -0
- package/dist/python/runner.js +615 -0
- package/dist/python/runner.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The persistent Python kernel embedded as source. One long-lived `python3`
|
|
3
|
+
* subprocess runs this script; the host and it speak NDJSON over stdio. The
|
|
4
|
+
* script is deliberately SELF-CONTAINED (standard library only) so no
|
|
5
|
+
* virtualenv, package install, or site customization is needed to run model
|
|
6
|
+
* code against a persistent namespace and event loop.
|
|
7
|
+
*
|
|
8
|
+
* Wire protocol (one JSON object per line):
|
|
9
|
+
* - host -> kernel: `{"type":"exec","id":str,"code":str,"namespaces":[...],"cwd"?,"env"?}`,
|
|
10
|
+
* `{"type":"reply","id":str,"seq":int,"ok":bool,"value"?|"message"}`,
|
|
11
|
+
* `{"type":"exit"}`
|
|
12
|
+
* - kernel -> host: `{"type":"ready","pid":int}`, then per exec:
|
|
13
|
+
* `{"type":"started","id"}`, `{"type":"log","id","text","stream"?}`,
|
|
14
|
+
* `{"type":"call","id","seq":int,"global","name","args"}`,
|
|
15
|
+
* `{"type":"error","id","ename","evalue","traceback":[]}`,
|
|
16
|
+
* `{"type":"done","id","status":"ok"|"error","value"?,"executionCount":int,"cancelled":bool}`
|
|
17
|
+
*
|
|
18
|
+
* The runner is a persistent kernel: `user_ns` and one asyncio event loop
|
|
19
|
+
* survive across cells, so top-level `await` interleaves and assignments
|
|
20
|
+
* persist; tool bindings are bridged through wire frames that the host
|
|
21
|
+
* resolves against the CURRENT run's namespaces.
|
|
22
|
+
*
|
|
23
|
+
* @module @deepseek-ai/dsh-code-runtime-python/src/runner
|
|
24
|
+
*/
|
|
25
|
+
export const PYTHON_RUNNER = String.raw `"""DSH Python kernel runner (embedded).
|
|
26
|
+
|
|
27
|
+
Persistent subprocess kernel for the dsh code-runtime seam's Python backend.
|
|
28
|
+
Protocol: one JSON object per line on stdin (host->kernel) and stdout
|
|
29
|
+
(kernel->host). Self-contained: standard library only.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import ast
|
|
35
|
+
import asyncio
|
|
36
|
+
import builtins
|
|
37
|
+
import contextvars
|
|
38
|
+
import io
|
|
39
|
+
import json
|
|
40
|
+
import os
|
|
41
|
+
import signal
|
|
42
|
+
import sys
|
|
43
|
+
import threading
|
|
44
|
+
import traceback
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# Frame output + fd-1 capture (children must not corrupt the NDJSON channel)
|
|
49
|
+
# ---------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
_FRAME_FD = os.dup(sys.__stdout__.fileno())
|
|
53
|
+
_RAW_FD = os.fdopen(_FRAME_FD, "w", encoding="utf-8", errors="backslashreplace")
|
|
54
|
+
_CAPTURE_READ_FD, _capture_write_fd = os.pipe()
|
|
55
|
+
os.dup2(_capture_write_fd, sys.__stdout__.fileno())
|
|
56
|
+
os.close(_capture_write_fd)
|
|
57
|
+
except (AttributeError, OSError, ValueError, io.UnsupportedOperation):
|
|
58
|
+
_RAW_FD = sys.__stdout__
|
|
59
|
+
_CAPTURE_READ_FD = None
|
|
60
|
+
|
|
61
|
+
_OUT_LOCK = threading.Lock()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _json_default(value: Any) -> Any:
|
|
65
|
+
try:
|
|
66
|
+
return repr(value)
|
|
67
|
+
except Exception:
|
|
68
|
+
return f"<unrepr {type(value).__name__}>"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _emit(frame: dict) -> None:
|
|
72
|
+
"""Serialize one frame and write it as a single NDJSON line."""
|
|
73
|
+
line = json.dumps(frame, ensure_ascii=False, default=_json_default)
|
|
74
|
+
with _OUT_LOCK:
|
|
75
|
+
_RAW_FD.write(line)
|
|
76
|
+
_RAW_FD.write("\n")
|
|
77
|
+
_RAW_FD.flush()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class _StreamProxy(io.TextIOBase):
|
|
81
|
+
"""Route python-side stdout/stderr into per-run log frames.
|
|
82
|
+
|
|
83
|
+
Coalesces writes per run id (one frame per complete line or per 8 KiB)
|
|
84
|
+
so a plain print() plus its newline costs one frame.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
_MAX_BUFFER = 8192
|
|
88
|
+
|
|
89
|
+
def __init__(self, stream: str) -> None:
|
|
90
|
+
super().__init__()
|
|
91
|
+
self._stream = stream
|
|
92
|
+
self._lock = threading.Lock()
|
|
93
|
+
self._buffers: dict[str, str] = {}
|
|
94
|
+
|
|
95
|
+
def writable(self) -> bool:
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
def isatty(self) -> bool:
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
def write(self, data: Any) -> int: # type: ignore[override]
|
|
102
|
+
if not isinstance(data, str):
|
|
103
|
+
data = str(data)
|
|
104
|
+
if not data:
|
|
105
|
+
return 0
|
|
106
|
+
rid = _CURRENT_RID.get()
|
|
107
|
+
if rid is None:
|
|
108
|
+
return len(data)
|
|
109
|
+
emit_text = None
|
|
110
|
+
with self._lock:
|
|
111
|
+
buf = self._buffers.pop(rid, "") + data
|
|
112
|
+
if len(buf) >= self._MAX_BUFFER:
|
|
113
|
+
emit_text = buf
|
|
114
|
+
else:
|
|
115
|
+
nl = buf.rfind("\n")
|
|
116
|
+
if nl >= 0:
|
|
117
|
+
emit_text = buf[: nl + 1]
|
|
118
|
+
rest = buf[nl + 1 :]
|
|
119
|
+
if rest:
|
|
120
|
+
self._buffers[rid] = rest
|
|
121
|
+
else:
|
|
122
|
+
self._buffers[rid] = buf
|
|
123
|
+
if emit_text:
|
|
124
|
+
_emit({"type": "log", "id": rid, "text": emit_text, "stream": self._stream})
|
|
125
|
+
return len(data)
|
|
126
|
+
|
|
127
|
+
def flush(self) -> None:
|
|
128
|
+
rid = _CURRENT_RID.get()
|
|
129
|
+
if rid is not None:
|
|
130
|
+
self.flush_rid(rid)
|
|
131
|
+
|
|
132
|
+
def flush_rid(self, rid: str) -> None:
|
|
133
|
+
with self._lock:
|
|
134
|
+
buf = self._buffers.pop(rid, None)
|
|
135
|
+
if buf:
|
|
136
|
+
_emit({"type": "log", "id": rid, "text": buf, "stream": self._stream})
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _flush_stream_proxies(rid: str) -> None:
|
|
140
|
+
for stream in (sys.stdout, sys.stderr):
|
|
141
|
+
if isinstance(stream, _StreamProxy):
|
|
142
|
+
stream.flush_rid(rid)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
_RAW_STDERR = sys.__stderr__
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _start_capture_drain() -> None:
|
|
149
|
+
"""Start the fd-1 drain thread (child-process output forwarder)."""
|
|
150
|
+
if _CAPTURE_READ_FD is None:
|
|
151
|
+
return
|
|
152
|
+
thread = threading.Thread(target=_drain_captured_stdout, name="dsh-fd1-capture", daemon=True)
|
|
153
|
+
thread.start()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _drain_captured_stdout() -> None:
|
|
157
|
+
"""Forward bytes written to the captured fd 1 (child processes) as log frames."""
|
|
158
|
+
if _CAPTURE_READ_FD is None:
|
|
159
|
+
return
|
|
160
|
+
codecs = __import__("codecs")
|
|
161
|
+
decoder = codecs.getincrementaldecoder("utf-8")("replace")
|
|
162
|
+
while True:
|
|
163
|
+
try:
|
|
164
|
+
chunk = os.read(_CAPTURE_READ_FD, 65536)
|
|
165
|
+
except OSError:
|
|
166
|
+
return
|
|
167
|
+
if not chunk:
|
|
168
|
+
return
|
|
169
|
+
text = decoder.decode(chunk)
|
|
170
|
+
if text:
|
|
171
|
+
rid = _STATE.capture_rid
|
|
172
|
+
if rid is not None:
|
|
173
|
+
_emit({"type": "log", "id": rid, "text": text, "stream": "stdout"})
|
|
174
|
+
else:
|
|
175
|
+
_RAW_STDERR.write(text)
|
|
176
|
+
_RAW_STDERR.flush()
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# Persistent runner state
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class _RunnerState:
|
|
185
|
+
def __init__(self) -> None:
|
|
186
|
+
self.user_ns: dict[str, Any] = {
|
|
187
|
+
"__name__": "__main__",
|
|
188
|
+
"__doc__": None,
|
|
189
|
+
"__builtins__": builtins,
|
|
190
|
+
}
|
|
191
|
+
self.loop: asyncio.AbstractEventLoop | None = None
|
|
192
|
+
self.execution_count: int = 0
|
|
193
|
+
self.cancel_requested: bool = False
|
|
194
|
+
self.capture_rid: str | None = None
|
|
195
|
+
# (run_id, seq) -> Future awaiting the host's reply to a binding call.
|
|
196
|
+
self.pending_calls: dict[tuple[str, int], asyncio.Future] = {}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
_CURRENT_RID: contextvars.ContextVar[str | None] = contextvars.ContextVar("dsh_rid", default=None)
|
|
200
|
+
_STATE = _RunnerState()
|
|
201
|
+
|
|
202
|
+
_call_seq = 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _next_call_seq() -> int:
|
|
206
|
+
global _call_seq
|
|
207
|
+
_call_seq += 1
|
|
208
|
+
return _call_seq
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------------------
|
|
212
|
+
# Tool binding proxy
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _make_tool_proxy_run(run_id: str, gname: str, names: list[str], member_prop: str, error_cls: Any) -> Any:
|
|
217
|
+
"""Build the program-visible proxy for one binding global of one run.
|
|
218
|
+
|
|
219
|
+
Only the declared member names are exposed; each is an async callable that
|
|
220
|
+
emits a call frame and awaits the host's reply. The injected error class
|
|
221
|
+
name and member-name property mirror the TypeScript worker's ToolCallError /
|
|
222
|
+
toolName contract (the shipped Code Mode consumer's descriptor).
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
class _BindingProxy:
|
|
226
|
+
def __init__(self) -> None:
|
|
227
|
+
for name in names:
|
|
228
|
+
setattr(self, name, _make_binding_callable(run_id, gname, name, member_prop, error_cls))
|
|
229
|
+
|
|
230
|
+
def __getattr__(self, name: str) -> Any:
|
|
231
|
+
raise AttributeError(f"binding {gname}.{name} is not declared for this run")
|
|
232
|
+
|
|
233
|
+
return _BindingProxy()
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class _ToolCallError(Exception):
|
|
237
|
+
"""Base of every program-visible binding failure raised in this run."""
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _make_binding_callable(run_id: str, global_name: str, name: str, member_prop: str, error_cls: Any):
|
|
241
|
+
async def _call(args: Any = None) -> Any:
|
|
242
|
+
seq = _next_call_seq()
|
|
243
|
+
future = asyncio.get_running_loop().create_future()
|
|
244
|
+
_STATE.pending_calls[(run_id, seq)] = (future, error_cls, member_prop)
|
|
245
|
+
_emit({
|
|
246
|
+
"type": "call",
|
|
247
|
+
"id": run_id,
|
|
248
|
+
"seq": seq,
|
|
249
|
+
"global": global_name,
|
|
250
|
+
"name": name,
|
|
251
|
+
"args": args,
|
|
252
|
+
})
|
|
253
|
+
try:
|
|
254
|
+
return await future
|
|
255
|
+
finally:
|
|
256
|
+
_STATE.pending_calls.pop((run_id, seq), None)
|
|
257
|
+
|
|
258
|
+
return _call
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _deliver_reply(reply: dict) -> None:
|
|
262
|
+
rid = reply.get("id")
|
|
263
|
+
seq = reply.get("seq")
|
|
264
|
+
if not isinstance(rid, str) or not isinstance(seq, int):
|
|
265
|
+
return
|
|
266
|
+
entry = _STATE.pending_calls.get((rid, seq))
|
|
267
|
+
if entry is None:
|
|
268
|
+
return
|
|
269
|
+
future, error_cls, member_prop = entry
|
|
270
|
+
if future.done():
|
|
271
|
+
return
|
|
272
|
+
if reply.get("ok") is True:
|
|
273
|
+
future.set_result(reply.get("value"))
|
|
274
|
+
return
|
|
275
|
+
message = reply.get("message")
|
|
276
|
+
message_text = str(message if isinstance(message, str) else "binding call failed")
|
|
277
|
+
err = error_cls(message_text)
|
|
278
|
+
err.message = message_text
|
|
279
|
+
tool_name = reply.get("name")
|
|
280
|
+
if isinstance(tool_name, str):
|
|
281
|
+
setattr(err, member_prop, tool_name)
|
|
282
|
+
future.set_exception(err)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
_MISSING = object()
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _materialize_namespaces(run_id: str, ns: dict, namespaces: list[dict]) -> list[tuple[str, Any]]:
|
|
289
|
+
"""Inject one run's binding globals + error classes; return restore ops.
|
|
290
|
+
|
|
291
|
+
Each op restores ns[name] to its pre-run value (or deletes it when the
|
|
292
|
+
name was not previously present). The runner calls them in the run's
|
|
293
|
+
finally, so a stale proxy can never leak into the next cell.
|
|
294
|
+
"""
|
|
295
|
+
restores: list[tuple[str, Any]] = []
|
|
296
|
+
for descriptor in namespaces:
|
|
297
|
+
gname = descriptor.get("global")
|
|
298
|
+
if not isinstance(gname, str) or not gname:
|
|
299
|
+
continue
|
|
300
|
+
names = descriptor.get("names")
|
|
301
|
+
if not isinstance(names, list):
|
|
302
|
+
names = []
|
|
303
|
+
member_prop = "toolName"
|
|
304
|
+
error_cls = _ToolCallError
|
|
305
|
+
ec = descriptor.get("errorClass")
|
|
306
|
+
if isinstance(ec, dict):
|
|
307
|
+
ecname = ec.get("name")
|
|
308
|
+
if isinstance(ecname, str) and ecname:
|
|
309
|
+
error_cls = type(ecname, (_ToolCallError,), {})
|
|
310
|
+
member_prop = ec.get("memberNameProperty")
|
|
311
|
+
if not isinstance(member_prop, str) or not member_prop:
|
|
312
|
+
member_prop = "toolName"
|
|
313
|
+
restores.append((ecname, ns.get(ecname, _MISSING)))
|
|
314
|
+
ns[ecname] = error_cls
|
|
315
|
+
restores.append((gname, ns.get(gname, _MISSING)))
|
|
316
|
+
ns[gname] = _make_tool_proxy_run(run_id, gname, [n for n in names if isinstance(n, str)], member_prop, error_cls)
|
|
317
|
+
return restores
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# ---------------------------------------------------------------------------
|
|
321
|
+
# Cell execution: persistent namespace + loop, top-level await, last expression
|
|
322
|
+
# ---------------------------------------------------------------------------
|
|
323
|
+
|
|
324
|
+
_TLA_FLAG = getattr(ast, "PyCF_ALLOW_TOP_LEVEL_AWAIT", 0x2000)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _compile_source(source: str) -> tuple[Any, Any | None, bool]:
|
|
328
|
+
"""Returns (body_code, expr_code, ok). A parse OR compile error yields (None, None, False)."""
|
|
329
|
+
try:
|
|
330
|
+
module = ast.parse(source, "<cell>", "exec")
|
|
331
|
+
except SyntaxError:
|
|
332
|
+
return None, None, False
|
|
333
|
+
if not module.body:
|
|
334
|
+
return None, None, True
|
|
335
|
+
try:
|
|
336
|
+
last = module.body[-1]
|
|
337
|
+
if isinstance(last, ast.Expr):
|
|
338
|
+
body_module = ast.Module(body=module.body[:-1], type_ignores=[])
|
|
339
|
+
expr_module = ast.Expression(body=last.value)
|
|
340
|
+
ast.copy_location(expr_module, last)
|
|
341
|
+
body_code = compile(body_module, "<cell>", "exec", flags=_TLA_FLAG)
|
|
342
|
+
expr_code = compile(expr_module, "<cell>", "eval", flags=_TLA_FLAG)
|
|
343
|
+
return body_code, expr_code, True
|
|
344
|
+
return compile(module, "<cell>", "exec", flags=_TLA_FLAG), None, True
|
|
345
|
+
except SyntaxError:
|
|
346
|
+
# ast.parse alone accepts some AST-valid modules that compile
|
|
347
|
+
# rejects (a top-level 'return', 'break'/'continue' outside a
|
|
348
|
+
# loop, 'yield' outside a function, ...). Route them to the normal
|
|
349
|
+
# per-cell error path so the run settles with "invalid Python
|
|
350
|
+
# source" instead of leaving the kernel waiting for a done frame
|
|
351
|
+
# that never arrives.
|
|
352
|
+
return None, None, False
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _lossless_json(value: Any):
|
|
356
|
+
"""Round-trip a completion value through strict JSON.
|
|
357
|
+
|
|
358
|
+
Returns (True, decoded) when lossless, (False, None) when the
|
|
359
|
+
value cannot be represented as lossless JSON. None IS valid JSON
|
|
360
|
+
(null), so the two outcomes must be distinguishable via the ok flag.
|
|
361
|
+
"""
|
|
362
|
+
try:
|
|
363
|
+
encoded = json.dumps(value, ensure_ascii=False)
|
|
364
|
+
except (TypeError, ValueError, OverflowError, RecursionError):
|
|
365
|
+
return False, None
|
|
366
|
+
try:
|
|
367
|
+
decoded = json.loads(encoded)
|
|
368
|
+
except (ValueError, RecursionError):
|
|
369
|
+
return False, None
|
|
370
|
+
return True, decoded
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def _finish_run(
|
|
374
|
+
run_id: str,
|
|
375
|
+
*,
|
|
376
|
+
status: str,
|
|
377
|
+
value: Any = None,
|
|
378
|
+
has_value: bool = False,
|
|
379
|
+
cancelled: bool = False,
|
|
380
|
+
invalid_output: bool = False,
|
|
381
|
+
message: str = "",
|
|
382
|
+
) -> None:
|
|
383
|
+
_STATE.execution_count += 1
|
|
384
|
+
frame: dict = {
|
|
385
|
+
"type": "done",
|
|
386
|
+
"id": run_id,
|
|
387
|
+
"status": status,
|
|
388
|
+
"executionCount": _STATE.execution_count,
|
|
389
|
+
"cancelled": cancelled,
|
|
390
|
+
}
|
|
391
|
+
if status == "ok" and has_value and not invalid_output:
|
|
392
|
+
frame["value"] = value
|
|
393
|
+
if invalid_output:
|
|
394
|
+
frame["invalidOutput"] = True
|
|
395
|
+
if message:
|
|
396
|
+
frame["message"] = message
|
|
397
|
+
_emit(frame)
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
async def _await_code(code_obj: Any, ns: dict) -> Any:
|
|
401
|
+
"""Evaluate a compiled code object in the live loop (awaiting coroutines).
|
|
402
|
+
|
|
403
|
+
A cell with top-level await compiles to a coroutine; we await it so
|
|
404
|
+
sibling requests and tool calls interleave. Statement/expression code runs
|
|
405
|
+
synchronously on the loop thread.
|
|
406
|
+
"""
|
|
407
|
+
coroutine_flag = getattr(__import__("inspect"), "CO_COROUTINE", 0)
|
|
408
|
+
if code_obj.co_flags & coroutine_flag:
|
|
409
|
+
return await eval(code_obj, ns)
|
|
410
|
+
return eval(code_obj, ns)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
async def _run_cell(code: str, run_id: str, namespaces: list[dict]) -> None:
|
|
414
|
+
sys.stdout = _StreamProxy("stdout")
|
|
415
|
+
sys.stderr = _StreamProxy("stderr") # noqa: F841 - kept for symmetry
|
|
416
|
+
ns = _STATE.user_ns
|
|
417
|
+
|
|
418
|
+
# Materialize this run's bindings (fresh proxies + error classes each run).
|
|
419
|
+
restores = _materialize_namespaces(run_id, ns, namespaces)
|
|
420
|
+
loop = asyncio.get_running_loop()
|
|
421
|
+
del loop # the loop is persistent state, not per-run
|
|
422
|
+
_STATE.capture_rid = run_id
|
|
423
|
+
token = _CURRENT_RID.set(run_id)
|
|
424
|
+
try:
|
|
425
|
+
body_code, expr_code, ok = _compile_source(code)
|
|
426
|
+
if not ok:
|
|
427
|
+
_finish_run(run_id, status="error", message="invalid Python source")
|
|
428
|
+
return
|
|
429
|
+
try:
|
|
430
|
+
if body_code is not None:
|
|
431
|
+
await _await_code(body_code, ns)
|
|
432
|
+
value = None
|
|
433
|
+
if expr_code is not None:
|
|
434
|
+
value = await _await_code(expr_code, ns)
|
|
435
|
+
except KeyboardInterrupt:
|
|
436
|
+
_flush_stream_proxies(run_id)
|
|
437
|
+
_STATE.cancel_requested = False
|
|
438
|
+
_finish_run(run_id, status="error", cancelled=True)
|
|
439
|
+
return
|
|
440
|
+
except asyncio.CancelledError:
|
|
441
|
+
_flush_stream_proxies(run_id)
|
|
442
|
+
_STATE.cancel_requested = False
|
|
443
|
+
_finish_run(run_id, status="error", cancelled=True)
|
|
444
|
+
return
|
|
445
|
+
except BaseException as exc: # noqa: BLE001 - model code may raise anything
|
|
446
|
+
_flush_stream_proxies(run_id)
|
|
447
|
+
tb = traceback.format_exception(type(exc), exc, exc.__traceback__)
|
|
448
|
+
_emit({
|
|
449
|
+
"type": "error",
|
|
450
|
+
"id": run_id,
|
|
451
|
+
"ename": type(exc).__name__,
|
|
452
|
+
"evalue": str(exc),
|
|
453
|
+
"traceback": tb,
|
|
454
|
+
})
|
|
455
|
+
_finish_run(run_id, status="error", message=str(exc))
|
|
456
|
+
return
|
|
457
|
+
_flush_stream_proxies(run_id)
|
|
458
|
+
if _STATE.cancel_requested:
|
|
459
|
+
_STATE.cancel_requested = False
|
|
460
|
+
_finish_run(run_id, status="error", cancelled=True)
|
|
461
|
+
return
|
|
462
|
+
if expr_code is not None:
|
|
463
|
+
serialized_ok, serialized = _lossless_json(value)
|
|
464
|
+
if not serialized_ok:
|
|
465
|
+
_finish_run(run_id, status="error", invalid_output=True)
|
|
466
|
+
return
|
|
467
|
+
_finish_run(run_id, status="ok", value=serialized, has_value=True)
|
|
468
|
+
else:
|
|
469
|
+
_finish_run(run_id, status="ok")
|
|
470
|
+
finally:
|
|
471
|
+
_CURRENT_RID.reset(token)
|
|
472
|
+
_STATE.capture_rid = None
|
|
473
|
+
for name, previous in restores:
|
|
474
|
+
if previous is _MISSING:
|
|
475
|
+
ns.pop(name, None)
|
|
476
|
+
else:
|
|
477
|
+
ns[name] = previous
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _install_idle_sigint() -> None:
|
|
481
|
+
"""SIGINT (host interrupt/escalation) interrupts the active run.
|
|
482
|
+
|
|
483
|
+
The handler raises KeyboardInterrupt synchronously: for sync exec code
|
|
484
|
+
(a busy loop, a time.sleep) that interrupts the blocking call between
|
|
485
|
+
bytecodes, and for await-ing coroutines it propagates out of the await
|
|
486
|
+
point. Either way _run_cell catches it and settles the run cancelled.
|
|
487
|
+
When SIGINT lands while no cell is active the exception unwinds the main
|
|
488
|
+
loop and the runner exits — a race the host covers by escalating to
|
|
489
|
+
termination, so accepting it keeps interrupt latency at zero.
|
|
490
|
+
"""
|
|
491
|
+
|
|
492
|
+
def handler_signum(_signum: int, _frame: Any) -> None:
|
|
493
|
+
_STATE.cancel_requested = True
|
|
494
|
+
raise KeyboardInterrupt
|
|
495
|
+
|
|
496
|
+
try:
|
|
497
|
+
signal.signal(signal.SIGINT, handler_signum)
|
|
498
|
+
except (ValueError, OSError): # not the main thread (tests)
|
|
499
|
+
pass
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
# ---------------------------------------------------------------------------
|
|
503
|
+
# Request loop
|
|
504
|
+
# ---------------------------------------------------------------------------
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _read_stdin(loop: asyncio.AbstractEventLoop, queue: asyncio.Queue, stdin: Any) -> None:
|
|
508
|
+
for raw in stdin:
|
|
509
|
+
line = raw.rstrip("\n")
|
|
510
|
+
if not line.strip():
|
|
511
|
+
continue
|
|
512
|
+
try:
|
|
513
|
+
req = json.loads(line)
|
|
514
|
+
except json.JSONDecodeError:
|
|
515
|
+
continue
|
|
516
|
+
loop.call_soon_threadsafe(queue.put_nowait, req)
|
|
517
|
+
loop.call_soon_threadsafe(queue.put_nowait, {"type": "exit"})
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
async def _handle_request_async(req: dict) -> None:
|
|
521
|
+
rid = req.get("id")
|
|
522
|
+
if not isinstance(rid, str) or not rid:
|
|
523
|
+
return
|
|
524
|
+
code = req.get("code")
|
|
525
|
+
if not isinstance(code, str):
|
|
526
|
+
_emit({"type": "error", "id": rid, "ename": "ProtocolError", "evalue": "code must be a string", "traceback": []})
|
|
527
|
+
_finish_run(rid, status="error", message="code must be a string")
|
|
528
|
+
return
|
|
529
|
+
cwd = req.get("cwd")
|
|
530
|
+
if isinstance(cwd, str):
|
|
531
|
+
try:
|
|
532
|
+
os.chdir(cwd)
|
|
533
|
+
if cwd not in sys.path:
|
|
534
|
+
sys.path.insert(0, cwd)
|
|
535
|
+
except OSError:
|
|
536
|
+
pass
|
|
537
|
+
env = req.get("env")
|
|
538
|
+
if isinstance(env, dict):
|
|
539
|
+
for key, value in env.items():
|
|
540
|
+
if isinstance(key, str):
|
|
541
|
+
if isinstance(value, str):
|
|
542
|
+
os.environ[key] = value
|
|
543
|
+
else:
|
|
544
|
+
os.environ.pop(key, None)
|
|
545
|
+
namespaces = req.get("namespaces")
|
|
546
|
+
if not isinstance(namespaces, list):
|
|
547
|
+
namespaces = []
|
|
548
|
+
_STATE.cancel_requested = False
|
|
549
|
+
try:
|
|
550
|
+
await _run_cell(code, rid, namespaces)
|
|
551
|
+
except asyncio.CancelledError:
|
|
552
|
+
_STATE.cancel_requested = False
|
|
553
|
+
_finish_run(rid, status="error", cancelled=True)
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
async def _main_async() -> None:
|
|
557
|
+
sys.stdout = _StreamProxy("stdout")
|
|
558
|
+
sys.stderr = _StreamProxy("stderr")
|
|
559
|
+
_install_idle_sigint()
|
|
560
|
+
_start_capture_drain()
|
|
561
|
+
|
|
562
|
+
stdin = sys.__stdin__
|
|
563
|
+
if stdin is None:
|
|
564
|
+
return
|
|
565
|
+
loop = asyncio.get_running_loop()
|
|
566
|
+
_STATE.loop = loop
|
|
567
|
+
queue: asyncio.Queue = asyncio.Queue()
|
|
568
|
+
reader = threading.Thread(target=_read_stdin, args=(loop, queue, stdin), name="dsh-stdin-reader", daemon=True)
|
|
569
|
+
reader.start()
|
|
570
|
+
|
|
571
|
+
_emit({"type": "ready", "pid": os.getpid()})
|
|
572
|
+
|
|
573
|
+
tasks: set[asyncio.Task] = set()
|
|
574
|
+
|
|
575
|
+
def _task_done(task: asyncio.Task) -> None:
|
|
576
|
+
tasks.discard(task)
|
|
577
|
+
try:
|
|
578
|
+
exc = task.exception()
|
|
579
|
+
except asyncio.CancelledError:
|
|
580
|
+
return
|
|
581
|
+
if exc is not None and not isinstance(exc, (KeyboardInterrupt, asyncio.CancelledError)):
|
|
582
|
+
_emit({"type": "error", "id": "", "ename": "ProtocolError", "evalue": str(exc), "traceback": []})
|
|
583
|
+
|
|
584
|
+
while True:
|
|
585
|
+
req = await queue.get()
|
|
586
|
+
rtype = req.get("type")
|
|
587
|
+
if rtype == "exit":
|
|
588
|
+
break
|
|
589
|
+
if rtype == "reply":
|
|
590
|
+
_deliver_reply(req)
|
|
591
|
+
continue
|
|
592
|
+
task = asyncio.create_task(_handle_request_async(req))
|
|
593
|
+
tasks.add(task)
|
|
594
|
+
task.add_done_callback(_task_done)
|
|
595
|
+
|
|
596
|
+
# Every request queued before an exit request is already a task; run them
|
|
597
|
+
# all to completion before shutting down (cancelling here would kill execs
|
|
598
|
+
# that never got a turn -- a race when stdin drains faster than the loop
|
|
599
|
+
# runs).
|
|
600
|
+
if tasks:
|
|
601
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
602
|
+
for task in tasks:
|
|
603
|
+
task.cancel()
|
|
604
|
+
if tasks:
|
|
605
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
def main() -> None:
|
|
609
|
+
asyncio.run(_main_async())
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
if __name__ == "__main__":
|
|
613
|
+
main()
|
|
614
|
+
`;
|
|
615
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/python/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA6kBtC,CAAA"}
|
package/package.json
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hy-sde-org/dsh-code-runtime-kernels",
|
|
3
|
+
"version": "0.1.0-rc.8",
|
|
4
|
+
"description": "The run_kernel_code tool for DeepSeek Harness: persistent Python and JavaScript kernels (a long-lived python3/node subprocess per session, sharing a core host driver) — a standalone plugin with no upstream harness changes required.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./invariant": {
|
|
14
|
+
"types": "./dist/core/invariant.d.ts",
|
|
15
|
+
"default": "./dist/core/invariant.js"
|
|
16
|
+
},
|
|
17
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist",
|
|
22
|
+
"cordis.patch.yml",
|
|
23
|
+
"README.md",
|
|
24
|
+
"README.zh.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/hy-sde/dsh-code-runtime-kernels.git"
|
|
36
|
+
},
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"dsh": {
|
|
39
|
+
"bundle": {
|
|
40
|
+
"patch": "./cordis.patch.yml"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@deepseek-ai/schemastery": "3.18.1"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
|
+
"@deepseek-ai/dsh-code-runtime": "^0.1.0-rc.8",
|
|
49
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
|
|
50
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.8",
|
|
51
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
|
|
52
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.8"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
+
"@deepseek-ai/dsh-code-runtime": "^0.1.0-rc.8",
|
|
57
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.8",
|
|
58
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.8",
|
|
59
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.8",
|
|
60
|
+
"@deepseek-ai/dsh-timeout": "^0.1.0-rc.8",
|
|
61
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.8",
|
|
62
|
+
"@deepseek-ai/schemastery": "3.18.1",
|
|
63
|
+
"@types/node": "^24.13.3",
|
|
64
|
+
"typescript": "^5.9.3",
|
|
65
|
+
"vitest": "^3.2.7",
|
|
66
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.8"
|
|
67
|
+
},
|
|
68
|
+
"keywords": [
|
|
69
|
+
"deepseek",
|
|
70
|
+
"agent",
|
|
71
|
+
"harness",
|
|
72
|
+
"dsh-plugin",
|
|
73
|
+
"code-runtime",
|
|
74
|
+
"persistent",
|
|
75
|
+
"kernel",
|
|
76
|
+
"python",
|
|
77
|
+
"javascript"
|
|
78
|
+
],
|
|
79
|
+
"scripts": {
|
|
80
|
+
"clean": "rm -rf dist",
|
|
81
|
+
"build": "tsc -p tsconfig.build.json",
|
|
82
|
+
"check": "tsc -p tsconfig.json --noEmit",
|
|
83
|
+
"test": "vitest run"
|
|
84
|
+
}
|
|
85
|
+
}
|