@code-yeongyu/senpi-codemode 2026.10.5 → 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 +41 -0
- package/README.md +20 -2
- package/package.json +4 -4
- package/src/bridge/kernel-tools-protocol.ts +1 -1
- 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/config/feature-settings.ts +110 -0
- package/src/config/memory-settings.ts +8 -0
- package/src/config/settings-constants.ts +7 -0
- package/src/config/settings-overrides.ts +72 -0
- package/src/config/settings.ts +35 -78
- package/src/config/unknown-settings.ts +19 -0
- package/src/extension/bridge-dispatch.ts +41 -0
- package/src/extension/bridge-tool-call.ts +41 -0
- package/src/extension/host-facts.ts +18 -0
- package/src/extension/idle-parking-kernel.ts +272 -0
- package/src/extension/kernel-registration.ts +25 -0
- package/src/extension/kernel-tools-probe.ts +24 -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 +58 -58
- 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/kernel.ts +3 -1
- package/src/kernels/jl/prelude.jl +9 -0
- package/src/kernels/jl/runner.jl +160 -1
- package/src/kernels/js/context-manager.ts +1 -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/kernel-tools-types.ts +2 -1
- package/src/kernels/js/worker-global-sizes.js +84 -14
- 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 +145 -4
- package/src/kernels/rb/handles.rb +97 -0
- package/src/kernels/rb/kernel.ts +3 -1
- package/src/kernels/rb/paths.rb +37 -0
- package/src/kernels/rb/prelude.rb +15 -38
- package/src/kernels/rb/runner.rb +141 -0
- package/src/kernels/shared/kernel-memory-host.ts +14 -6
- package/src/kernels/{js/kernel-tools-host.ts → shared/kernel-tools-pump.ts} +2 -2
- package/src/kernels/shared/process-group-cpu.ts +139 -0
- package/src/kernels/shared/subprocess-contract.ts +3 -0
- package/src/kernels/shared/subprocess-kernel.ts +33 -1
- package/src/kernels/shared/subprocess-startup.ts +90 -0
- 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 +7 -6
- package/src/tool/types.ts +15 -0
package/src/kernels/jl/runner.jl
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
# allow: SIZE_OK — parser, stream capture, bridge calls, and the persistent execution loop share Main globals.
|
|
2
|
+
write(stdout, "{\"type\":\"status\",\"event\":{\"op\":\"kernel-startup\",\"stage\":\"stdlib-imports\"}}\n")
|
|
3
|
+
flush(stdout)
|
|
2
4
|
using Sockets
|
|
3
5
|
|
|
4
6
|
const SENPI_ORIGINAL_STDOUT = stdout
|
|
@@ -7,6 +9,8 @@ out_read, out_write = redirect_stdout()
|
|
|
7
9
|
err_read, err_write = redirect_stderr()
|
|
8
10
|
redirect_stdin(devnull)
|
|
9
11
|
|
|
12
|
+
write(SENPI_ORIGINAL_STDOUT, "{\"type\":\"status\",\"event\":{\"op\":\"kernel-startup\",\"stage\":\"runtime-init\"}}\n")
|
|
13
|
+
flush(SENPI_ORIGINAL_STDOUT)
|
|
10
14
|
include("prelude.jl")
|
|
11
15
|
|
|
12
16
|
const senpi_connection = Dict{String, Any}()
|
|
@@ -220,12 +224,15 @@ function senpi_http_body(response::AbstractString)
|
|
|
220
224
|
String(output)
|
|
221
225
|
end
|
|
222
226
|
|
|
223
|
-
|
|
227
|
+
# `read_timeout` (seconds) bounds only the long-lived `wait()` request; ordinary calls read until the host closes.
|
|
228
|
+
function senpi_bridge_request(path::String, payload; read_timeout=nothing)
|
|
224
229
|
port = get(senpi_connection, "port", nothing)
|
|
225
230
|
token = get(senpi_connection, "token", nothing)
|
|
226
231
|
port isa Integer && token isa AbstractString || error("Julia tool bridge is not initialized")
|
|
227
232
|
body = senpi_json(payload)
|
|
228
233
|
socket = connect(ip"127.0.0.1", port)
|
|
234
|
+
timed_out = Ref(false)
|
|
235
|
+
timer = read_timeout === nothing ? nothing : Timer(_ -> (timed_out[] = true; close(socket)), Float64(read_timeout))
|
|
229
236
|
try
|
|
230
237
|
request = join([
|
|
231
238
|
"POST " * path * " HTTP/1.1",
|
|
@@ -240,12 +247,14 @@ function senpi_bridge_request(path::String, payload)
|
|
|
240
247
|
Base.write(socket, request)
|
|
241
248
|
flush(socket)
|
|
242
249
|
response = Base.read(socket, String)
|
|
250
|
+
timed_out[] && throw(SenpiBridgeError("bridge request timed out after $(read_timeout)s", "bridge_timeout"))
|
|
243
251
|
parsed = senpi_json_parse(senpi_http_body(response))
|
|
244
252
|
parsed isa AbstractDict || error("Bridge returned invalid JSON")
|
|
245
253
|
get(parsed, "ok", false) === true && return get(parsed, "value", nothing)
|
|
246
254
|
failure = get(parsed, "error", parsed)
|
|
247
255
|
throw(SenpiBridgeError(failure isa AbstractDict ? string(get(failure, "message", failure)) : string(failure), failure isa AbstractDict ? get(failure, "code", nothing) : nothing))
|
|
248
256
|
finally
|
|
257
|
+
timer === nothing || close(timer)
|
|
249
258
|
close(socket)
|
|
250
259
|
end
|
|
251
260
|
end
|
|
@@ -281,6 +290,152 @@ function senpi_set_connection(value)
|
|
|
281
290
|
end
|
|
282
291
|
end
|
|
283
292
|
|
|
293
|
+
const senpi_memory = Ref{Any}(nothing)
|
|
294
|
+
|
|
295
|
+
const SENPI_MEMORY_INTERNALS = Set([:senpi_current_cell, :senpi_memory, :senpi_connection, :senpi_frame_io, :senpi_stdout_capture, :senpi_stderr_capture, :senpi_protocol_stdin])
|
|
296
|
+
|
|
297
|
+
const SENPI_SIZER_SAMPLE = 1_000
|
|
298
|
+
const SENPI_SIZER_NODE_BUDGET = 5_000
|
|
299
|
+
const SENPI_SIZER_MAX_DEPTH = 64
|
|
300
|
+
const SENPI_SIZER_POINTER = 8
|
|
301
|
+
const SENPI_SIZER_OBJECT = 16
|
|
302
|
+
const SENPI_SIZER_MIN_REPORTED = 1024 * 1024
|
|
303
|
+
|
|
304
|
+
# Sizes a global with sampling and a node budget per global (leaves count too), so one deep global
|
|
305
|
+
# never hides the ones measured after it and a huge container costs a bounded walk. Only concrete Base
|
|
306
|
+
# containers are iterated; any other AbstractDict or AbstractSet is sized as an opaque struct, so no user
|
|
307
|
+
# length or iterate method runs during a memory report.
|
|
308
|
+
mutable struct SenpiSizer
|
|
309
|
+
seen::IdDict{Any, Nothing}
|
|
310
|
+
nodes::Int
|
|
311
|
+
approximate::Bool
|
|
312
|
+
end
|
|
313
|
+
SenpiSizer() = SenpiSizer(IdDict{Any, Nothing}(), 0, false)
|
|
314
|
+
|
|
315
|
+
senpi_over_budget(sizer::SenpiSizer) = sizer.nodes >= SENPI_SIZER_NODE_BUDGET
|
|
316
|
+
|
|
317
|
+
function senpi_size(sizer::SenpiSizer, value, depth::Int)::Int
|
|
318
|
+
sizer.nodes += 1
|
|
319
|
+
value isa Type && return 0
|
|
320
|
+
value isa Union{Number, Char, Bool, Symbol, Nothing} && return isbits(value) ? sizeof(value) : 0
|
|
321
|
+
value isa String && return SENPI_SIZER_OBJECT + sizeof(value)
|
|
322
|
+
if ismutable(value)
|
|
323
|
+
haskey(sizer.seen, value) && return 0
|
|
324
|
+
sizer.seen[value] = nothing
|
|
325
|
+
end
|
|
326
|
+
if depth >= SENPI_SIZER_MAX_DEPTH || senpi_over_budget(sizer)
|
|
327
|
+
sizer.approximate = true
|
|
328
|
+
return SENPI_SIZER_OBJECT
|
|
329
|
+
end
|
|
330
|
+
if value isa Array
|
|
331
|
+
isbitstype(eltype(value)) && return SENPI_SIZER_OBJECT + sizeof(value)
|
|
332
|
+
return SENPI_SIZER_OBJECT + length(value) * SENPI_SIZER_POINTER +
|
|
333
|
+
senpi_sampled(sizer, length(value), depth) do index
|
|
334
|
+
isassigned(value, index) ? value[index] : nothing
|
|
335
|
+
end
|
|
336
|
+
elseif value isa Union{Dict, IdDict}
|
|
337
|
+
count = length(value)
|
|
338
|
+
total = 0
|
|
339
|
+
taken = 0
|
|
340
|
+
for (key, item) in value
|
|
341
|
+
senpi_over_budget(sizer) && break
|
|
342
|
+
total += senpi_size(sizer, key, depth + 1) + senpi_size(sizer, item, depth + 1)
|
|
343
|
+
taken += 1
|
|
344
|
+
taken >= SENPI_SIZER_SAMPLE && break
|
|
345
|
+
end
|
|
346
|
+
taken < count && (sizer.approximate = true)
|
|
347
|
+
return SENPI_SIZER_OBJECT + count * 2 * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
|
|
348
|
+
elseif value isa Union{Set, Tuple}
|
|
349
|
+
count = length(value)
|
|
350
|
+
total = 0
|
|
351
|
+
taken = 0
|
|
352
|
+
for item in value
|
|
353
|
+
senpi_over_budget(sizer) && break
|
|
354
|
+
total += senpi_size(sizer, item, depth + 1)
|
|
355
|
+
taken += 1
|
|
356
|
+
taken >= SENPI_SIZER_SAMPLE && break
|
|
357
|
+
end
|
|
358
|
+
taken < count && (sizer.approximate = true)
|
|
359
|
+
return SENPI_SIZER_OBJECT + count * SENPI_SIZER_POINTER + (taken == 0 ? 0 : div(total * count, taken))
|
|
360
|
+
end
|
|
361
|
+
isbits(value) && return sizeof(value)
|
|
362
|
+
fields = fieldcount(typeof(value))
|
|
363
|
+
return SENPI_SIZER_OBJECT + fields * SENPI_SIZER_POINTER +
|
|
364
|
+
senpi_sampled(sizer, fields, depth) do index
|
|
365
|
+
isdefined(value, index) ? getfield(value, index) : nothing
|
|
366
|
+
end
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Up to SENPI_SIZER_SAMPLE evenly spaced elements; once the budget runs out part-way, the elements measured
|
|
370
|
+
# so far stand in for the rest.
|
|
371
|
+
function senpi_sampled(at, sizer::SenpiSizer, count::Int, depth::Int)::Int
|
|
372
|
+
count == 0 && return 0
|
|
373
|
+
picks = min(count, SENPI_SIZER_SAMPLE)
|
|
374
|
+
picks < count && (sizer.approximate = true)
|
|
375
|
+
step = count / picks
|
|
376
|
+
total = 0
|
|
377
|
+
measured = 0
|
|
378
|
+
for sample in 1:picks
|
|
379
|
+
if senpi_over_budget(sizer)
|
|
380
|
+
sizer.approximate = true
|
|
381
|
+
break
|
|
382
|
+
end
|
|
383
|
+
total += senpi_size(sizer, at(1 + floor(Int, (sample - 1) * step)), depth + 1)
|
|
384
|
+
measured += 1
|
|
385
|
+
end
|
|
386
|
+
return measured == 0 ? 0 : round(Int, total / measured * count)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
function senpi_largest_globals(limit::Int)
|
|
390
|
+
try
|
|
391
|
+
sizer = SenpiSizer()
|
|
392
|
+
candidates = Tuple{String, Int, Bool}[]
|
|
393
|
+
Base.invokelatest() do
|
|
394
|
+
for name in names(Main, all = true)
|
|
395
|
+
name in SENPI_MEMORY_INTERNALS && continue
|
|
396
|
+
name in (:Main, :Base, :Core, :Ans, :ans) && continue
|
|
397
|
+
lowered = lowercase(string(name))
|
|
398
|
+
startswith(lowered, "senpi_") && continue
|
|
399
|
+
startswith(string(name), "Senpi") && continue
|
|
400
|
+
startswith(string(name), "#") && continue
|
|
401
|
+
isdefined(Main, name) || continue
|
|
402
|
+
value = getfield(Main, name)
|
|
403
|
+
value isa Module && continue
|
|
404
|
+
value isa IO && continue
|
|
405
|
+
value isa Type && continue
|
|
406
|
+
value isa Function && continue
|
|
407
|
+
sizer.approximate = false
|
|
408
|
+
sizer.nodes = 0
|
|
409
|
+
bytes = try
|
|
410
|
+
senpi_size(sizer, value, 0) + SENPI_SIZER_POINTER
|
|
411
|
+
catch
|
|
412
|
+
0
|
|
413
|
+
end
|
|
414
|
+
bytes >= SENPI_SIZER_MIN_REPORTED && push!(candidates, (string(name), bytes, sizer.approximate))
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
sort!(candidates, by = entry -> entry[2], rev = true)
|
|
418
|
+
[approximate ? Dict{String, Any}("name" => name, "bytes" => bytes, "approximate" => true) : Dict{String, Any}("name" => name, "bytes" => bytes) for (name, bytes, approximate) in candidates[1:min(limit, end)]]
|
|
419
|
+
catch
|
|
420
|
+
Dict{String, Any}[]
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
function senpi_memory_report()
|
|
425
|
+
try
|
|
426
|
+
senpi_memory[] === nothing && return nothing
|
|
427
|
+
# The host reads the interpreter's current footprint and decides on the notice; the runner only
|
|
428
|
+
# names its largest globals, with a bounded walk, so a cell never pays for an unbounded scan.
|
|
429
|
+
named = senpi_largest_globals(5)
|
|
430
|
+
# The host replaces liveBytes with the interpreter footprint it reads itself; only globals is used.
|
|
431
|
+
report = Dict{String, Any}("liveBytes" => 0, "measure" => "footprint")
|
|
432
|
+
isempty(named) || (report["globals"] = named)
|
|
433
|
+
report
|
|
434
|
+
catch
|
|
435
|
+
nothing
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
284
439
|
function senpi_run_cell(message)
|
|
285
440
|
cell_id = string(get(message, "cellId", ""))
|
|
286
441
|
code = string(get(message, "code", ""))
|
|
@@ -297,6 +452,8 @@ function senpi_run_cell(message)
|
|
|
297
452
|
yield()
|
|
298
453
|
frame = Dict{String, Any}("type" => "result", "cellId" => cell_id, "ok" => true, "durationMs" => round(Int, (time() - started) * 1000))
|
|
299
454
|
value !== nothing && senpi_should_display_result(parsed) && (frame["valueRepr"] = senpi_json(value))
|
|
455
|
+
memory = senpi_memory_report()
|
|
456
|
+
memory !== nothing && (frame["memory"] = memory)
|
|
300
457
|
senpi_emit(frame)
|
|
301
458
|
catch error
|
|
302
459
|
senpi_emit(Dict("type" => "result", "cellId" => cell_id, "ok" => false, "error" => senpi_error(error), "durationMs" => round(Int, (time() - started) * 1000)))
|
|
@@ -313,7 +470,9 @@ while !eof(SENPI_ORIGINAL_STDIN)
|
|
|
313
470
|
message isa AbstractDict || error("Bridge frame must be an object")
|
|
314
471
|
kind = get(message, "type", nothing)
|
|
315
472
|
if kind == "init"
|
|
473
|
+
senpi_emit(Dict("type" => "status", "event" => Dict("op" => "kernel-startup", "stage" => "host-init")))
|
|
316
474
|
senpi_set_connection(get(message, "connection", nothing))
|
|
475
|
+
senpi_memory[] = get(message, "memory", nothing)
|
|
317
476
|
senpi_emit(Dict("type" => "ready"))
|
|
318
477
|
elseif kind == "run"
|
|
319
478
|
senpi_run_cell(message)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
2
|
import { CHILD_LIFECYCLE_OP, INTERRUPT_ACK_OP } from "../../bridge/reserved.ts";
|
|
3
3
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
4
|
+
import { KernelToolHostPump } from "../shared/kernel-tools-pump.ts";
|
|
4
5
|
import { ActiveCellControl } from "./active-cell-control.ts";
|
|
5
6
|
import { DEFAULT_INTERRUPT_BOUNDS, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
6
7
|
import {
|
|
@@ -14,7 +15,6 @@ import {
|
|
|
14
15
|
} from "./kernel-contract.ts";
|
|
15
16
|
import { type JavaScriptMemoryReading, KernelMemoryBridge } from "./kernel-memory-bridge.ts";
|
|
16
17
|
import { kernelToolError } from "./kernel-tools-errors.ts";
|
|
17
|
-
import { KernelToolHostPump } from "./kernel-tools-host.ts";
|
|
18
18
|
import type {
|
|
19
19
|
KernelToolsDescribeResult,
|
|
20
20
|
KernelToolsInvokeOptions,
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// In-cell `wait()` / `handle()` over the host's handle capability. Mirrors src/bridge/reserved.ts; this
|
|
2
|
+
// worker asset cannot import TypeScript. Every call rides the ordinary bridge-call path (`callTool`), so
|
|
3
|
+
// the run budget pauses while the cell is parked and the hard limit still applies.
|
|
4
|
+
const RESERVED_WAIT_TOOL = "__wait__";
|
|
5
|
+
const RESERVED_HANDLE_STATUS_TOOL = "__handle_status__";
|
|
6
|
+
const RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__";
|
|
7
|
+
const RESERVED_HANDLE_SEND_TOOL = "__handle_send__";
|
|
8
|
+
const RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__";
|
|
9
|
+
const KINDS = new Set(["agent", "completion", "workpool"]);
|
|
10
|
+
const WAIT_MODES = new Set(["all", "any", "settled"]);
|
|
11
|
+
const USAGE = "handle() expects an agent(..., {handle: true}) record, a workpool, a completion handle, or a saved {kind, id, run_epoch} reference";
|
|
12
|
+
|
|
13
|
+
export function createHandleHelpers(callTool) {
|
|
14
|
+
const wait = async (handles, options) => {
|
|
15
|
+
const list = Array.isArray(handles) ? handles : handles === undefined || handles === null ? [] : [handles];
|
|
16
|
+
const refs = list.map(toHandleRef);
|
|
17
|
+
const { timeout, mode } = waitOptions(options);
|
|
18
|
+
return await callTool(RESERVED_WAIT_TOOL, {
|
|
19
|
+
refs,
|
|
20
|
+
...(timeout === undefined ? {} : { timeout }),
|
|
21
|
+
...(mode === undefined ? {} : { mode }),
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const handle = value => {
|
|
25
|
+
const ref = toHandleRef(value);
|
|
26
|
+
const view = Object.create(Object.prototype);
|
|
27
|
+
if (isPlainObject(value)) {
|
|
28
|
+
for (const [key, field] of Object.entries(value)) {
|
|
29
|
+
if (typeof field !== "function") view[key] = field;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!Object.hasOwn(view, "id")) view.id = ref.id;
|
|
33
|
+
if (!Object.hasOwn(view, "run_epoch")) view.run_epoch = ref.run_epoch;
|
|
34
|
+
if (!Object.hasOwn(view, "handle")) view.handle = `${ref.kind}://${ref.id}`;
|
|
35
|
+
const control = Object.freeze({
|
|
36
|
+
status: async () => await callTool(RESERVED_HANDLE_STATUS_TOOL, { ref }),
|
|
37
|
+
output: async (opts = {}) => await callTool(RESERVED_HANDLE_OUTPUT_TOOL, { ref, ...outputOptions(opts) }),
|
|
38
|
+
send: async message => await callTool(RESERVED_HANDLE_SEND_TOOL, { ref, message: String(message) }),
|
|
39
|
+
cancel: async () => await callTool(RESERVED_HANDLE_CANCEL_TOOL, { ref }),
|
|
40
|
+
wait: async (opts = {}) => (await wait([ref], { ...waitOptions(opts), mode: "all" }))[0],
|
|
41
|
+
});
|
|
42
|
+
Object.defineProperty(view, "ref", { value: Object.freeze({ ...ref }), enumerable: false });
|
|
43
|
+
Object.defineProperty(view, "control", { value: control, enumerable: false });
|
|
44
|
+
return view;
|
|
45
|
+
};
|
|
46
|
+
return { wait, handle };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Accepts legacy agent records, handle() views, completion handles, workpools and saved refs. */
|
|
50
|
+
export function toHandleRef(value) {
|
|
51
|
+
if (!isPlainObject(value)) throw new TypeError(USAGE);
|
|
52
|
+
if (isPlainObject(value.ref) && KINDS.has(value.ref.kind)) return toHandleRef(value.ref);
|
|
53
|
+
if (typeof value.pool_id === "string") return { kind: "workpool", id: value.pool_id, run_epoch: 0 };
|
|
54
|
+
const kind = KINDS.has(value.kind) ? value.kind : kindFromHandle(value.handle);
|
|
55
|
+
const id = value.id ?? value.task_id;
|
|
56
|
+
if (kind === undefined || typeof id !== "string" || id.length === 0) throw new TypeError(USAGE);
|
|
57
|
+
const run_epoch = value.run_epoch ?? (kind === "workpool" ? 0 : undefined);
|
|
58
|
+
if (!Number.isInteger(run_epoch) || run_epoch < 0) throw new TypeError(`${USAGE}; run_epoch must be a non-negative integer`);
|
|
59
|
+
return { kind, id, run_epoch };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function kindFromHandle(handle) {
|
|
63
|
+
if (typeof handle !== "string") return undefined;
|
|
64
|
+
const scheme = handle.split("://")[0];
|
|
65
|
+
return KINDS.has(scheme) ? scheme : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function waitOptions(options) {
|
|
69
|
+
if (options === undefined || options === null) return {};
|
|
70
|
+
if (!isPlainObject(options)) throw new TypeError("wait() accepts { timeout, mode }");
|
|
71
|
+
for (const key of Object.keys(options)) {
|
|
72
|
+
if (key !== "timeout" && key !== "mode") throw new TypeError("wait() accepts { timeout, mode }");
|
|
73
|
+
}
|
|
74
|
+
const { timeout, mode } = options;
|
|
75
|
+
if (timeout !== undefined && timeout !== null && (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout < 0)) {
|
|
76
|
+
throw new TypeError("wait() timeout must be a finite number of seconds >= 0");
|
|
77
|
+
}
|
|
78
|
+
if (mode !== undefined && mode !== null && !WAIT_MODES.has(mode)) throw new TypeError('wait() mode must be "all", "any" or "settled"');
|
|
79
|
+
return { ...(timeout === undefined || timeout === null ? {} : { timeout }), ...(mode === undefined || mode === null ? {} : { mode }) };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function outputOptions(options) {
|
|
83
|
+
if (!isPlainObject(options)) throw new TypeError("control.output() accepts { format, offset, limit }");
|
|
84
|
+
const picked = {};
|
|
85
|
+
for (const key of ["format", "offset", "limit"]) {
|
|
86
|
+
if (options[key] !== undefined && options[key] !== null) picked[key] = options[key];
|
|
87
|
+
}
|
|
88
|
+
return picked;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isPlainObject(value) {
|
|
92
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
93
|
+
}
|
|
@@ -2,9 +2,15 @@ export type KernelToolFunction = {
|
|
|
2
2
|
readonly name: string;
|
|
3
3
|
};
|
|
4
4
|
|
|
5
|
+
export type KernelToolNamespaceRegistry = {
|
|
6
|
+
readonly defined?: () => string[];
|
|
7
|
+
readonly undefine?: (name: unknown) => boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
5
10
|
export function createToolNamespace(
|
|
6
11
|
define: (fn: KernelToolFunction, metadata?: unknown) => unknown,
|
|
7
12
|
callHost: (name: string, args: unknown) => Promise<unknown>,
|
|
13
|
+
registry?: KernelToolNamespaceRegistry,
|
|
8
14
|
): ((fn: KernelToolFunction, metadata?: unknown) => unknown) & {
|
|
9
15
|
readonly [name: string]: (args?: unknown) => Promise<unknown>;
|
|
10
16
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function createToolNamespace(define, callHost) {
|
|
1
|
+
export function createToolNamespace(define, callHost, registry = {}) {
|
|
2
2
|
const registrar = function tool(fn, metadata) {
|
|
3
3
|
return define(fn, metadata);
|
|
4
4
|
};
|
|
@@ -6,6 +6,8 @@ export function createToolNamespace(define, callHost) {
|
|
|
6
6
|
get(target, prop) {
|
|
7
7
|
if (typeof prop !== "string") return undefined;
|
|
8
8
|
if (prop in Function.prototype || prop === "arguments" || prop === "caller") return target[prop];
|
|
9
|
+
if (prop === "defined" && registry.defined) return () => registry.defined();
|
|
10
|
+
if (prop === "undefine" && registry.undefine) return (name) => registry.undefine(name);
|
|
9
11
|
return async (args) => await callHost(prop, args ?? {});
|
|
10
12
|
},
|
|
11
13
|
});
|
|
@@ -22,6 +22,8 @@ export function createKernelToolRegistry(options?: KernelToolRegistryOptions): {
|
|
|
22
22
|
},
|
|
23
23
|
signal?: AbortSignal,
|
|
24
24
|
): Promise<unknown>;
|
|
25
|
+
defined(): string[];
|
|
26
|
+
undefine(name: unknown): boolean;
|
|
25
27
|
bumpGeneration(): number;
|
|
26
28
|
setCollisionNames(hostToolNames?: readonly string[], foreignLanguageNames?: readonly string[]): void;
|
|
27
29
|
};
|
|
@@ -6,7 +6,8 @@ import { orderedArgs, resolveToolMetadata, validateInvokeArgs } from "./kernel-t
|
|
|
6
6
|
|
|
7
7
|
export { createToolNamespace };
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// "defined" and "undefine" are the tool namespace's own members, so a kernel tool by those names could never be reached.
|
|
10
|
+
const DEFAULT_RESERVED = Object.freeze(["__agent__", "__output__", "__schema__", "defined", "undefine"]);
|
|
10
11
|
|
|
11
12
|
function currentNames(source) {
|
|
12
13
|
return typeof source === "function" ? source() : (source ?? []);
|
|
@@ -47,21 +48,22 @@ export function createKernelToolRegistry(options = {}) {
|
|
|
47
48
|
assertJs();
|
|
48
49
|
const parsed = parseToolFunction(fn);
|
|
49
50
|
const resolved = resolveToolMetadata(metadata, parsed.params);
|
|
50
|
-
|
|
51
|
+
const name = resolved.name ?? parsed.name;
|
|
52
|
+
if (sanitizeNamePart(name) !== name || name.length > MCP_TOOL_NAME_MAX_LENGTH) {
|
|
51
53
|
throw kernelToolError("invalid_tool_definition", "Kernel tool name must match MCP name grammar");
|
|
52
54
|
}
|
|
53
|
-
const normalizedName =
|
|
54
|
-
const key = kernelToolKey(
|
|
55
|
-
if (reservedKeys.has(key)) throw kernelToolError("reserved_tool_name", `Kernel tool name is reserved: ${
|
|
55
|
+
const normalizedName = name;
|
|
56
|
+
const key = kernelToolKey(name);
|
|
57
|
+
if (reservedKeys.has(key)) throw kernelToolError("reserved_tool_name", `Kernel tool name is reserved: ${name}`);
|
|
56
58
|
if (hasNameKey(hostSource, key) || hasNameKey(foreignSource, key)) {
|
|
57
|
-
throw kernelToolError("tool_name_collision", `Kernel tool name collides: ${
|
|
59
|
+
throw kernelToolError("tool_name_collision", `Kernel tool name collides: ${name}`);
|
|
58
60
|
}
|
|
59
61
|
const existing = entries.get(key);
|
|
60
|
-
if (existing && existing.originalName !==
|
|
61
|
-
throw kernelToolError("tool_name_collision", `Kernel tool name collides: ${
|
|
62
|
+
if (existing && existing.originalName !== name) {
|
|
63
|
+
throw kernelToolError("tool_name_collision", `Kernel tool name collides: ${name}`);
|
|
62
64
|
}
|
|
63
65
|
const entry = {
|
|
64
|
-
originalName:
|
|
66
|
+
originalName: name,
|
|
65
67
|
normalizedName,
|
|
66
68
|
fn,
|
|
67
69
|
params: parsed.params,
|
|
@@ -114,6 +116,13 @@ export function createKernelToolRegistry(options = {}) {
|
|
|
114
116
|
throw kernelToolError("kernel_tool_failed", error instanceof Error ? error.message : String(error));
|
|
115
117
|
}
|
|
116
118
|
},
|
|
119
|
+
defined() {
|
|
120
|
+
return [...entries.values()].map((entry) => entry.normalizedName).sort();
|
|
121
|
+
},
|
|
122
|
+
undefine(name) {
|
|
123
|
+
if (typeof name !== "string") return false;
|
|
124
|
+
return entries.delete(kernelToolKey(name));
|
|
125
|
+
},
|
|
117
126
|
bumpGeneration() {
|
|
118
127
|
generation += 1;
|
|
119
128
|
entries.clear();
|
|
@@ -18,7 +18,11 @@ export function resolveToolMetadata(metadata, params) {
|
|
|
18
18
|
if (typeof description !== "string") {
|
|
19
19
|
throw kernelToolError("invalid_tool_definition", "tool() description must be a string");
|
|
20
20
|
}
|
|
21
|
-
if (metadata.
|
|
21
|
+
if (metadata.name !== undefined && typeof metadata.name !== "string") {
|
|
22
|
+
throw kernelToolError("invalid_tool_definition", "tool() name must be a string");
|
|
23
|
+
}
|
|
24
|
+
const named = metadata.name === undefined ? {} : { name: metadata.name };
|
|
25
|
+
if (metadata.schema === undefined) return { ...named, description, input_schema: defaultInputSchema(params) };
|
|
22
26
|
const schema = metadata.schema;
|
|
23
27
|
if (!isPlainJsonObject(schema) || schema.type !== "object" || !isPlainJsonObject(schema.properties)) {
|
|
24
28
|
throw kernelToolError("invalid_tool_definition", "tool() metadata schema must be a JSON object schema");
|
|
@@ -27,7 +31,7 @@ export function resolveToolMetadata(metadata, params) {
|
|
|
27
31
|
if (propertyKeys.length !== params.length || params.some((name) => !Object.hasOwn(schema.properties, name))) {
|
|
28
32
|
throw kernelToolError("invalid_tool_definition", "tool() metadata schema properties must match parameters");
|
|
29
33
|
}
|
|
30
|
-
return { description, input_schema: schema };
|
|
34
|
+
return { ...named, description, input_schema: schema };
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export function validateInvokeArgs(schema, args) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ExtensionKernelTools, KernelToolInvokeOptions, KernelToolInvokeScope } from "@code-yeongyu/senpi";
|
|
2
|
+
import type { EvalLanguage } from "../../tool/types.ts";
|
|
2
3
|
import type { KernelToolErrorCode, KernelToolHostDenial, KernelToolHostDenialReason } from "./kernel-tools-errors.ts";
|
|
3
4
|
|
|
4
5
|
export type { KernelToolErrorCode, KernelToolHostDenial, KernelToolHostDenialReason };
|
|
@@ -7,7 +8,7 @@ export type KernelToolDescriptor = {
|
|
|
7
8
|
readonly name: string;
|
|
8
9
|
readonly description: string;
|
|
9
10
|
readonly input_schema: unknown;
|
|
10
|
-
readonly language:
|
|
11
|
+
readonly language: EvalLanguage;
|
|
11
12
|
readonly kernel_generation: number;
|
|
12
13
|
readonly definition_revision: number;
|
|
13
14
|
};
|
|
@@ -9,6 +9,54 @@ const FUNCTION_BYTES = 64;
|
|
|
9
9
|
const INTERNAL_GLOBAL_PREFIX = "__senpi";
|
|
10
10
|
const MIN_REPORTED_BYTES = 1024 * 1024;
|
|
11
11
|
|
|
12
|
+
function intrinsicGetter(prototype, name) {
|
|
13
|
+
return Object.getOwnPropertyDescriptor(prototype, name)?.get;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Built-in getters captured at load: a user subclass that overrides `byteLength` or `size` is never called.
|
|
17
|
+
const TYPED_ARRAY_BYTE_LENGTH = intrinsicGetter(Object.getPrototypeOf(Uint8Array.prototype), "byteLength");
|
|
18
|
+
const DATA_VIEW_BYTE_LENGTH = intrinsicGetter(DataView.prototype, "byteLength");
|
|
19
|
+
const ARRAY_BUFFER_BYTE_LENGTH = intrinsicGetter(ArrayBuffer.prototype, "byteLength");
|
|
20
|
+
const SHARED_ARRAY_BUFFER_BYTE_LENGTH =
|
|
21
|
+
typeof SharedArrayBuffer === "function" ? intrinsicGetter(SharedArrayBuffer.prototype, "byteLength") : undefined;
|
|
22
|
+
const MAP_SIZE = intrinsicGetter(Map.prototype, "size");
|
|
23
|
+
const SET_SIZE = intrinsicGetter(Set.prototype, "size");
|
|
24
|
+
const BLOB_SIZE = typeof Blob === "function" ? intrinsicGetter(Blob.prototype, "size") : undefined;
|
|
25
|
+
// Iteration captured at load too: a user who replaces Map.prototype.entries or an iterator's next() is never called.
|
|
26
|
+
const MAP_ENTRIES = Map.prototype.entries;
|
|
27
|
+
const SET_VALUES = Set.prototype.values;
|
|
28
|
+
const MAP_ITERATOR_NEXT = Object.getPrototypeOf(new Map().entries()).next;
|
|
29
|
+
const SET_ITERATOR_NEXT = Object.getPrototypeOf(new Set().values()).next;
|
|
30
|
+
|
|
31
|
+
function read(getter, value) {
|
|
32
|
+
return Reflect.apply(getter, value, []);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const GET_PROTOTYPE_OF = Object.getPrototypeOf;
|
|
36
|
+
const BLOB_PROTOTYPE = typeof Blob === "function" ? Blob.prototype : undefined;
|
|
37
|
+
|
|
38
|
+
function inheritsFromBlob(value) {
|
|
39
|
+
for (let prototype = GET_PROTOTYPE_OF(value); prototype !== null; prototype = GET_PROTOTYPE_OF(prototype)) {
|
|
40
|
+
// A Proxy in the chain would run its getPrototypeOf trap on the next step.
|
|
41
|
+
if (types.isProxy(prototype)) return false;
|
|
42
|
+
if (prototype === BLOB_PROTOTYPE) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The Blob brand: a cheap prototype-chain pre-check (no user hook; proxies never reach here) so ordinary
|
|
48
|
+
// objects skip the try, then the size getter as the proof, which throws on a forged look-alike.
|
|
49
|
+
// instanceof would call a user-redefinable Symbol.hasInstance.
|
|
50
|
+
function isBlob(value) {
|
|
51
|
+
if (BLOB_SIZE === undefined || !inheritsFromBlob(value)) return false;
|
|
52
|
+
try {
|
|
53
|
+
read(BLOB_SIZE, value);
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
12
60
|
export function captureGlobalBaseline() {
|
|
13
61
|
return new Set(Object.getOwnPropertyNames(globalThis));
|
|
14
62
|
}
|
|
@@ -55,11 +103,12 @@ function createSizer() {
|
|
|
55
103
|
return (total / SAMPLED_ELEMENTS) * length;
|
|
56
104
|
}
|
|
57
105
|
|
|
58
|
-
function entriesOf(
|
|
106
|
+
function entriesOf(iterator, next, count, depth, sizeOf) {
|
|
59
107
|
const taken = [];
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (
|
|
108
|
+
while (taken.length < SAMPLED_ELEMENTS) {
|
|
109
|
+
const step = Reflect.apply(next, iterator, []);
|
|
110
|
+
if (step.done) break;
|
|
111
|
+
taken.push(step.value);
|
|
63
112
|
}
|
|
64
113
|
if (taken.length < count) approximate = true;
|
|
65
114
|
let total = 0;
|
|
@@ -67,21 +116,42 @@ function createSizer() {
|
|
|
67
116
|
return total * (count / Math.max(1, taken.length));
|
|
68
117
|
}
|
|
69
118
|
|
|
119
|
+
// An own data element, never a getter: an accessor is left unsized and makes the estimate approximate.
|
|
120
|
+
function elementOf(array, index) {
|
|
121
|
+
const descriptor = Object.getOwnPropertyDescriptor(array, index);
|
|
122
|
+
if (descriptor === undefined) return undefined;
|
|
123
|
+
if ("value" in descriptor) return descriptor.value;
|
|
124
|
+
approximate = true;
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
70
128
|
function objectSize(value, depth) {
|
|
71
129
|
if (types.isProxy(value)) return OBJECT_BYTES;
|
|
72
|
-
if (
|
|
73
|
-
if (types.
|
|
74
|
-
if (
|
|
75
|
-
if (
|
|
76
|
-
if (value
|
|
77
|
-
|
|
78
|
-
|
|
130
|
+
if (types.isTypedArray(value)) return read(TYPED_ARRAY_BYTE_LENGTH, value);
|
|
131
|
+
if (types.isDataView(value)) return read(DATA_VIEW_BYTE_LENGTH, value);
|
|
132
|
+
if (types.isArrayBuffer(value)) return read(ARRAY_BUFFER_BYTE_LENGTH, value);
|
|
133
|
+
if (types.isSharedArrayBuffer(value) && SHARED_ARRAY_BUFFER_BYTE_LENGTH) return read(SHARED_ARRAY_BUFFER_BYTE_LENGTH, value);
|
|
134
|
+
if (isBlob(value)) return read(BLOB_SIZE, value);
|
|
135
|
+
if (Array.isArray(value)) {
|
|
136
|
+
const length = value.length;
|
|
137
|
+
return OBJECT_BYTES + length * POINTER_BYTES + sampled(length, (index) => elementOf(value, index), depth);
|
|
138
|
+
}
|
|
139
|
+
if (types.isMap(value)) {
|
|
140
|
+
const count = read(MAP_SIZE, value);
|
|
141
|
+
const entries = Reflect.apply(MAP_ENTRIES, value, []);
|
|
142
|
+
return OBJECT_BYTES + count * 2 * POINTER_BYTES + entriesOf(entries, MAP_ITERATOR_NEXT, count, depth, ([key, item], at) => size(key, at) + size(item, at));
|
|
143
|
+
}
|
|
144
|
+
if (types.isSet(value)) {
|
|
145
|
+
const count = read(SET_SIZE, value);
|
|
146
|
+
return OBJECT_BYTES + count * POINTER_BYTES + entriesOf(Reflect.apply(SET_VALUES, value, []), SET_ITERATOR_NEXT, count, depth, size);
|
|
79
147
|
}
|
|
80
|
-
if (value instanceof Set) return OBJECT_BYTES + value.size * POINTER_BYTES + entriesOf(Set.prototype.values.call(value), value.size, depth, size);
|
|
81
148
|
const keys = Object.keys(value);
|
|
82
149
|
const propertyValue = (index) => {
|
|
83
150
|
const descriptor = Object.getOwnPropertyDescriptor(value, keys[index]);
|
|
84
|
-
|
|
151
|
+
if (descriptor === undefined) return undefined;
|
|
152
|
+
if ("value" in descriptor) return descriptor.value;
|
|
153
|
+
approximate = true;
|
|
154
|
+
return undefined;
|
|
85
155
|
};
|
|
86
156
|
return OBJECT_BYTES + keys.length * POINTER_BYTES + sampled(keys.length, propertyValue, depth);
|
|
87
157
|
}
|
|
@@ -114,7 +184,7 @@ function createSizer() {
|
|
|
114
184
|
try {
|
|
115
185
|
return { bytes: POINTER_BYTES + size(value, 0), approximate };
|
|
116
186
|
} catch (error) {
|
|
117
|
-
// A
|
|
187
|
+
// A forged built-in look-alike (a Blob-prototype object without its slot) throws from the intrinsic getter; it is left unsized.
|
|
118
188
|
if (error instanceof Error) return undefined;
|
|
119
189
|
throw error;
|
|
120
190
|
}
|
|
@@ -8,6 +8,7 @@ import { awaitMaybePromise, indirectEval, wrapUserCode } from "./worker-indirect
|
|
|
8
8
|
import { installShellCapture } from "./worker-shell-capture.js";
|
|
9
9
|
import { bindKernelBun } from "./worker-webview.js";
|
|
10
10
|
import { createWorkpool } from "./workpool.js";
|
|
11
|
+
import { createHandleHelpers } from "./handles.js";
|
|
11
12
|
import { inKernelToolInvoke } from "./kernel-tools-context.js";
|
|
12
13
|
import { kernelToolError } from "./kernel-tools-errors.js";
|
|
13
14
|
import { createKernelToolRegistry, createToolNamespace } from "./kernel-tools-registry.js";
|
|
@@ -137,10 +138,18 @@ export class JsWorkerRuntime {
|
|
|
137
138
|
globalThis.workpool = (agent, name, options) => createWorkpool((toolName, args) => this.#callTool(toolName, args), agent, name, options);
|
|
138
139
|
globalThis.parallel = async thunks => await this.#parallel(thunks);
|
|
139
140
|
globalThis.pipeline = async (items, ...stages) => await this.#pipeline(items, stages);
|
|
140
|
-
|
|
141
|
+
const handles = createHandleHelpers(async (toolName, args) => await this.#callTool(toolName, args));
|
|
142
|
+
globalThis.completion = async (prompt, opts) => {
|
|
143
|
+
const value = await this.#callTool("completion", { prompt, opts });
|
|
144
|
+
// The host answers a {handle: true} request with a saved reference; the cell gets the control view.
|
|
145
|
+
return isPlainObject(opts) && opts.handle === true ? handles.handle(value) : value;
|
|
146
|
+
};
|
|
147
|
+
globalThis.wait = async (list, options) => await handles.wait(list, options);
|
|
148
|
+
globalThis.handle = value => handles.handle(value);
|
|
141
149
|
globalThis.tool = createToolNamespace(
|
|
142
150
|
(fn, metadata) => this.#tools.define(fn, metadata),
|
|
143
151
|
async (name, args) => await this.#callTool(name, args),
|
|
152
|
+
{ defined: () => this.#tools.defined(), undefine: (name) => this.#tools.undefine(name) },
|
|
144
153
|
);
|
|
145
154
|
globalThis.tools = globalThis.tool;
|
|
146
155
|
const originalLog = console.log.bind(console);
|