@code-yeongyu/senpi-codemode 2026.7.25-2
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 +250 -0
- package/LICENSE +22 -0
- package/README.md +161 -0
- package/package.json +58 -0
- package/src/bridge/http-server.ts +236 -0
- package/src/bridge/protocol.ts +198 -0
- package/src/bridge/reserved.ts +9 -0
- package/src/bridges/agent-bridge.ts +197 -0
- package/src/bridges/output-bridge.ts +96 -0
- package/src/bridges/schema-injection.ts +3 -0
- package/src/codemode/runtime.ts +258 -0
- package/src/codemode/tools.ts +106 -0
- package/src/completion/handler.ts +192 -0
- package/src/completion/tool-bridge.ts +55 -0
- package/src/config/settings.ts +215 -0
- package/src/extension/runtime-factory.ts +114 -0
- package/src/extension/session-manager-proxy.ts +116 -0
- package/src/extension/session-manager.ts +215 -0
- package/src/host-sdk.ts +1 -0
- package/src/index.ts +181 -0
- package/src/interpreters/detect.ts +161 -0
- package/src/kernels/jl/kernel.ts +37 -0
- package/src/kernels/jl/prelude.jl +283 -0
- package/src/kernels/jl/runner.jl +327 -0
- package/src/kernels/js/context-manager.ts +296 -0
- package/src/kernels/js/inline-worker-entry.js +23 -0
- package/src/kernels/js/inline-worker.ts +15 -0
- package/src/kernels/js/kernel-contract.ts +38 -0
- package/src/kernels/js/local-module-loader.ts +108 -0
- package/src/kernels/js/prelude.ts +15 -0
- package/src/kernels/js/rewrite-imports.ts +164 -0
- package/src/kernels/js/run-queue.ts +82 -0
- package/src/kernels/js/worker-core.d.ts +18 -0
- package/src/kernels/js/worker-core.js +94 -0
- package/src/kernels/js/worker-entry.js +23 -0
- package/src/kernels/js/worker-host.ts +117 -0
- package/src/kernels/js/worker-indirect-eval.js +88 -0
- package/src/kernels/js/worker-runtime.js +401 -0
- package/src/kernels/py/kernel-contract.ts +32 -0
- package/src/kernels/py/kernel.ts +290 -0
- package/src/kernels/py/prelude.py +954 -0
- package/src/kernels/py/process.ts +119 -0
- package/src/kernels/py/transport.ts +237 -0
- package/src/kernels/rb/kernel.ts +26 -0
- package/src/kernels/rb/prelude.rb +270 -0
- package/src/kernels/rb/runner.rb +204 -0
- package/src/kernels/shared/subprocess-contract.ts +22 -0
- package/src/kernels/shared/subprocess-kernel.ts +266 -0
- package/src/kernels/shared/subprocess-process.ts +174 -0
- package/src/kernels/shared/subprocess-queue.ts +101 -0
- package/src/kernels/shared/subprocess-run.ts +98 -0
- package/src/output/output-meta.ts +89 -0
- package/src/output/streaming-output.ts +296 -0
- package/src/prompt/eval-prompt.ts +319 -0
- package/src/timeouts/bridge-timeout.ts +16 -0
- package/src/timeouts/idle-timeout.ts +84 -0
- package/src/tool/cell-handler.ts +279 -0
- package/src/tool/eval-tool.ts +285 -0
- package/src/tool/image.ts +274 -0
- package/src/tool/json-tree.ts +247 -0
- package/src/tool/render.ts +876 -0
- package/src/tool/status-events.ts +12 -0
- package/src/tool/types.ts +114 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# allow: SIZE_OK — this dependency-free kernel prelude is loaded as one runtime asset.
|
|
2
|
+
const SENPI_B64_ALPHABET = collect("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
|
|
3
|
+
|
|
4
|
+
function senpi_base64(text::AbstractString)
|
|
5
|
+
bytes = codeunits(text)
|
|
6
|
+
out = IOBuffer()
|
|
7
|
+
index = 1
|
|
8
|
+
while index <= length(bytes)
|
|
9
|
+
first_byte = bytes[index]
|
|
10
|
+
second_byte = index + 1 <= length(bytes) ? bytes[index + 1] : UInt8(0)
|
|
11
|
+
third_byte = index + 2 <= length(bytes) ? bytes[index + 2] : UInt8(0)
|
|
12
|
+
Base.write(out, SENPI_B64_ALPHABET[(first_byte >> 2) + 1])
|
|
13
|
+
Base.write(out, SENPI_B64_ALPHABET[(((first_byte & 0x03) << 4) | (second_byte >> 4)) + 1])
|
|
14
|
+
Base.write(out, index + 1 <= length(bytes) ? SENPI_B64_ALPHABET[(((second_byte & 0x0f) << 2) | (third_byte >> 6)) + 1] : '=')
|
|
15
|
+
Base.write(out, index + 2 <= length(bytes) ? SENPI_B64_ALPHABET[(third_byte & 0x3f) + 1] : '=')
|
|
16
|
+
index += 3
|
|
17
|
+
end
|
|
18
|
+
String(take!(out))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
function senpi_status_events_enabled()
|
|
22
|
+
get(senpi_connection, "statusEvents", true) !== false
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
function senpi_emit_status(op::AbstractString, fields::AbstractDict=Dict{String, Any}(); force::Bool=false)
|
|
26
|
+
(force || senpi_status_events_enabled()) || return nothing
|
|
27
|
+
event = Dict{String, Any}("op" => string(op))
|
|
28
|
+
for (key, value) in fields
|
|
29
|
+
event[string(key)] = value
|
|
30
|
+
end
|
|
31
|
+
senpi_emit(Dict("type" => "status", "event" => event))
|
|
32
|
+
nothing
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
function senpi_with_bridge_timeout_pause(operation::Function)
|
|
36
|
+
senpi_emit_status("timeout-pause"; force=true)
|
|
37
|
+
try
|
|
38
|
+
return operation()
|
|
39
|
+
finally
|
|
40
|
+
senpi_emit_status("timeout-resume"; force=true)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
function senpi_url_decode(value::AbstractString)
|
|
45
|
+
out = IOBuffer()
|
|
46
|
+
index = 1
|
|
47
|
+
while index <= ncodeunits(value)
|
|
48
|
+
character = Char(codeunit(value, index))
|
|
49
|
+
if character == '%' && index + 2 <= ncodeunits(value)
|
|
50
|
+
hex = value[index + 1:index + 2]
|
|
51
|
+
parsed = tryparse(UInt8, hex; base=16)
|
|
52
|
+
if parsed !== nothing
|
|
53
|
+
Base.write(out, parsed)
|
|
54
|
+
index += 3
|
|
55
|
+
continue
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
Base.write(out, character)
|
|
59
|
+
index += 1
|
|
60
|
+
end
|
|
61
|
+
String(take!(out))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
function senpi_resolve_path(value::AbstractString)
|
|
65
|
+
raw = string(value)
|
|
66
|
+
matched = match(r"^([a-z][a-z0-9+.\-]*)://(.*)$"i, raw)
|
|
67
|
+
matched === nothing && return abspath(raw)
|
|
68
|
+
scheme = lowercase(string(matched.captures[1]))
|
|
69
|
+
roots = get(senpi_connection, "localRoots", nothing)
|
|
70
|
+
root = roots isa AbstractDict ? get(roots, scheme, nothing) : nothing
|
|
71
|
+
root isa AbstractString && !isempty(root) || error("Protocol paths are not supported by this helper: $raw")
|
|
72
|
+
relative = senpi_url_decode(replace(string(matched.captures[2]), '\\' => '/'))
|
|
73
|
+
root_path = abspath(string(root))
|
|
74
|
+
isempty(relative) && return root_path
|
|
75
|
+
(startswith(relative, '/') || ".." in split(relative, '/')) && error("Unsafe $scheme:// path (absolute or traversal): $raw")
|
|
76
|
+
resolved = abspath(joinpath(root_path, relative))
|
|
77
|
+
(resolved == root_path || startswith(resolved, root_path * string(Base.Filesystem.path_separator))) || error("$scheme:// path escapes its root: $raw")
|
|
78
|
+
resolved
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
function senpi_display_payload(value)
|
|
82
|
+
if value isa AbstractDict
|
|
83
|
+
kind = get(value, "type", get(value, :type, nothing))
|
|
84
|
+
text_value = get(value, "text", get(value, :text, nothing))
|
|
85
|
+
if kind == "markdown" && text_value !== nothing
|
|
86
|
+
return "text/markdown", string(text_value), false
|
|
87
|
+
end
|
|
88
|
+
mime_type = get(value, "mimeType", get(value, :mimeType, nothing))
|
|
89
|
+
data = get(value, "data", get(value, :data, nothing))
|
|
90
|
+
if kind == "image" && mime_type isa AbstractString && data !== nothing
|
|
91
|
+
return string(mime_type), string(data), true
|
|
92
|
+
end
|
|
93
|
+
return "application/json", senpi_json(value), false
|
|
94
|
+
end
|
|
95
|
+
value isa AbstractVector && return "application/json", senpi_json(value), false
|
|
96
|
+
"text/plain", string(value), false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
function display(value)
|
|
100
|
+
mime_type, payload, encoded = senpi_display_payload(value)
|
|
101
|
+
senpi_emit(Dict("type" => "display", "mimeType" => mime_type, "dataBase64" => encoded ? payload : senpi_base64(payload)))
|
|
102
|
+
nothing
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
function display_image(base64_value::AbstractString, mime_type::AbstractString="image/png")
|
|
106
|
+
senpi_emit(Dict("type" => "display", "mimeType" => string(mime_type), "dataBase64" => string(base64_value)))
|
|
107
|
+
nothing
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
function text(value)
|
|
111
|
+
senpi_emit(Dict("type" => "text", "stream" => "stdout", "data" => string(value)))
|
|
112
|
+
nothing
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
function print(values...)
|
|
116
|
+
text(join(string.(values), ""))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
function read(path::AbstractString, offset::Integer=1, limit::Union{Integer, Nothing}=nothing)
|
|
120
|
+
resolved = senpi_resolve_path(path)
|
|
121
|
+
content = Base.read(resolved, String)
|
|
122
|
+
if offset > 1 || limit !== nothing
|
|
123
|
+
lines = split(content, '\n'; keepempty=true)
|
|
124
|
+
start = max(1, Int(offset))
|
|
125
|
+
finish = limit === nothing ? length(lines) : min(length(lines), start + Int(limit) - 1)
|
|
126
|
+
content = start <= length(lines) ? join(lines[start:finish], '\n') : ""
|
|
127
|
+
end
|
|
128
|
+
senpi_emit_status("read", Dict("path" => resolved, "chars" => length(content), "preview" => first(content, min(length(content), 500))))
|
|
129
|
+
content
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
function write(path::AbstractString, content)
|
|
133
|
+
resolved = senpi_resolve_path(path)
|
|
134
|
+
mkpath(dirname(resolved))
|
|
135
|
+
data = string(content)
|
|
136
|
+
open(resolved, "w") do io
|
|
137
|
+
Base.write(io, data)
|
|
138
|
+
end
|
|
139
|
+
senpi_emit_status("write", Dict("path" => resolved, "chars" => length(data)))
|
|
140
|
+
resolved
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
function env(key=nothing, value=nothing)
|
|
144
|
+
if key === nothing
|
|
145
|
+
values = Dict{String, String}(string(name) => string(item) for (name, item) in ENV)
|
|
146
|
+
senpi_emit_status("env", Dict("count" => length(values), "keys" => sort(collect(keys(values)))[1:min(20, length(values))]))
|
|
147
|
+
return values
|
|
148
|
+
end
|
|
149
|
+
name = string(key)
|
|
150
|
+
if value === nothing
|
|
151
|
+
resolved = get(ENV, name, nothing)
|
|
152
|
+
senpi_emit_status("env", Dict("key" => name, "value" => resolved, "action" => "get"))
|
|
153
|
+
return resolved
|
|
154
|
+
end
|
|
155
|
+
resolved = string(value)
|
|
156
|
+
ENV[name] = resolved
|
|
157
|
+
senpi_emit_status("env", Dict("key" => name, "value" => resolved, "action" => "set"))
|
|
158
|
+
resolved
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
struct SenpiToolProxy end
|
|
162
|
+
|
|
163
|
+
struct SenpiToolCallable
|
|
164
|
+
name::String
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
function (callable::SenpiToolCallable)(args=nothing; kwargs...)
|
|
168
|
+
values = Dict{String, Any}()
|
|
169
|
+
if args !== nothing
|
|
170
|
+
args isa AbstractDict || error("tool.$(callable.name)(...) expects a dictionary of arguments")
|
|
171
|
+
for (key, value) in args
|
|
172
|
+
values[string(key)] = value
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
for (key, value) in kwargs
|
|
176
|
+
values[string(key)] = value
|
|
177
|
+
end
|
|
178
|
+
senpi_with_bridge_timeout_pause(() -> senpi_call_tool(callable.name, values))
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
Base.getproperty(::SenpiToolProxy, name::Symbol) = SenpiToolCallable(string(name))
|
|
182
|
+
const tool = SenpiToolProxy()
|
|
183
|
+
|
|
184
|
+
function completion(prompt::AbstractString; model="default", system=nothing, schema=nothing, kwargs...)
|
|
185
|
+
options = Dict{String, Any}("model" => model)
|
|
186
|
+
system !== nothing && (options["system"] = system)
|
|
187
|
+
schema !== nothing && (options["schema"] = schema)
|
|
188
|
+
for (key, value) in kwargs
|
|
189
|
+
options[string(key)] = value
|
|
190
|
+
end
|
|
191
|
+
response = senpi_with_bridge_timeout_pause(() -> senpi_completion(string(prompt), options))
|
|
192
|
+
response isa AbstractDict || return response
|
|
193
|
+
haskey(response, "value") && return response["value"]
|
|
194
|
+
get(response, "text", response)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
function output(ids...; format="raw", offset=nothing, limit=nothing)
|
|
198
|
+
isempty(ids) && error("At least one output ID is required")
|
|
199
|
+
format in ("raw", "tail") || error("output() format must be 'raw' or 'tail'")
|
|
200
|
+
arguments = Dict{String, Any}("ids" => string.(ids), "format" => format)
|
|
201
|
+
offset !== nothing && (arguments["offset"] = offset)
|
|
202
|
+
limit !== nothing && (arguments["limit"] = limit)
|
|
203
|
+
senpi_with_bridge_timeout_pause(() -> senpi_call_tool("__output__", arguments))
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
function agent(prompt::AbstractString; agent="task", model=nothing, label=nothing, schema=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
|
|
207
|
+
arguments = Dict{String, Any}("prompt" => string(prompt), "agent" => agent)
|
|
208
|
+
for (key, value) in (("model", model), ("label", label), ("schema", schema), ("isolated", isolated), ("apply", apply), ("merge", merge))
|
|
209
|
+
value !== nothing && (arguments[key] = value)
|
|
210
|
+
end
|
|
211
|
+
for (key, value) in kwargs
|
|
212
|
+
arguments[string(key)] = value
|
|
213
|
+
end
|
|
214
|
+
handle && (arguments["handle"] = true)
|
|
215
|
+
response = senpi_with_bridge_timeout_pause(() -> senpi_call_tool("__agent__", arguments))
|
|
216
|
+
record = response isa AbstractDict ? response : Dict{String, Any}()
|
|
217
|
+
text_value = get(record, "text", response)
|
|
218
|
+
parsed = schema === nothing ? text_value : haskey(record, "data") ? record["data"] : senpi_json_parse(string(text_value))
|
|
219
|
+
handle || return parsed
|
|
220
|
+
result = Dict{String, Any}("text" => text_value, "output" => text_value, "id" => get(record, "id", nothing), "agent" => get(record, "agent", agent))
|
|
221
|
+
result["handle"] = get(record, "handle", result["id"] === nothing ? nothing : "agent://" * string(result["id"]))
|
|
222
|
+
schema !== nothing && (result["data"] = parsed)
|
|
223
|
+
result
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
function senpi_pool_map(items, callback)
|
|
227
|
+
values = collect(items)
|
|
228
|
+
isempty(values) && return Any[]
|
|
229
|
+
configured_width = get(senpi_connection, "parallelPoolWidth", 4)
|
|
230
|
+
width = configured_width isa Real && isfinite(configured_width) ? Int(floor(configured_width)) : 4
|
|
231
|
+
workers = min(max(width, 1), length(values))
|
|
232
|
+
tokens = Channel{Nothing}(workers)
|
|
233
|
+
for _ in 1:workers
|
|
234
|
+
put!(tokens, nothing)
|
|
235
|
+
end
|
|
236
|
+
results = Vector{Any}(undef, length(values))
|
|
237
|
+
errors = Dict{Int, Any}()
|
|
238
|
+
lock = ReentrantLock()
|
|
239
|
+
@sync for index in eachindex(values)
|
|
240
|
+
@async begin
|
|
241
|
+
take!(tokens)
|
|
242
|
+
try
|
|
243
|
+
results[index] = callback(values[index])
|
|
244
|
+
catch error
|
|
245
|
+
Base.lock(lock) do
|
|
246
|
+
errors[index] = error
|
|
247
|
+
end
|
|
248
|
+
finally
|
|
249
|
+
put!(tokens, nothing)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
isempty(errors) || throw(errors[minimum(keys(errors))])
|
|
254
|
+
results
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
function parallel(thunks)
|
|
258
|
+
values = collect(thunks)
|
|
259
|
+
for thunk in values
|
|
260
|
+
applicable(thunk) || error("parallel() expects zero-argument callables")
|
|
261
|
+
end
|
|
262
|
+
senpi_pool_map(values, thunk -> thunk())
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
function pipeline(items, stages...)
|
|
266
|
+
values = collect(items)
|
|
267
|
+
for stage in stages
|
|
268
|
+
isempty(values) && return values
|
|
269
|
+
applicable(stage, first(values)) || error("pipeline() stages must be callables")
|
|
270
|
+
values = senpi_pool_map(values, stage)
|
|
271
|
+
end
|
|
272
|
+
values
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
function log(message)
|
|
276
|
+
senpi_emit(Dict("type" => "log", "message" => string(message)))
|
|
277
|
+
nothing
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
function phase(title)
|
|
281
|
+
senpi_emit(Dict("type" => "phase", "title" => string(title)))
|
|
282
|
+
nothing
|
|
283
|
+
end
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# allow: SIZE_OK — parser, stream capture, bridge calls, and the persistent execution loop share Main globals.
|
|
2
|
+
using Sockets
|
|
3
|
+
|
|
4
|
+
const SENPI_ORIGINAL_STDOUT = stdout
|
|
5
|
+
const SENPI_ORIGINAL_STDIN = stdin
|
|
6
|
+
out_read, out_write = redirect_stdout()
|
|
7
|
+
err_read, err_write = redirect_stderr()
|
|
8
|
+
redirect_stdin(devnull)
|
|
9
|
+
|
|
10
|
+
include("prelude.jl")
|
|
11
|
+
|
|
12
|
+
const senpi_connection = Dict{String, Any}()
|
|
13
|
+
const senpi_write_lock = ReentrantLock()
|
|
14
|
+
global senpi_current_cell = nothing
|
|
15
|
+
|
|
16
|
+
function senpi_escape(text::AbstractString)
|
|
17
|
+
out = IOBuffer()
|
|
18
|
+
for character in text
|
|
19
|
+
if character == '"'
|
|
20
|
+
Base.write(out, "\\\"")
|
|
21
|
+
elseif character == '\\'
|
|
22
|
+
Base.write(out, "\\\\")
|
|
23
|
+
elseif character == '\n'
|
|
24
|
+
Base.write(out, "\\n")
|
|
25
|
+
elseif character == '\r'
|
|
26
|
+
Base.write(out, "\\r")
|
|
27
|
+
elseif character == '\t'
|
|
28
|
+
Base.write(out, "\\t")
|
|
29
|
+
else
|
|
30
|
+
Base.write(out, character)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
String(take!(out))
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
function senpi_json(value)
|
|
37
|
+
if value === nothing
|
|
38
|
+
return "null"
|
|
39
|
+
elseif value isa Bool
|
|
40
|
+
return value ? "true" : "false"
|
|
41
|
+
elseif value isa Number
|
|
42
|
+
return string(value)
|
|
43
|
+
elseif value isa AbstractString
|
|
44
|
+
return "\"" * senpi_escape(value) * "\""
|
|
45
|
+
elseif value isa AbstractDict
|
|
46
|
+
return "{" * join(["\"" * senpi_escape(string(key)) * "\":" * senpi_json(item) for (key, item) in value], ",") * "}"
|
|
47
|
+
elseif value isa AbstractVector || value isa Tuple
|
|
48
|
+
return "[" * join([senpi_json(item) for item in value], ",") * "]"
|
|
49
|
+
end
|
|
50
|
+
"\"" * senpi_escape(string(value)) * "\""
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
function senpi_json_parse(input::AbstractString)
|
|
54
|
+
characters = collect(input)
|
|
55
|
+
cursor = Ref(1)
|
|
56
|
+
length_value = length(characters)
|
|
57
|
+
function skip_space()
|
|
58
|
+
while cursor[] <= length_value && isspace(characters[cursor[]])
|
|
59
|
+
cursor[] += 1
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
function parse_string()
|
|
63
|
+
characters[cursor[]] == '"' || error("Expected JSON string")
|
|
64
|
+
cursor[] += 1
|
|
65
|
+
out = IOBuffer()
|
|
66
|
+
while cursor[] <= length_value
|
|
67
|
+
character = characters[cursor[]]
|
|
68
|
+
cursor[] += 1
|
|
69
|
+
character == '"' && return String(take!(out))
|
|
70
|
+
if character != '\\'
|
|
71
|
+
Base.write(out, character)
|
|
72
|
+
continue
|
|
73
|
+
end
|
|
74
|
+
cursor[] <= length_value || error("Unexpected JSON string escape")
|
|
75
|
+
escaped = characters[cursor[]]
|
|
76
|
+
cursor[] += 1
|
|
77
|
+
if escaped == 'u'
|
|
78
|
+
cursor[] + 3 <= length_value || error("Incomplete JSON unicode escape")
|
|
79
|
+
code = parse(Int, String(characters[cursor[]:cursor[] + 3]); base=16)
|
|
80
|
+
Base.write(out, Char(code))
|
|
81
|
+
cursor[] += 4
|
|
82
|
+
else
|
|
83
|
+
mapped = escaped == 'n' ? '\n' : escaped == 'r' ? '\r' : escaped == 't' ? '\t' : escaped == 'b' ? '\b' : escaped == 'f' ? '\f' : escaped
|
|
84
|
+
Base.write(out, mapped)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
error("Unterminated JSON string")
|
|
88
|
+
end
|
|
89
|
+
function parse_value()
|
|
90
|
+
skip_space()
|
|
91
|
+
cursor[] <= length_value || error("Unexpected JSON end")
|
|
92
|
+
character = characters[cursor[]]
|
|
93
|
+
if character == '"'
|
|
94
|
+
return parse_string()
|
|
95
|
+
elseif character == '{'
|
|
96
|
+
cursor[] += 1
|
|
97
|
+
object_result = Dict{String, Any}()
|
|
98
|
+
skip_space()
|
|
99
|
+
if cursor[] <= length_value && characters[cursor[]] == '}'
|
|
100
|
+
cursor[] += 1
|
|
101
|
+
return object_result
|
|
102
|
+
end
|
|
103
|
+
while true
|
|
104
|
+
skip_space()
|
|
105
|
+
key = parse_string()
|
|
106
|
+
skip_space()
|
|
107
|
+
cursor[] <= length_value && characters[cursor[]] == ':' || error("Expected JSON object colon")
|
|
108
|
+
cursor[] += 1
|
|
109
|
+
object_result[key] = parse_value()
|
|
110
|
+
skip_space()
|
|
111
|
+
cursor[] <= length_value || error("Unexpected JSON object end")
|
|
112
|
+
characters[cursor[]] == '}' && (cursor[] += 1; return object_result)
|
|
113
|
+
characters[cursor[]] == ',' || error("Expected JSON object separator")
|
|
114
|
+
cursor[] += 1
|
|
115
|
+
end
|
|
116
|
+
elseif character == '['
|
|
117
|
+
cursor[] += 1
|
|
118
|
+
array_result = Any[]
|
|
119
|
+
skip_space()
|
|
120
|
+
if cursor[] <= length_value && characters[cursor[]] == ']'
|
|
121
|
+
cursor[] += 1
|
|
122
|
+
return array_result
|
|
123
|
+
end
|
|
124
|
+
while true
|
|
125
|
+
push!(array_result, parse_value())
|
|
126
|
+
skip_space()
|
|
127
|
+
cursor[] <= length_value || error("Unexpected JSON array end")
|
|
128
|
+
characters[cursor[]] == ']' && (cursor[] += 1; return array_result)
|
|
129
|
+
characters[cursor[]] == ',' || error("Expected JSON array separator")
|
|
130
|
+
cursor[] += 1
|
|
131
|
+
end
|
|
132
|
+
elseif startswith(String(characters[cursor[]:end]), "true")
|
|
133
|
+
cursor[] += 4
|
|
134
|
+
return true
|
|
135
|
+
elseif startswith(String(characters[cursor[]:end]), "false")
|
|
136
|
+
cursor[] += 5
|
|
137
|
+
return false
|
|
138
|
+
elseif startswith(String(characters[cursor[]:end]), "null")
|
|
139
|
+
cursor[] += 4
|
|
140
|
+
return nothing
|
|
141
|
+
end
|
|
142
|
+
start = cursor[]
|
|
143
|
+
while cursor[] <= length_value && characters[cursor[]] in ['-', '+', '.', 'e', 'E', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
|
|
144
|
+
cursor[] += 1
|
|
145
|
+
end
|
|
146
|
+
number = String(characters[start:cursor[] - 1])
|
|
147
|
+
integer = tryparse(Int, number)
|
|
148
|
+
integer === nothing || return integer
|
|
149
|
+
decimal = tryparse(Float64, number)
|
|
150
|
+
decimal === nothing && error("Invalid JSON value")
|
|
151
|
+
decimal
|
|
152
|
+
end
|
|
153
|
+
parsed_result = parse_value()
|
|
154
|
+
skip_space()
|
|
155
|
+
cursor[] > length_value || error("Trailing JSON data")
|
|
156
|
+
parsed_result
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
function senpi_emit(frame)
|
|
160
|
+
lock(senpi_write_lock) do
|
|
161
|
+
println(SENPI_ORIGINAL_STDOUT, senpi_json(frame))
|
|
162
|
+
flush(SENPI_ORIGINAL_STDOUT)
|
|
163
|
+
end
|
|
164
|
+
nothing
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
function senpi_emit_stream(stream::String, bytes::Vector{UInt8})
|
|
168
|
+
senpi_current_cell === nothing && return nothing
|
|
169
|
+
isempty(bytes) && return nothing
|
|
170
|
+
data = try
|
|
171
|
+
String(copy(bytes))
|
|
172
|
+
catch
|
|
173
|
+
repr(bytes)
|
|
174
|
+
end
|
|
175
|
+
senpi_emit(Dict("type" => "text", "stream" => stream, "data" => data))
|
|
176
|
+
nothing
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
function senpi_drain_stream(io, stream::String)
|
|
180
|
+
while true
|
|
181
|
+
bytes = readavailable(io)
|
|
182
|
+
if !isempty(bytes)
|
|
183
|
+
senpi_emit_stream(stream, bytes)
|
|
184
|
+
elseif eof(io)
|
|
185
|
+
return nothing
|
|
186
|
+
else
|
|
187
|
+
yield()
|
|
188
|
+
sleep(0.001)
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
@async senpi_drain_stream(out_read, "stdout")
|
|
194
|
+
@async senpi_drain_stream(err_read, "stderr")
|
|
195
|
+
|
|
196
|
+
function senpi_http_body(response::AbstractString)
|
|
197
|
+
parts = split(response, "\r\n\r\n"; limit=2)
|
|
198
|
+
length(parts) == 2 || return response
|
|
199
|
+
headers, body = parts
|
|
200
|
+
occursin("transfer-encoding: chunked", lowercase(headers)) || return body
|
|
201
|
+
bytes = collect(codeunits(body))
|
|
202
|
+
output = UInt8[]
|
|
203
|
+
cursor = 1
|
|
204
|
+
while cursor <= length(bytes)
|
|
205
|
+
header_end = nothing
|
|
206
|
+
for index in cursor:length(bytes) - 1
|
|
207
|
+
if bytes[index] == 0x0d && bytes[index + 1] == 0x0a
|
|
208
|
+
header_end = index
|
|
209
|
+
break
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
header_end === nothing && error("Invalid chunked bridge response")
|
|
213
|
+
chunk_size = parse(Int, split(String(bytes[cursor:header_end - 1]), ";"; limit=2)[1]; base=16)
|
|
214
|
+
cursor = header_end + 2
|
|
215
|
+
chunk_size == 0 && break
|
|
216
|
+
cursor + chunk_size - 1 <= length(bytes) || error("Truncated chunked bridge response")
|
|
217
|
+
append!(output, bytes[cursor:cursor + chunk_size - 1])
|
|
218
|
+
cursor += chunk_size + 2
|
|
219
|
+
end
|
|
220
|
+
String(output)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
function senpi_bridge_request(path::String, payload)
|
|
224
|
+
port = get(senpi_connection, "port", nothing)
|
|
225
|
+
token = get(senpi_connection, "token", nothing)
|
|
226
|
+
port isa Integer && token isa AbstractString || error("Julia tool bridge is not initialized")
|
|
227
|
+
body = senpi_json(payload)
|
|
228
|
+
socket = connect(ip"127.0.0.1", port)
|
|
229
|
+
try
|
|
230
|
+
request = join([
|
|
231
|
+
"POST " * path * " HTTP/1.1",
|
|
232
|
+
"Host: 127.0.0.1",
|
|
233
|
+
"Authorization: Bearer " * string(token),
|
|
234
|
+
"Content-Type: application/json",
|
|
235
|
+
"Content-Length: " * string(sizeof(body)),
|
|
236
|
+
"Connection: close",
|
|
237
|
+
"",
|
|
238
|
+
body,
|
|
239
|
+
], "\r\n")
|
|
240
|
+
Base.write(socket, request)
|
|
241
|
+
flush(socket)
|
|
242
|
+
response = Base.read(socket, String)
|
|
243
|
+
parsed = senpi_json_parse(senpi_http_body(response))
|
|
244
|
+
parsed isa AbstractDict || error("Bridge returned invalid JSON")
|
|
245
|
+
get(parsed, "ok", false) === true && return get(parsed, "value", nothing)
|
|
246
|
+
failure = get(parsed, "error", parsed)
|
|
247
|
+
error(failure isa AbstractDict ? string(get(failure, "message", failure)) : string(failure))
|
|
248
|
+
finally
|
|
249
|
+
close(socket)
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
function senpi_call_tool(name::String, arguments)
|
|
254
|
+
senpi_bridge_request("/call", Dict("callId" => "jl-" * string(time_ns()), "toolName" => name, "args" => arguments))
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
function senpi_completion(prompt::String, options)
|
|
258
|
+
senpi_bridge_request("/completion", Dict("prompt" => prompt, "opts" => options))
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
function senpi_error(error)
|
|
262
|
+
message = sprint(showerror, error)
|
|
263
|
+
Dict("name" => string(typeof(error)), "message" => message)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
function senpi_should_display_result(parsed)
|
|
267
|
+
if parsed isa Expr && parsed.head === :block && !isempty(parsed.args)
|
|
268
|
+
last = parsed.args[end]
|
|
269
|
+
if last isa Expr && last.head in [Symbol("="), :function, :struct, :using, :import, :const, :global, :local, :macro]
|
|
270
|
+
return false
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
true
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
function senpi_set_connection(value)
|
|
277
|
+
value isa AbstractDict || error("missing bridge connection")
|
|
278
|
+
empty!(senpi_connection)
|
|
279
|
+
for (key, item) in value
|
|
280
|
+
senpi_connection[string(key)] = item
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
function senpi_run_cell(message)
|
|
285
|
+
cell_id = string(get(message, "cellId", ""))
|
|
286
|
+
code = string(get(message, "code", ""))
|
|
287
|
+
started = time()
|
|
288
|
+
global senpi_current_cell = cell_id
|
|
289
|
+
try
|
|
290
|
+
parsed = Meta.parse("begin\n" * code * "\nend")
|
|
291
|
+
if parsed isa Expr && parsed.head === :error
|
|
292
|
+
error(string(parsed.args[1]))
|
|
293
|
+
end
|
|
294
|
+
value = Core.eval(Main, parsed)
|
|
295
|
+
flush(stdout)
|
|
296
|
+
flush(stderr)
|
|
297
|
+
yield()
|
|
298
|
+
frame = Dict{String, Any}("type" => "result", "cellId" => cell_id, "ok" => true, "durationMs" => round(Int, (time() - started) * 1000))
|
|
299
|
+
value !== nothing && senpi_should_display_result(parsed) && (frame["valueRepr"] = senpi_json(value))
|
|
300
|
+
senpi_emit(frame)
|
|
301
|
+
catch error
|
|
302
|
+
senpi_emit(Dict("type" => "result", "cellId" => cell_id, "ok" => false, "error" => senpi_error(error), "durationMs" => round(Int, (time() - started) * 1000)))
|
|
303
|
+
finally
|
|
304
|
+
global senpi_current_cell = nothing
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
while !eof(SENPI_ORIGINAL_STDIN)
|
|
309
|
+
line = readline(SENPI_ORIGINAL_STDIN)
|
|
310
|
+
isempty(line) && continue
|
|
311
|
+
try
|
|
312
|
+
message = senpi_json_parse(line)
|
|
313
|
+
message isa AbstractDict || error("Bridge frame must be an object")
|
|
314
|
+
kind = get(message, "type", nothing)
|
|
315
|
+
if kind == "init"
|
|
316
|
+
senpi_set_connection(get(message, "connection", nothing))
|
|
317
|
+
senpi_emit(Dict("type" => "ready"))
|
|
318
|
+
elseif kind == "run"
|
|
319
|
+
senpi_run_cell(message)
|
|
320
|
+
elseif kind == "close"
|
|
321
|
+
senpi_emit(Dict("type" => "closed"))
|
|
322
|
+
break
|
|
323
|
+
end
|
|
324
|
+
catch error
|
|
325
|
+
senpi_emit(Dict("type" => "init-failed", "error" => senpi_error(error)))
|
|
326
|
+
end
|
|
327
|
+
end
|