rubik 0.2.0 → 0.3.1
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.
- checksums.yaml +4 -4
- data/ATTRIBUTION.md +4 -0
- data/CHANGELOG.md +115 -0
- data/README.md +137 -268
- data/bin/rubik +9 -0
- data/lib/rubik/cli/workspace_trust.rb +78 -0
- data/lib/rubik/kernel/init.rb +15 -0
- data/lib/rubik/lsp/client.rb +3 -2
- data/lib/rubik/orchestrator/auto_apply.rb +1 -1
- data/lib/rubik/orchestrator/subagent/activation.rb +29 -4
- data/lib/rubik/orchestrator/subagent/builtin_types.rb +117 -0
- data/lib/rubik/orchestrator/subagent/dynamic_store.rb +119 -0
- data/lib/rubik/orchestrator/subagent/instance.rb +8 -36
- data/lib/rubik/orchestrator/subagent/journal.rb +77 -4
- data/lib/rubik/orchestrator/subagent/profile_contract.rb +187 -0
- data/lib/rubik/orchestrator/subagent/profile_snapshot.rb +72 -0
- data/lib/rubik/orchestrator/subagent/reuse.rb +2 -1
- data/lib/rubik/orchestrator/subagent/scoped_registry.rb +2 -0
- data/lib/rubik/orchestrator/subagent/spawn_transaction.rb +170 -0
- data/lib/rubik/orchestrator/subagent/start_gate.rb +98 -0
- data/lib/rubik/orchestrator/subagent/supervisor.rb +4 -67
- data/lib/rubik/orchestrator/subagent/type_definition.rb +84 -0
- data/lib/rubik/orchestrator/subagent/type_registry.rb +139 -159
- data/lib/rubik/orchestrator/turn_executor/subagent_channel.rb +4 -2
- data/lib/rubik/platform/process.rb +78 -1
- data/lib/rubik/prompt/context_builder.rb +54 -20
- data/lib/rubik/prompt/doctrine.rb +158 -22
- data/lib/rubik/prompt/token_estimator.rb +5 -3
- data/lib/rubik/prompt/tool_teachings.rb +19 -12
- data/lib/rubik/sandbox/driver/bubblewrap.rb +13 -59
- data/lib/rubik/sandbox/driver/docker.rb +14 -60
- data/lib/rubik/sandbox/driver/podman.rb +13 -56
- data/lib/rubik/sandbox/driver/process.rb +12 -64
- data/lib/rubik/security/guard.rb +1 -1
- data/lib/rubik/storage/store.rb +52 -11
- data/lib/rubik/tools/builtins/define_subagent.rb +69 -0
- data/lib/rubik/tools/builtins/storage.rb +8 -5
- data/lib/rubik/tools/builtins/subagent.rb +20 -53
- data/lib/rubik/tools/builtins/synthesis.rb +6 -1
- data/lib/rubik/tools/builtins.rb +1 -0
- data/lib/rubik/tools/diagnostic_reporter.rb +2 -1
- data/lib/rubik/tools/fuzzy_matcher.rb +39 -19
- data/lib/rubik/tools/truncator.rb +54 -21
- data/lib/rubik/version.rb +1 -1
- metadata +10 -1
|
@@ -18,7 +18,6 @@ module Rubik
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def run(cmd, timeout: 15, chdir: Dir.pwd, allow_network: false, memory_limit_mb: 512, cpus: "1.0")
|
|
21
|
-
start_time = Time.now
|
|
22
21
|
image = @options[:image] || DEFAULT_IMAGE
|
|
23
22
|
workspace = File.expand_path(chdir)
|
|
24
23
|
container_name = "rubik-box-#{SecureRandom.hex(6)}"
|
|
@@ -40,70 +39,25 @@ module Rubik
|
|
|
40
39
|
docker_args.push("--network", "none") unless allow_network
|
|
41
40
|
docker_args.push(image, "sh", "-c", cmd)
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
wait_thr_ref = wait_thr
|
|
52
|
-
begin
|
|
53
|
-
stdin.close
|
|
54
|
-
rescue StandardError
|
|
55
|
-
nil
|
|
56
|
-
end
|
|
57
|
-
t_out = Thread.new do
|
|
58
|
-
stdout.read
|
|
59
|
-
rescue StandardError
|
|
60
|
-
""
|
|
61
|
-
end
|
|
62
|
-
t_err = Thread.new do
|
|
63
|
-
stderr.read
|
|
64
|
-
rescue StandardError
|
|
65
|
-
""
|
|
66
|
-
end
|
|
67
|
-
begin
|
|
68
|
-
stdout_str = t_out.value.to_s
|
|
69
|
-
stderr_str = t_err.value.to_s
|
|
70
|
-
status = wait_thr.value
|
|
71
|
-
exit_code = status.exitstatus || (status.signaled? ? (128 + (status.termsig || 1)) : 1)
|
|
72
|
-
ensure
|
|
73
|
-
begin
|
|
74
|
-
t_out.kill
|
|
75
|
-
rescue StandardError
|
|
76
|
-
nil
|
|
77
|
-
end
|
|
78
|
-
begin
|
|
79
|
-
t_err.kill
|
|
80
|
-
rescue StandardError
|
|
81
|
-
nil
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
rescue Timeout::Error
|
|
87
|
-
Rubik::Platform.kill_tree(wait_thr_ref.pid) if wait_thr_ref.respond_to?(:pid) && wait_thr_ref.pid
|
|
88
|
-
system("docker rm -f #{container_name} > #{Rubik::Platform.dev_null} 2>&1")
|
|
89
|
-
stderr_str = "Docker sandbox execution timed out after #{timeout} seconds"
|
|
90
|
-
exit_code = 124
|
|
91
|
-
rescue StandardError => e
|
|
92
|
-
stderr_str = "Docker sandbox error: #{e.message}"
|
|
93
|
-
exit_code = 1
|
|
42
|
+
res = begin
|
|
43
|
+
Rubik::Platform.popen_capture(
|
|
44
|
+
*docker_args,
|
|
45
|
+
timeout: timeout,
|
|
46
|
+
chdir: workspace,
|
|
47
|
+
timeout_message: "Docker sandbox execution timed out after #{timeout} seconds",
|
|
48
|
+
error_message: "Docker sandbox error"
|
|
49
|
+
)
|
|
94
50
|
ensure
|
|
95
|
-
Rubik::Platform.kill_tree(wait_thr_ref.pid) if wait_thr_ref.respond_to?(:pid) && wait_thr_ref.pid
|
|
96
51
|
system("docker rm -f #{container_name} > #{Rubik::Platform.dev_null} 2>&1")
|
|
97
52
|
end
|
|
98
53
|
|
|
99
|
-
duration = Time.now - start_time
|
|
100
54
|
{
|
|
101
|
-
ok:
|
|
102
|
-
exit_code: exit_code,
|
|
103
|
-
stdout:
|
|
104
|
-
stderr:
|
|
105
|
-
output: [
|
|
106
|
-
duration: duration
|
|
55
|
+
ok: res[:ok],
|
|
56
|
+
exit_code: res[:exit_code],
|
|
57
|
+
stdout: res[:stdout],
|
|
58
|
+
stderr: res[:stderr],
|
|
59
|
+
output: res[:output],
|
|
60
|
+
duration: res[:duration],
|
|
107
61
|
driver: "docker"
|
|
108
62
|
}
|
|
109
63
|
end
|
|
@@ -16,7 +16,6 @@ module Rubik
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def run(cmd, timeout: 15, chdir: Dir.pwd, allow_network: false, memory_limit_mb: 512, cpus: "1.0")
|
|
19
|
-
start_time = Time.now
|
|
20
19
|
image = @options[:image] || DEFAULT_IMAGE
|
|
21
20
|
workspace = File.expand_path(chdir)
|
|
22
21
|
|
|
@@ -32,63 +31,21 @@ module Rubik
|
|
|
32
31
|
podman_args.push("--network", "none") unless allow_network
|
|
33
32
|
podman_args.push(image, "sh", "-c", cmd)
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
res = Rubik::Platform.popen_capture(
|
|
35
|
+
*podman_args,
|
|
36
|
+
timeout: timeout,
|
|
37
|
+
chdir: workspace,
|
|
38
|
+
timeout_message: "Podman sandbox execution timed out after #{timeout} seconds",
|
|
39
|
+
error_message: "Podman sandbox error"
|
|
40
|
+
)
|
|
38
41
|
|
|
39
|
-
begin
|
|
40
|
-
Timeout.timeout(timeout) do
|
|
41
|
-
Open3.popen3(*podman_args) do |stdin, stdout, stderr, wait_thr|
|
|
42
|
-
begin
|
|
43
|
-
stdin.close
|
|
44
|
-
rescue
|
|
45
|
-
nil
|
|
46
|
-
end
|
|
47
|
-
t_out = Thread.new do
|
|
48
|
-
stdout.read
|
|
49
|
-
rescue
|
|
50
|
-
""
|
|
51
|
-
end
|
|
52
|
-
t_err = Thread.new do
|
|
53
|
-
stderr.read
|
|
54
|
-
rescue
|
|
55
|
-
""
|
|
56
|
-
end
|
|
57
|
-
begin
|
|
58
|
-
stdout_str = t_out.value.to_s
|
|
59
|
-
stderr_str = t_err.value.to_s
|
|
60
|
-
status = wait_thr.value
|
|
61
|
-
exit_code = status.exitstatus || (status.signaled? ? (128 + (status.termsig || 1)) : 1)
|
|
62
|
-
ensure
|
|
63
|
-
begin
|
|
64
|
-
t_out.kill
|
|
65
|
-
rescue
|
|
66
|
-
nil
|
|
67
|
-
end
|
|
68
|
-
begin
|
|
69
|
-
t_err.kill
|
|
70
|
-
rescue
|
|
71
|
-
nil
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
rescue Timeout::Error
|
|
77
|
-
stderr_str = "Podman sandbox execution timed out after #{timeout} seconds"
|
|
78
|
-
exit_code = 124
|
|
79
|
-
rescue StandardError => e
|
|
80
|
-
stderr_str = "Podman sandbox error: #{e.message}"
|
|
81
|
-
exit_code = 1
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
duration = Time.now - start_time
|
|
85
42
|
{
|
|
86
|
-
ok:
|
|
87
|
-
exit_code: exit_code,
|
|
88
|
-
stdout:
|
|
89
|
-
stderr:
|
|
90
|
-
output: [
|
|
91
|
-
duration: duration
|
|
43
|
+
ok: res[:ok],
|
|
44
|
+
exit_code: res[:exit_code],
|
|
45
|
+
stdout: res[:stdout],
|
|
46
|
+
stderr: res[:stderr],
|
|
47
|
+
output: res[:output],
|
|
48
|
+
duration: res[:duration],
|
|
92
49
|
driver: "podman"
|
|
93
50
|
}
|
|
94
51
|
end
|
|
@@ -15,80 +15,28 @@ module Rubik
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def run(cmd, timeout: 15, chdir: Dir.pwd, allow_network: false, memory_limit_mb: 512, cpus: "1.0") # rubocop:disable Lint/UnusedMethodArgument -- sürücü arayüzü kwarg'leri korunuyor
|
|
18
|
-
start_time = Time.now
|
|
19
|
-
stdout_str = ""
|
|
20
|
-
stderr_str = ""
|
|
21
|
-
exit_code = 127
|
|
22
|
-
|
|
23
|
-
# Explicit-argv spawn (Plan 55 D-1): never leave interpreter choice
|
|
24
|
-
# to Open3's string form — on Windows that silently means cmd.exe.
|
|
25
18
|
resolved = ShellResolver.resolve(cmd, preference: @options[:shell] || "auto")
|
|
26
|
-
|
|
27
19
|
env = {}
|
|
28
|
-
# Direct host execution does NOT inject misleading dead proxy variables (127.0.0.1:9999).
|
|
29
|
-
# Hardware/namespace network isolation is only provided when a true sandbox
|
|
30
|
-
# (Bubblewrap, Docker, Podman) is enabled.
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
stdin.close
|
|
39
|
-
rescue
|
|
40
|
-
nil
|
|
41
|
-
end
|
|
42
|
-
t_out = Thread.new do
|
|
43
|
-
stdout.read
|
|
44
|
-
rescue
|
|
45
|
-
""
|
|
46
|
-
end
|
|
47
|
-
t_err = Thread.new do
|
|
48
|
-
stderr.read
|
|
49
|
-
rescue
|
|
50
|
-
""
|
|
51
|
-
end
|
|
52
|
-
begin
|
|
53
|
-
stdout_str = t_out.value.to_s
|
|
54
|
-
stderr_str = t_err.value.to_s
|
|
55
|
-
status = wait_thr.value
|
|
56
|
-
exit_code = status.exitstatus || (status.signaled? ? (128 + (status.termsig || 1)) : 1)
|
|
57
|
-
ensure
|
|
58
|
-
begin
|
|
59
|
-
t_out.kill
|
|
60
|
-
rescue
|
|
61
|
-
nil
|
|
62
|
-
end
|
|
63
|
-
begin
|
|
64
|
-
t_err.kill
|
|
65
|
-
rescue
|
|
66
|
-
nil
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
end
|
|
71
|
-
rescue Timeout::Error
|
|
72
|
-
Rubik::Platform.kill_tree(pid) if pid
|
|
73
|
-
stderr_str = "Execution timed out after #{timeout} seconds"
|
|
74
|
-
exit_code = 124
|
|
75
|
-
rescue StandardError => e
|
|
76
|
-
stderr_str = "Execution error: #{e.message}"
|
|
77
|
-
exit_code = 1
|
|
78
|
-
end
|
|
21
|
+
res = Rubik::Platform.popen_capture(
|
|
22
|
+
env,
|
|
23
|
+
*spawn_args_for(resolved, cmd),
|
|
24
|
+
timeout: timeout,
|
|
25
|
+
chdir: chdir
|
|
26
|
+
)
|
|
79
27
|
|
|
80
|
-
|
|
81
|
-
stderr_out = resolved[:note] ? "#{resolved[:note]}\n#{stderr_str}" : stderr_str
|
|
28
|
+
stderr_out = resolved[:note] ? "#{resolved[:note]}\n#{res[:stderr]}" : res[:stderr]
|
|
82
29
|
require_relative "../../security/redactor" unless defined?(Rubik::Security::Redactor)
|
|
83
|
-
clean_stdout = Rubik::Security::Redactor.redact(
|
|
30
|
+
clean_stdout = Rubik::Security::Redactor.redact(res[:stdout])
|
|
84
31
|
clean_stderr = Rubik::Security::Redactor.redact(stderr_out)
|
|
32
|
+
|
|
85
33
|
{
|
|
86
|
-
ok:
|
|
87
|
-
exit_code: exit_code,
|
|
34
|
+
ok: res[:ok],
|
|
35
|
+
exit_code: res[:exit_code],
|
|
88
36
|
stdout: clean_stdout,
|
|
89
37
|
stderr: clean_stderr,
|
|
90
38
|
output: [clean_stdout, clean_stderr].reject(&:empty?).join("\n"),
|
|
91
|
-
duration: duration
|
|
39
|
+
duration: res[:duration],
|
|
92
40
|
driver: "process",
|
|
93
41
|
shell: resolved[:shell]
|
|
94
42
|
}
|
data/lib/rubik/security/guard.rb
CHANGED
|
@@ -808,7 +808,7 @@ module Rubik
|
|
|
808
808
|
|
|
809
809
|
# ── Synthesis capability table (Candidate 07) ───────────────────────
|
|
810
810
|
# The ONE declaration point for everything the synthesis AST walk
|
|
811
|
-
# denies (
|
|
811
|
+
# denies (docs/explanation/architecture.md §2 "single declaration point" doctrine; §8.3
|
|
812
812
|
# "the seam is enforced where it is declared"). Each row is a capability
|
|
813
813
|
# a synthesized tool must never exercise; layer fields say WHERE the
|
|
814
814
|
# denial bites:
|
data/lib/rubik/storage/store.rb
CHANGED
|
@@ -178,7 +178,8 @@ module Rubik
|
|
|
178
178
|
next if records.empty?
|
|
179
179
|
|
|
180
180
|
haystacks = haystacks_for(t, records)
|
|
181
|
-
|
|
181
|
+
folded_fields = folded_fields_for(t, records)
|
|
182
|
+
{ records: records, haystacks: haystacks, folded_fields: folded_fields,
|
|
182
183
|
has: tokens.map { |token| haystacks.map { |h| h.include?(token) } } }
|
|
183
184
|
end
|
|
184
185
|
return [] if corpus.empty?
|
|
@@ -194,14 +195,17 @@ module Rubik
|
|
|
194
195
|
df = dfs[ti]
|
|
195
196
|
next 0.0 if df.zero?
|
|
196
197
|
|
|
197
|
-
|
|
198
|
+
# PERF: count_occurrences using String#index avoids Array allocation of String#scan
|
|
199
|
+
tf = count_occurrences(c[:haystacks][i], token)
|
|
198
200
|
next 0.0 if tf.zero?
|
|
199
201
|
|
|
200
202
|
idf = Math.log(((total - df + 0.5) / (df + 0.5)) + 1)
|
|
201
203
|
dl = c[:haystacks][i].bytesize
|
|
202
204
|
idf * (tf * (BM25_K1 + 1)) / (tf + (BM25_K1 * (1 - BM25_B + (BM25_B * dl / avgdl))))
|
|
203
205
|
end
|
|
204
|
-
|
|
206
|
+
# PERF: pass cached folded title and tags to avoid String folding & array join allocations per score
|
|
207
|
+
f_title, f_tags = c[:folded_fields][i]
|
|
208
|
+
[score + field_tier_bonus(tokens, f_title, f_tags), c[:records][i]]
|
|
205
209
|
end
|
|
206
210
|
end
|
|
207
211
|
ordered = scored.sort do |(score_a, rec_a), (score_b, rec_b)|
|
|
@@ -224,22 +228,44 @@ module Rubik
|
|
|
224
228
|
end
|
|
225
229
|
end
|
|
226
230
|
|
|
231
|
+
# PERF: Zero-allocation string occurrence counting via String#index loop.
|
|
232
|
+
# Replaces String#scan(token).size to avoid array allocation per matched term.
|
|
233
|
+
def count_occurrences(haystack, token)
|
|
234
|
+
return 0 if token.empty?
|
|
235
|
+
|
|
236
|
+
count = 0
|
|
237
|
+
offset = 0
|
|
238
|
+
token_len = token.length
|
|
239
|
+
while (idx = haystack.index(token, offset))
|
|
240
|
+
count += 1
|
|
241
|
+
offset = idx + token_len
|
|
242
|
+
end
|
|
243
|
+
count
|
|
244
|
+
end
|
|
245
|
+
|
|
227
246
|
# Terim başına alan izi: katlanmış başlıkta geçen terim +1.0,
|
|
228
247
|
# yalnız katlanmış etiketlerde geçen +0.5, gövde izi 0 — bkz.
|
|
229
248
|
# TITLE_TIER_BONUS/TAGS_TIER_BONUS. Yalnız AND eşiğini geçen
|
|
230
249
|
# kayıtlar için hesaplanır (ek tarama yok).
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
tags
|
|
234
|
-
|
|
250
|
+
# PERF: accepts pre-folded title & tags strings or fallback record for backward compatibility.
|
|
251
|
+
def field_tier_bonus(tokens, title_or_record, tags = nil)
|
|
252
|
+
if tags.nil? && title_or_record.is_a?(Hash)
|
|
253
|
+
title = Folding.fold(title_or_record["title"])
|
|
254
|
+
tags = Folding.fold(Array(title_or_record["tags"]).join("\n"))
|
|
255
|
+
else
|
|
256
|
+
title = title_or_record.to_s
|
|
257
|
+
tags = tags.to_s
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
sum = 0.0
|
|
261
|
+
tokens.each do |token|
|
|
235
262
|
if title.include?(token)
|
|
236
|
-
TITLE_TIER_BONUS
|
|
263
|
+
sum += TITLE_TIER_BONUS
|
|
237
264
|
elsif tags.include?(token)
|
|
238
|
-
TAGS_TIER_BONUS
|
|
239
|
-
else
|
|
240
|
-
0.0
|
|
265
|
+
sum += TAGS_TIER_BONUS
|
|
241
266
|
end
|
|
242
267
|
end
|
|
268
|
+
sum
|
|
243
269
|
end
|
|
244
270
|
|
|
245
271
|
def set_ref(name, id)
|
|
@@ -430,6 +456,21 @@ module Rubik
|
|
|
430
456
|
entry[:haystacks] ||= records.map { |r| haystack_of(r) }.freeze
|
|
431
457
|
end
|
|
432
458
|
|
|
459
|
+
# PERF: Memoize folded title and tags per record in @records_cache
|
|
460
|
+
# to avoid re-folding and array joining on every search_ranked query.
|
|
461
|
+
def folded_fields_for(type, records)
|
|
462
|
+
entry = @records_cache[type]
|
|
463
|
+
unless entry
|
|
464
|
+
return records.map do |r|
|
|
465
|
+
[Folding.fold(r["title"]), Folding.fold(Array(r["tags"]).join("\n"))]
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
entry[:folded_fields] ||= records.map do |r|
|
|
470
|
+
[Folding.fold(r["title"]), Folding.fold(Array(r["tags"]).join("\n"))].freeze
|
|
471
|
+
end.freeze
|
|
472
|
+
end
|
|
473
|
+
|
|
433
474
|
def haystack_of(record)
|
|
434
475
|
Folding.fold("#{record['title']}\n#{record['body']}\n#{Array(record['tags']).join("\n")}")
|
|
435
476
|
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../base"
|
|
4
|
+
require_relative "../output"
|
|
5
|
+
require_relative "../../orchestrator/subagent/profile_contract"
|
|
6
|
+
require_relative "subagent"
|
|
7
|
+
|
|
8
|
+
module Rubik
|
|
9
|
+
module Tools
|
|
10
|
+
# Tool for dynamically defining a new subagent archetype at runtime (Plan 94).
|
|
11
|
+
class DefineSubagentTool < BaseTool
|
|
12
|
+
include SubagentToolHelpers
|
|
13
|
+
|
|
14
|
+
DECLARATION = Declaration.new(
|
|
15
|
+
name: "define_subagent", aliases: ["register_subagent_type", "create_subagent_type"],
|
|
16
|
+
description: "Defines a new archetype of subagent that can be invoked via invoke_subagent.",
|
|
17
|
+
risk: :execute, subagent_spawn: true, maestro_blocked: false, # self-evolution plane (TOOLS.md maestro policy)
|
|
18
|
+
params_schema: {
|
|
19
|
+
type: "object",
|
|
20
|
+
properties: {
|
|
21
|
+
name: { type: "string", maxLength: Rubik::Orchestrator::Subagent::ProfileContract::FIELD_LIMITS.fetch(:name),
|
|
22
|
+
description: "Unique name for archetype" },
|
|
23
|
+
description: { type: "string", maxLength: Rubik::Orchestrator::Subagent::ProfileContract::FIELD_LIMITS.fetch(:description),
|
|
24
|
+
description: "Human-readable description" },
|
|
25
|
+
system_prompt: { type: "string", maxLength: Rubik::Orchestrator::Subagent::ProfileContract::FIELD_LIMITS.fetch(:system_prompt),
|
|
26
|
+
description: "Detailed system prompt" },
|
|
27
|
+
requires: { type: "array", maxItems: Rubik::Orchestrator::Subagent::ProfileContract::MAX_COEFFECTS,
|
|
28
|
+
items: { type: "string", maxLength: Rubik::Orchestrator::Subagent::ProfileContract::MAX_COEFFECT_LENGTH },
|
|
29
|
+
description: "Runtime coeffect keys required by this profile" },
|
|
30
|
+
grants: { type: "array", maxItems: Rubik::Orchestrator::Subagent::ProfileContract::MAX_GRANTS,
|
|
31
|
+
items: { type: "string", enum: Rubik::Orchestrator::Subagent::ProfileContract::CAPABILITY_GRANTS },
|
|
32
|
+
description: "Optional capabilities: write and subagent_spawn (default: none)" },
|
|
33
|
+
replace: { type: "boolean", description: "Explicitly allow replacing an existing dynamic archetype (default: false)" }
|
|
34
|
+
},
|
|
35
|
+
required: ["name", "description", "system_prompt"]
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
def initialize(supervisor: nil, runtime_context: nil)
|
|
40
|
+
super(DECLARATION)
|
|
41
|
+
@supervisor = supervisor
|
|
42
|
+
@runtime_context = runtime_context
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def execute(args = {})
|
|
46
|
+
sup = resolve_supervisor(@supervisor, @runtime_context)
|
|
47
|
+
return ToolOutput.error("SubagentSupervisor is not available in runtime context.") unless sup
|
|
48
|
+
|
|
49
|
+
argument = ->(key) { args.key?(key.to_s) ? args[key.to_s] : args[key.to_sym] }
|
|
50
|
+
provided = ->(key) { args.key?(key.to_s) || args.key?(key.to_sym) }
|
|
51
|
+
|
|
52
|
+
owner_ctx = @runtime_context || (sup.respond_to?(:root_context) ? sup.root_context : nil)
|
|
53
|
+
kwargs = {
|
|
54
|
+
name: argument.call(:name), description: argument.call(:description),
|
|
55
|
+
system_prompt: argument.call(:system_prompt), replace: argument.call(:replace) || false
|
|
56
|
+
}
|
|
57
|
+
[:requires, :grants, :enable_write_tools, :enable_subagent_tools].each do |key|
|
|
58
|
+
kwargs[key] = argument.call(key) if provided.call(key)
|
|
59
|
+
end
|
|
60
|
+
kwargs[:context] = owner_ctx if owner_ctx
|
|
61
|
+
|
|
62
|
+
type_def = sup.type_registry.define_custom(**kwargs)
|
|
63
|
+
"Subagent archetype '#{type_def.name}' successfully defined and available for invoke_subagent."
|
|
64
|
+
rescue ArgumentError => e
|
|
65
|
+
ToolOutput.error(e.message)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -24,9 +24,12 @@ module Rubik
|
|
|
24
24
|
DECLARATION = Declaration.new(
|
|
25
25
|
name: "storage", aliases: ["rubik_storage"],
|
|
26
26
|
description: "Central rubik://storage database for goals, plans, memory, wiki, transcripts, reviews, " \
|
|
27
|
-
"and notes. Actions: 'put' (create/update
|
|
28
|
-
"'search', 'set_ref' (alias), 'delete'.
|
|
29
|
-
"
|
|
27
|
+
"and notes. Actions: 'put' (create/update work artifacts — goal, plan, review, note; " \
|
|
28
|
+
"returns id and rubik://storage URI), 'get', 'list', 'search', 'set_ref' (alias), 'delete'. " \
|
|
29
|
+
"When `vakanuvis` is in your toolbelt, write durable `memory` via `vakanuvis(action: 'record')` " \
|
|
30
|
+
"and compiled `wiki` via `vakanuvis(action: 'digest')`, reserving direct `put` on `memory`/`wiki` " \
|
|
31
|
+
"for roles or sessions where `vakanuvis` is unavailable. Keep agent workfiles HERE — not in " \
|
|
32
|
+
"the worktree. Read back any entity with read_file path 'rubik://storage/<type>/<id>'.",
|
|
30
33
|
risk: :write, archetypes: [:coder], write_grant: true,
|
|
31
34
|
maestro_blocked: false, # storage plane stays direct at root (TOOLS.md maestro policy)
|
|
32
35
|
params_schema: {
|
|
@@ -35,12 +38,12 @@ module Rubik
|
|
|
35
38
|
action: {
|
|
36
39
|
type: "string",
|
|
37
40
|
enum: ["put", "get", "list", "search", "set_ref", "delete"],
|
|
38
|
-
description: "Operation (default: 'list')."
|
|
41
|
+
description: "Operation (default: 'list'). Use 'put' for goal/plan/review/note workfiles (or fallback writes when vakanuvis is unavailable)."
|
|
39
42
|
},
|
|
40
43
|
type: {
|
|
41
44
|
type: "string",
|
|
42
45
|
enum: Rubik::Storage::Store::TYPES,
|
|
43
|
-
description: "Entity type: goal, plan, memory, wiki, transcript, review, or note."
|
|
46
|
+
description: "Entity type: goal, plan, memory, wiki, transcript, review, or note (prefer vakanuvis record/digest for memory/wiki writes when available)."
|
|
44
47
|
},
|
|
45
48
|
title: { type: "string", description: "Entity title (required for put)." },
|
|
46
49
|
body: { type: "string", description: "Entity body (markdown text)." },
|
|
@@ -101,20 +101,32 @@ module Rubik
|
|
|
101
101
|
return ToolOutput.error("Failed to invoke subagent(s): #{e.message}")
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
launched,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
104
|
+
launched, remaining = results.partition { |outcome, _obj| outcome == :launched }
|
|
105
|
+
queued, failed = remaining.partition { |outcome, _obj| outcome == :queued }
|
|
106
|
+
lines = launched.empty? ? ["No subagents launched."] : ["Launched #{launched.size} background subagent(s):"]
|
|
107
|
+
lines.concat(launched.map do |_o, i|
|
|
108
|
+
" • [#{i.id}] '#{i.role}' (#{i.type_name}) - #{i.state.to_s.upcase} | transcript: #{i.transcript_ref}"
|
|
109
|
+
end)
|
|
109
110
|
unless queued.empty?
|
|
110
111
|
rows = queued.map { |_o, s| " • [#{s.id}] '#{s.role}' - QUEUED (waiting on: #{s.depends_on.join(', ')})" }
|
|
111
112
|
lines << ("\nQueued #{queued.size} subagent(s) on unmet dependencies (no slot, no cost until released): " + rows.join("\n"))
|
|
112
113
|
end
|
|
114
|
+
unless failed.empty?
|
|
115
|
+
rows = failed.map do |_outcome, spec|
|
|
116
|
+
reason = spec.respond_to?(:last_error) && spec.last_error
|
|
117
|
+
reason = "cancelled before launch" if reason.to_s.empty?
|
|
118
|
+
" • [#{spec.id}] '#{spec.role}' - FAILED TO START (#{reason})"
|
|
119
|
+
end
|
|
120
|
+
lines << ("\n#{failed.size} subagent(s) failed to start: " + rows.join("\n"))
|
|
121
|
+
end
|
|
113
122
|
all_ids = (launched + queued).map { |_o, obj| obj.id }
|
|
114
123
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
124
|
+
unless all_ids.empty?
|
|
125
|
+
lines << "\nSubagents run asynchronously in the background. Conclude your turn to yield the prompt to the user, or call " \
|
|
126
|
+
"#{Rubik::Orchestrator::Subagent::Notices.wait_hint(all_ids)} only if you strictly need results in this turn (queued ids block until the chain settles)."
|
|
127
|
+
end
|
|
128
|
+
result = lines.join("\n")
|
|
129
|
+
failed.empty? ? result : ToolOutput.error(result)
|
|
118
130
|
end
|
|
119
131
|
|
|
120
132
|
private
|
|
@@ -315,50 +327,5 @@ module Rubik
|
|
|
315
327
|
"Result: #{res[0, 1500]}... [truncated — full result: #{more}]"
|
|
316
328
|
end
|
|
317
329
|
end
|
|
318
|
-
|
|
319
|
-
# Tool for dynamically defining a new subagent archetype at runtime (Plan 94).
|
|
320
|
-
class DefineSubagentTool < BaseTool
|
|
321
|
-
include SubagentToolHelpers
|
|
322
|
-
|
|
323
|
-
DECLARATION = Declaration.new(
|
|
324
|
-
name: "define_subagent", aliases: ["register_subagent_type", "create_subagent_type"],
|
|
325
|
-
description: "Defines a new archetype of subagent that can be invoked via invoke_subagent.",
|
|
326
|
-
risk: :execute, subagent_spawn: true, maestro_blocked: false, # self-evolution plane (TOOLS.md maestro policy)
|
|
327
|
-
params_schema: {
|
|
328
|
-
type: "object",
|
|
329
|
-
properties: {
|
|
330
|
-
name: { type: "string", description: "Unique name for archetype" },
|
|
331
|
-
description: { type: "string", description: "Human-readable description" },
|
|
332
|
-
system_prompt: { type: "string", description: "Detailed system prompt" },
|
|
333
|
-
enable_write_tools: { type: "boolean", description: "Equip subagent with write tools (default: false)" },
|
|
334
|
-
enable_subagent_tools: { type: "boolean", description: "Allow subagent to spawn subagents (default: false)" }
|
|
335
|
-
},
|
|
336
|
-
required: ["name", "description", "system_prompt"]
|
|
337
|
-
}
|
|
338
|
-
)
|
|
339
|
-
def initialize(supervisor: nil, runtime_context: nil)
|
|
340
|
-
super(DECLARATION)
|
|
341
|
-
@supervisor = supervisor
|
|
342
|
-
@runtime_context = runtime_context
|
|
343
|
-
end
|
|
344
|
-
|
|
345
|
-
def execute(args = {})
|
|
346
|
-
sup = resolve_supervisor(@supervisor, @runtime_context)
|
|
347
|
-
return ToolOutput.error("SubagentSupervisor is not available in runtime context.") unless sup
|
|
348
|
-
|
|
349
|
-
name = (args["name"] || args[:name]).to_s.strip
|
|
350
|
-
desc = (args["description"] || args[:description]).to_s.strip
|
|
351
|
-
prompt = (args["system_prompt"] || args[:system_prompt]).to_s.strip
|
|
352
|
-
return ToolOutput.error("Parameters 'name', 'description', and 'system_prompt' are required.") if name.empty? || desc.empty? || prompt.empty?
|
|
353
|
-
|
|
354
|
-
type_def = sup.type_registry.define_custom(
|
|
355
|
-
name: name, description: desc, system_prompt: prompt,
|
|
356
|
-
enable_write_tools: args["enable_write_tools"] || args[:enable_write_tools] || false,
|
|
357
|
-
enable_subagent_tools: args["enable_subagent_tools"] || args[:enable_subagent_tools] || false
|
|
358
|
-
)
|
|
359
|
-
|
|
360
|
-
"Subagent archetype '#{type_def.name}' successfully defined and available for invoke_subagent."
|
|
361
|
-
end
|
|
362
|
-
end
|
|
363
330
|
end
|
|
364
331
|
end
|
|
@@ -176,7 +176,12 @@ module Rubik
|
|
|
176
176
|
description: "Storage scope: session (in-memory only), project (.rubik/tools), global (~/.rubik/tools)" },
|
|
177
177
|
risk: { type: "string", enum: ["read", "write", "execute"], default: "execute",
|
|
178
178
|
description: "Risk level of tool (optional; read/write/execute, default: execute)" },
|
|
179
|
-
params_schema: {
|
|
179
|
+
params_schema: {
|
|
180
|
+
type: "object",
|
|
181
|
+
description: "JSON Schema definition of the tool parameters (provide properties/required when the tool " \
|
|
182
|
+
"accepts arguments; defaults to an empty object schema `{ type: 'object', properties: {} }` " \
|
|
183
|
+
"for tools that take no arguments)"
|
|
184
|
+
},
|
|
180
185
|
code: { type: "string", description: "Ruby implementation of tool handler: ->(args) { ... } or class extending Rubik::Tools::BaseTool" },
|
|
181
186
|
test_args: { type: "object", description: "Optional test arguments to perform pre-flight verification in an isolated sandbox" }
|
|
182
187
|
},
|
data/lib/rubik/tools/builtins.rb
CHANGED
|
@@ -20,6 +20,7 @@ require_relative "builtins/runtime"
|
|
|
20
20
|
require_relative "builtins/lifecycle"
|
|
21
21
|
require_relative "builtins/interactive"
|
|
22
22
|
require_relative "builtins/subagent"
|
|
23
|
+
require_relative "builtins/define_subagent"
|
|
23
24
|
require_relative "builtins/send_message"
|
|
24
25
|
require_relative "builtins/subagent_lifecycle"
|
|
25
26
|
require_relative "builtins/dmesg"
|
|
@@ -119,7 +119,8 @@ module Rubik
|
|
|
119
119
|
|
|
120
120
|
lines << "💡 Need a specialized capability?"
|
|
121
121
|
lines << " You can dynamically create and register your own custom tool at runtime using:"
|
|
122
|
-
lines << " `synthesize_tool(name: '#{tool_name.to_s.downcase}', description: '...',
|
|
122
|
+
lines << " `synthesize_tool(name: '#{tool_name.to_s.downcase}', description: '...', " \
|
|
123
|
+
"params_schema: { type: 'object', properties: { ... } }, code: '->(args) { ... }')`"
|
|
123
124
|
|
|
124
125
|
lines.join("\n")
|
|
125
126
|
end
|