@oh-my-pi/pi-coding-agent 16.1.13 → 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.
- package/CHANGELOG.md +27 -0
- package/dist/cli.js +5547 -2759
- package/dist/types/config/settings-schema.d.ts +44 -14
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
- package/dist/types/eval/backend-helpers.d.ts +23 -0
- package/dist/types/eval/executor-base.d.ts +118 -0
- package/dist/types/eval/index.d.ts +2 -0
- package/dist/types/eval/jl/executor.d.ts +44 -0
- package/dist/types/eval/jl/index.d.ts +11 -0
- package/dist/types/eval/jl/kernel.d.ts +28 -0
- package/dist/types/eval/jl/prelude.d.ts +1 -0
- package/dist/types/eval/jl/runtime.d.ts +22 -0
- package/dist/types/eval/kernel-base.d.ts +105 -0
- package/dist/types/eval/py/kernel.d.ts +3 -61
- package/dist/types/eval/rb/executor.d.ts +77 -0
- package/dist/types/eval/rb/index.d.ts +11 -0
- package/dist/types/eval/rb/kernel.d.ts +31 -0
- package/dist/types/eval/rb/prelude.d.ts +1 -0
- package/dist/types/eval/rb/runtime.d.ts +23 -0
- package/dist/types/eval/runtime-env.d.ts +24 -0
- package/dist/types/eval/types.d.ts +3 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -15
- package/dist/types/extensibility/hooks/loader.d.ts +1 -25
- package/dist/types/extensibility/session-handler-types.d.ts +23 -0
- package/dist/types/jsonrpc/message-framing.d.ts +35 -0
- package/dist/types/mnemopi/embed-client.d.ts +7 -24
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +11 -0
- package/dist/types/modes/components/selector-helpers.d.ts +53 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +8 -1
- package/dist/types/modes/types.d.ts +0 -2
- package/dist/types/modes/utils/copy-targets.d.ts +1 -1
- package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/stt/asr-client.d.ts +3 -29
- package/dist/types/subprocess/worker-client.d.ts +149 -0
- package/dist/types/subprocess/worker-runtime.d.ts +107 -0
- package/dist/types/tiny/title-client.d.ts +14 -34
- package/dist/types/tools/eval-backends.d.ts +6 -3
- package/dist/types/tools/eval-render.d.ts +6 -5
- package/dist/types/tools/eval.d.ts +13 -15
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tts/tts-client.d.ts +3 -28
- package/dist/types/tui/code-cell.d.ts +7 -0
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +6 -0
- package/package.json +12 -12
- package/src/config/settings-schema.ts +50 -18
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +13 -107
- package/src/eval/__tests__/julia-prelude.test.ts +77 -0
- package/src/eval/backend-helpers.ts +48 -0
- package/src/eval/executor-base.ts +425 -0
- package/src/eval/index.ts +2 -0
- package/src/eval/jl/executor.ts +540 -0
- package/src/eval/jl/index.ts +54 -0
- package/src/eval/jl/kernel.ts +235 -0
- package/src/eval/jl/prelude.jl +930 -0
- package/src/eval/jl/prelude.ts +3 -0
- package/src/eval/jl/runner.jl +634 -0
- package/src/eval/jl/runtime.ts +118 -0
- package/src/eval/js/index.ts +3 -14
- package/src/eval/kernel-base.ts +569 -0
- package/src/eval/py/executor.ts +43 -252
- package/src/eval/py/index.ts +9 -20
- package/src/eval/py/kernel.ts +29 -544
- package/src/eval/rb/executor.ts +504 -0
- package/src/eval/rb/index.ts +54 -0
- package/src/eval/rb/kernel.ts +230 -0
- package/src/eval/rb/prelude.rb +721 -0
- package/src/eval/rb/prelude.ts +3 -0
- package/src/eval/rb/runner.rb +474 -0
- package/src/eval/rb/runtime.ts +132 -0
- package/src/eval/runtime-env.ts +104 -0
- package/src/eval/types.ts +3 -3
- package/src/extensibility/extensions/runner.ts +4 -11
- package/src/extensibility/hooks/loader.ts +3 -21
- package/src/extensibility/session-handler-types.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/jsonrpc/message-framing.ts +142 -0
- package/src/lsp/client.ts +13 -109
- package/src/mnemopi/embed-client.ts +43 -198
- package/src/modes/components/agent-dashboard.ts +17 -40
- package/src/modes/components/chat-transcript-builder.ts +18 -102
- package/src/modes/components/custom-editor.ts +19 -0
- package/src/modes/components/extensions/extension-dashboard.ts +4 -13
- package/src/modes/components/extensions/extension-list.ts +16 -44
- package/src/modes/components/history-search.ts +4 -16
- package/src/modes/components/selector-helpers.ts +129 -0
- package/src/modes/components/settings-selector.ts +7 -5
- package/src/modes/components/tree-selector.ts +13 -18
- package/src/modes/controllers/event-controller.ts +3 -9
- package/src/modes/controllers/input-controller.ts +32 -54
- package/src/modes/interactive-mode.ts +5 -7
- package/src/modes/theme/theme.ts +35 -0
- package/src/modes/types.ts +0 -2
- package/src/modes/utils/copy-targets.ts +3 -2
- package/src/modes/utils/interactive-context-helpers.ts +27 -0
- package/src/modes/utils/transcript-render-helpers.ts +157 -0
- package/src/modes/utils/ui-helpers.ts +21 -126
- package/src/prompts/system/system-prompt.md +10 -10
- package/src/prompts/tools/bash.md +0 -1
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/read.md +1 -2
- package/src/prompts/tools/replace.md +1 -1
- package/src/prompts/tools/search.md +0 -1
- package/src/sdk.ts +13 -7
- package/src/session/agent-session.ts +9 -7
- package/src/stt/asr-client.ts +35 -215
- package/src/stt/asr-worker.ts +29 -181
- package/src/subprocess/worker-client.ts +297 -0
- package/src/subprocess/worker-runtime.ts +277 -0
- package/src/task/executor.ts +4 -4
- package/src/tiny/title-client.ts +53 -219
- package/src/tiny/worker.ts +29 -180
- package/src/tools/eval-backends.ts +10 -3
- package/src/tools/eval-render.ts +17 -8
- package/src/tools/eval.ts +187 -22
- package/src/tools/index.ts +51 -28
- package/src/tts/tts-client.ts +38 -206
- package/src/tts/tts-worker.ts +23 -97
- package/src/tui/code-cell.ts +12 -1
- package/src/utils/file-display-mode.ts +2 -3
- package/src/web/parallel.ts +43 -42
- package/src/web/search/providers/parallel.ts +10 -99
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
# frozen_string_literal: false
|
|
2
|
+
# OMP Ruby prelude helpers (loaded once into the runner's TOPLEVEL_BINDING).
|
|
3
|
+
#
|
|
4
|
+
# Mirrors eval/py/prelude.py: defines the cross-runtime helper surface
|
|
5
|
+
# (display/read/write/append/tree/diff/env/output, the `tool` bridge proxy,
|
|
6
|
+
# completion/agent/parallel/pipeline/log/phase/budget). Host-side helpers reach
|
|
7
|
+
# the coding-agent over the same loopback HTTP tool bridge the Python prelude
|
|
8
|
+
# uses (PI_TOOL_BRIDGE_URL/TOKEN/SESSION). Path helpers honor PI_EVAL_LOCAL_ROOTS
|
|
9
|
+
# so `write("local://x")` lands where `read local://x` resolves.
|
|
10
|
+
#
|
|
11
|
+
# `__omp_*` primitives (emit/present/status/scrub/run_id) are provided by
|
|
12
|
+
# runner.rb; this file only consumes them.
|
|
13
|
+
|
|
14
|
+
unless defined?($__omp_prelude_loaded) && $__omp_prelude_loaded
|
|
15
|
+
$__omp_prelude_loaded = true
|
|
16
|
+
|
|
17
|
+
require "json"
|
|
18
|
+
|
|
19
|
+
# -------------------------------------------------------------------------
|
|
20
|
+
# Internal-URL path resolution
|
|
21
|
+
# -------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
def __omp_url_decode(str)
|
|
24
|
+
str.gsub(/%([0-9A-Fa-f]{2})/) { [Regexp.last_match(1)].pack("H2") }.force_encoding(Encoding::UTF_8)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Map a helper path to a real filesystem path. A `scheme://…` whose scheme has
|
|
28
|
+
# an injected on-disk root (PI_EVAL_LOCAL_ROOTS, e.g. `local://`) is rewritten
|
|
29
|
+
# under that root; plain paths pass through; any other `scheme://` is rejected.
|
|
30
|
+
def __omp_resolve_path(path)
|
|
31
|
+
return path unless path.is_a?(String)
|
|
32
|
+
m = path.match(%r{\A([a-z][a-z0-9+.\-]*)://(.*)\z}i)
|
|
33
|
+
return path unless m
|
|
34
|
+
scheme = m[1].downcase
|
|
35
|
+
roots =
|
|
36
|
+
begin
|
|
37
|
+
raw = ENV["PI_EVAL_LOCAL_ROOTS"]
|
|
38
|
+
raw && !raw.empty? ? JSON.parse(raw) : {}
|
|
39
|
+
rescue StandardError
|
|
40
|
+
{}
|
|
41
|
+
end
|
|
42
|
+
root = roots.is_a?(Hash) ? roots[scheme] : nil
|
|
43
|
+
raise "Protocol paths are not supported by this helper: #{path}" if root.nil? || root.to_s.empty?
|
|
44
|
+
relative = __omp_url_decode(m[2].tr("\\", "/"))
|
|
45
|
+
root_path = File.absolute_path(root.to_s)
|
|
46
|
+
return root_path if relative.empty?
|
|
47
|
+
if relative.start_with?("/") || relative.split("/").include?("..")
|
|
48
|
+
raise "Unsafe #{scheme}:// path (absolute or traversal): #{path}"
|
|
49
|
+
end
|
|
50
|
+
resolved = File.absolute_path(File.join(root_path, relative))
|
|
51
|
+
unless resolved == root_path || resolved.start_with?(root_path + File::SEPARATOR)
|
|
52
|
+
raise "#{scheme}:// path escapes its root: #{path}"
|
|
53
|
+
end
|
|
54
|
+
resolved
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# -------------------------------------------------------------------------
|
|
58
|
+
# Display + status
|
|
59
|
+
# -------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def display(value)
|
|
62
|
+
__omp_present(value, "display")
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Emit a base64 image as a display output. `mime_type` is "image/png" (default)
|
|
67
|
+
# or "image/jpeg"; the host surfaces it as an inspectable image block.
|
|
68
|
+
def display_image(base64, mime_type: "image/png")
|
|
69
|
+
__omp_emit_display({ mime_type.to_s => base64.to_s })
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def env(key = nil, value = nil)
|
|
74
|
+
if key.nil?
|
|
75
|
+
items = ENV.to_h.sort.to_h
|
|
76
|
+
__omp_emit_status("env", "count" => items.size, "keys" => items.keys.first(20))
|
|
77
|
+
return items
|
|
78
|
+
end
|
|
79
|
+
unless value.nil?
|
|
80
|
+
ENV[key.to_s] = value.to_s
|
|
81
|
+
__omp_emit_status("env", "key" => key.to_s, "value" => value.to_s, "action" => "set")
|
|
82
|
+
return value
|
|
83
|
+
end
|
|
84
|
+
val = ENV[key.to_s]
|
|
85
|
+
__omp_emit_status("env", "key" => key.to_s, "value" => val, "action" => "get")
|
|
86
|
+
val
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# -------------------------------------------------------------------------
|
|
90
|
+
# File helpers
|
|
91
|
+
# -------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def read(path, offset = 1, limit = nil)
|
|
94
|
+
resolved = __omp_resolve_path(path)
|
|
95
|
+
data = File.read(resolved.to_s, encoding: Encoding::UTF_8)
|
|
96
|
+
if offset > 1 || !limit.nil?
|
|
97
|
+
lines = data.lines
|
|
98
|
+
start = [offset - 1, 0].max
|
|
99
|
+
finish = limit ? start + limit : lines.length
|
|
100
|
+
data = lines[start...finish].to_a.join
|
|
101
|
+
end
|
|
102
|
+
__omp_emit_status("read", "path" => resolved.to_s, "chars" => data.length, "preview" => __omp_scrub(data[0, 500].to_s))
|
|
103
|
+
data
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def write(path, content)
|
|
107
|
+
resolved = __omp_resolve_path(path)
|
|
108
|
+
require "fileutils"
|
|
109
|
+
FileUtils.mkdir_p(File.dirname(resolved.to_s))
|
|
110
|
+
File.write(resolved.to_s, content.to_s)
|
|
111
|
+
__omp_emit_status("write", "path" => resolved.to_s, "chars" => content.to_s.length)
|
|
112
|
+
resolved.to_s
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def append(path, content)
|
|
116
|
+
resolved = __omp_resolve_path(path)
|
|
117
|
+
require "fileutils"
|
|
118
|
+
FileUtils.mkdir_p(File.dirname(resolved.to_s))
|
|
119
|
+
File.open(resolved.to_s, "a") { |f| f.write(content.to_s) }
|
|
120
|
+
__omp_emit_status("append", "path" => resolved.to_s, "chars" => content.to_s.length)
|
|
121
|
+
resolved.to_s
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def tree(path = ".", max_depth: 3, show_hidden: false)
|
|
125
|
+
base = path.to_s
|
|
126
|
+
lines = []
|
|
127
|
+
walk = lambda do |dir, prefix, depth|
|
|
128
|
+
return if depth > max_depth
|
|
129
|
+
entries = (Dir.children(dir) rescue [])
|
|
130
|
+
entries = entries.reject { |e| e.start_with?(".") } unless show_hidden
|
|
131
|
+
entries = entries.sort_by { |e| [File.directory?(File.join(dir, e)) ? 0 : 1, e.downcase] }
|
|
132
|
+
entries.each_with_index do |name, i|
|
|
133
|
+
full = File.join(dir, name)
|
|
134
|
+
is_last = i == entries.length - 1
|
|
135
|
+
is_dir = File.directory?(full)
|
|
136
|
+
lines << "#{prefix}#{is_last ? "└── " : "├── "}#{name}#{is_dir ? "/" : ""}"
|
|
137
|
+
walk.call(full, prefix + (is_last ? " " : "│ "), depth + 1) if is_dir
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
lines << "#{base}/"
|
|
141
|
+
walk.call(base, "", 1)
|
|
142
|
+
out = lines.join("\n")
|
|
143
|
+
__omp_emit_status("tree", "path" => base, "entries" => lines.length - 1, "preview" => __omp_scrub(out[0, 1000].to_s))
|
|
144
|
+
out
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def diff(a, b)
|
|
148
|
+
pa = a.to_s
|
|
149
|
+
pb = b.to_s
|
|
150
|
+
lines_a = File.read(pa, encoding: Encoding::UTF_8).lines
|
|
151
|
+
lines_b = File.read(pb, encoding: Encoding::UTF_8).lines
|
|
152
|
+
out = __omp_unified_diff(lines_a, lines_b, pa, pb)
|
|
153
|
+
__omp_emit_status("diff", "file_a" => pa, "file_b" => pb, "identical" => out.empty?, "preview" => __omp_scrub(out[0, 500].to_s))
|
|
154
|
+
out
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# LCS-based op list ([:equal/:delete/:insert, aIndex, bIndex]) per consumed line.
|
|
158
|
+
def __omp_diff_ops(a, b)
|
|
159
|
+
n = a.length
|
|
160
|
+
m = b.length
|
|
161
|
+
if n * m > 4_000_000
|
|
162
|
+
# Too large for the DP table — fall back to a coarse all-delete/all-insert.
|
|
163
|
+
ops = []
|
|
164
|
+
n.times { |i| ops << [:delete, i, 0] }
|
|
165
|
+
m.times { |j| ops << [:insert, n, j] }
|
|
166
|
+
return ops
|
|
167
|
+
end
|
|
168
|
+
dp = Array.new(n + 1) { Array.new(m + 1, 0) }
|
|
169
|
+
(n - 1).downto(0) do |i|
|
|
170
|
+
row = dp[i]
|
|
171
|
+
nrow = dp[i + 1]
|
|
172
|
+
(m - 1).downto(0) do |j|
|
|
173
|
+
row[j] = a[i] == b[j] ? nrow[j + 1] + 1 : (nrow[j] >= row[j + 1] ? nrow[j] : row[j + 1])
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
ops = []
|
|
177
|
+
i = 0
|
|
178
|
+
j = 0
|
|
179
|
+
while i < n && j < m
|
|
180
|
+
if a[i] == b[j]
|
|
181
|
+
ops << [:equal, i, j]; i += 1; j += 1
|
|
182
|
+
elsif dp[i + 1][j] >= dp[i][j + 1]
|
|
183
|
+
ops << [:delete, i, j]; i += 1
|
|
184
|
+
else
|
|
185
|
+
ops << [:insert, i, j]; j += 1
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
ops << [:delete, i, j].tap { i += 1 } while i < n
|
|
189
|
+
ops << [:insert, i, j].tap { j += 1 } while j < m
|
|
190
|
+
ops
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def __omp_unified_diff(a, b, from_file, to_file, context = 3)
|
|
194
|
+
ops = __omp_diff_ops(a, b)
|
|
195
|
+
return "" unless ops.any? { |tag, _, _| tag != :equal }
|
|
196
|
+
|
|
197
|
+
entries = ops.map do |tag, ai, bi|
|
|
198
|
+
{ tag: tag, ai: ai, bi: bi, text: (tag == :insert ? b[bi] : a[ai]) }
|
|
199
|
+
end
|
|
200
|
+
changed = entries.each_index.select { |k| entries[k][:tag] != :equal }
|
|
201
|
+
|
|
202
|
+
groups = []
|
|
203
|
+
start = nil
|
|
204
|
+
prev = nil
|
|
205
|
+
changed.each do |k|
|
|
206
|
+
if start.nil?
|
|
207
|
+
start = k
|
|
208
|
+
prev = k
|
|
209
|
+
elsif k - prev <= (2 * context) + 1
|
|
210
|
+
prev = k
|
|
211
|
+
else
|
|
212
|
+
groups << [start, prev]
|
|
213
|
+
start = k
|
|
214
|
+
prev = k
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
groups << [start, prev] unless start.nil?
|
|
218
|
+
|
|
219
|
+
out = +""
|
|
220
|
+
out << "--- #{from_file}\n"
|
|
221
|
+
out << "+++ #{to_file}\n"
|
|
222
|
+
groups.each do |gs, ge|
|
|
223
|
+
lo = [gs - context, 0].max
|
|
224
|
+
hi = [ge + context, entries.length - 1].min
|
|
225
|
+
slice = entries[lo..hi]
|
|
226
|
+
a_start = nil
|
|
227
|
+
a_count = 0
|
|
228
|
+
b_start = nil
|
|
229
|
+
b_count = 0
|
|
230
|
+
slice.each do |e|
|
|
231
|
+
if e[:tag] != :insert
|
|
232
|
+
a_start ||= e[:ai]
|
|
233
|
+
a_count += 1
|
|
234
|
+
end
|
|
235
|
+
if e[:tag] != :delete
|
|
236
|
+
b_start ||= e[:bi]
|
|
237
|
+
b_count += 1
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
out << "@@ -#{(a_start || 0) + 1},#{a_count} +#{(b_start || 0) + 1},#{b_count} @@\n"
|
|
241
|
+
slice.each do |e|
|
|
242
|
+
prefix = e[:tag] == :equal ? " " : (e[:tag] == :delete ? "-" : "+")
|
|
243
|
+
text = e[:text].to_s
|
|
244
|
+
text = "#{text}\n" unless text.end_with?("\n")
|
|
245
|
+
out << "#{prefix}#{text}"
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
out
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# -------------------------------------------------------------------------
|
|
252
|
+
# Text helpers (sort / uniq / counter)
|
|
253
|
+
# -------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
def sort(text, reverse: false, unique: false)
|
|
256
|
+
lines = text.to_s.lines.map(&:chomp)
|
|
257
|
+
lines = lines.uniq if unique
|
|
258
|
+
lines = lines.sort
|
|
259
|
+
lines = lines.reverse if reverse
|
|
260
|
+
out = lines.join("\n")
|
|
261
|
+
__omp_emit_status("sort", "lines" => lines.length, "unique" => unique, "reverse" => reverse)
|
|
262
|
+
out
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def uniq(text, count: false)
|
|
266
|
+
lines = text.to_s.lines.map(&:chomp)
|
|
267
|
+
if lines.empty?
|
|
268
|
+
__omp_emit_status("uniq", "groups" => 0)
|
|
269
|
+
return count ? [] : ""
|
|
270
|
+
end
|
|
271
|
+
groups = []
|
|
272
|
+
current = lines[0]
|
|
273
|
+
run = 1
|
|
274
|
+
lines[1..].each do |line|
|
|
275
|
+
if line == current
|
|
276
|
+
run += 1
|
|
277
|
+
else
|
|
278
|
+
groups << [run, current]
|
|
279
|
+
current = line
|
|
280
|
+
run = 1
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
groups << [run, current]
|
|
284
|
+
__omp_emit_status("uniq", "groups" => groups.length, "count_mode" => count)
|
|
285
|
+
count ? groups : groups.map { |_, l| l }.join("\n")
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def counter(items, limit: nil, reverse: true)
|
|
289
|
+
arr = items.is_a?(String) ? items.lines.map(&:chomp) : items.to_a
|
|
290
|
+
counts = Hash.new(0)
|
|
291
|
+
arr.each { |i| counts[i] += 1 }
|
|
292
|
+
sorted = counts.sort_by { |item, c| [c, item] }
|
|
293
|
+
sorted = sorted.reverse if reverse
|
|
294
|
+
sorted = sorted.first(limit) if limit
|
|
295
|
+
result = sorted.map { |item, c| [c, item] }
|
|
296
|
+
__omp_emit_status("counter", "unique" => counts.size, "total" => arr.length, "top" => result.first(10))
|
|
297
|
+
result
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# -------------------------------------------------------------------------
|
|
301
|
+
# Task/agent output reader
|
|
302
|
+
# -------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
def __omp_apply_query(data, query)
|
|
305
|
+
return data if query.nil? || query.empty?
|
|
306
|
+
q = query.strip
|
|
307
|
+
q = q[1..] if q.start_with?(".")
|
|
308
|
+
return data if q.empty?
|
|
309
|
+
tokens = []
|
|
310
|
+
buf = +""
|
|
311
|
+
i = 0
|
|
312
|
+
while i < q.length
|
|
313
|
+
ch = q[i]
|
|
314
|
+
if ch == "."
|
|
315
|
+
unless buf.empty?
|
|
316
|
+
tokens << [:key, buf]
|
|
317
|
+
buf = +""
|
|
318
|
+
end
|
|
319
|
+
elsif ch == "["
|
|
320
|
+
unless buf.empty?
|
|
321
|
+
tokens << [:key, buf]
|
|
322
|
+
buf = +""
|
|
323
|
+
end
|
|
324
|
+
j = i + 1
|
|
325
|
+
j += 1 while j < q.length && q[j] != "]"
|
|
326
|
+
inner = q[(i + 1)...j]
|
|
327
|
+
if inner.start_with?('"') && inner.end_with?('"')
|
|
328
|
+
tokens << [:key, inner[1..-2]]
|
|
329
|
+
else
|
|
330
|
+
tokens << [:index, inner.to_i]
|
|
331
|
+
end
|
|
332
|
+
i = j
|
|
333
|
+
else
|
|
334
|
+
buf << ch
|
|
335
|
+
end
|
|
336
|
+
i += 1
|
|
337
|
+
end
|
|
338
|
+
tokens << [:key, buf] unless buf.empty?
|
|
339
|
+
|
|
340
|
+
current = data
|
|
341
|
+
tokens.each do |kind, value|
|
|
342
|
+
if kind == :index
|
|
343
|
+
return nil unless current.is_a?(Array) && value < current.length
|
|
344
|
+
current = current[value]
|
|
345
|
+
else
|
|
346
|
+
return nil unless current.is_a?(Hash) && current.key?(value)
|
|
347
|
+
current = current[value]
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
current
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def output(*ids, format: "raw", query: nil, offset: nil, limit: nil)
|
|
354
|
+
artifacts_dir = ENV["PI_ARTIFACTS_DIR"]
|
|
355
|
+
if artifacts_dir.nil? || artifacts_dir.empty?
|
|
356
|
+
session_file = ENV["PI_SESSION_FILE"]
|
|
357
|
+
if session_file.nil? || session_file.empty?
|
|
358
|
+
__omp_emit_status("output", "error" => "No session file available")
|
|
359
|
+
raise "No session - output artifacts unavailable"
|
|
360
|
+
end
|
|
361
|
+
artifacts_dir = session_file.sub(/\.[^.]*\z/, "")
|
|
362
|
+
end
|
|
363
|
+
unless File.directory?(artifacts_dir)
|
|
364
|
+
__omp_emit_status("output", "error" => "Artifacts directory not found", "path" => artifacts_dir)
|
|
365
|
+
raise "No artifacts directory found: #{artifacts_dir}"
|
|
366
|
+
end
|
|
367
|
+
raise ArgumentError, "At least one output ID is required" if ids.empty?
|
|
368
|
+
if query && (!offset.nil? || !limit.nil?)
|
|
369
|
+
__omp_emit_status("output", "error" => "query cannot be combined with offset/limit")
|
|
370
|
+
raise ArgumentError, "query cannot be combined with offset/limit"
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
results = []
|
|
374
|
+
not_found = []
|
|
375
|
+
ids.each do |output_id|
|
|
376
|
+
path = File.join(artifacts_dir, "#{output_id}.md")
|
|
377
|
+
unless File.exist?(path)
|
|
378
|
+
not_found << output_id
|
|
379
|
+
next
|
|
380
|
+
end
|
|
381
|
+
raw = File.read(path, encoding: Encoding::UTF_8)
|
|
382
|
+
raw_lines = raw.split("\n", -1)
|
|
383
|
+
total_lines = raw_lines.length
|
|
384
|
+
selected = raw
|
|
385
|
+
range_info = nil
|
|
386
|
+
|
|
387
|
+
if query
|
|
388
|
+
json_value =
|
|
389
|
+
begin
|
|
390
|
+
JSON.parse(raw)
|
|
391
|
+
rescue JSON::ParserError => e
|
|
392
|
+
__omp_emit_status("output", "id" => output_id, "error" => "Not valid JSON: #{e.message}")
|
|
393
|
+
raise "Output #{output_id} is not valid JSON: #{e.message}"
|
|
394
|
+
end
|
|
395
|
+
result_value = __omp_apply_query(json_value, query)
|
|
396
|
+
selected =
|
|
397
|
+
begin
|
|
398
|
+
result_value.nil? ? "null" : JSON.pretty_generate(result_value)
|
|
399
|
+
rescue StandardError
|
|
400
|
+
result_value.to_s
|
|
401
|
+
end
|
|
402
|
+
elsif !offset.nil? || !limit.nil?
|
|
403
|
+
start_line = [offset || 1, 1].max
|
|
404
|
+
if start_line > total_lines
|
|
405
|
+
__omp_emit_status("output", "id" => output_id, "error" => "Offset #{start_line} beyond end (#{total_lines} lines)")
|
|
406
|
+
raise "Offset #{start_line} is beyond end of output (#{total_lines} lines) for #{output_id}"
|
|
407
|
+
end
|
|
408
|
+
effective_limit = limit || (total_lines - start_line + 1)
|
|
409
|
+
end_line = [total_lines, start_line + effective_limit - 1].min
|
|
410
|
+
selected = raw_lines[(start_line - 1)...end_line].join("\n")
|
|
411
|
+
range_info = { "start_line" => start_line, "end_line" => end_line, "total_lines" => total_lines }
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
selected = selected.gsub(/\e\[[0-9;]*m/, "") if format == "stripped"
|
|
415
|
+
|
|
416
|
+
if format == "json"
|
|
417
|
+
entry = {
|
|
418
|
+
"id" => output_id,
|
|
419
|
+
"path" => path,
|
|
420
|
+
"line_count" => query ? selected.split("\n").length : total_lines,
|
|
421
|
+
"char_count" => query ? selected.length : raw.length,
|
|
422
|
+
"content" => selected,
|
|
423
|
+
}
|
|
424
|
+
entry["range"] = range_info if range_info
|
|
425
|
+
entry["query"] = query if query
|
|
426
|
+
results << entry
|
|
427
|
+
else
|
|
428
|
+
results << { "id" => output_id, "content" => selected }
|
|
429
|
+
end
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
unless not_found.empty?
|
|
433
|
+
available = Dir.glob(File.join(artifacts_dir, "*.md")).map { |f| File.basename(f, ".md") }.sort
|
|
434
|
+
msg = "Output not found: #{not_found.join(", ")}"
|
|
435
|
+
unless available.empty?
|
|
436
|
+
msg += "\n\nAvailable outputs: #{available.first(20).join(", ")}"
|
|
437
|
+
msg += " (and #{available.length - 20} more)" if available.length > 20
|
|
438
|
+
end
|
|
439
|
+
__omp_emit_status("output", "not_found" => not_found, "available_count" => available.length)
|
|
440
|
+
raise msg
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
if ids.length == 1
|
|
444
|
+
if format == "json"
|
|
445
|
+
__omp_emit_status("output", "id" => ids[0], "chars" => results[0]["char_count"])
|
|
446
|
+
return results[0]
|
|
447
|
+
end
|
|
448
|
+
__omp_emit_status("output", "id" => ids[0], "chars" => results[0]["content"].length)
|
|
449
|
+
return results[0]["content"]
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
if format == "json"
|
|
453
|
+
__omp_emit_status("output", "count" => results.length, "total_chars" => results.sum { |r| r["char_count"] })
|
|
454
|
+
return results
|
|
455
|
+
end
|
|
456
|
+
combined = results.map { |r| { "id" => r["id"], "content" => r["content"] } }
|
|
457
|
+
__omp_emit_status("output", "count" => combined.length, "total_chars" => combined.sum { |r| r["content"].length })
|
|
458
|
+
combined
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# -------------------------------------------------------------------------
|
|
462
|
+
# Host tool bridge (loopback HTTP) — `tool.<name>(args)`, completion, agent.
|
|
463
|
+
# -------------------------------------------------------------------------
|
|
464
|
+
|
|
465
|
+
module OmpBridge
|
|
466
|
+
INTENT_FIELD = "i"
|
|
467
|
+
|
|
468
|
+
module_function
|
|
469
|
+
|
|
470
|
+
def proxy_env
|
|
471
|
+
base = ENV["PI_TOOL_BRIDGE_URL"]
|
|
472
|
+
token = ENV["PI_TOOL_BRIDGE_TOKEN"]
|
|
473
|
+
session = ENV["PI_TOOL_BRIDGE_SESSION"]
|
|
474
|
+
if base.nil? || base.empty? || token.nil? || token.empty? || session.nil? || session.empty?
|
|
475
|
+
raise "tool bridge is unavailable in this kernel"
|
|
476
|
+
end
|
|
477
|
+
[base.sub(%r{/+\z}, ""), token, session]
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def call(name, args)
|
|
481
|
+
require "net/http"
|
|
482
|
+
require "uri"
|
|
483
|
+
base, token, session = proxy_env
|
|
484
|
+
uri = URI("#{base}/v1/tool")
|
|
485
|
+
payload = JSON.generate("session" => session, "run" => $__omp_current_rid, "name" => name, "args" => args)
|
|
486
|
+
http = Net::HTTP.new(uri.hostname, uri.port)
|
|
487
|
+
http.open_timeout = 10
|
|
488
|
+
http.read_timeout = 7 * 24 * 3600
|
|
489
|
+
req = Net::HTTP::Post.new(uri)
|
|
490
|
+
req["Content-Type"] = "application/json"
|
|
491
|
+
req["Authorization"] = "Bearer #{token}"
|
|
492
|
+
req.body = payload
|
|
493
|
+
resp = http.request(req)
|
|
494
|
+
data =
|
|
495
|
+
begin
|
|
496
|
+
JSON.parse(resp.body.to_s)
|
|
497
|
+
rescue JSON::ParserError
|
|
498
|
+
raise "bridge call #{name.inspect}: non-JSON response: #{resp.body.to_s[0, 200].inspect}"
|
|
499
|
+
end
|
|
500
|
+
unless data.is_a?(Hash) && data["ok"]
|
|
501
|
+
raise((data.is_a?(Hash) ? data["error"] : nil) || "bridge call #{name.inspect} failed")
|
|
502
|
+
end
|
|
503
|
+
data["value"]
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def stringify_keys(hash)
|
|
507
|
+
out = {}
|
|
508
|
+
hash.each { |k, v| out[k.to_s] = v }
|
|
509
|
+
out
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def tool_call(name, positional, kwargs)
|
|
513
|
+
merged =
|
|
514
|
+
if positional.nil?
|
|
515
|
+
{}
|
|
516
|
+
elsif positional.is_a?(Hash)
|
|
517
|
+
stringify_keys(positional)
|
|
518
|
+
else
|
|
519
|
+
raise ArgumentError, "tool.#{name}(...) expects a Hash of arguments (got #{positional.class})"
|
|
520
|
+
end
|
|
521
|
+
merged.merge!(stringify_keys(kwargs)) if kwargs && !kwargs.empty?
|
|
522
|
+
merged[INTENT_FIELD] = "rb prelude" unless merged.key?(INTENT_FIELD)
|
|
523
|
+
call(name, merged)
|
|
524
|
+
end
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# `tool[:name]` form — a reusable one-tool callable.
|
|
528
|
+
class OmpToolCallable
|
|
529
|
+
def initialize(name)
|
|
530
|
+
@name = name
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def call(args = nil, **kwargs)
|
|
534
|
+
OmpBridge.tool_call(@name, args, kwargs)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def to_proc
|
|
538
|
+
method(:call).to_proc
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def inspect
|
|
542
|
+
"#<tool.#{@name}>"
|
|
543
|
+
end
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# `tool.<name>(args)` proxy. BasicObject so helper methods defined on Object
|
|
547
|
+
# (read/write/…) never shadow a tool name — every call routes to the bridge.
|
|
548
|
+
class OmpToolProxy < BasicObject
|
|
549
|
+
def method_missing(name, args = nil, **kwargs)
|
|
550
|
+
::OmpBridge.tool_call(name.to_s, args, kwargs)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def [](name)
|
|
554
|
+
::OmpToolCallable.new(name.to_s)
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
def respond_to_missing?(_name, _include_private = false)
|
|
558
|
+
true
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def inspect
|
|
562
|
+
session = ::ENV["PI_TOOL_BRIDGE_SESSION"]
|
|
563
|
+
session ? "#<tool proxy session=#{session}>" : "#<tool proxy unavailable>"
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
def tool
|
|
568
|
+
$__omp_tool_proxy ||= OmpToolProxy.new
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
def completion(prompt, model: "default", system: nil, schema: nil)
|
|
572
|
+
args = { "prompt" => prompt, "model" => model }
|
|
573
|
+
args["system"] = system unless system.nil?
|
|
574
|
+
args["schema"] = schema unless schema.nil?
|
|
575
|
+
res = OmpBridge.call("__completion__", args)
|
|
576
|
+
text = res.is_a?(Hash) ? res["text"] : res
|
|
577
|
+
schema.nil? ? text : JSON.parse(text)
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def agent(prompt, agent_type: "task", model: nil, label: nil, schema: nil, return_handle: false)
|
|
581
|
+
args = { "prompt" => prompt }
|
|
582
|
+
args["agentType"] = agent_type unless agent_type.nil?
|
|
583
|
+
args["model"] = model unless model.nil?
|
|
584
|
+
args["label"] = label unless label.nil?
|
|
585
|
+
args["schema"] = schema unless schema.nil?
|
|
586
|
+
res = OmpBridge.call("__agent__", args)
|
|
587
|
+
text = res.is_a?(Hash) ? res["text"] : res
|
|
588
|
+
parsed = schema.nil? ? text : JSON.parse(text)
|
|
589
|
+
return parsed unless return_handle
|
|
590
|
+
details = res.is_a?(Hash) ? res["details"] : nil
|
|
591
|
+
if !details.is_a?(Hash) || details["id"].nil?
|
|
592
|
+
return { "text" => text, "output" => text, "handle" => nil, "id" => nil, "agent" => nil }
|
|
593
|
+
end
|
|
594
|
+
node = {
|
|
595
|
+
"text" => text,
|
|
596
|
+
"output" => text,
|
|
597
|
+
"handle" => "agent://#{details["id"]}",
|
|
598
|
+
"id" => details["id"],
|
|
599
|
+
"agent" => details["agent"],
|
|
600
|
+
}
|
|
601
|
+
node["data"] = parsed unless schema.nil?
|
|
602
|
+
node
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# -------------------------------------------------------------------------
|
|
606
|
+
# Concurrency: parallel / pipeline over a bounded pool (task.maxConcurrency).
|
|
607
|
+
# -------------------------------------------------------------------------
|
|
608
|
+
|
|
609
|
+
def __omp_concurrency_limit
|
|
610
|
+
snap = (OmpBridge.call("__concurrency__", {}) rescue nil) || {}
|
|
611
|
+
n = (snap["limit"] || 0).to_i
|
|
612
|
+
n > 0 ? n : 0
|
|
613
|
+
rescue StandardError
|
|
614
|
+
0
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
def __omp_pool_map(items)
|
|
618
|
+
arr = items.to_a
|
|
619
|
+
return [] if arr.empty?
|
|
620
|
+
limit = __omp_concurrency_limit
|
|
621
|
+
workers = limit > 0 ? [limit, arr.length].min : arr.length
|
|
622
|
+
results = Array.new(arr.length)
|
|
623
|
+
errors = {}
|
|
624
|
+
emut = Mutex.new
|
|
625
|
+
queue = Queue.new
|
|
626
|
+
arr.each_index { |i| queue << i }
|
|
627
|
+
threads = workers.times.map do
|
|
628
|
+
Thread.new do
|
|
629
|
+
loop do
|
|
630
|
+
idx =
|
|
631
|
+
begin
|
|
632
|
+
queue.pop(true)
|
|
633
|
+
rescue ThreadError
|
|
634
|
+
break
|
|
635
|
+
end
|
|
636
|
+
begin
|
|
637
|
+
results[idx] = yield(arr[idx])
|
|
638
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
639
|
+
emut.synchronize { errors[idx] = e }
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
begin
|
|
645
|
+
threads.each(&:join)
|
|
646
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
647
|
+
threads.each { |t| (t.kill rescue nil) }
|
|
648
|
+
raise
|
|
649
|
+
end
|
|
650
|
+
raise errors[errors.keys.min] unless errors.empty?
|
|
651
|
+
results
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def parallel(thunks)
|
|
655
|
+
list = thunks.to_a
|
|
656
|
+
list.each do |t|
|
|
657
|
+
raise TypeError, "parallel() expects an iterable of zero-arg callables" unless t.respond_to?(:call)
|
|
658
|
+
end
|
|
659
|
+
__omp_pool_map(list) { |t| t.call }
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
def pipeline(items, *stages)
|
|
663
|
+
current = items.to_a
|
|
664
|
+
stages.each do |stage|
|
|
665
|
+
raise TypeError, "pipeline() stages must be callables" unless stage.respond_to?(:call)
|
|
666
|
+
current = __omp_pool_map(current) { |item| stage.call(item) }
|
|
667
|
+
end
|
|
668
|
+
current
|
|
669
|
+
end
|
|
670
|
+
|
|
671
|
+
# -------------------------------------------------------------------------
|
|
672
|
+
# Progress + budget
|
|
673
|
+
# -------------------------------------------------------------------------
|
|
674
|
+
|
|
675
|
+
def log(message)
|
|
676
|
+
__omp_emit_status("log", "message" => message.to_s)
|
|
677
|
+
nil
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
def phase(title)
|
|
681
|
+
$__omp_current_phase = title.to_s
|
|
682
|
+
__omp_emit_status("phase", "title" => title.to_s)
|
|
683
|
+
nil
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Live view of the host Goal Mode token budget via the host bridge.
|
|
687
|
+
class OmpBudget
|
|
688
|
+
def total
|
|
689
|
+
snap = (OmpBridge.call("__budget__", {}) || {})
|
|
690
|
+
snap["total"]
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def hard
|
|
694
|
+
snap = (OmpBridge.call("__budget__", {}) || {})
|
|
695
|
+
snap["hard"] ? true : false
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def spent
|
|
699
|
+
snap = (OmpBridge.call("__budget__", {}) || {})
|
|
700
|
+
(snap["spent"] || 0).to_i
|
|
701
|
+
end
|
|
702
|
+
|
|
703
|
+
def remaining
|
|
704
|
+
snap = (OmpBridge.call("__budget__", {}) || {})
|
|
705
|
+
total = snap["total"]
|
|
706
|
+
return Float::INFINITY if total.nil?
|
|
707
|
+
[0, total - (snap["spent"] || 0).to_i].max
|
|
708
|
+
end
|
|
709
|
+
|
|
710
|
+
def inspect
|
|
711
|
+
snap = ((OmpBridge.call("__budget__", {}) rescue nil) || {})
|
|
712
|
+
"#<budget total=#{snap["total"].inspect} spent=#{snap["spent"].inspect}>"
|
|
713
|
+
rescue StandardError
|
|
714
|
+
"#<budget unavailable>"
|
|
715
|
+
end
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
def budget
|
|
719
|
+
$__omp_budget ||= OmpBudget.new
|
|
720
|
+
end
|
|
721
|
+
end
|