@code-yeongyu/senpi-codemode 2026.10.6 → 2026.10.7
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/CHANGELOG.md +19 -0
- package/README.md +4 -0
- package/package.json +4 -4
- package/src/bridge/reserved.ts +10 -0
- package/src/bridges/eval-virtual-schemas.ts +79 -0
- package/src/bridges/handle-bridge.ts +72 -0
- package/src/bridges/reserved-dispatch.ts +20 -2
- package/src/bridges/schema-bridge.ts +4 -0
- package/src/completion/handle.ts +69 -0
- package/src/completion/tool-bridge.ts +31 -12
- package/src/extension/bridge-dispatch.ts +41 -0
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/host-facts.ts +18 -0
- package/src/extension/removed-tool-hints.ts +15 -0
- package/src/extension/runtime-factory.ts +3 -0
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +17 -4
- package/src/extension/start-recovery.ts +111 -0
- package/src/handles/completion-handles.ts +202 -0
- package/src/handles/handle-args.ts +103 -0
- package/src/handles/handle-registry.ts +164 -0
- package/src/handles/wait.ts +214 -0
- package/src/handles/watch-queue.ts +102 -0
- package/src/index.ts +99 -94
- package/src/kernels/jl/handles.jl +124 -0
- package/src/kernels/jl/prelude.jl +9 -0
- package/src/kernels/jl/runner.jl +6 -1
- package/src/kernels/js/handles.js +93 -0
- package/src/kernels/js/kernel-tools-define.d.ts +6 -0
- package/src/kernels/js/kernel-tools-define.js +3 -1
- package/src/kernels/js/kernel-tools-registry.d.ts +2 -0
- package/src/kernels/js/kernel-tools-registry.js +18 -9
- package/src/kernels/js/kernel-tools-schema.js +6 -2
- package/src/kernels/js/worker-runtime.js +10 -1
- package/src/kernels/py/host-cell.ts +42 -0
- package/src/kernels/py/kernel-contract.ts +2 -0
- package/src/kernels/py/kernel.ts +48 -4
- package/src/kernels/py/prelude.py +140 -4
- package/src/kernels/rb/handles.rb +97 -0
- package/src/kernels/rb/paths.rb +37 -0
- package/src/kernels/rb/prelude.rb +15 -38
- package/src/kernels/rb/runner.rb +7 -7
- package/src/tool/cell-handler.ts +21 -18
- package/src/tool/cell-reserved-dispatch.ts +51 -0
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/run-eval-cell.ts +2 -0
- package/src/tool/types.ts +15 -0
|
@@ -18,6 +18,7 @@ import inspect
|
|
|
18
18
|
import io
|
|
19
19
|
import json
|
|
20
20
|
import locale
|
|
21
|
+
import math
|
|
21
22
|
import os
|
|
22
23
|
import re
|
|
23
24
|
import signal
|
|
@@ -48,6 +49,11 @@ EMIT_LOCK = Lock()
|
|
|
48
49
|
RESERVED_AGENT_TOOL = "__agent__"
|
|
49
50
|
RESERVED_OUTPUT_TOOL = "__output__"
|
|
50
51
|
RESERVED_SCHEMA_TOOL = "__schema__"
|
|
52
|
+
RESERVED_WAIT_TOOL = "__wait__"
|
|
53
|
+
RESERVED_HANDLE_STATUS_TOOL = "__handle_status__"
|
|
54
|
+
RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__"
|
|
55
|
+
RESERVED_HANDLE_SEND_TOOL = "__handle_send__"
|
|
56
|
+
RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__"
|
|
51
57
|
TIMEOUT_PAUSE_OP = "timeout-pause"
|
|
52
58
|
TIMEOUT_RESUME_OP = "timeout-resume"
|
|
53
59
|
|
|
@@ -357,9 +363,12 @@ def write(path: str | Path, content: str) -> Path:
|
|
|
357
363
|
# The bridge is a loopback call: urllib's default opener would route it through a configured proxy
|
|
358
364
|
# (the environment everywhere, the registry on Windows), which a 127.0.0.1 request must never use.
|
|
359
365
|
_BRIDGE_OPENER = urllib.request.build_opener(urllib.request.ProxyHandler({}))
|
|
366
|
+
# Ordinary bridge calls keep this socket timeout; only `__wait__` passes its own bound (see `_bridge_wait_post`).
|
|
367
|
+
_BRIDGE_SOCKET_TIMEOUT_SECONDS = 60
|
|
368
|
+
_WAIT_SOCKET_GRACE_SECONDS = 30
|
|
360
369
|
|
|
361
370
|
|
|
362
|
-
def bridge_post(path: str, payload: dict[str, Any]) -> Any:
|
|
371
|
+
def bridge_post(path: str, payload: dict[str, Any], *, socket_timeout: float | None = _BRIDGE_SOCKET_TIMEOUT_SECONDS) -> Any:
|
|
363
372
|
port = CONNECTION.get("port")
|
|
364
373
|
token = CONNECTION.get("token")
|
|
365
374
|
if not isinstance(port, int) or not isinstance(token, str):
|
|
@@ -374,7 +383,7 @@ def bridge_post(path: str, payload: dict[str, Any]) -> Any:
|
|
|
374
383
|
emit_status(TIMEOUT_PAUSE_OP, force=True)
|
|
375
384
|
try:
|
|
376
385
|
try:
|
|
377
|
-
with _BRIDGE_OPENER.open(request, timeout=
|
|
386
|
+
with _BRIDGE_OPENER.open(request, timeout=socket_timeout) as response:
|
|
378
387
|
response_data = response.read()
|
|
379
388
|
except urllib.error.HTTPError as exc:
|
|
380
389
|
response_data = exc.read()
|
|
@@ -500,6 +509,8 @@ def completion(
|
|
|
500
509
|
if schema is not None:
|
|
501
510
|
options["schema"] = schema
|
|
502
511
|
response = bridge_post("/completion", {"prompt": prompt, "opts": options})
|
|
512
|
+
if options.get("handle") is True:
|
|
513
|
+
return handle(response)
|
|
503
514
|
if not isinstance(response, dict):
|
|
504
515
|
return response
|
|
505
516
|
if "value" in response:
|
|
@@ -616,6 +627,129 @@ def agent(
|
|
|
616
627
|
return node
|
|
617
628
|
|
|
618
629
|
|
|
630
|
+
_HANDLE_KINDS = ("agent", "completion", "workpool")
|
|
631
|
+
_WAIT_MODES = ("all", "any", "settled")
|
|
632
|
+
_HANDLE_USAGE = (
|
|
633
|
+
"handle() expects an agent(..., handle=True) record, a workpool, a completion handle, "
|
|
634
|
+
"or a saved {kind, id, run_epoch} reference"
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _handle_ref(value: Any) -> dict[str, Any]:
|
|
639
|
+
if isinstance(value, HandleView):
|
|
640
|
+
return dict(value.ref)
|
|
641
|
+
if isinstance(value, Workpool):
|
|
642
|
+
return {"kind": "workpool", "id": value.pool_id, "run_epoch": 0}
|
|
643
|
+
if not isinstance(value, dict):
|
|
644
|
+
raise PreludeTypeError(_HANDLE_USAGE)
|
|
645
|
+
if isinstance(value.get("pool_id"), str):
|
|
646
|
+
return {"kind": "workpool", "id": value["pool_id"], "run_epoch": 0}
|
|
647
|
+
kind = value.get("kind")
|
|
648
|
+
if kind not in _HANDLE_KINDS:
|
|
649
|
+
handle_uri = value.get("handle")
|
|
650
|
+
scheme = handle_uri.split("://", 1)[0] if isinstance(handle_uri, str) else None
|
|
651
|
+
kind = scheme if scheme in _HANDLE_KINDS else None
|
|
652
|
+
identity = value.get("id", value.get("task_id"))
|
|
653
|
+
if kind is None or not isinstance(identity, str) or not identity:
|
|
654
|
+
raise PreludeTypeError(_HANDLE_USAGE)
|
|
655
|
+
run_epoch = value.get("run_epoch", 0 if kind == "workpool" else None)
|
|
656
|
+
if isinstance(run_epoch, bool) or not isinstance(run_epoch, int) or run_epoch < 0:
|
|
657
|
+
raise PreludeTypeError(f"{_HANDLE_USAGE}; run_epoch must be a non-negative integer")
|
|
658
|
+
return {"kind": kind, "id": identity, "run_epoch": run_epoch}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def _handle_call(tool_name: str, args: dict[str, Any]) -> Any:
|
|
662
|
+
return bridge_post("/call", {"callId": f"py-{uuid.uuid4()}", "toolName": tool_name, "args": args})
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
def _bridge_wait_post(args: dict[str, Any], timeout: float | None) -> Any:
|
|
666
|
+
# A long-lived request: no 60 s socket cap. An explicit timeout bounds it (plus grace for the host's
|
|
667
|
+
# reply); without one only the cell's own end does: SIGINT from the host (cancel, the cell's hard limit,
|
|
668
|
+
# which the eval timeout can raise) interrupts the blocking read, which closes the socket and so the
|
|
669
|
+
# host-side subscription.
|
|
670
|
+
socket_timeout = None if timeout is None else float(timeout) + _WAIT_SOCKET_GRACE_SECONDS
|
|
671
|
+
return bridge_post(
|
|
672
|
+
"/call",
|
|
673
|
+
{"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_WAIT_TOOL, "args": args},
|
|
674
|
+
socket_timeout=socket_timeout,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def wait(handles: Any, *, timeout: float | None = None, mode: str = "all") -> Any:
|
|
679
|
+
"""Block until the handles settle; never cancels work. See tool_schema("eval:wait")."""
|
|
680
|
+
if handles is None:
|
|
681
|
+
items: list[Any] = []
|
|
682
|
+
elif isinstance(handles, (list, tuple)):
|
|
683
|
+
items = list(handles)
|
|
684
|
+
else:
|
|
685
|
+
items = [handles]
|
|
686
|
+
if timeout is not None and (
|
|
687
|
+
isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or timeout < 0
|
|
688
|
+
):
|
|
689
|
+
raise PreludeValueError("wait() timeout must be a finite number of seconds >= 0")
|
|
690
|
+
if mode not in _WAIT_MODES:
|
|
691
|
+
raise PreludeValueError("wait() mode must be 'all', 'any' or 'settled'")
|
|
692
|
+
args: dict[str, Any] = {"refs": [_handle_ref(item) for item in items], "mode": mode}
|
|
693
|
+
if timeout is not None:
|
|
694
|
+
args["timeout"] = timeout
|
|
695
|
+
return _bridge_wait_post(args, timeout)
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
class HandleControl:
|
|
699
|
+
"""Epoch-fenced control over one handle; every call goes through the host capability."""
|
|
700
|
+
|
|
701
|
+
__slots__ = ("_ref",)
|
|
702
|
+
|
|
703
|
+
def __init__(self, ref: dict[str, Any]) -> None:
|
|
704
|
+
self._ref = dict(ref)
|
|
705
|
+
|
|
706
|
+
def __repr__(self) -> str:
|
|
707
|
+
return f"<handle.control {self._ref['kind']}://{self._ref['id']}@{self._ref['run_epoch']}>"
|
|
708
|
+
|
|
709
|
+
def status(self) -> Any:
|
|
710
|
+
return _handle_call(RESERVED_HANDLE_STATUS_TOOL, {"ref": self._ref})
|
|
711
|
+
|
|
712
|
+
def output(self, format: str = "raw", offset: int | None = None, limit: int | None = None) -> Any:
|
|
713
|
+
args: dict[str, Any] = {"ref": self._ref, "format": format}
|
|
714
|
+
if offset is not None:
|
|
715
|
+
args["offset"] = offset
|
|
716
|
+
if limit is not None:
|
|
717
|
+
args["limit"] = limit
|
|
718
|
+
return _handle_call(RESERVED_HANDLE_OUTPUT_TOOL, args)
|
|
719
|
+
|
|
720
|
+
def send(self, message: str) -> Any:
|
|
721
|
+
return _handle_call(RESERVED_HANDLE_SEND_TOOL, {"ref": self._ref, "message": str(message)})
|
|
722
|
+
|
|
723
|
+
def cancel(self) -> Any:
|
|
724
|
+
return _handle_call(RESERVED_HANDLE_CANCEL_TOOL, {"ref": self._ref})
|
|
725
|
+
|
|
726
|
+
def wait(self, timeout: float | None = None) -> Any:
|
|
727
|
+
return wait([self._ref], timeout=timeout, mode="all")[0]
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
class HandleView(dict):
|
|
731
|
+
"""The legacy record's fields as a dict, plus `control` and `ref` as attributes (never keys)."""
|
|
732
|
+
|
|
733
|
+
__slots__ = ("control", "ref")
|
|
734
|
+
|
|
735
|
+
def __init__(self, fields: dict[str, Any], ref: dict[str, Any]) -> None:
|
|
736
|
+
super().__init__(fields)
|
|
737
|
+
self.ref = dict(ref)
|
|
738
|
+
self.control = HandleControl(ref)
|
|
739
|
+
|
|
740
|
+
|
|
741
|
+
def handle(value: Any) -> HandleView:
|
|
742
|
+
"""Rich view of an agent record, workpool, completion handle or saved reference; see tool_schema("eval:helpers")."""
|
|
743
|
+
ref = _handle_ref(value)
|
|
744
|
+
fields: dict[str, Any] = {}
|
|
745
|
+
if isinstance(value, dict):
|
|
746
|
+
fields.update({key: item for key, item in value.items() if not callable(item)})
|
|
747
|
+
fields.setdefault("id", ref["id"])
|
|
748
|
+
fields.setdefault("run_epoch", ref["run_epoch"])
|
|
749
|
+
fields.setdefault("handle", f"{ref['kind']}://{ref['id']}")
|
|
750
|
+
return HandleView(fields, ref)
|
|
751
|
+
|
|
752
|
+
|
|
619
753
|
def _pool_map(items: Iterable[Any], function: Callable[[Any], Any]) -> list[Any]:
|
|
620
754
|
values = list(items)
|
|
621
755
|
if not values:
|
|
@@ -997,6 +1131,8 @@ USER_NS.update(
|
|
|
997
1131
|
"workpool": workpool,
|
|
998
1132
|
"output": output,
|
|
999
1133
|
"tool_schema": tool_schema,
|
|
1134
|
+
"wait": wait,
|
|
1135
|
+
"handle": handle,
|
|
1000
1136
|
"__senpi_magic": _magic,
|
|
1001
1137
|
"__senpi_magic_cell": _magic_cell,
|
|
1002
1138
|
"__senpi_shell": _shell,
|
|
@@ -1405,7 +1541,7 @@ def elapsed(start: float) -> int:
|
|
|
1405
1541
|
return max(0, int((time.monotonic() - start) * 1000))
|
|
1406
1542
|
|
|
1407
1543
|
|
|
1408
|
-
def
|
|
1544
|
+
def _handle_message(message: dict[str, Any]) -> bool:
|
|
1409
1545
|
global CONNECTION
|
|
1410
1546
|
message_type = message.get("type")
|
|
1411
1547
|
if message_type == "init":
|
|
@@ -1483,7 +1619,7 @@ def main() -> None:
|
|
|
1483
1619
|
host_closed = False
|
|
1484
1620
|
for raw in sys.stdin:
|
|
1485
1621
|
try:
|
|
1486
|
-
if not
|
|
1622
|
+
if not _handle_message(json.loads(raw)):
|
|
1487
1623
|
host_closed = True
|
|
1488
1624
|
break
|
|
1489
1625
|
except BaseException as exc: # noqa: BROAD_EXCEPT_OK — process boundary serializes malformed input and interrupts.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# In-cell wait()/handle() over the host's handle capability. Mirrors src/bridge/reserved.ts.
|
|
2
|
+
SENPI_RESERVED_WAIT_TOOL = "__wait__"
|
|
3
|
+
SENPI_RESERVED_HANDLE_STATUS_TOOL = "__handle_status__"
|
|
4
|
+
SENPI_RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__"
|
|
5
|
+
SENPI_RESERVED_HANDLE_SEND_TOOL = "__handle_send__"
|
|
6
|
+
SENPI_RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__"
|
|
7
|
+
SENPI_HANDLE_KINDS = ["agent", "completion", "workpool"].freeze
|
|
8
|
+
SENPI_WAIT_MODES = ["all", "any", "settled"].freeze
|
|
9
|
+
SENPI_WAIT_SOCKET_GRACE_SECONDS = 30
|
|
10
|
+
SENPI_HANDLE_USAGE = "handle() expects an agent(..., handle: true) record, a workpool, a completion handle, or a saved {kind, id, run_epoch} reference"
|
|
11
|
+
|
|
12
|
+
def __senpi_handle_ref(value)
|
|
13
|
+
return value.ref.dup if value.respond_to?(:ref) && value.respond_to?(:control)
|
|
14
|
+
return { "kind" => "workpool", "id" => value.pool_id, "run_epoch" => 0 } if value.is_a?(SenpiWorkpool)
|
|
15
|
+
raise TypeError, SENPI_HANDLE_USAGE unless value.is_a?(Hash)
|
|
16
|
+
record = value.transform_keys(&:to_s)
|
|
17
|
+
return { "kind" => "workpool", "id" => record["pool_id"], "run_epoch" => 0 } if record["pool_id"].is_a?(String)
|
|
18
|
+
kind = record["kind"]
|
|
19
|
+
unless SENPI_HANDLE_KINDS.include?(kind)
|
|
20
|
+
scheme = record["handle"].is_a?(String) ? record["handle"].split("://", 2).first : nil
|
|
21
|
+
kind = SENPI_HANDLE_KINDS.include?(scheme) ? scheme : nil
|
|
22
|
+
end
|
|
23
|
+
identity = record.fetch("id", record["task_id"])
|
|
24
|
+
raise TypeError, SENPI_HANDLE_USAGE if kind.nil? || !identity.is_a?(String) || identity.empty?
|
|
25
|
+
run_epoch = record.fetch("run_epoch", kind == "workpool" ? 0 : nil)
|
|
26
|
+
raise TypeError, "#{SENPI_HANDLE_USAGE}; run_epoch must be a non-negative integer" unless run_epoch.is_a?(Integer) && run_epoch >= 0
|
|
27
|
+
{ "kind" => kind, "id" => identity, "run_epoch" => run_epoch }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A long-lived request: ordinary calls keep their 60 s read timeout; this one is bounded by the explicit
|
|
31
|
+
# timeout (plus grace for the host's reply) or else only by the cell's own end: SIGINT from the host (cancel,
|
|
32
|
+
# the cell's hard limit) interrupts the read, which closes the socket and the host-side subscription.
|
|
33
|
+
def __senpi_wait_post(args, timeout)
|
|
34
|
+
read_timeout = timeout.nil? ? nil : timeout.to_f + SENPI_WAIT_SOCKET_GRACE_SECONDS
|
|
35
|
+
payload = { "callId" => "rb-#{Process.pid}-#{rand(1_000_000)}", "toolName" => SENPI_RESERVED_WAIT_TOOL, "args" => args }
|
|
36
|
+
__senpi_bridge_request("/call", payload, read_timeout: read_timeout)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def __senpi_wait(handles, timeout: nil, mode: "all")
|
|
40
|
+
items = handles.nil? ? [] : handles.is_a?(Array) ? handles : [handles]
|
|
41
|
+
unless timeout.nil? || (timeout.is_a?(Numeric) && timeout.to_f.finite? && timeout >= 0)
|
|
42
|
+
raise ArgumentError, "wait() timeout must be a finite number of seconds >= 0"
|
|
43
|
+
end
|
|
44
|
+
raise ArgumentError, "wait() mode must be 'all', 'any' or 'settled'" unless SENPI_WAIT_MODES.include?(mode.to_s)
|
|
45
|
+
args = { "refs" => items.map { |item| __senpi_handle_ref(item) }, "mode" => mode.to_s }
|
|
46
|
+
args["timeout"] = timeout unless timeout.nil?
|
|
47
|
+
__senpi_wait_post(args, timeout)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class SenpiHandleControl
|
|
51
|
+
def initialize(ref)
|
|
52
|
+
@ref = ref.dup.freeze
|
|
53
|
+
freeze
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def status
|
|
57
|
+
__senpi_call_tool(SENPI_RESERVED_HANDLE_STATUS_TOOL, { "ref" => @ref })
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def output(format: "raw", offset: nil, limit: nil)
|
|
61
|
+
args = { "ref" => @ref, "format" => format }
|
|
62
|
+
args["offset"] = offset unless offset.nil?
|
|
63
|
+
args["limit"] = limit unless limit.nil?
|
|
64
|
+
__senpi_call_tool(SENPI_RESERVED_HANDLE_OUTPUT_TOOL, args)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def send_message(message)
|
|
68
|
+
__senpi_call_tool(SENPI_RESERVED_HANDLE_SEND_TOOL, { "ref" => @ref, "message" => message.to_s })
|
|
69
|
+
end
|
|
70
|
+
# Deliberately shadows Object#send so control.send(msg) matches the other languages; use __send__ for reflection.
|
|
71
|
+
alias send send_message
|
|
72
|
+
|
|
73
|
+
def cancel
|
|
74
|
+
__senpi_call_tool(SENPI_RESERVED_HANDLE_CANCEL_TOOL, { "ref" => @ref })
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def wait(timeout: nil)
|
|
78
|
+
__senpi_wait([@ref], timeout: timeout, mode: "all")[0]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def inspect
|
|
82
|
+
"#<handle.control #{@ref["kind"]}://#{@ref["id"]}@#{@ref["run_epoch"]}>"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# The legacy record's fields as a Hash copy; `control` and `ref` are singleton methods, never keys.
|
|
87
|
+
def __senpi_handle_view(value)
|
|
88
|
+
ref = __senpi_handle_ref(value)
|
|
89
|
+
view = value.is_a?(Hash) ? value.transform_keys(&:to_s).reject { |_key, item| item.respond_to?(:call) } : {}
|
|
90
|
+
view["id"] ||= ref["id"]
|
|
91
|
+
view["run_epoch"] ||= ref["run_epoch"]
|
|
92
|
+
view["handle"] ||= "#{ref["kind"]}://#{ref["id"]}"
|
|
93
|
+
control = SenpiHandleControl.new(ref)
|
|
94
|
+
view.define_singleton_method(:control) { control }
|
|
95
|
+
view.define_singleton_method(:ref) { ref.dup }
|
|
96
|
+
view
|
|
97
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Path resolution (local:// roots) and display payload shaping for the Ruby prelude helpers.
|
|
2
|
+
def __senpi_resolve_path(value)
|
|
3
|
+
raw = value.to_s
|
|
4
|
+
match = SENPI_INTERNAL_URL.match(raw)
|
|
5
|
+
return File.expand_path(raw) unless match
|
|
6
|
+
|
|
7
|
+
scheme = match[1].downcase
|
|
8
|
+
roots = $__senpi_connection.is_a?(Hash) ? $__senpi_connection["localRoots"] : nil
|
|
9
|
+
root = roots[scheme] if roots.is_a?(Hash)
|
|
10
|
+
raise "Protocol paths are not supported by this helper: #{raw}" unless root.is_a?(String) && !root.empty?
|
|
11
|
+
|
|
12
|
+
relative = URI::DEFAULT_PARSER.unescape(match[2].tr("\\", "/"))
|
|
13
|
+
root_path = File.expand_path(root)
|
|
14
|
+
return root_path if relative.empty?
|
|
15
|
+
if relative.start_with?("/") || relative.split("/").include?("..")
|
|
16
|
+
raise "Unsafe #{scheme}:// path (absolute or traversal): #{raw}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
resolved = File.expand_path(relative, root_path)
|
|
20
|
+
unless resolved == root_path || resolved.start_with?(root_path + File::SEPARATOR)
|
|
21
|
+
raise "#{scheme}:// path escapes its root: #{raw}"
|
|
22
|
+
end
|
|
23
|
+
resolved
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def __senpi_display_payload(value)
|
|
27
|
+
if value.is_a?(Hash)
|
|
28
|
+
kind = value["type"] || value[:type]
|
|
29
|
+
text_value = value["text"] || value[:text]
|
|
30
|
+
return ["text/markdown", text_value.to_s] if kind == "markdown" && !text_value.nil?
|
|
31
|
+
return ["image/png", value["data"].to_s] if kind == "image" && value["mimeType"] == "image/png"
|
|
32
|
+
return ["image/jpeg", value["data"].to_s] if kind == "image" && value["mimeType"] == "image/jpeg"
|
|
33
|
+
return ["application/json", JSON.generate(value)]
|
|
34
|
+
end
|
|
35
|
+
return ["application/json", JSON.generate(value)] if value.is_a?(Array)
|
|
36
|
+
["text/plain", value.to_s]
|
|
37
|
+
end
|
|
@@ -19,42 +19,7 @@ def __senpi_emit_status(op, fields = {}, force: false)
|
|
|
19
19
|
__senpi_emit({ "type" => "status", "event" => { "op" => op }.merge(fields.transform_keys(&:to_s)) })
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
raw = value.to_s
|
|
24
|
-
match = SENPI_INTERNAL_URL.match(raw)
|
|
25
|
-
return File.expand_path(raw) unless match
|
|
26
|
-
|
|
27
|
-
scheme = match[1].downcase
|
|
28
|
-
roots = $__senpi_connection.is_a?(Hash) ? $__senpi_connection["localRoots"] : nil
|
|
29
|
-
root = roots[scheme] if roots.is_a?(Hash)
|
|
30
|
-
raise "Protocol paths are not supported by this helper: #{raw}" unless root.is_a?(String) && !root.empty?
|
|
31
|
-
|
|
32
|
-
relative = URI::DEFAULT_PARSER.unescape(match[2].tr("\\", "/"))
|
|
33
|
-
root_path = File.expand_path(root)
|
|
34
|
-
return root_path if relative.empty?
|
|
35
|
-
if relative.start_with?("/") || relative.split("/").include?("..")
|
|
36
|
-
raise "Unsafe #{scheme}:// path (absolute or traversal): #{raw}"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
resolved = File.expand_path(relative, root_path)
|
|
40
|
-
unless resolved == root_path || resolved.start_with?(root_path + File::SEPARATOR)
|
|
41
|
-
raise "#{scheme}:// path escapes its root: #{raw}"
|
|
42
|
-
end
|
|
43
|
-
resolved
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def __senpi_display_payload(value)
|
|
47
|
-
if value.is_a?(Hash)
|
|
48
|
-
kind = value["type"] || value[:type]
|
|
49
|
-
text_value = value["text"] || value[:text]
|
|
50
|
-
return ["text/markdown", text_value.to_s] if kind == "markdown" && !text_value.nil?
|
|
51
|
-
return ["image/png", value["data"].to_s] if kind == "image" && value["mimeType"] == "image/png"
|
|
52
|
-
return ["image/jpeg", value["data"].to_s] if kind == "image" && value["mimeType"] == "image/jpeg"
|
|
53
|
-
return ["application/json", JSON.generate(value)]
|
|
54
|
-
end
|
|
55
|
-
return ["application/json", JSON.generate(value)] if value.is_a?(Array)
|
|
56
|
-
["text/plain", value.to_s]
|
|
57
|
-
end
|
|
22
|
+
require_relative "paths"
|
|
58
23
|
|
|
59
24
|
def display(value)
|
|
60
25
|
mime_type, payload = __senpi_display_payload(value)
|
|
@@ -118,7 +83,7 @@ def env(key = nil, value = nil)
|
|
|
118
83
|
resolved
|
|
119
84
|
end
|
|
120
85
|
|
|
121
|
-
def __senpi_bridge_request(path, payload)
|
|
86
|
+
def __senpi_bridge_request(path, payload, read_timeout: 60)
|
|
122
87
|
connection = $__senpi_connection
|
|
123
88
|
raise "Ruby tool bridge is not initialized" unless connection.is_a?(Hash)
|
|
124
89
|
port = connection["port"]
|
|
@@ -132,7 +97,7 @@ def __senpi_bridge_request(path, payload)
|
|
|
132
97
|
request.body = JSON.generate(payload)
|
|
133
98
|
__senpi_emit_status("timeout-pause", force: true)
|
|
134
99
|
begin
|
|
135
|
-
response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 10, read_timeout:
|
|
100
|
+
response = Net::HTTP.start(uri.hostname, uri.port, open_timeout: 10, read_timeout: read_timeout) { |http| http.request(request) }
|
|
136
101
|
ensure
|
|
137
102
|
__senpi_emit_status("timeout-resume", force: true)
|
|
138
103
|
end
|
|
@@ -187,12 +152,24 @@ def tool
|
|
|
187
152
|
end
|
|
188
153
|
|
|
189
154
|
require_relative "workpool"
|
|
155
|
+
require_relative "handles"
|
|
156
|
+
|
|
157
|
+
# Block until the handles settle; never cancels work. See tool_schema("eval:wait").
|
|
158
|
+
def wait(handles, timeout: nil, mode: "all")
|
|
159
|
+
__senpi_wait(handles, timeout: timeout, mode: mode)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Rich view of an agent record, workpool, completion handle or saved reference; see tool_schema("eval:helpers").
|
|
163
|
+
def handle(value)
|
|
164
|
+
__senpi_handle_view(value)
|
|
165
|
+
end
|
|
190
166
|
|
|
191
167
|
def completion(prompt, model: "default", system: nil, schema: nil, **kwargs)
|
|
192
168
|
options = { "model" => model }.merge(kwargs.transform_keys(&:to_s))
|
|
193
169
|
options["system"] = system unless system.nil?
|
|
194
170
|
options["schema"] = schema unless schema.nil?
|
|
195
171
|
result = __senpi_bridge_request("/completion", { "prompt" => prompt.to_s, "opts" => options })
|
|
172
|
+
return handle(result) if options["handle"] == true
|
|
196
173
|
return result unless result.is_a?(Hash)
|
|
197
174
|
return result["value"] if result.key?("value")
|
|
198
175
|
|
package/src/kernels/rb/runner.rb
CHANGED
|
@@ -165,27 +165,27 @@ class SenpiGlobalSizer
|
|
|
165
165
|
return ObjectSpace.memsize_of(value) if Integer === value || Float === value
|
|
166
166
|
return 0 if @seen.key?(value)
|
|
167
167
|
@seen[value] = true
|
|
168
|
-
return SENPI_SIZER_OBJECT + SENPI_STRING_BYTESIZE.
|
|
168
|
+
return SENPI_SIZER_OBJECT + SENPI_STRING_BYTESIZE.bind(value).call() if String === value
|
|
169
169
|
if depth >= SENPI_SIZER_MAX_DEPTH || @nodes >= SENPI_SIZER_NODE_BUDGET
|
|
170
170
|
@approximate = true
|
|
171
171
|
return ObjectSpace.memsize_of(value)
|
|
172
172
|
end
|
|
173
173
|
if Array === value
|
|
174
|
-
length = SENPI_ARRAY_SIZE.
|
|
175
|
-
SENPI_SIZER_OBJECT + length * SENPI_SIZER_POINTER + sampled(length, depth) { |index| SENPI_ARRAY_AT.
|
|
174
|
+
length = SENPI_ARRAY_SIZE.bind(value).call()
|
|
175
|
+
SENPI_SIZER_OBJECT + length * SENPI_SIZER_POINTER + sampled(length, depth) { |index| SENPI_ARRAY_AT.bind(value).call(index) }
|
|
176
176
|
elsif Hash === value
|
|
177
177
|
hash_size(value, depth)
|
|
178
178
|
else
|
|
179
|
-
ivars = SENPI_IVARS.
|
|
180
|
-
ObjectSpace.memsize_of(value) + sampled(ivars.length, depth) { |index| SENPI_IVAR_GET.
|
|
179
|
+
ivars = SENPI_IVARS.bind(value).call()
|
|
180
|
+
ObjectSpace.memsize_of(value) + sampled(ivars.length, depth) { |index| SENPI_IVAR_GET.bind(value).call(ivars[index]) }
|
|
181
181
|
end
|
|
182
182
|
end
|
|
183
183
|
|
|
184
184
|
def hash_size(hash, depth)
|
|
185
|
-
count = SENPI_HASH_SIZE.
|
|
185
|
+
count = SENPI_HASH_SIZE.bind(hash).call()
|
|
186
186
|
taken = 0
|
|
187
187
|
total = 0
|
|
188
|
-
SENPI_HASH_EACH.
|
|
188
|
+
SENPI_HASH_EACH.bind(hash).call() do |key, item|
|
|
189
189
|
total += size(key, depth + 1) + size(item, depth + 1)
|
|
190
190
|
taken += 1
|
|
191
191
|
break if taken >= SENPI_SIZER_SAMPLE
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
|
14
14
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
15
15
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
16
16
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
17
|
+
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
17
18
|
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
18
19
|
import {
|
|
19
20
|
boundToolCallArgs,
|
|
@@ -24,6 +25,7 @@ import {
|
|
|
24
25
|
type ToolCallCapture,
|
|
25
26
|
toolCallResultPreview,
|
|
26
27
|
} from "./call-capture.ts";
|
|
28
|
+
import { completionCallOptions, reservedDispatchContext } from "./cell-reserved-dispatch.ts";
|
|
27
29
|
import { CellResultBuilder, type CellState } from "./cell-runtime.ts";
|
|
28
30
|
import { type EvalImageResizer, marshalToolResult, toolResultIsError } from "./image.ts";
|
|
29
31
|
import { upsertStatusEvent } from "./status-events.ts";
|
|
@@ -48,6 +50,10 @@ export interface CellBridgeRuntime {
|
|
|
48
50
|
readonly imageResizer?: EvalImageResizer;
|
|
49
51
|
/** This cell's live kernel-tool capability; only a JS kernel has one (#1754). */
|
|
50
52
|
readonly kernelTools?: KernelToolsCapability;
|
|
53
|
+
/** The session generation's handle registry behind `wait()` / `handle()` / completion handles. */
|
|
54
|
+
readonly handles?: HandleRegistry;
|
|
55
|
+
/** Absolute wall-clock deadline of this cell; a completion handle it creates is bounded by it. */
|
|
56
|
+
readonly hardDeadlineMs?: number;
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
export class CellHandler {
|
|
@@ -159,17 +165,12 @@ export class CellHandler {
|
|
|
159
165
|
await this.#deliverToolReply(
|
|
160
166
|
message,
|
|
161
167
|
async () => ({
|
|
162
|
-
value: await runReservedTool(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
listTools: this.#runtime.listTools,
|
|
169
|
-
signal: this.#state.signal,
|
|
170
|
-
emitStatus: (event) => this.#recordStatus(event),
|
|
171
|
-
marshalToolResult,
|
|
172
|
-
}),
|
|
168
|
+
value: await runReservedTool(
|
|
169
|
+
message.toolName,
|
|
170
|
+
reservedDispatchContext(message, this.#runtime, this.#state.signal, (event) =>
|
|
171
|
+
this.#recordStatus(event),
|
|
172
|
+
),
|
|
173
|
+
),
|
|
173
174
|
toolCallOk: true,
|
|
174
175
|
}),
|
|
175
176
|
capture,
|
|
@@ -177,13 +178,15 @@ export class CellHandler {
|
|
|
177
178
|
return;
|
|
178
179
|
}
|
|
179
180
|
if (message.toolName === "completion" && this.#runtime.complete) {
|
|
180
|
-
const result = await handleCompletionToolCall(
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
181
|
+
const result = await handleCompletionToolCall(
|
|
182
|
+
completionCallOptions(
|
|
183
|
+
message,
|
|
184
|
+
this.#kernel,
|
|
185
|
+
this.#runtime,
|
|
186
|
+
this.#runtime.complete,
|
|
187
|
+
() => this.#state.active,
|
|
188
|
+
),
|
|
189
|
+
);
|
|
187
190
|
if (!this.#state.active) return;
|
|
188
191
|
recordToolCall(this.#state.toolCalls, result.ok, capture, undefined, result.ok ? undefined : result.error);
|
|
189
192
|
this.#resultBuilder.emitUpdate(false);
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
|
+
import type { ReservedDispatchContext } from "../bridges/reserved-dispatch.ts";
|
|
4
|
+
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
5
|
+
import type { CompletionToolCallOptions } from "../completion/tool-bridge.ts";
|
|
6
|
+
import type { CellBridgeRuntime } from "./cell-handler.ts";
|
|
7
|
+
import { marshalToolResult } from "./image.ts";
|
|
8
|
+
import type { EvalKernel, EvalStatusEvent } from "./types.ts";
|
|
9
|
+
|
|
10
|
+
type ToolCallMessage = Extract<KernelToHostMessage, { type: "tool-call" }>;
|
|
11
|
+
|
|
12
|
+
/** The in-process (JS kernel) dispatch context for a reserved helper call; mirrors the bridge route for py/rb/jl. */
|
|
13
|
+
export function reservedDispatchContext(
|
|
14
|
+
message: ToolCallMessage,
|
|
15
|
+
runtime: CellBridgeRuntime,
|
|
16
|
+
signal: AbortSignal,
|
|
17
|
+
emitStatus: (event: EvalStatusEvent) => void,
|
|
18
|
+
): ReservedDispatchContext {
|
|
19
|
+
const host = runtime.ctx.evalHandleHost;
|
|
20
|
+
return {
|
|
21
|
+
callId: message.callId,
|
|
22
|
+
args: message.args,
|
|
23
|
+
executeTool: runtime.executeTool,
|
|
24
|
+
taskToolName: runtime.settings.taskTools.task,
|
|
25
|
+
taskOutputToolName: runtime.settings.taskTools.output,
|
|
26
|
+
listTools: runtime.listTools,
|
|
27
|
+
signal,
|
|
28
|
+
emitStatus,
|
|
29
|
+
marshalToolResult,
|
|
30
|
+
...(runtime.handles === undefined ? {} : { handles: runtime.handles }),
|
|
31
|
+
...(host === undefined ? {} : { evalHandleHost: host }),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function completionCallOptions(
|
|
36
|
+
message: ToolCallMessage,
|
|
37
|
+
kernel: EvalKernel,
|
|
38
|
+
runtime: CellBridgeRuntime,
|
|
39
|
+
complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>,
|
|
40
|
+
isActive: () => boolean,
|
|
41
|
+
): CompletionToolCallOptions {
|
|
42
|
+
return {
|
|
43
|
+
message,
|
|
44
|
+
kernel,
|
|
45
|
+
complete,
|
|
46
|
+
ctx: runtime.ctx,
|
|
47
|
+
isActive,
|
|
48
|
+
...(runtime.handles === undefined ? {} : { handles: runtime.handles }),
|
|
49
|
+
...(runtime.hardDeadlineMs === undefined ? {} : { hardDeadlineMs: runtime.hardDeadlineMs }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
|
9
9
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
10
10
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
11
11
|
import type { EvalExecutionTracker } from "../extension/session-manager.ts";
|
|
12
|
+
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
12
13
|
import type { EvalTimeoutFactory } from "./cell-execution.ts";
|
|
13
14
|
import type { EvalDetachedCellManager } from "./detached-cell-manager.ts";
|
|
14
15
|
import type { EvalExecutionEventPayload } from "./eval-execution-event.ts";
|
|
@@ -50,6 +51,8 @@ export interface CreateEvalToolOptions {
|
|
|
50
51
|
readonly imageResizer?: EvalImageResizer;
|
|
51
52
|
readonly executionTracker?: EvalExecutionTracker;
|
|
52
53
|
readonly cellManager?: EvalDetachedCellManager;
|
|
54
|
+
/** The session generation's handle registry (`wait()`, `handle()`, completion handles); absent before session_start. */
|
|
55
|
+
readonly handles?: HandleRegistry;
|
|
53
56
|
readonly onCellSettled?: (payload: EvalExecutionEventPayload) => void;
|
|
54
57
|
readonly timeoutFactory?: EvalTimeoutFactory;
|
|
55
58
|
readonly proxyExecutor?: (params: EvalToolInput, signal?: AbortSignal) => Promise<AgentToolResult<EvalToolDetails>>;
|
|
@@ -224,6 +224,8 @@ async function executeCell(
|
|
|
224
224
|
: { artifactPath: join(options.artifactsDir, `eval-${randomUUID()}.log`) }),
|
|
225
225
|
...(options.imageResizer === undefined ? {} : { imageResizer: options.imageResizer }),
|
|
226
226
|
...(kernelTools === undefined ? {} : { kernelTools }),
|
|
227
|
+
...(options.handles === undefined ? {} : { handles: options.handles }),
|
|
228
|
+
hardDeadlineMs: cell.startedAtMs + cell.hardLimitSeconds * 1_000,
|
|
227
229
|
});
|
|
228
230
|
handler = activeHandler;
|
|
229
231
|
cellManager.bindKernel(
|
package/src/tool/types.ts
CHANGED
|
@@ -144,9 +144,24 @@ export function createEvalInputSchema(
|
|
|
144
144
|
export type EvalKernelResult = Extract<KernelToHostMessage, { type: "result" }>;
|
|
145
145
|
export type EvalToolCallMessage = Extract<KernelToHostMessage, { type: "tool-call" }>;
|
|
146
146
|
|
|
147
|
+
export type HostCellOutcome =
|
|
148
|
+
| { readonly ok: true; readonly valueRepr?: string }
|
|
149
|
+
| { readonly ok: false; readonly error: { readonly message: string; readonly name?: string } };
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Work the host runs in the cell's place when the entry reaches the front of the kernel's queue: no code is
|
|
153
|
+
* sent to the interpreter, and aborting the signal (interrupt, timeout, kernel close) ends it without
|
|
154
|
+
* restarting the interpreter.
|
|
155
|
+
*/
|
|
156
|
+
export type HostCellExecutor = (context: {
|
|
157
|
+
readonly signal: AbortSignal;
|
|
158
|
+
readonly emit: (message: KernelToHostMessage) => void;
|
|
159
|
+
}) => Promise<HostCellOutcome>;
|
|
160
|
+
|
|
147
161
|
export interface EvalKernelRunInput {
|
|
148
162
|
readonly cellId: string;
|
|
149
163
|
readonly code: string;
|
|
164
|
+
readonly host?: HostCellExecutor;
|
|
150
165
|
readonly timeoutMs?: number;
|
|
151
166
|
readonly onStarted?: () => void;
|
|
152
167
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|