@oh-my-pi/pi-coding-agent 16.1.15 → 16.1.17

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/cli.js +3531 -3812
  3. package/dist/types/auto-thinking/classifier.d.ts +4 -2
  4. package/dist/types/cli/args.d.ts +2 -5
  5. package/dist/types/cli/flag-tables.d.ts +2 -2
  6. package/dist/types/cli/session-picker.d.ts +4 -2
  7. package/dist/types/commands/launch.d.ts +1 -1
  8. package/dist/types/config/model-discovery.d.ts +1 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/settings-schema.d.ts +12 -1
  11. package/dist/types/eval/agent-bridge.d.ts +19 -0
  12. package/dist/types/eval/js/shared/helpers.d.ts +1 -13
  13. package/dist/types/eval/js/shared/types.d.ts +1 -1
  14. package/dist/types/eval/js/worker-protocol.d.ts +1 -1
  15. package/dist/types/eval/py/executor.d.ts +1 -1
  16. package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +22 -7
  17. package/dist/types/extensibility/plugins/loader.d.ts +16 -9
  18. package/dist/types/extensibility/tool-event-input.d.ts +2 -0
  19. package/dist/types/hindsight/content.d.ts +2 -10
  20. package/dist/types/internal-urls/local-protocol.d.ts +18 -1
  21. package/dist/types/main.d.ts +2 -0
  22. package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +3 -0
  24. package/dist/types/modes/components/plugin-settings.d.ts +5 -0
  25. package/dist/types/modes/components/session-selector.d.ts +25 -0
  26. package/dist/types/modes/components/status-line/types.d.ts +1 -0
  27. package/dist/types/modes/controllers/command-controller.d.ts +2 -0
  28. package/dist/types/modes/controllers/input-controller.d.ts +14 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +11 -0
  30. package/dist/types/session/agent-session.d.ts +6 -6
  31. package/dist/types/session/provider-image-budget.d.ts +3 -0
  32. package/dist/types/session/session-context.d.ts +6 -5
  33. package/dist/types/task/isolation-runner.d.ts +128 -0
  34. package/dist/types/task/parallel.d.ts +4 -0
  35. package/dist/types/task/worktree.d.ts +14 -1
  36. package/dist/types/thinking.d.ts +23 -1
  37. package/dist/types/tiny/title-client.d.ts +2 -2
  38. package/dist/types/tools/eval-render.d.ts +3 -0
  39. package/dist/types/tools/eval.d.ts +11 -17
  40. package/dist/types/tools/todo.d.ts +26 -28
  41. package/dist/types/tui/output-block.d.ts +8 -0
  42. package/dist/types/utils/image-resize.d.ts +2 -0
  43. package/dist/types/utils/tools-manager.d.ts +2 -0
  44. package/dist/types/web/search/providers/exa.d.ts +2 -0
  45. package/package.json +12 -12
  46. package/scripts/build-binary.ts +18 -4
  47. package/src/auto-thinking/classifier.ts +7 -2
  48. package/src/cli/args.ts +4 -5
  49. package/src/cli/flag-tables.ts +3 -3
  50. package/src/cli/gallery-fixtures/interaction.ts +6 -9
  51. package/src/cli/gallery-fixtures/shell.ts +15 -23
  52. package/src/cli/profile-alias.ts +38 -7
  53. package/src/cli/session-picker.ts +17 -3
  54. package/src/cli/usage-cli.ts +5 -1
  55. package/src/commands/launch.ts +3 -3
  56. package/src/config/model-discovery.ts +59 -8
  57. package/src/config/model-registry.ts +74 -3
  58. package/src/config/settings-schema.ts +13 -1
  59. package/src/discovery/omp-extension-roots.ts +1 -3
  60. package/src/edit/renderer.ts +34 -12
  61. package/src/eval/__tests__/agent-bridge.test.ts +462 -3
  62. package/src/eval/__tests__/helpers-local-roots.test.ts +2 -5
  63. package/src/eval/__tests__/julia-prelude.test.ts +1 -30
  64. package/src/eval/__tests__/prelude-agent.test.ts +42 -8
  65. package/src/eval/agent-bridge.ts +301 -71
  66. package/src/eval/jl/prelude.jl +32 -227
  67. package/src/eval/jl/runner.jl +38 -12
  68. package/src/eval/js/shared/helpers.ts +1 -114
  69. package/src/eval/js/shared/prelude.txt +13 -27
  70. package/src/eval/js/shared/runtime.ts +0 -6
  71. package/src/eval/js/shared/types.ts +1 -1
  72. package/src/eval/js/worker-protocol.ts +1 -1
  73. package/src/eval/py/__tests__/prelude.test.ts +13 -0
  74. package/src/eval/py/executor.ts +1 -1
  75. package/src/eval/py/prelude.py +47 -105
  76. package/src/eval/py/runner.py +0 -6
  77. package/src/eval/rb/prelude.rb +21 -189
  78. package/src/eval/rb/runner.rb +116 -9
  79. package/src/export/html/tool-views.generated.js +29 -29
  80. package/src/extensibility/extensions/wrapper.ts +3 -2
  81. package/src/extensibility/hooks/tool-wrapper.ts +4 -3
  82. package/src/extensibility/plugins/legacy-pi-compat.ts +40 -8
  83. package/src/extensibility/plugins/loader.ts +71 -23
  84. package/src/extensibility/plugins/marketplace/manager.ts +134 -0
  85. package/src/extensibility/tool-event-input.ts +57 -0
  86. package/src/hindsight/content.ts +21 -12
  87. package/src/internal-urls/docs-index.generated.txt +1 -1
  88. package/src/internal-urls/local-protocol.ts +100 -53
  89. package/src/main.ts +15 -4
  90. package/src/mcp/tool-bridge.ts +27 -2
  91. package/src/mnemopi/state.ts +5 -2
  92. package/src/modes/acp/acp-event-mapper.ts +7 -2
  93. package/src/modes/components/agent-transcript-viewer.ts +193 -41
  94. package/src/modes/components/chat-transcript-builder.ts +6 -0
  95. package/src/modes/components/custom-editor.test.ts +18 -1
  96. package/src/modes/components/custom-editor.ts +77 -45
  97. package/src/modes/components/hook-editor.ts +15 -2
  98. package/src/modes/components/plugin-settings.ts +7 -1
  99. package/src/modes/components/session-selector.ts +143 -29
  100. package/src/modes/components/settings-selector.ts +2 -2
  101. package/src/modes/components/status-line/component.ts +52 -8
  102. package/src/modes/components/status-line/segments.ts +5 -1
  103. package/src/modes/components/status-line/types.ts +1 -0
  104. package/src/modes/components/welcome.ts +12 -14
  105. package/src/modes/controllers/command-controller.ts +21 -5
  106. package/src/modes/controllers/input-controller.ts +115 -3
  107. package/src/modes/controllers/selector-controller.ts +19 -1
  108. package/src/modes/interactive-mode.ts +3 -3
  109. package/src/modes/rpc/rpc-mode.ts +6 -0
  110. package/src/modes/utils/copy-targets.ts +7 -2
  111. package/src/modes/utils/ui-helpers.ts +3 -3
  112. package/src/prompts/system/system-prompt.md +3 -3
  113. package/src/prompts/system/workflow-notice.md +3 -3
  114. package/src/prompts/tools/bash.md +16 -0
  115. package/src/prompts/tools/eval.md +19 -19
  116. package/src/prompts/tools/todo.md +1 -1
  117. package/src/sdk.ts +8 -10
  118. package/src/session/agent-session.ts +422 -97
  119. package/src/session/provider-image-budget.ts +86 -0
  120. package/src/session/session-context.ts +14 -7
  121. package/src/session/session-storage.ts +24 -2
  122. package/src/session/snapcompact-inline.ts +19 -3
  123. package/src/slash-commands/builtin-registry.ts +0 -22
  124. package/src/slash-commands/helpers/usage-report.ts +9 -1
  125. package/src/task/index.ts +61 -207
  126. package/src/task/isolation-runner.ts +354 -0
  127. package/src/task/parallel.ts +6 -1
  128. package/src/task/worktree.ts +46 -9
  129. package/src/thinking.ts +29 -2
  130. package/src/tiny/title-client.ts +75 -21
  131. package/src/tools/ask.ts +44 -38
  132. package/src/tools/bash.ts +9 -2
  133. package/src/tools/browser/tab-worker.ts +1 -1
  134. package/src/tools/eval-render.ts +34 -27
  135. package/src/tools/eval.ts +100 -103
  136. package/src/tools/index.ts +8 -1
  137. package/src/tools/read.ts +136 -60
  138. package/src/tools/todo.ts +60 -64
  139. package/src/tui/code-cell.ts +1 -1
  140. package/src/tui/output-block.ts +11 -0
  141. package/src/utils/image-resize.ts +30 -0
  142. package/src/utils/tools-manager.ts +67 -10
  143. package/src/web/search/providers/exa.ts +85 -1
@@ -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; agent_type="task", model=nothing, label=nothing, schema=nothing, return_handle=false, kwargs...)
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 agent_type !== nothing
740
- args_dict["agentType"] = agent_type
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
- handle_result = return_handle
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
- key = string(k)
754
- if key == "agent_type" || key == "agentType"
755
- args_dict["agentType"] = v
756
- elseif key == "return_handle" || key == "returnHandle"
757
- handle_result = Bool(v)
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
 
@@ -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
- io_plain = IOBuffer()
447
- show(io_plain, MIME"text/plain"(), value)
448
- bundle["text/plain"] = String(take!(io_plain))
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(m, value)
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(b64_io, m, value)
478
+ Base.invokelatest(show, b64_io, m, value)
459
479
  close(b64_io)
460
480
  else
461
- show(io, m, value)
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
- showerror(io, err)
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`, `tree`, ...)
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("agent", opts, rest, ["agentType", "model", "label", "schema"], "{ agentType, model, label, schema, returnHandle }");
121
- const { returnHandle, ...callArgs } = o;
122
- const res = await globalThis.__omp_call_tool__("__agent__", { prompt, ...callArgs });
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 (!returnHandle) return parsed;
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
  }
@@ -72,12 +72,6 @@ const PRELUDE_GLOBAL_KEYS = [
72
72
  "__pool",
73
73
  "read",
74
74
  "write",
75
- "append",
76
- "sort",
77
- "uniq",
78
- "counter",
79
- "diff",
80
- "tree",
81
75
  "env",
82
76
  ];
83
77
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Structured status payload emitted by helpers (`read`, `write`, `tree`, etc.) and the
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`/`append`
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
  });
@@ -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`/`append`) substitute for
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.