@oh-my-pi/pi-coding-agent 16.1.14 → 16.1.16
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 +69 -0
- package/dist/cli.js +3705 -4015
- package/dist/types/advisor/runtime.d.ts +3 -0
- package/dist/types/cli/args.d.ts +2 -5
- package/dist/types/cli/flag-tables.d.ts +2 -2
- package/dist/types/cli/session-picker.d.ts +4 -2
- package/dist/types/commands/launch.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +32 -1
- package/dist/types/eval/agent-bridge.d.ts +19 -0
- package/dist/types/eval/js/shared/helpers.d.ts +1 -13
- package/dist/types/eval/js/shared/types.d.ts +1 -1
- package/dist/types/eval/js/worker-protocol.d.ts +1 -1
- package/dist/types/eval/py/executor.d.ts +1 -1
- package/dist/types/export/share.d.ts +8 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -1
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/transports/stdio.d.ts +12 -1
- package/dist/types/modes/components/plugin-settings.d.ts +5 -0
- package/dist/types/modes/components/session-selector.d.ts +25 -0
- package/dist/types/modes/components/status-line/context-thresholds.d.ts +4 -3
- package/dist/types/modes/components/status-line/types.d.ts +1 -0
- package/dist/types/secrets/obfuscator.d.ts +3 -3
- package/dist/types/task/isolation-runner.d.ts +128 -0
- package/dist/types/task/worktree.d.ts +14 -1
- package/dist/types/thinking.d.ts +15 -0
- package/dist/types/tools/eval-render.d.ts +3 -0
- package/dist/types/tools/eval.d.ts +11 -17
- package/dist/types/tools/todo.d.ts +26 -28
- package/dist/types/tui/output-block.d.ts +8 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/shell-snapshot.d.ts +10 -0
- package/dist/types/web/search/providers/exa.d.ts +2 -0
- package/dist/types/web/search/providers/perplexity.d.ts +17 -3
- package/package.json +12 -12
- package/scripts/build-binary.ts +18 -4
- package/src/advisor/__tests__/advisor.test.ts +114 -0
- package/src/advisor/runtime.ts +129 -1
- package/src/cli/args.ts +4 -5
- package/src/cli/flag-tables.ts +3 -3
- package/src/cli/gallery-fixtures/interaction.ts +6 -9
- package/src/cli/gallery-fixtures/shell.ts +15 -23
- package/src/cli/session-picker.ts +17 -3
- package/src/commands/launch.ts +3 -3
- package/src/config/model-registry.ts +12 -4
- package/src/config/settings-schema.ts +37 -1
- package/src/edit/renderer.ts +34 -12
- package/src/eval/__tests__/agent-bridge.test.ts +462 -3
- package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
- package/src/eval/__tests__/julia-prelude.test.ts +1 -30
- package/src/eval/__tests__/prelude-agent.test.ts +42 -8
- package/src/eval/agent-bridge.ts +301 -71
- package/src/eval/jl/prelude.jl +32 -227
- package/src/eval/jl/runner.jl +38 -12
- package/src/eval/js/shared/helpers.ts +1 -114
- package/src/eval/js/shared/prelude.txt +13 -27
- package/src/eval/js/shared/runtime.ts +0 -6
- package/src/eval/js/shared/types.ts +1 -1
- package/src/eval/js/worker-protocol.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +13 -0
- package/src/eval/py/executor.ts +1 -1
- package/src/eval/py/prelude.py +47 -105
- package/src/eval/py/runner.py +0 -6
- package/src/eval/rb/prelude.rb +21 -189
- package/src/eval/rb/runner.rb +116 -9
- package/src/exec/bash-executor.ts +44 -0
- package/src/export/html/tool-views.generated.js +29 -29
- package/src/export/share.ts +51 -28
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/local-protocol.ts +100 -53
- package/src/main.ts +15 -4
- package/src/mcp/transports/stdio.ts +20 -4
- package/src/modes/acp/acp-event-mapper.ts +7 -2
- package/src/modes/components/custom-editor.test.ts +22 -0
- package/src/modes/components/custom-editor.ts +10 -1
- package/src/modes/components/footer.ts +4 -3
- package/src/modes/components/plugin-settings.ts +7 -1
- package/src/modes/components/session-selector.ts +143 -29
- package/src/modes/components/status-line/component.ts +5 -1
- package/src/modes/components/status-line/context-thresholds.ts +11 -3
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/controllers/command-controller.ts +6 -0
- package/src/modes/rpc/rpc-mode.ts +6 -0
- package/src/modes/utils/copy-targets.ts +7 -2
- package/src/prompts/system/system-prompt.md +6 -6
- package/src/prompts/system/workflow-notice.md +3 -3
- package/src/prompts/tools/bash.md +18 -2
- package/src/prompts/tools/eval.md +19 -19
- package/src/prompts/tools/search.md +1 -0
- package/src/prompts/tools/todo.md +1 -1
- package/src/secrets/obfuscator.ts +3 -9
- package/src/session/agent-session.ts +264 -52
- package/src/slash-commands/builtin-registry.ts +2 -1
- package/src/task/index.ts +61 -207
- package/src/task/isolation-runner.ts +354 -0
- package/src/task/worktree.ts +46 -9
- package/src/thinking.ts +20 -0
- package/src/tools/ask.ts +44 -38
- package/src/tools/bash.ts +9 -2
- package/src/tools/browser/tab-worker.ts +1 -1
- package/src/tools/eval-render.ts +34 -27
- package/src/tools/eval.ts +100 -103
- package/src/tools/index.ts +8 -1
- package/src/tools/read.ts +136 -60
- package/src/tools/todo.ts +60 -64
- package/src/tui/code-cell.ts +1 -1
- package/src/tui/output-block.ts +11 -0
- package/src/utils/image-resize.ts +30 -0
- package/src/utils/shell-snapshot.ts +63 -1
- package/src/web/search/providers/exa.ts +85 -1
- package/src/web/search/providers/perplexity.ts +18 -6
package/src/eval/jl/prelude.jl
CHANGED
|
@@ -146,221 +146,6 @@ function Base.write(path::AbstractString, content::Any)
|
|
|
146
146
|
return resolved
|
|
147
147
|
end
|
|
148
148
|
|
|
149
|
-
function append(path, content)
|
|
150
|
-
resolved = __omp_resolve_path(string(path))
|
|
151
|
-
mkpath(dirname(resolved))
|
|
152
|
-
open(resolved, "a") do f
|
|
153
|
-
Base.write(f, string(content))
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
Main.emit_frame(Dict(
|
|
157
|
-
"type" => "display",
|
|
158
|
-
"id" => Main.current_rid,
|
|
159
|
-
"bundle" => Dict(
|
|
160
|
-
"application/x-omp-status" => Dict(
|
|
161
|
-
"op" => "append",
|
|
162
|
-
"path" => resolved,
|
|
163
|
-
"chars" => length(string(content))
|
|
164
|
-
)
|
|
165
|
-
)
|
|
166
|
-
))
|
|
167
|
-
return resolved
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
function tree(path=".", positional_max_depth=3, positional_show_hidden=false; max_depth=positional_max_depth, show_hidden=positional_show_hidden)
|
|
171
|
-
base = string(path)
|
|
172
|
-
resolved = __omp_resolve_path(base)
|
|
173
|
-
lines = String[]
|
|
174
|
-
|
|
175
|
-
function walk(dir, prefix, depth)
|
|
176
|
-
if depth > max_depth
|
|
177
|
-
return
|
|
178
|
-
end
|
|
179
|
-
entries = try
|
|
180
|
-
readdir(dir)
|
|
181
|
-
catch
|
|
182
|
-
String[]
|
|
183
|
-
end
|
|
184
|
-
if !show_hidden
|
|
185
|
-
entries = filter(e -> !startswith(e, '.'), entries)
|
|
186
|
-
end
|
|
187
|
-
sort!(entries, by = e -> (ispath(joinpath(dir, e)) && isdir(joinpath(dir, e)) ? 0 : 1, lowercase(e)))
|
|
188
|
-
|
|
189
|
-
for (i, name) in enumerate(entries)
|
|
190
|
-
full = joinpath(dir, name)
|
|
191
|
-
is_last = i == length(entries)
|
|
192
|
-
is_dir = isdir(full)
|
|
193
|
-
push!(lines, "$(prefix)$(is_last ? "└── " : "├── ")$(name)$(is_dir ? "/" : "")")
|
|
194
|
-
if is_dir
|
|
195
|
-
walk(full, prefix * (is_last ? " " : "│ "), depth + 1)
|
|
196
|
-
end
|
|
197
|
-
end
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
walk(resolved, "", 1)
|
|
201
|
-
out = join(lines, '\n')
|
|
202
|
-
|
|
203
|
-
Main.emit_frame(Dict(
|
|
204
|
-
"type" => "display",
|
|
205
|
-
"id" => Main.current_rid,
|
|
206
|
-
"bundle" => Dict(
|
|
207
|
-
"application/x-omp-status" => Dict(
|
|
208
|
-
"op" => "tree",
|
|
209
|
-
"path" => resolved,
|
|
210
|
-
"lines" => length(lines)
|
|
211
|
-
)
|
|
212
|
-
)
|
|
213
|
-
))
|
|
214
|
-
return out
|
|
215
|
-
end
|
|
216
|
-
|
|
217
|
-
function __omp_lines_keepends(content::String)
|
|
218
|
-
parts = split(content, '\n'; keepempty=true)
|
|
219
|
-
if length(parts) == 1 && isempty(parts[1])
|
|
220
|
-
return String[]
|
|
221
|
-
end
|
|
222
|
-
lines = String[]
|
|
223
|
-
for i in eachindex(parts)
|
|
224
|
-
if i < length(parts)
|
|
225
|
-
push!(lines, string(parts[i], "\n"))
|
|
226
|
-
elseif !isempty(parts[i])
|
|
227
|
-
push!(lines, string(parts[i]))
|
|
228
|
-
end
|
|
229
|
-
end
|
|
230
|
-
return lines
|
|
231
|
-
end
|
|
232
|
-
|
|
233
|
-
function __omp_diff_ops(a::Vector{String}, b::Vector{String})
|
|
234
|
-
n = length(a)
|
|
235
|
-
m = length(b)
|
|
236
|
-
ops = Vector{Tuple{Symbol, Int, Int}}()
|
|
237
|
-
if n * m > 4_000_000
|
|
238
|
-
for i in 1:n
|
|
239
|
-
push!(ops, (:delete, i, 1))
|
|
240
|
-
end
|
|
241
|
-
for j in 1:m
|
|
242
|
-
push!(ops, (:insert, n + 1, j))
|
|
243
|
-
end
|
|
244
|
-
return ops
|
|
245
|
-
end
|
|
246
|
-
|
|
247
|
-
dp = [zeros(Int, m + 1) for _ in 1:(n + 1)]
|
|
248
|
-
for i in n:-1:1
|
|
249
|
-
for j in m:-1:1
|
|
250
|
-
dp[i][j] = a[i] == b[j] ? dp[i + 1][j + 1] + 1 : max(dp[i + 1][j], dp[i][j + 1])
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
i = 1
|
|
255
|
-
j = 1
|
|
256
|
-
while i <= n && j <= m
|
|
257
|
-
if a[i] == b[j]
|
|
258
|
-
push!(ops, (:equal, i, j))
|
|
259
|
-
i += 1
|
|
260
|
-
j += 1
|
|
261
|
-
elseif dp[i + 1][j] >= dp[i][j + 1]
|
|
262
|
-
push!(ops, (:delete, i, j))
|
|
263
|
-
i += 1
|
|
264
|
-
else
|
|
265
|
-
push!(ops, (:insert, i, j))
|
|
266
|
-
j += 1
|
|
267
|
-
end
|
|
268
|
-
end
|
|
269
|
-
while i <= n
|
|
270
|
-
push!(ops, (:delete, i, j))
|
|
271
|
-
i += 1
|
|
272
|
-
end
|
|
273
|
-
while j <= m
|
|
274
|
-
push!(ops, (:insert, i, j))
|
|
275
|
-
j += 1
|
|
276
|
-
end
|
|
277
|
-
return ops
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
function __omp_unified_diff(a::Vector{String}, b::Vector{String}, from_file::String, to_file::String, context::Int=3)
|
|
281
|
-
ops = __omp_diff_ops(a, b)
|
|
282
|
-
if !any(op -> op[1] != :equal, ops)
|
|
283
|
-
return ""
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
entries = [Dict{Symbol, Any}(:tag => tag, :ai => ai, :bi => bi, :text => tag == :insert ? b[bi] : a[ai]) for (tag, ai, bi) in ops]
|
|
287
|
-
changed = [i for i in eachindex(entries) if entries[i][:tag] != :equal]
|
|
288
|
-
groups = Vector{Tuple{Int, Int}}()
|
|
289
|
-
start = nothing
|
|
290
|
-
prev = nothing
|
|
291
|
-
for idx in changed
|
|
292
|
-
if start === nothing
|
|
293
|
-
start = idx
|
|
294
|
-
prev = idx
|
|
295
|
-
elseif idx - prev <= (2 * context) + 1
|
|
296
|
-
prev = idx
|
|
297
|
-
else
|
|
298
|
-
push!(groups, (start, prev))
|
|
299
|
-
start = idx
|
|
300
|
-
prev = idx
|
|
301
|
-
end
|
|
302
|
-
end
|
|
303
|
-
if start !== nothing
|
|
304
|
-
push!(groups, (start, prev))
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
out = IOBuffer()
|
|
308
|
-
write(out, "--- $from_file\n")
|
|
309
|
-
write(out, "+++ $to_file\n")
|
|
310
|
-
for (group_start, group_end) in groups
|
|
311
|
-
lo = max(group_start - context, 1)
|
|
312
|
-
hi = min(group_end + context, length(entries))
|
|
313
|
-
slice = entries[lo:hi]
|
|
314
|
-
a_start = nothing
|
|
315
|
-
a_count = 0
|
|
316
|
-
b_start = nothing
|
|
317
|
-
b_count = 0
|
|
318
|
-
for entry in slice
|
|
319
|
-
if entry[:tag] != :insert
|
|
320
|
-
if a_start === nothing
|
|
321
|
-
a_start = entry[:ai]
|
|
322
|
-
end
|
|
323
|
-
a_count += 1
|
|
324
|
-
end
|
|
325
|
-
if entry[:tag] != :delete
|
|
326
|
-
if b_start === nothing
|
|
327
|
-
b_start = entry[:bi]
|
|
328
|
-
end
|
|
329
|
-
b_count += 1
|
|
330
|
-
end
|
|
331
|
-
end
|
|
332
|
-
write(out, "@@ -$(a_start === nothing ? 1 : a_start),$a_count +$(b_start === nothing ? 1 : b_start),$b_count @@\n")
|
|
333
|
-
for entry in slice
|
|
334
|
-
prefix = entry[:tag] == :equal ? " " : (entry[:tag] == :delete ? "-" : "+")
|
|
335
|
-
text = string(entry[:text])
|
|
336
|
-
if !endswith(text, "\n")
|
|
337
|
-
text *= "\n"
|
|
338
|
-
end
|
|
339
|
-
write(out, prefix * text)
|
|
340
|
-
end
|
|
341
|
-
end
|
|
342
|
-
return String(take!(out))
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
function Base.diff(a::AbstractString, b::AbstractString)
|
|
346
|
-
path_a = __omp_resolve_path(string(a))
|
|
347
|
-
path_b = __omp_resolve_path(string(b))
|
|
348
|
-
lines_a = __omp_lines_keepends(open(path_a, "r") do io
|
|
349
|
-
Base.read(io, String)
|
|
350
|
-
end)
|
|
351
|
-
lines_b = __omp_lines_keepends(open(path_b, "r") do io
|
|
352
|
-
Base.read(io, String)
|
|
353
|
-
end)
|
|
354
|
-
out = __omp_unified_diff(lines_a, lines_b, path_a, path_b)
|
|
355
|
-
__omp_emit_status("diff", Dict{String, Any}(
|
|
356
|
-
"file_a" => path_a,
|
|
357
|
-
"file_b" => path_b,
|
|
358
|
-
"identical" => isempty(out),
|
|
359
|
-
"preview" => first(out, min(500, length(out)))
|
|
360
|
-
))
|
|
361
|
-
return out
|
|
362
|
-
end
|
|
363
|
-
|
|
364
149
|
function __omp_apply_query(data, query)
|
|
365
150
|
if query === nothing || isempty(string(query))
|
|
366
151
|
return data
|
|
@@ -734,10 +519,10 @@ function completion(prompt::String; model="default", system=nothing, schema=noth
|
|
|
734
519
|
return schema === nothing ? text : Main.json_parse(string(text))
|
|
735
520
|
end
|
|
736
521
|
|
|
737
|
-
function agent(prompt::String;
|
|
522
|
+
function agent(prompt::String; agent="task", model=nothing, label=nothing, schema=nothing, isolated=nothing, apply=nothing, merge=nothing, handle=false, kwargs...)
|
|
738
523
|
args_dict = Dict{String, Any}("prompt" => prompt)
|
|
739
|
-
if
|
|
740
|
-
args_dict["
|
|
524
|
+
if agent !== nothing
|
|
525
|
+
args_dict["agent"] = agent
|
|
741
526
|
end
|
|
742
527
|
if model !== nothing
|
|
743
528
|
args_dict["model"] = model
|
|
@@ -748,16 +533,24 @@ function agent(prompt::String; agent_type="task", model=nothing, label=nothing,
|
|
|
748
533
|
if schema !== nothing
|
|
749
534
|
args_dict["schema"] = schema
|
|
750
535
|
end
|
|
751
|
-
|
|
536
|
+
# Isolation knobs mirror the `task` tool: strict opt-in via `isolated`,
|
|
537
|
+
# with `apply`/`merge` controlling the post-run patch/branch merge.
|
|
538
|
+
if isolated !== nothing
|
|
539
|
+
args_dict["isolated"] = Bool(isolated)
|
|
540
|
+
end
|
|
541
|
+
if apply !== nothing
|
|
542
|
+
args_dict["apply"] = Bool(apply)
|
|
543
|
+
end
|
|
544
|
+
if merge !== nothing
|
|
545
|
+
args_dict["merge"] = Bool(merge)
|
|
546
|
+
end
|
|
547
|
+
handle_result = handle
|
|
752
548
|
for (k, v) in kwargs
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
else
|
|
759
|
-
args_dict[key] = v
|
|
760
|
-
end
|
|
549
|
+
args_dict[string(k)] = v
|
|
550
|
+
end
|
|
551
|
+
# Tell the bridge a handle is wanted so it preserves the backing artifacts.
|
|
552
|
+
if handle_result
|
|
553
|
+
args_dict["handle"] = true
|
|
761
554
|
end
|
|
762
555
|
res = __omp_call_bridge("__agent__", args_dict)
|
|
763
556
|
text = res isa AbstractDict ? get(res, "text", res) : res
|
|
@@ -779,6 +572,18 @@ function agent(prompt::String; agent_type="task", model=nothing, label=nothing,
|
|
|
779
572
|
if schema !== nothing
|
|
780
573
|
node["data"] = parsed
|
|
781
574
|
end
|
|
575
|
+
for (src_key, dst_key) in (
|
|
576
|
+
("isolated", "isolated"),
|
|
577
|
+
("patchPath", "patch_path"),
|
|
578
|
+
("branchName", "branch_name"),
|
|
579
|
+
("nestedPatches", "nested_patches"),
|
|
580
|
+
("changesApplied", "changes_applied"),
|
|
581
|
+
("isolationSummary", "isolation_summary"),
|
|
582
|
+
)
|
|
583
|
+
if haskey(details, src_key)
|
|
584
|
+
node[dst_key] = details[src_key]
|
|
585
|
+
end
|
|
586
|
+
end
|
|
782
587
|
return node
|
|
783
588
|
end
|
|
784
589
|
|
package/src/eval/jl/runner.jl
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
using Base64
|
|
5
5
|
|
|
6
|
+
# Force GR (the default Plots.jl backend) into a headless workstation so a plot
|
|
7
|
+
# never pops up a native gksqt GUI window — the harness renders the inline PNG
|
|
8
|
+
# from `show(io, MIME"image/png", plt)` itself. `get!` keeps an explicit
|
|
9
|
+
# user-provided value, mirroring the Python runner's MPLBACKEND=Agg default.
|
|
10
|
+
get!(ENV, "GKSwstype", "100")
|
|
11
|
+
|
|
6
12
|
const ORIGINAL_STDOUT = stdout
|
|
7
13
|
const ORIGINAL_STDERR = stderr
|
|
8
14
|
const ORIGINAL_STDIN = stdin
|
|
@@ -441,24 +447,38 @@ end
|
|
|
441
447
|
|
|
442
448
|
function build_mime_bundle(value)
|
|
443
449
|
bundle = Dict{String, Any}()
|
|
444
|
-
|
|
445
|
-
# text/plain
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
+
|
|
451
|
+
# text/plain — every mime probe below uses `Base.invokelatest` because this
|
|
452
|
+
# function runs from the frozen-world `main()` loop: `show`/`showable`
|
|
453
|
+
# methods that a package adds when it is `using`-ed *inside* a cell (e.g.
|
|
454
|
+
# Plots/Makie/GraphRecipes registering rich `show` for their plot types) are
|
|
455
|
+
# invisible to direct dispatch here and fall back to the default struct show,
|
|
456
|
+
# which can itself throw. Guard text/plain too so a failing repr never aborts
|
|
457
|
+
# the whole bundle before the image mime is reached.
|
|
458
|
+
try
|
|
459
|
+
io_plain = IOBuffer()
|
|
460
|
+
Base.invokelatest(show, io_plain, MIME"text/plain"(), value)
|
|
461
|
+
bundle["text/plain"] = String(take!(io_plain))
|
|
462
|
+
catch
|
|
463
|
+
bundle["text/plain"] = try
|
|
464
|
+
summary(value)
|
|
465
|
+
catch
|
|
466
|
+
string(typeof(value))
|
|
467
|
+
end
|
|
468
|
+
end
|
|
469
|
+
|
|
450
470
|
# rich mime types
|
|
451
471
|
for mime_str in ["text/html", "text/markdown", "image/png", "image/jpeg"]
|
|
452
472
|
m = MIME(Symbol(mime_str))
|
|
453
|
-
if showable
|
|
473
|
+
if Base.invokelatest(showable, m, value)
|
|
454
474
|
try
|
|
455
475
|
io = IOBuffer()
|
|
456
476
|
if mime_str in ["image/png", "image/jpeg"]
|
|
457
477
|
b64_io = Base64EncodePipe(io)
|
|
458
|
-
show
|
|
478
|
+
Base.invokelatest(show, b64_io, m, value)
|
|
459
479
|
close(b64_io)
|
|
460
480
|
else
|
|
461
|
-
show
|
|
481
|
+
Base.invokelatest(show, io, m, value)
|
|
462
482
|
end
|
|
463
483
|
bundle[mime_str] = String(take!(io))
|
|
464
484
|
catch
|
|
@@ -466,7 +486,7 @@ function build_mime_bundle(value)
|
|
|
466
486
|
end
|
|
467
487
|
end
|
|
468
488
|
end
|
|
469
|
-
|
|
489
|
+
|
|
470
490
|
if value isa AbstractDict || value isa AbstractVector
|
|
471
491
|
try
|
|
472
492
|
bundle["application/json"] = value
|
|
@@ -474,7 +494,7 @@ function build_mime_bundle(value)
|
|
|
474
494
|
# ignore
|
|
475
495
|
end
|
|
476
496
|
end
|
|
477
|
-
|
|
497
|
+
|
|
478
498
|
return bundle
|
|
479
499
|
end
|
|
480
500
|
|
|
@@ -493,7 +513,13 @@ pushdisplay(OmpDisplay())
|
|
|
493
513
|
|
|
494
514
|
function emit_error(rid, err, bt)
|
|
495
515
|
io = IOBuffer()
|
|
496
|
-
|
|
516
|
+
# invokelatest + guard: custom error types from packages loaded inside the
|
|
517
|
+
# cell define `showerror` methods invisible to this frozen-world function.
|
|
518
|
+
try
|
|
519
|
+
Base.invokelatest(showerror, io, err)
|
|
520
|
+
catch
|
|
521
|
+
print(io, string(err))
|
|
522
|
+
end
|
|
497
523
|
err_str = String(take!(io))
|
|
498
524
|
|
|
499
525
|
tb = String[]
|
|
@@ -1,19 +1,11 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
1
|
import * as path from "node:path";
|
|
3
2
|
|
|
4
|
-
import * as Diff from "diff";
|
|
5
3
|
import { ToolError } from "../../../tools/tool-errors";
|
|
6
4
|
import type { JsStatusEvent } from "./types";
|
|
7
5
|
|
|
8
6
|
export interface HelperOptions {
|
|
9
|
-
path?: string;
|
|
10
|
-
hidden?: boolean;
|
|
11
|
-
maxDepth?: number;
|
|
12
7
|
limit?: number;
|
|
13
8
|
offset?: number;
|
|
14
|
-
reverse?: boolean;
|
|
15
|
-
unique?: boolean;
|
|
16
|
-
count?: boolean;
|
|
17
9
|
}
|
|
18
10
|
|
|
19
11
|
/**
|
|
@@ -35,18 +27,12 @@ export interface HelperContext {
|
|
|
35
27
|
|
|
36
28
|
/**
|
|
37
29
|
* The set of functions exposed to user code via `globalThis.__omp_helpers__`. The JS
|
|
38
|
-
* prelude reads from this bag and attaches short aliases (`read`, `write`, `
|
|
30
|
+
* prelude reads from this bag and attaches short aliases (`read`, `write`, `env`, ...)
|
|
39
31
|
* onto the global scope.
|
|
40
32
|
*/
|
|
41
33
|
export interface HelperBundle {
|
|
42
34
|
read(rawPath: string, options?: HelperOptions): Promise<string>;
|
|
43
35
|
writeFile(rawPath: string, data: unknown): Promise<string>;
|
|
44
|
-
append(rawPath: string, content: string): Promise<string>;
|
|
45
|
-
sortText(text: string, options?: HelperOptions): string;
|
|
46
|
-
uniqText(text: string, options?: HelperOptions): string | Array<[number, string]>;
|
|
47
|
-
counter(items: string | string[], options?: HelperOptions): Array<[number, string]>;
|
|
48
|
-
diff(rawA: string, rawB: string): Promise<string>;
|
|
49
|
-
tree(searchPath?: string, options?: HelperOptions): Promise<string>;
|
|
50
36
|
env(key?: string, value?: string): string | Record<string, string> | undefined;
|
|
51
37
|
}
|
|
52
38
|
|
|
@@ -81,105 +67,6 @@ export function createHelpers(ctx: HelperContext): HelperBundle {
|
|
|
81
67
|
ctx.emitStatus({ op: "write", path: filePath, bytes: getDataSize(data) });
|
|
82
68
|
return filePath;
|
|
83
69
|
},
|
|
84
|
-
append: async (rawPath, content) => {
|
|
85
|
-
const target = resolveHelperPath(ctx, rawPath, "write");
|
|
86
|
-
// O(1) append; read-all+rewrite both raced concurrent writers and went
|
|
87
|
-
// quadratic when called in a loop. Bun.write creates parent dirs, so
|
|
88
|
-
// keep that behavior for the append path too.
|
|
89
|
-
await fs.promises.mkdir(path.dirname(target), { recursive: true });
|
|
90
|
-
await fs.promises.appendFile(target, content, "utf-8");
|
|
91
|
-
ctx.emitStatus({
|
|
92
|
-
op: "append",
|
|
93
|
-
path: target,
|
|
94
|
-
chars: content.length,
|
|
95
|
-
bytes: utf8Encoder.encode(content).byteLength,
|
|
96
|
-
});
|
|
97
|
-
return target;
|
|
98
|
-
},
|
|
99
|
-
sortText: (text, options = {}) => {
|
|
100
|
-
const lines = String(text).split(/\r?\n/);
|
|
101
|
-
const deduped = options.unique ? Array.from(new Set(lines)) : lines;
|
|
102
|
-
const sorted = deduped.sort((a, b) => a.localeCompare(b));
|
|
103
|
-
if (options.reverse) sorted.reverse();
|
|
104
|
-
const result = sorted.join("\n");
|
|
105
|
-
ctx.emitStatus({
|
|
106
|
-
op: "sort",
|
|
107
|
-
lines: sorted.length,
|
|
108
|
-
reverse: options.reverse === true,
|
|
109
|
-
unique: options.unique === true,
|
|
110
|
-
});
|
|
111
|
-
return result;
|
|
112
|
-
},
|
|
113
|
-
uniqText: (text, options = {}) => {
|
|
114
|
-
const lines = String(text)
|
|
115
|
-
.split(/\r?\n/)
|
|
116
|
-
.filter(line => line.length > 0);
|
|
117
|
-
const groups: Array<[number, string]> = [];
|
|
118
|
-
for (const line of lines) {
|
|
119
|
-
const last = groups.at(-1);
|
|
120
|
-
if (last && last[1] === line) {
|
|
121
|
-
last[0] += 1;
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
groups.push([1, line]);
|
|
125
|
-
}
|
|
126
|
-
ctx.emitStatus({ op: "uniq", groups: groups.length, count_mode: options.count === true });
|
|
127
|
-
if (options.count) return groups;
|
|
128
|
-
return groups.map(([, line]) => line).join("\n");
|
|
129
|
-
},
|
|
130
|
-
counter: (items, options = {}) => {
|
|
131
|
-
const values = Array.isArray(items) ? items : String(items).split(/\r?\n/).filter(Boolean);
|
|
132
|
-
const counts = new Map<string, number>();
|
|
133
|
-
for (const item of values) counts.set(item, (counts.get(item) ?? 0) + 1);
|
|
134
|
-
const entries = Array.from(counts.entries())
|
|
135
|
-
.map(([item, count]) => [count, item] as [number, string])
|
|
136
|
-
.sort((a, b) => (options.reverse === false ? a[0] - b[0] : b[0] - a[0]) || a[1].localeCompare(b[1]));
|
|
137
|
-
const limited = entries.slice(0, options.limit ?? entries.length);
|
|
138
|
-
ctx.emitStatus({ op: "counter", unique: counts.size, total: values.length, top: limited.slice(0, 10) });
|
|
139
|
-
return limited;
|
|
140
|
-
},
|
|
141
|
-
diff: async (rawA, rawB) => {
|
|
142
|
-
const fileA = resolvePath(ctx, rawA);
|
|
143
|
-
const fileB = resolvePath(ctx, rawB);
|
|
144
|
-
const [a, b] = await Promise.all([Bun.file(fileA).text(), Bun.file(fileB).text()]);
|
|
145
|
-
const result = Diff.createTwoFilesPatch(fileA, fileB, a, b, "", "", { context: 3 });
|
|
146
|
-
ctx.emitStatus({
|
|
147
|
-
op: "diff",
|
|
148
|
-
file_a: fileA,
|
|
149
|
-
file_b: fileB,
|
|
150
|
-
identical: a === b,
|
|
151
|
-
preview: result.slice(0, 500),
|
|
152
|
-
});
|
|
153
|
-
return result;
|
|
154
|
-
},
|
|
155
|
-
tree: async (searchPath = ".", options = {}) => {
|
|
156
|
-
const root = resolvePath(ctx, searchPath);
|
|
157
|
-
const maxDepth = options.maxDepth ?? 3;
|
|
158
|
-
const showHidden = options.hidden ?? false;
|
|
159
|
-
const lines: string[] = [`${root}/`];
|
|
160
|
-
let entryCount = 0;
|
|
161
|
-
const walk = async (dir: string, prefix: string, depth: number): Promise<void> => {
|
|
162
|
-
if (depth > maxDepth) return;
|
|
163
|
-
const entries = (await fs.promises.readdir(dir, { withFileTypes: true }))
|
|
164
|
-
.filter(entry => showHidden || !entry.name.startsWith("."))
|
|
165
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
166
|
-
for (let index = 0; index < entries.length; index++) {
|
|
167
|
-
const entry = entries[index];
|
|
168
|
-
const isLast = index === entries.length - 1;
|
|
169
|
-
const connector = isLast ? "└── " : "├── ";
|
|
170
|
-
const suffix = entry.isDirectory() ? "/" : "";
|
|
171
|
-
lines.push(`${prefix}${connector}${entry.name}${suffix}`);
|
|
172
|
-
entryCount += 1;
|
|
173
|
-
if (entry.isDirectory()) {
|
|
174
|
-
await walk(path.join(dir, entry.name), `${prefix}${isLast ? " " : "│ "}`, depth + 1);
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
|
-
await walk(root, "", 1);
|
|
179
|
-
const result = lines.join("\n");
|
|
180
|
-
ctx.emitStatus({ op: "tree", path: root, entries: entryCount, preview: result.slice(0, 1000) });
|
|
181
|
-
return result;
|
|
182
|
-
},
|
|
183
70
|
env: (key, value) => {
|
|
184
71
|
if (!key) {
|
|
185
72
|
const merged = Object.fromEntries(Object.entries(getMergedEnv(ctx)).sort(([a], [b]) => a.localeCompare(b)));
|
|
@@ -63,23 +63,6 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
63
63
|
return callHelper("read", path, options);
|
|
64
64
|
};
|
|
65
65
|
const write = async (path, data) => callHelper("writeFile", path, data);
|
|
66
|
-
const append = (path, content) => callHelper("append", path, content);
|
|
67
|
-
const sort = (text, opts, ...rest) =>
|
|
68
|
-
callHelper("sortText", text, optionsArg("sort", opts, rest, ["reverse", "unique"], "{ reverse, unique }"));
|
|
69
|
-
const uniq = (text, opts, ...rest) => callHelper("uniqText", text, optionsArg("uniq", opts, rest, ["count"], "{ count }"));
|
|
70
|
-
const counter = (items, opts, ...rest) =>
|
|
71
|
-
callHelper("counter", items, optionsArg("counter", opts, rest, ["limit", "reverse"], "{ limit, reverse }"));
|
|
72
|
-
const diff = (a, b) => callHelper("diff", a, b);
|
|
73
|
-
const tree = (path = ".", opts, ...rest) => {
|
|
74
|
-
if (isPlainObject(path) && opts === undefined && rest.length === 0) {
|
|
75
|
-
return callHelper("tree", ".", path);
|
|
76
|
-
}
|
|
77
|
-
return callHelper(
|
|
78
|
-
"tree",
|
|
79
|
-
isNil(path) ? "." : path,
|
|
80
|
-
optionsArg("tree", opts, rest, ["maxDepth", "showHidden"], "{ maxDepth, showHidden }"),
|
|
81
|
-
);
|
|
82
|
-
};
|
|
83
66
|
const env = (key, value) => callHelper("env", key, value);
|
|
84
67
|
|
|
85
68
|
const tool = new Proxy(
|
|
@@ -117,18 +100,27 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
117
100
|
};
|
|
118
101
|
|
|
119
102
|
const agent = async (prompt, opts, ...rest) => {
|
|
120
|
-
const o = optionsArg(
|
|
121
|
-
|
|
122
|
-
|
|
103
|
+
const o = optionsArg(
|
|
104
|
+
"agent",
|
|
105
|
+
opts,
|
|
106
|
+
rest,
|
|
107
|
+
["agent", "model", "label", "schema", "isolated", "apply", "merge"],
|
|
108
|
+
"{ agent, model, label, schema, isolated, apply, merge, handle }",
|
|
109
|
+
);
|
|
110
|
+
const { handle, ...callArgs } = o;
|
|
111
|
+
const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...callArgs, handle: Boolean(handle) });
|
|
123
112
|
const text = res && typeof res === "object" ? res.text : res;
|
|
124
113
|
const parsed = hasOwn(callArgs, "schema") ? JSON.parse(text) : text;
|
|
125
|
-
if (!
|
|
114
|
+
if (!handle) return parsed;
|
|
126
115
|
const details = res && typeof res === "object" ? res.details : undefined;
|
|
127
116
|
if (!details || typeof details !== "object" || details.id == null) {
|
|
128
117
|
return { text, output: text, handle: null, id: null, agent: null };
|
|
129
118
|
}
|
|
130
119
|
const node = { text, output: text, handle: `agent://${details.id}`, id: details.id, agent: details.agent ?? null };
|
|
131
120
|
if (hasOwn(callArgs, "schema")) node.data = parsed;
|
|
121
|
+
for (const key of ["isolated", "patchPath", "branchName", "nestedPatches", "changesApplied", "isolationSummary"]) {
|
|
122
|
+
if (details[key] !== undefined) node[key] = details[key];
|
|
123
|
+
}
|
|
132
124
|
return node;
|
|
133
125
|
};
|
|
134
126
|
|
|
@@ -297,11 +289,5 @@ if (!globalThis.__omp_js_prelude_loaded__) {
|
|
|
297
289
|
globalThis.__pool = __pool;
|
|
298
290
|
globalThis.read = read;
|
|
299
291
|
globalThis.write = write;
|
|
300
|
-
globalThis.append = append;
|
|
301
|
-
globalThis.sort = sort;
|
|
302
|
-
globalThis.uniq = uniq;
|
|
303
|
-
globalThis.counter = counter;
|
|
304
|
-
globalThis.diff = diff;
|
|
305
|
-
globalThis.tree = tree;
|
|
306
292
|
globalThis.env = env;
|
|
307
293
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Structured status payload emitted by helpers (`read`, `write`, `
|
|
2
|
+
* Structured status payload emitted by helpers (`read`, `write`, `env`, etc.) and the
|
|
3
3
|
* tool-call bridge. Surfaces to the model as part of `displays` so it has machine-readable
|
|
4
4
|
* context about what side effects happened.
|
|
5
5
|
*/
|
|
@@ -7,7 +7,7 @@ export interface SessionSnapshot {
|
|
|
7
7
|
sessionId: string;
|
|
8
8
|
/**
|
|
9
9
|
* On-disk roots the helpers substitute for internal-URL schemes
|
|
10
|
-
* (e.g. `{ local: "/…/artifacts/local" }`). Lets `read`/`write
|
|
10
|
+
* (e.g. `{ local: "/…/artifacts/local" }`). Lets `read`/`write`
|
|
11
11
|
* accept `local://…` paths instead of writing a literal `local:/` directory.
|
|
12
12
|
*/
|
|
13
13
|
localRoots?: Record<string, string>;
|
|
@@ -16,4 +16,17 @@ describe("python prelude", () => {
|
|
|
16
16
|
expect(signature).toContain("offset");
|
|
17
17
|
expect(signature).toContain("limit");
|
|
18
18
|
});
|
|
19
|
+
|
|
20
|
+
it("exposes isolation artifacts on the agent() handle node", () => {
|
|
21
|
+
// agent(..., handle=True) is the only escape hatch for
|
|
22
|
+
// recovering apply=False patch/branch/nested artifacts (the bare
|
|
23
|
+
// schema return is just the parsed object), so the helper MUST
|
|
24
|
+
// translate the bridge's camelCase details onto the node — otherwise
|
|
25
|
+
// an isolated apply=False workflow loses captured nested patches.
|
|
26
|
+
expect(PYTHON_PRELUDE).toContain('("patchPath", "patch_path")');
|
|
27
|
+
expect(PYTHON_PRELUDE).toContain('("branchName", "branch_name")');
|
|
28
|
+
expect(PYTHON_PRELUDE).toContain('("nestedPatches", "nested_patches")');
|
|
29
|
+
expect(PYTHON_PRELUDE).toContain('("changesApplied", "changes_applied")');
|
|
30
|
+
expect(PYTHON_PRELUDE).toContain('("isolationSummary", "isolation_summary")');
|
|
31
|
+
});
|
|
19
32
|
});
|
package/src/eval/py/executor.ts
CHANGED
|
@@ -71,7 +71,7 @@ export interface PythonExecutorOptions {
|
|
|
71
71
|
artifactPath?: string;
|
|
72
72
|
artifactId?: string;
|
|
73
73
|
/**
|
|
74
|
-
* On-disk roots the prelude helpers (`read`/`write
|
|
74
|
+
* On-disk roots the prelude helpers (`read`/`write`) substitute for
|
|
75
75
|
* internal-URL schemes (e.g. `{ local: "/…/artifacts/local" }`). Exported to
|
|
76
76
|
* the kernel as `PI_EVAL_LOCAL_ROOTS` (JSON) so `write("local://x")` lands
|
|
77
77
|
* where `read local://x` resolves instead of a literal `local:/` directory.
|