@oh-my-pi/pi-coding-agent 16.1.12 → 16.1.14

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 (130) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cli.js +5650 -2860
  3. package/dist/types/config/settings-schema.d.ts +44 -14
  4. package/dist/types/cursor.d.ts +9 -9
  5. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
  6. package/dist/types/eval/backend-helpers.d.ts +23 -0
  7. package/dist/types/eval/executor-base.d.ts +118 -0
  8. package/dist/types/eval/index.d.ts +2 -0
  9. package/dist/types/eval/jl/executor.d.ts +44 -0
  10. package/dist/types/eval/jl/index.d.ts +11 -0
  11. package/dist/types/eval/jl/kernel.d.ts +28 -0
  12. package/dist/types/eval/jl/prelude.d.ts +1 -0
  13. package/dist/types/eval/jl/runtime.d.ts +22 -0
  14. package/dist/types/eval/kernel-base.d.ts +105 -0
  15. package/dist/types/eval/py/kernel.d.ts +3 -61
  16. package/dist/types/eval/rb/executor.d.ts +77 -0
  17. package/dist/types/eval/rb/index.d.ts +11 -0
  18. package/dist/types/eval/rb/kernel.d.ts +31 -0
  19. package/dist/types/eval/rb/prelude.d.ts +1 -0
  20. package/dist/types/eval/rb/runtime.d.ts +23 -0
  21. package/dist/types/eval/runtime-env.d.ts +24 -0
  22. package/dist/types/eval/types.d.ts +3 -3
  23. package/dist/types/extensibility/extensions/runner.d.ts +2 -15
  24. package/dist/types/extensibility/hooks/loader.d.ts +1 -25
  25. package/dist/types/extensibility/session-handler-types.d.ts +23 -0
  26. package/dist/types/jsonrpc/message-framing.d.ts +35 -0
  27. package/dist/types/mnemopi/embed-client.d.ts +7 -24
  28. package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
  29. package/dist/types/modes/components/custom-editor.d.ts +11 -0
  30. package/dist/types/modes/components/selector-helpers.d.ts +53 -0
  31. package/dist/types/modes/interactive-mode.d.ts +0 -2
  32. package/dist/types/modes/theme/theme.d.ts +8 -1
  33. package/dist/types/modes/types.d.ts +0 -2
  34. package/dist/types/modes/utils/copy-targets.d.ts +1 -1
  35. package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
  36. package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
  37. package/dist/types/sdk.d.ts +1 -1
  38. package/dist/types/session/agent-session.d.ts +2 -2
  39. package/dist/types/stt/asr-client.d.ts +3 -29
  40. package/dist/types/subprocess/worker-client.d.ts +149 -0
  41. package/dist/types/subprocess/worker-runtime.d.ts +107 -0
  42. package/dist/types/tiny/title-client.d.ts +14 -34
  43. package/dist/types/tools/eval-backends.d.ts +6 -3
  44. package/dist/types/tools/eval-render.d.ts +6 -5
  45. package/dist/types/tools/eval.d.ts +13 -15
  46. package/dist/types/tools/index.d.ts +3 -3
  47. package/dist/types/tts/tts-client.d.ts +3 -28
  48. package/dist/types/tui/code-cell.d.ts +7 -0
  49. package/dist/types/utils/file-display-mode.d.ts +1 -1
  50. package/dist/types/web/parallel.d.ts +6 -0
  51. package/package.json +12 -12
  52. package/src/config/settings-schema.ts +50 -18
  53. package/src/config/settings.ts +5 -0
  54. package/src/dap/client.ts +13 -107
  55. package/src/eval/__tests__/julia-prelude.test.ts +77 -0
  56. package/src/eval/backend-helpers.ts +48 -0
  57. package/src/eval/executor-base.ts +425 -0
  58. package/src/eval/index.ts +2 -0
  59. package/src/eval/jl/executor.ts +540 -0
  60. package/src/eval/jl/index.ts +54 -0
  61. package/src/eval/jl/kernel.ts +235 -0
  62. package/src/eval/jl/prelude.jl +930 -0
  63. package/src/eval/jl/prelude.ts +3 -0
  64. package/src/eval/jl/runner.jl +634 -0
  65. package/src/eval/jl/runtime.ts +118 -0
  66. package/src/eval/js/index.ts +3 -14
  67. package/src/eval/kernel-base.ts +569 -0
  68. package/src/eval/py/executor.ts +43 -252
  69. package/src/eval/py/index.ts +9 -20
  70. package/src/eval/py/kernel.ts +29 -544
  71. package/src/eval/rb/executor.ts +504 -0
  72. package/src/eval/rb/index.ts +54 -0
  73. package/src/eval/rb/kernel.ts +230 -0
  74. package/src/eval/rb/prelude.rb +721 -0
  75. package/src/eval/rb/prelude.ts +3 -0
  76. package/src/eval/rb/runner.rb +474 -0
  77. package/src/eval/rb/runtime.ts +132 -0
  78. package/src/eval/runtime-env.ts +104 -0
  79. package/src/eval/types.ts +3 -3
  80. package/src/extensibility/extensions/runner.ts +4 -11
  81. package/src/extensibility/hooks/loader.ts +3 -21
  82. package/src/extensibility/session-handler-types.ts +21 -0
  83. package/src/internal-urls/docs-index.generated.txt +1 -1
  84. package/src/jsonrpc/message-framing.ts +142 -0
  85. package/src/lsp/client.ts +13 -109
  86. package/src/mnemopi/embed-client.ts +43 -198
  87. package/src/modes/components/agent-dashboard.ts +17 -40
  88. package/src/modes/components/chat-transcript-builder.ts +18 -102
  89. package/src/modes/components/custom-editor.ts +19 -0
  90. package/src/modes/components/extensions/extension-dashboard.ts +4 -13
  91. package/src/modes/components/extensions/extension-list.ts +16 -44
  92. package/src/modes/components/history-search.ts +4 -16
  93. package/src/modes/components/selector-helpers.ts +129 -0
  94. package/src/modes/components/settings-selector.ts +7 -5
  95. package/src/modes/components/tree-selector.ts +13 -18
  96. package/src/modes/controllers/event-controller.ts +3 -9
  97. package/src/modes/controllers/input-controller.ts +32 -54
  98. package/src/modes/interactive-mode.ts +5 -7
  99. package/src/modes/theme/theme.ts +35 -0
  100. package/src/modes/types.ts +0 -2
  101. package/src/modes/utils/copy-targets.ts +3 -2
  102. package/src/modes/utils/interactive-context-helpers.ts +27 -0
  103. package/src/modes/utils/transcript-render-helpers.ts +157 -0
  104. package/src/modes/utils/ui-helpers.ts +21 -126
  105. package/src/prompts/system/system-prompt.md +10 -10
  106. package/src/prompts/tools/bash.md +0 -1
  107. package/src/prompts/tools/eval.md +6 -4
  108. package/src/prompts/tools/find.md +0 -4
  109. package/src/prompts/tools/read.md +1 -2
  110. package/src/prompts/tools/replace.md +1 -1
  111. package/src/prompts/tools/search.md +0 -1
  112. package/src/sdk.ts +13 -7
  113. package/src/session/agent-session.ts +9 -7
  114. package/src/stt/asr-client.ts +35 -215
  115. package/src/stt/asr-worker.ts +29 -181
  116. package/src/subprocess/worker-client.ts +297 -0
  117. package/src/subprocess/worker-runtime.ts +277 -0
  118. package/src/task/executor.ts +4 -4
  119. package/src/tiny/title-client.ts +53 -219
  120. package/src/tiny/worker.ts +29 -180
  121. package/src/tools/eval-backends.ts +10 -3
  122. package/src/tools/eval-render.ts +17 -8
  123. package/src/tools/eval.ts +187 -22
  124. package/src/tools/index.ts +51 -28
  125. package/src/tts/tts-client.ts +38 -206
  126. package/src/tts/tts-worker.ts +23 -97
  127. package/src/tui/code-cell.ts +12 -1
  128. package/src/utils/file-display-mode.ts +2 -3
  129. package/src/web/parallel.ts +43 -42
  130. package/src/web/search/providers/parallel.ts +10 -99
@@ -0,0 +1,930 @@
1
+ # OMP Julia prelude helpers (loaded once into the runner's top-level scope).
2
+
3
+ if !isdefined(Main, :__omp_prelude_loaded)
4
+ global __omp_prelude_loaded = true
5
+ end
6
+
7
+ # -------------------------------------------------------------------------
8
+ # Internal-URL path resolution
9
+ # -------------------------------------------------------------------------
10
+
11
+ function __omp_url_decode(s::String)
12
+ res = IOBuffer()
13
+ i = 1
14
+ len = ncodeunits(s)
15
+ while i <= len
16
+ c = Char(codeunit(s, i))
17
+ if c == '%' && i + 2 <= len
18
+ h_str = s[i+1:i+2]
19
+ try
20
+ b = parse(UInt8, h_str, base=16)
21
+ write(res, b)
22
+ i += 3
23
+ continue
24
+ catch
25
+ # ignore format error
26
+ end
27
+ end
28
+ write(res, c)
29
+ i += 1
30
+ end
31
+ return String(take!(res))
32
+ end
33
+
34
+ function __omp_resolve_path(p::AbstractString)
35
+ m = match(r"^([a-z][a-z0-9+.\-]*)://(.*)$"i, p)
36
+ if m === nothing
37
+ return abspath(p)
38
+ end
39
+ scheme = lowercase(string(m.captures[1]))
40
+ roots_env = get(ENV, "PI_EVAL_LOCAL_ROOTS", "{}")
41
+ roots = try
42
+ Main.json_parse(roots_env)
43
+ catch
44
+ Dict{String, Any}()
45
+ end
46
+ root = get(roots, scheme, nothing)
47
+ if root === nothing || isempty(root)
48
+ error("Protocol paths are not supported by this helper: $p")
49
+ end
50
+
51
+ relative = __omp_url_decode(replace(string(m.captures[2]), '\\' => '/'))
52
+ root_path = abspath(string(root))
53
+ if isempty(relative)
54
+ return root_path
55
+ end
56
+
57
+ if startswith(relative, '/') || ".." in split(relative, '/')
58
+ error("Unsafe $scheme:// path (absolute or traversal): $p")
59
+ end
60
+
61
+ resolved = abspath(joinpath(root_path, relative))
62
+ if resolved != root_path && !startswith(resolved, root_path * Base.Filesystem.path_separator)
63
+ error("$scheme:// path escapes its root: $p")
64
+ end
65
+ return resolved
66
+ end
67
+
68
+ # -------------------------------------------------------------------------
69
+ # Display + status
70
+ # -------------------------------------------------------------------------
71
+
72
+
73
+ function display_image(base64_str::String, mime_type::String = "image/png")
74
+ bundle = Dict(mime_type => base64_str)
75
+ Main.emit_frame(Dict("type" => "display", "id" => Main.current_rid, "bundle" => bundle))
76
+ return nothing
77
+ end
78
+
79
+ function __omp_emit_status(op::String, fields::AbstractDict=Dict{String, Any}())
80
+ status = Dict{String, Any}("op" => op)
81
+ for (k, v) in fields
82
+ status[string(k)] = v
83
+ end
84
+ Main.emit_frame(Dict(
85
+ "type" => "display",
86
+ "id" => Main.current_rid,
87
+ "bundle" => Dict("application/x-omp-status" => status)
88
+ ))
89
+ return nothing
90
+ end
91
+
92
+ # -------------------------------------------------------------------------
93
+ # File helpers
94
+ # -------------------------------------------------------------------------
95
+
96
+ function Base.read(path::AbstractString, offset::Integer=1, limit::Union{Integer, Nothing}=nothing)
97
+ resolved = __omp_resolve_path(string(path))
98
+ content = open(resolved, "r") do io
99
+ Base.read(io, String)
100
+ end
101
+ lines = split(content, '\n')
102
+ if offset > 1 || limit !== nothing
103
+ st = max(1, offset)
104
+ en = limit !== nothing ? min(length(lines), st + limit - 1) : length(lines)
105
+ if st <= length(lines)
106
+ content = join(lines[st:en], '\n')
107
+ else
108
+ content = ""
109
+ end
110
+ end
111
+
112
+ preview = length(content) > 500 ? content[1:500] : content
113
+ Main.emit_frame(Dict(
114
+ "type" => "display",
115
+ "id" => Main.current_rid,
116
+ "bundle" => Dict(
117
+ "application/x-omp-status" => Dict(
118
+ "op" => "read",
119
+ "path" => resolved,
120
+ "chars" => length(content),
121
+ "preview" => preview
122
+ )
123
+ )
124
+ ))
125
+ return content
126
+ end
127
+
128
+ function Base.write(path::AbstractString, content::Any)
129
+ resolved = __omp_resolve_path(string(path))
130
+ mkpath(dirname(resolved))
131
+ open(resolved, "w") do io
132
+ Base.write(io, string(content))
133
+ end
134
+
135
+ Main.emit_frame(Dict(
136
+ "type" => "display",
137
+ "id" => Main.current_rid,
138
+ "bundle" => Dict(
139
+ "application/x-omp-status" => Dict(
140
+ "op" => "write",
141
+ "path" => resolved,
142
+ "chars" => length(string(content))
143
+ )
144
+ )
145
+ ))
146
+ return resolved
147
+ end
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
+ function __omp_apply_query(data, query)
365
+ if query === nothing || isempty(string(query))
366
+ return data
367
+ end
368
+ q = strip(string(query))
369
+ if startswith(q, ".")
370
+ q = length(q) == 1 ? "" : q[2:end]
371
+ end
372
+ if isempty(q)
373
+ return data
374
+ end
375
+
376
+ tokens = Vector{Tuple{Symbol, Any}}()
377
+ buf = ""
378
+ chars = collect(q)
379
+ i = 1
380
+ while i <= length(chars)
381
+ ch = chars[i]
382
+ if ch == '.'
383
+ if !isempty(buf)
384
+ push!(tokens, (:key, buf))
385
+ buf = ""
386
+ end
387
+ elseif ch == '['
388
+ if !isempty(buf)
389
+ push!(tokens, (:key, buf))
390
+ buf = ""
391
+ end
392
+ j = i + 1
393
+ while j <= length(chars) && chars[j] != ']'
394
+ j += 1
395
+ end
396
+ inner = j > i + 1 ? String(chars[(i + 1):(j - 1)]) : ""
397
+ if startswith(inner, "\"") && endswith(inner, "\"")
398
+ push!(tokens, (:key, length(inner) <= 2 ? "" : inner[2:end-1]))
399
+ else
400
+ push!(tokens, (:index, parse(Int, inner)))
401
+ end
402
+ i = j
403
+ else
404
+ buf *= string(ch)
405
+ end
406
+ i += 1
407
+ end
408
+ if !isempty(buf)
409
+ push!(tokens, (:key, buf))
410
+ end
411
+
412
+ current = data
413
+ for (kind, value) in tokens
414
+ if kind == :index
415
+ if !(current isa AbstractVector)
416
+ return nothing
417
+ end
418
+ idx = Int(value)
419
+ julia_idx = idx >= 0 ? idx + 1 : length(current) + idx + 1
420
+ if julia_idx < 1 || julia_idx > length(current)
421
+ return nothing
422
+ end
423
+ current = current[julia_idx]
424
+ else
425
+ key = string(value)
426
+ if !(current isa AbstractDict) || !haskey(current, key)
427
+ return nothing
428
+ end
429
+ current = current[key]
430
+ end
431
+ end
432
+ return current
433
+ end
434
+
435
+ function __omp_json_text(value)
436
+ if value === nothing
437
+ return "null"
438
+ end
439
+ try
440
+ return Main.json_serialize(value)
441
+ catch
442
+ return string(value)
443
+ end
444
+ end
445
+
446
+ function __omp_optional_int(value, default::Int)
447
+ if value === nothing
448
+ return default
449
+ elseif value isa Integer
450
+ return Int(value)
451
+ elseif value isa AbstractFloat
452
+ return Int(trunc(value))
453
+ elseif value isa AbstractString
454
+ return parse(Int, value)
455
+ end
456
+ return Int(value)
457
+ end
458
+
459
+ function output(ids...; format="raw", query=nothing, offset=nothing, limit=nothing)
460
+ artifacts_dir = get(ENV, "PI_ARTIFACTS_DIR", "")
461
+ if isempty(artifacts_dir)
462
+ session_file = get(ENV, "PI_SESSION_FILE", "")
463
+ if isempty(session_file)
464
+ __omp_emit_status("output", Dict{String, Any}("error" => "No session file available"))
465
+ error("No session - output artifacts unavailable")
466
+ end
467
+ artifacts_dir = replace(session_file, r"\.[^.]*$" => "")
468
+ end
469
+ if !isdir(artifacts_dir)
470
+ __omp_emit_status("output", Dict{String, Any}("error" => "Artifacts directory not found", "path" => artifacts_dir))
471
+ error("No artifacts directory found: $artifacts_dir")
472
+ end
473
+ if isempty(ids)
474
+ __omp_emit_status("output", Dict{String, Any}("error" => "No IDs provided"))
475
+ error("At least one output ID is required")
476
+ end
477
+ if query !== nothing && (offset !== nothing || limit !== nothing)
478
+ __omp_emit_status("output", Dict{String, Any}("error" => "query cannot be combined with offset/limit"))
479
+ error("query cannot be combined with offset/limit")
480
+ end
481
+
482
+ results = Vector{Dict{String, Any}}()
483
+ not_found = String[]
484
+ for output_id_value in ids
485
+ output_id = string(output_id_value)
486
+ output_path = joinpath(artifacts_dir, output_id * ".md")
487
+ if !isfile(output_path)
488
+ push!(not_found, output_id)
489
+ continue
490
+ end
491
+
492
+ raw = open(output_path, "r") do io
493
+ Base.read(io, String)
494
+ end
495
+ raw_lines = split(raw, '\n'; keepempty=true)
496
+ total_lines = length(raw_lines)
497
+ selected = raw
498
+ range_info = nothing
499
+
500
+ if query !== nothing
501
+ json_value = try
502
+ Main.json_parse(raw)
503
+ catch err
504
+ __omp_emit_status("output", Dict{String, Any}("id" => output_id, "error" => "Not valid JSON: $(err)"))
505
+ error("Output $output_id is not valid JSON: $(err)")
506
+ end
507
+ result_value = __omp_apply_query(json_value, query)
508
+ selected = __omp_json_text(result_value)
509
+ elseif offset !== nothing || limit !== nothing
510
+ start_line = max(1, __omp_optional_int(offset, 1))
511
+ if start_line > total_lines
512
+ __omp_emit_status("output", Dict{String, Any}("id" => output_id, "error" => "Offset $start_line beyond end ($total_lines lines)"))
513
+ error("Offset $start_line is beyond end of output ($total_lines lines) for $output_id")
514
+ end
515
+ effective_limit = limit === nothing ? total_lines - start_line + 1 : __omp_optional_int(limit, total_lines - start_line + 1)
516
+ end_line = min(total_lines, start_line + effective_limit - 1)
517
+ selected = join(raw_lines[start_line:end_line], '\n')
518
+ range_info = Dict{String, Any}("start_line" => start_line, "end_line" => end_line, "total_lines" => total_lines)
519
+ end
520
+
521
+ if format == "stripped"
522
+ selected = replace(selected, r"\x1b\[[0-9;]*m" => "")
523
+ end
524
+
525
+ if format == "json"
526
+ entry = Dict{String, Any}(
527
+ "id" => output_id,
528
+ "path" => output_path,
529
+ "line_count" => query !== nothing ? length(split(selected, '\n')) : total_lines,
530
+ "char_count" => query !== nothing ? length(selected) : length(raw),
531
+ "content" => selected
532
+ )
533
+ if range_info !== nothing
534
+ entry["range"] = range_info
535
+ end
536
+ if query !== nothing
537
+ entry["query"] = query
538
+ end
539
+ push!(results, entry)
540
+ else
541
+ push!(results, Dict{String, Any}("id" => output_id, "content" => selected))
542
+ end
543
+ end
544
+
545
+ if !isempty(not_found)
546
+ available = sort([replace(name, r"\.md$" => "") for name in readdir(artifacts_dir) if endswith(name, ".md")])
547
+ msg = "Output not found: $(join(not_found, ", "))"
548
+ if !isempty(available)
549
+ shown = available[1:min(20, length(available))]
550
+ msg *= "\n\nAvailable outputs: $(join(shown, ", "))"
551
+ if length(available) > 20
552
+ msg *= " (and $(length(available) - 20) more)"
553
+ end
554
+ end
555
+ __omp_emit_status("output", Dict{String, Any}("not_found" => not_found, "available_count" => length(available)))
556
+ error(msg)
557
+ end
558
+
559
+ if length(ids) == 1
560
+ if format == "json"
561
+ __omp_emit_status("output", Dict{String, Any}("id" => string(ids[1]), "chars" => results[1]["char_count"]))
562
+ return results[1]
563
+ end
564
+ __omp_emit_status("output", Dict{String, Any}("id" => string(ids[1]), "chars" => length(results[1]["content"])))
565
+ return results[1]["content"]
566
+ end
567
+
568
+ if format == "json"
569
+ __omp_emit_status("output", Dict{String, Any}("count" => length(results), "total_chars" => sum(r["char_count"] for r in results)))
570
+ return results
571
+ end
572
+
573
+ __omp_emit_status("output", Dict{String, Any}("count" => length(results), "total_chars" => sum(length(r["content"]) for r in results)))
574
+ return results
575
+ end
576
+
577
+ function env(key=nothing, value=nothing)
578
+ if key === nothing
579
+ items = Dict{String, String}()
580
+ for (k, v) in ENV
581
+ items[k] = v
582
+ end
583
+ keys_list = sort(collect(keys(items)))
584
+ Main.emit_frame(Dict(
585
+ "type" => "display",
586
+ "id" => Main.current_rid,
587
+ "bundle" => Dict(
588
+ "application/x-omp-status" => Dict(
589
+ "op" => "env",
590
+ "count" => length(items),
591
+ "keys" => keys_list[1:min(20, length(keys_list))]
592
+ )
593
+ )
594
+ ))
595
+ return items
596
+ end
597
+
598
+ k = string(key)
599
+ if value !== nothing
600
+ v = string(value)
601
+ ENV[k] = v
602
+ Main.emit_frame(Dict(
603
+ "type" => "display",
604
+ "id" => Main.current_rid,
605
+ "bundle" => Dict(
606
+ "application/x-omp-status" => Dict(
607
+ "op" => "env",
608
+ "key" => k,
609
+ "value" => v,
610
+ "action" => "set"
611
+ )
612
+ )
613
+ ))
614
+ return v
615
+ end
616
+
617
+ v = get(ENV, k, nothing)
618
+ Main.emit_frame(Dict(
619
+ "type" => "display",
620
+ "id" => Main.current_rid,
621
+ "bundle" => Dict(
622
+ "application/x-omp-status" => Dict(
623
+ "op" => "env",
624
+ "key" => k,
625
+ "value" => v,
626
+ "action" => "get"
627
+ )
628
+ )
629
+ ))
630
+ return v
631
+ end
632
+
633
+ # -------------------------------------------------------------------------
634
+ # Dynamic bridge proxy
635
+ # -------------------------------------------------------------------------
636
+
637
+ using Downloads
638
+
639
+ function __omp_call_bridge(name::String, args::Dict{String, Any})
640
+ base_url = get(ENV, "PI_TOOL_BRIDGE_URL", nothing)
641
+ token = get(ENV, "PI_TOOL_BRIDGE_TOKEN", nothing)
642
+ session = get(ENV, "PI_TOOL_BRIDGE_SESSION", nothing)
643
+
644
+ if base_url === nothing || token === nothing || session === nothing
645
+ error("Tool bridge is not available in this cell.")
646
+ end
647
+
648
+ url = base_url
649
+ if !endswith(url, "/v1/tool")
650
+ url = endswith(url, "/") ? (url * "v1/tool") : (url * "/v1/tool")
651
+ end
652
+
653
+ payload_dict = Dict(
654
+ "session" => session,
655
+ "run" => Main.current_rid,
656
+ "name" => name,
657
+ "args" => args
658
+ )
659
+ payload_json = Main.json_serialize(payload_dict)
660
+
661
+ headers = [
662
+ "Authorization" => "Bearer $token",
663
+ "Content-Type" => "application/json"
664
+ ]
665
+
666
+ io_out = IOBuffer()
667
+ response = Downloads.request(
668
+ url,
669
+ method="POST",
670
+ headers=headers,
671
+ input=IOBuffer(payload_json),
672
+ output=io_out
673
+ )
674
+
675
+ resp_str = String(take!(io_out))
676
+ if response.status != 200
677
+ error("Tool bridge call failed with status $(response.status): $resp_str")
678
+ end
679
+
680
+ parsed_resp = Main.json_parse(resp_str)
681
+
682
+ ok = get(parsed_resp, "ok", false)
683
+ if !ok
684
+ err_msg = get(parsed_resp, "error", "Unknown error")
685
+ error("Tool bridge error: $err_msg")
686
+ end
687
+
688
+ return get(parsed_resp, "value", nothing)
689
+ end
690
+
691
+ struct OmpToolProxy end
692
+
693
+ struct OmpToolCallable
694
+ name::String
695
+ end
696
+
697
+ function (tc::OmpToolCallable)(args...; kwargs...)
698
+ args_dict = Dict{String, Any}()
699
+ if length(args) == 1 && args[1] isa AbstractDict
700
+ for (k, v) in args[1]
701
+ args_dict[string(k)] = v
702
+ end
703
+ end
704
+ for (k, v) in kwargs
705
+ args_dict[string(k)] = v
706
+ end
707
+
708
+ return __omp_call_bridge("tool:" * tc.name, args_dict)
709
+ end
710
+
711
+ function Base.getproperty(::OmpToolProxy, sym::Symbol)
712
+ return OmpToolCallable(string(sym))
713
+ end
714
+
715
+ const tool = OmpToolProxy()
716
+
717
+ # -------------------------------------------------------------------------
718
+ # Agent calls
719
+ # -------------------------------------------------------------------------
720
+
721
+ function completion(prompt::String; model="default", system=nothing, schema=nothing, kwargs...)
722
+ args_dict = Dict{String, Any}("prompt" => prompt, "model" => model)
723
+ if system !== nothing
724
+ args_dict["system"] = system
725
+ end
726
+ if schema !== nothing
727
+ args_dict["schema"] = schema
728
+ end
729
+ for (k, v) in kwargs
730
+ args_dict[string(k)] = v
731
+ end
732
+ res = __omp_call_bridge("__completion__", args_dict)
733
+ text = res isa AbstractDict ? get(res, "text", res) : res
734
+ return schema === nothing ? text : Main.json_parse(string(text))
735
+ end
736
+
737
+ function agent(prompt::String; agent_type="task", model=nothing, label=nothing, schema=nothing, return_handle=false, kwargs...)
738
+ args_dict = Dict{String, Any}("prompt" => prompt)
739
+ if agent_type !== nothing
740
+ args_dict["agentType"] = agent_type
741
+ end
742
+ if model !== nothing
743
+ args_dict["model"] = model
744
+ end
745
+ if label !== nothing
746
+ args_dict["label"] = label
747
+ end
748
+ if schema !== nothing
749
+ args_dict["schema"] = schema
750
+ end
751
+ handle_result = return_handle
752
+ 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
761
+ end
762
+ res = __omp_call_bridge("__agent__", args_dict)
763
+ text = res isa AbstractDict ? get(res, "text", res) : res
764
+ parsed = schema === nothing ? text : Main.json_parse(string(text))
765
+ if !handle_result
766
+ return parsed
767
+ end
768
+ details = res isa AbstractDict ? get(res, "details", nothing) : nothing
769
+ if !(details isa AbstractDict) || get(details, "id", nothing) === nothing
770
+ return Dict{String, Any}("text" => text, "output" => text, "handle" => nothing, "id" => nothing, "agent" => nothing)
771
+ end
772
+ node = Dict{String, Any}(
773
+ "text" => text,
774
+ "output" => text,
775
+ "handle" => "agent://" * string(get(details, "id", nothing)),
776
+ "id" => get(details, "id", nothing),
777
+ "agent" => get(details, "agent", nothing)
778
+ )
779
+ if schema !== nothing
780
+ node["data"] = parsed
781
+ end
782
+ return node
783
+ end
784
+
785
+ function Base.log(message::AbstractString)
786
+ Main.emit_frame(Dict(
787
+ "type" => "display",
788
+ "id" => Main.current_rid,
789
+ "bundle" => Dict(
790
+ "application/x-omp-status" => Dict(
791
+ "op" => "log",
792
+ "message" => message
793
+ )
794
+ )
795
+ ))
796
+ return nothing
797
+ end
798
+
799
+ function phase(title::String)
800
+ Main.emit_frame(Dict(
801
+ "type" => "display",
802
+ "id" => Main.current_rid,
803
+ "bundle" => Dict(
804
+ "application/x-omp-status" => Dict(
805
+ "op" => "phase",
806
+ "title" => title
807
+ )
808
+ )
809
+ ))
810
+ return nothing
811
+ end
812
+
813
+ # -------------------------------------------------------------------------
814
+ # Concurrency
815
+ # -------------------------------------------------------------------------
816
+
817
+ function _concurrency_limit()
818
+ try
819
+ snap = __omp_call_bridge("__concurrency__", Dict{String, Any}())
820
+ limit_val = snap isa AbstractDict ? get(snap, "limit", 0) : snap
821
+ return limit_val isa Number ? max(Int(limit_val), 0) : 0
822
+ catch
823
+ return 0
824
+ end
825
+ end
826
+
827
+ function _pool_map(items, fn)
828
+ if isempty(items)
829
+ return []
830
+ end
831
+ limit = _concurrency_limit()
832
+
833
+ n = length(items)
834
+ results = Vector{Any}(undef, n)
835
+ errors = Dict{Int, Any}()
836
+
837
+ sem = limit > 0 ? Channel{Nothing}(limit) : nothing
838
+
839
+ @sync for i in 1:n
840
+ if sem !== nothing
841
+ put!(sem, nothing)
842
+ end
843
+ item = items[i]
844
+ idx = i
845
+ @async begin
846
+ try
847
+ res = fn(item)
848
+ results[idx] = res
849
+ catch err
850
+ errors[idx] = err
851
+ finally
852
+ if sem !== nothing
853
+ take!(sem)
854
+ end
855
+ end
856
+ end
857
+ end
858
+
859
+ if !isempty(errors)
860
+ min_idx = minimum(keys(errors))
861
+ throw(errors[min_idx])
862
+ end
863
+ return results
864
+ end
865
+
866
+ function parallel(thunks)
867
+ return _pool_map(thunks, t -> t())
868
+ end
869
+
870
+ function pipeline(items, stages...)
871
+ curr = collect(items)
872
+ for stage in stages
873
+ curr = _pool_map(curr, stage)
874
+ end
875
+ return curr
876
+ end
877
+
878
+ # -------------------------------------------------------------------------
879
+ # Budget
880
+ # -------------------------------------------------------------------------
881
+
882
+ struct OmpBudgetProxy end
883
+
884
+ function __omp_budget_snapshot()
885
+ try
886
+ snap = __omp_call_bridge("__budget__", Dict{String, Any}())
887
+ return snap isa AbstractDict ? snap : Dict{String, Any}()
888
+ catch
889
+ return Dict{String, Any}()
890
+ end
891
+ end
892
+
893
+ function __omp_budget_int(value, default::Int=0)
894
+ if value isa Integer
895
+ return Int(value)
896
+ elseif value isa AbstractFloat
897
+ return Int(trunc(value))
898
+ elseif value isa AbstractString
899
+ try
900
+ return parse(Int, value)
901
+ catch
902
+ return default
903
+ end
904
+ end
905
+ return default
906
+ end
907
+
908
+ function Base.getproperty(::OmpBudgetProxy, sym::Symbol)
909
+ if sym === :total
910
+ snap = __omp_budget_snapshot()
911
+ return get(snap, "total", nothing)
912
+ elseif sym === :hard
913
+ snap = __omp_budget_snapshot()
914
+ return get(snap, "hard", false) == true
915
+ elseif sym === :spent
916
+ return () -> __omp_budget_int(get(__omp_budget_snapshot(), "spent", 0), 0)
917
+ elseif sym === :remaining
918
+ return () -> begin
919
+ snap = __omp_budget_snapshot()
920
+ total = get(snap, "total", nothing)
921
+ if total === nothing
922
+ return Inf
923
+ end
924
+ return max(0, __omp_budget_int(total, 0) - __omp_budget_int(get(snap, "spent", 0), 0))
925
+ end
926
+ end
927
+ error("Unknown budget metric: $sym")
928
+ end
929
+
930
+ const budget = OmpBudgetProxy()