rubik 0.4.2 → 0.5.0
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 +1 -1
- data/CHANGELOG.md +48 -1
- data/lib/rubik/ast/cache_store.rb +33 -0
- data/lib/rubik/ast/graph.rb +2 -1
- data/lib/rubik/config/defaults.rb +7 -1
- data/lib/rubik/orchestrator/budget_manager.rb +2 -0
- data/lib/rubik/orchestrator/loop.rb +1 -0
- data/lib/rubik/orchestrator/shell_deps.rb +4 -0
- data/lib/rubik/orchestrator/turn_executor/user_channel.rb +2 -0
- data/lib/rubik/prompt/doctrine.rb +10 -15
- data/lib/rubik/prompt/tool_teachings.rb +29 -35
- data/lib/rubik/repl/commands/agent_context.rb +1 -0
- data/lib/rubik/repl/commands/system.rb +2 -0
- data/lib/rubik/repl/turn_recorder.rb +3 -0
- data/lib/rubik/security/redactor.rb +13 -9
- data/lib/rubik/storage/vakanuvis/gate.rb +13 -8
- data/lib/rubik/storage/vakanuvis/harness.rb +115 -112
- data/lib/rubik/storage/vakanuvis/orientation.rb +277 -0
- data/lib/rubik/storage/vakanuvis/service.rb +5 -1
- data/lib/rubik/storage/vakanuvis.rb +8 -1
- data/lib/rubik/tools/builtins/vakanuvis.rb +13 -15
- data/lib/rubik/version.rb +1 -1
- metadata +2 -1
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../layout"
|
|
4
|
+
|
|
5
|
+
module Rubik
|
|
6
|
+
module Storage
|
|
7
|
+
module Vakanuvis
|
|
8
|
+
# Soğuk-başlangıç ve sıkıştırma sonrası yönelim enjeksiyonu (issue #340):
|
|
9
|
+
# Sistem istemi (system prompt) hiç şişirilmeden, oturumun ilk model
|
|
10
|
+
# çağrısından önce ve her bağlam sıkıştırması / budaması tamamlandığında
|
|
11
|
+
# kök konuşma geçmişine kısa, deterministik bir `role: "user"` yönelim
|
|
12
|
+
# iletisi eklenir. Kart yoksa (N == 0), kronikçi kapalıysa veya
|
|
13
|
+
# `subagents.vakanuvis.orientation == false` ise hiçbir şey enjekte
|
|
14
|
+
# edilmez. Kanca `ctx` içine tersiyle (`on_dispose`) kaydedilir (§1A).
|
|
15
|
+
module Orientation
|
|
16
|
+
DEFAULT_MAX_TOPICS = 10
|
|
17
|
+
HOOK_KEY = :vakanuvis_orientation_hook
|
|
18
|
+
TAG_OPEN = "<vakanuvis_orientation>"
|
|
19
|
+
TAG_CLOSE = "</vakanuvis_orientation>"
|
|
20
|
+
DEPTH_HINT = "Ask `vakanuvis(action: 'recall', query: '<2-3 terms>')` for depth."
|
|
21
|
+
|
|
22
|
+
# Oturum-içi durum ve terslenebilir kanca taşıyıcısı.
|
|
23
|
+
class Hook
|
|
24
|
+
attr_reader :harness
|
|
25
|
+
|
|
26
|
+
def initialize(harness, config: nil)
|
|
27
|
+
@harness = harness
|
|
28
|
+
@config = config
|
|
29
|
+
@session_oriented = false
|
|
30
|
+
@disposed = false
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def dispose!
|
|
34
|
+
@disposed = true
|
|
35
|
+
@session_oriented = false
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def disposed?
|
|
39
|
+
@disposed
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def reset_session!
|
|
43
|
+
@session_oriented = false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def enabled?(override_config = nil)
|
|
47
|
+
return false if @disposed || @harness.nil? || @harness.stopped?
|
|
48
|
+
|
|
49
|
+
cfg = override_config || @config || @harness.config
|
|
50
|
+
return false unless Service.enabled?(cfg)
|
|
51
|
+
|
|
52
|
+
Layout.config_get(cfg, "subagents.vakanuvis.orientation", true) != false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def max_topics(override_config = nil)
|
|
56
|
+
cfg = override_config || @config || @harness.config
|
|
57
|
+
raw = Layout.config_get(cfg, "subagents.vakanuvis.orientation_max_topics", DEFAULT_MAX_TOPICS)
|
|
58
|
+
val = raw.to_i
|
|
59
|
+
val.positive? ? val : DEFAULT_MAX_TOPICS
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_message(override_config = nil)
|
|
63
|
+
return nil unless enabled?(override_config)
|
|
64
|
+
|
|
65
|
+
excerpt = Orientation.format_excerpt(@harness, limit: max_topics(override_config))
|
|
66
|
+
return nil unless excerpt
|
|
67
|
+
|
|
68
|
+
{ role: "user", content: excerpt }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def inject_session_start!(history, config: nil)
|
|
72
|
+
return nil unless history.is_a?(Array)
|
|
73
|
+
|
|
74
|
+
if history.any? { |m| Orientation.orientation_message?(m) }
|
|
75
|
+
@session_oriented = true
|
|
76
|
+
return nil
|
|
77
|
+
end
|
|
78
|
+
return nil if @session_oriented
|
|
79
|
+
return nil unless enabled?(config)
|
|
80
|
+
|
|
81
|
+
msg = build_message(config)
|
|
82
|
+
return nil unless msg
|
|
83
|
+
|
|
84
|
+
@session_oriented = true
|
|
85
|
+
Orientation.deduplicate_and_insert_start!(history, msg)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def inject_after_compaction!(history, config: nil)
|
|
89
|
+
return nil unless history.is_a?(Array)
|
|
90
|
+
|
|
91
|
+
Orientation.strip_orientation_messages!(history)
|
|
92
|
+
return nil unless enabled?(config)
|
|
93
|
+
|
|
94
|
+
msg = build_message(config)
|
|
95
|
+
return nil unless msg
|
|
96
|
+
|
|
97
|
+
@session_oriented = true
|
|
98
|
+
Orientation.insert_after_compaction!(history, msg)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
module_function
|
|
103
|
+
|
|
104
|
+
# Kancayı `ctx` (veya child sub-context) içine tersiyle kaydeder (§1A).
|
|
105
|
+
# Kronikçi devre dışı bırakıldığında veya child dispose edildiğinde
|
|
106
|
+
# kanca otomatik olarak sökülür; reload döngülerinde yetim kanca kalmaz.
|
|
107
|
+
def register_hook(ctx, harness, config: nil, inverse_ctx: nil)
|
|
108
|
+
return nil unless ctx.respond_to?(:provide) && harness
|
|
109
|
+
|
|
110
|
+
cfg = config || harness.config
|
|
111
|
+
hook = Hook.new(harness, config: cfg)
|
|
112
|
+
ctx.provide(HOOK_KEY, hook)
|
|
113
|
+
target_ctx = inverse_ctx || ctx
|
|
114
|
+
cfg_inverse = wire_config_observer(ctx, hook, cfg)
|
|
115
|
+
if target_ctx.respond_to?(:on_dispose)
|
|
116
|
+
target_ctx.on_dispose do
|
|
117
|
+
cfg_inverse&.call
|
|
118
|
+
hook.dispose!
|
|
119
|
+
ctx.unprovide(HOOK_KEY) if ctx.respond_to?(:unprovide) && ctx.get(HOOK_KEY).equal?(hook)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
hook
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def wire_config_observer(ctx, hook, cfg)
|
|
126
|
+
return nil unless cfg.respond_to?(:on_change)
|
|
127
|
+
|
|
128
|
+
cfg.on_change("subagents.vakanuvis") do |_key, _val, _old|
|
|
129
|
+
next if hook.disposed?
|
|
130
|
+
|
|
131
|
+
if hook.enabled?(cfg)
|
|
132
|
+
ctx.provide(HOOK_KEY, hook) unless ctx.get(HOOK_KEY).equal?(hook)
|
|
133
|
+
elsif ctx.respond_to?(:unprovide) && ctx.get(HOOK_KEY).equal?(hook)
|
|
134
|
+
ctx.unprovide(HOOK_KEY)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Coeffect üzerinden kancayı veya doğrudan VAKANUIS harness'ini çözer.
|
|
140
|
+
# VAKANUIS coeffect'i karşılanmamışsa nil döner.
|
|
141
|
+
def resolve_hook(context_or_manager, config: nil)
|
|
142
|
+
ctx = extract_context(context_or_manager)
|
|
143
|
+
return nil unless ctx
|
|
144
|
+
|
|
145
|
+
harness = Rubik::Coeffects.get(ctx, Rubik::Coeffects::VAKANUIS)
|
|
146
|
+
return nil unless harness.is_a?(Harness) && !harness.stopped?
|
|
147
|
+
|
|
148
|
+
cfg = config || harness.config
|
|
149
|
+
return nil unless Service.enabled?(cfg)
|
|
150
|
+
return nil if Layout.config_get(cfg, "subagents.vakanuvis.orientation", true) == false
|
|
151
|
+
|
|
152
|
+
hook = Rubik::Coeffects.get(ctx, HOOK_KEY)
|
|
153
|
+
return hook if hook.is_a?(Hook) && !hook.disposed? && hook.harness.equal?(harness)
|
|
154
|
+
|
|
155
|
+
register_hook(ctx, harness, config: cfg)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def reset_session!(context_or_manager)
|
|
159
|
+
ctx = extract_context(context_or_manager)
|
|
160
|
+
return false unless ctx
|
|
161
|
+
|
|
162
|
+
hook = Rubik::Coeffects.get(ctx, HOOK_KEY)
|
|
163
|
+
hook.reset_session! if hook.is_a?(Hook)
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def inject_session_start!(history, context_or_manager, config: nil)
|
|
168
|
+
hook = resolve_hook(context_or_manager, config: config)
|
|
169
|
+
return nil unless hook
|
|
170
|
+
|
|
171
|
+
hook.inject_session_start!(history, config: config)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def inject_after_compaction!(history, context_or_manager, config: nil)
|
|
175
|
+
strip_orientation_messages!(history) if history.is_a?(Array)
|
|
176
|
+
hook = resolve_hook(context_or_manager, config: config)
|
|
177
|
+
return nil unless hook
|
|
178
|
+
|
|
179
|
+
hook.inject_after_compaction!(history, config: config)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Aktif mem.* kartlarını [ref, row] sırasıyla listeler.
|
|
183
|
+
# Dönen çift: [lines_array, total_active_cards_count].
|
|
184
|
+
def topic_lines(store, limit: nil)
|
|
185
|
+
return [[], 0] unless store
|
|
186
|
+
|
|
187
|
+
bound = store.refs.select { |ref, _| Vocabulary.memory_ref?(ref) }
|
|
188
|
+
cards = store.list(type: "memory").select do |s|
|
|
189
|
+
s.status == Vocabulary::STATUS_ACTIVE && bound.value?(s.id)
|
|
190
|
+
end
|
|
191
|
+
return [[], 0] if cards.empty?
|
|
192
|
+
|
|
193
|
+
all_lines = cards.sort_by { |s| [bound.key(s.id), s.row] }.filter_map do |s|
|
|
194
|
+
record = store.get(type: "memory", id: s.id)
|
|
195
|
+
next unless record # list→get arası silme yarışı (review L2)
|
|
196
|
+
|
|
197
|
+
"- #{bound.key(s.id)} — #{s.title} — #{first_body_line(record)} — #{store.uri_for('memory', s.id)}"
|
|
198
|
+
end
|
|
199
|
+
total = all_lines.size
|
|
200
|
+
shown = limit ? all_lines.first(limit.to_i) : all_lines
|
|
201
|
+
[shown, total]
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def first_body_line(record)
|
|
205
|
+
slice = record["body"].to_s.lines.map(&:strip).reject(&:empty?).first.to_s.slice(0, Harness::RECALL_BODY_SLICE)
|
|
206
|
+
slice.empty? ? "(no body)" : slice
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Aktif mem.* kartlarından deterministik [ref, row] sırasıyla en fazla
|
|
210
|
+
# `limit` satır üretir. Aktif kart yoksa (N == 0) nil döner.
|
|
211
|
+
def format_excerpt(harness, limit: DEFAULT_MAX_TOPICS)
|
|
212
|
+
store = harness.respond_to?(:store) ? harness.store : nil
|
|
213
|
+
lines, total = topic_lines(store, limit: limit)
|
|
214
|
+
return nil if lines.empty?
|
|
215
|
+
|
|
216
|
+
count_label = total > lines.size ? "#{lines.size} of #{total}" : lines.size.to_s
|
|
217
|
+
"#{TAG_OPEN}\nRecorded topics (#{count_label}):\n#{lines.join("\n")}\n#{DEPTH_HINT}\n#{TAG_CLOSE}"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def orientation_message?(msg)
|
|
221
|
+
return false unless msg.is_a?(Hash)
|
|
222
|
+
|
|
223
|
+
role = (msg[:role] || msg["role"]).to_s
|
|
224
|
+
return false unless role == "user"
|
|
225
|
+
|
|
226
|
+
content = (msg[:content] || msg["content"]).to_s
|
|
227
|
+
content.include?(TAG_OPEN) && content.include?(DEPTH_HINT)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def strip_orientation_messages!(history)
|
|
231
|
+
return history unless history.is_a?(Array)
|
|
232
|
+
|
|
233
|
+
history.reject! { |msg| orientation_message?(msg) }
|
|
234
|
+
history
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# Oturum başında: sistem mesajı varsa hemen ardına (ilk user mesajından
|
|
238
|
+
# önce), yoksa dizinin başına yerleştirir. Eski kopyaları temizler.
|
|
239
|
+
def deduplicate_and_insert_start!(history, msg)
|
|
240
|
+
strip_orientation_messages!(history)
|
|
241
|
+
idx = first_non_system_index(history)
|
|
242
|
+
history.insert(idx, msg)
|
|
243
|
+
msg
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Sıkıştırma sonrasında: eski yönelim iletilerini siler ve yeni yönelim
|
|
247
|
+
# iletisini sistem mesajlarının hemen ardına deterministik olarak ekler.
|
|
248
|
+
def insert_after_compaction!(history, msg)
|
|
249
|
+
strip_orientation_messages!(history)
|
|
250
|
+
idx = first_non_system_index(history)
|
|
251
|
+
history.insert(idx, msg)
|
|
252
|
+
msg
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def first_non_system_index(history)
|
|
256
|
+
idx = history.index do |m|
|
|
257
|
+
role = m.is_a?(Hash) ? (m[:role] || m["role"]).to_s : ""
|
|
258
|
+
role != "system"
|
|
259
|
+
end
|
|
260
|
+
idx || history.size
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def extract_context(context_or_manager)
|
|
264
|
+
return nil unless context_or_manager
|
|
265
|
+
|
|
266
|
+
if context_or_manager.respond_to?(:context)
|
|
267
|
+
context_or_manager.context
|
|
268
|
+
elsif context_or_manager.respond_to?(:runtime_manager) && context_or_manager.runtime_manager.respond_to?(:context)
|
|
269
|
+
context_or_manager.runtime_manager.context
|
|
270
|
+
else
|
|
271
|
+
context_or_manager
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
@@ -57,7 +57,11 @@ module Rubik
|
|
|
57
57
|
)
|
|
58
58
|
# Ömür (§2.7): child ctx'in cascade inverse'i worker'ı kapatır;
|
|
59
59
|
# kuyruk FIFO'su drain olur, yarım kronik kalmaz.
|
|
60
|
-
child&.on_dispose
|
|
60
|
+
child&.on_dispose do
|
|
61
|
+
harness.shutdown
|
|
62
|
+
ctx.unprovide(Rubik::Coeffects::VAKANUIS) if ctx.respond_to?(:unprovide) && ctx.get(Rubik::Coeffects::VAKANUIS).equal?(harness)
|
|
63
|
+
end
|
|
64
|
+
Orientation.register_hook(ctx, harness, config: config, inverse_ctx: child)
|
|
61
65
|
harness.start!
|
|
62
66
|
ctx.provide(Rubik::Coeffects::VAKANUIS, harness)
|
|
63
67
|
context_or_manager.reconcile_all if context_or_manager.respond_to?(:reconcile_all)
|
|
@@ -50,10 +50,16 @@ module Rubik
|
|
|
50
50
|
# gereği notice YOK — sadece dmesg/stderr düzlemi.
|
|
51
51
|
SUBSYSTEM = :storage
|
|
52
52
|
|
|
53
|
+
# Konu-adlı uyarıların SON İZİ buradır: iki havuz da patlarsa bile
|
|
54
|
+
# mesaj stderr'e düşer — kayıp iz yok (review #346 F2).
|
|
53
55
|
def log_via(dmesg, level, message)
|
|
54
56
|
dmesg.respond_to?(:log) ? dmesg.log(SUBSYSTEM, level, message) : Rubik::Log.send(level, SUBSYSTEM, message)
|
|
55
57
|
rescue StandardError
|
|
56
|
-
|
|
58
|
+
begin
|
|
59
|
+
Rubik::Log.warn(SUBSYSTEM, message)
|
|
60
|
+
rescue StandardError
|
|
61
|
+
warn("[vakanuvis] #{message}")
|
|
62
|
+
end
|
|
57
63
|
end
|
|
58
64
|
|
|
59
65
|
def content_unchanged?(existing, title, body, tags, links, status)
|
|
@@ -74,4 +80,5 @@ require_relative "vakanuvis/wiki_skeleton"
|
|
|
74
80
|
require_relative "vakanuvis/distill_gate"
|
|
75
81
|
require_relative "vakanuvis/gate"
|
|
76
82
|
require_relative "vakanuvis/harness"
|
|
83
|
+
require_relative "vakanuvis/orientation"
|
|
77
84
|
require_relative "vakanuvis/service"
|
|
@@ -24,15 +24,12 @@ module Rubik
|
|
|
24
24
|
DECLARATION = Declaration.new(
|
|
25
25
|
name: "vakanuvis", aliases: ["chronicler"],
|
|
26
26
|
description: "Ask the background chronicler (compiled knowledge in rubik://storage): " \
|
|
27
|
-
"'recall' (deterministic digest over memory+wiki with source links — ask at task start " \
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"evidence layer; memory cards are the durable conclusions you want recalled by default. " \
|
|
34
|
-
"Hygiene: same topic, same session = one note — update it (or retire it with forget; " \
|
|
35
|
-
"superseded rows keep a trace).",
|
|
27
|
+
"'recall' (deterministic digest over memory+wiki with source links — ask at task start), " \
|
|
28
|
+
"'record' (persist durable knowledge as a memory card; the ONE path for memory writes), " \
|
|
29
|
+
"'forget' (supersede/purge a mem.* card or a note), 'digest' (queue an async wiki compilation; " \
|
|
30
|
+
"returns a deterministic receipt, never blocks on the LLM), 'topics' (list recorded memory topics " \
|
|
31
|
+
"— orient before the first query), 'digest_status' (is my queued digest pending, in flight, or done?). " \
|
|
32
|
+
"Notes (annal.*) are the chronicler's evidence layer; memory cards are the durable conclusions you want recalled by default.",
|
|
36
33
|
risk: :write, archetypes: [], write_grant: false,
|
|
37
34
|
maestro_blocked: false, # storage emsali: kronikçi düzlemi kökte doğrudan
|
|
38
35
|
params_schema: {
|
|
@@ -42,9 +39,11 @@ module Rubik
|
|
|
42
39
|
type: "string", enum: ["recall", "record", "forget", "digest", "topics", "digest_status"],
|
|
43
40
|
description: "Operation (default: 'recall')."
|
|
44
41
|
},
|
|
45
|
-
query: {
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
query: {
|
|
43
|
+
type: "string",
|
|
44
|
+
description: "recall/digest: knowledge query. Recall is keyword-AND — use 2-3 core terms (every " \
|
|
45
|
+
"space-separated term must literally appear); retry with other-language synonyms when empty."
|
|
46
|
+
},
|
|
48
47
|
types: {
|
|
49
48
|
type: "array", items: { type: "string", enum: ["memory", "wiki", "note"] },
|
|
50
49
|
description: "recall: entity scopes to search (default: memory + wiki)."
|
|
@@ -52,9 +51,8 @@ module Rubik
|
|
|
52
51
|
topic: { type: "string", description: "record: card topic — mints ref mem.<topic-slug> (upsert by topic)." },
|
|
53
52
|
body: {
|
|
54
53
|
type: "string",
|
|
55
|
-
description: "record: card body (markdown; end it with a Keywords: line of 5-10 TR+EN synonyms
|
|
56
|
-
"
|
|
57
|
-
"from the body at write time)."
|
|
54
|
+
description: "record: card body (markdown; end it with a Keywords: line of 5-10 TR+EN synonyms if you " \
|
|
55
|
+
"can — a missing Keywords: line is auto-extracted from the body at write time)."
|
|
58
56
|
},
|
|
59
57
|
tags: {
|
|
60
58
|
type: "array", items: { type: "string" },
|
data/lib/rubik/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rubik
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Özgür Ulukır
|
|
@@ -721,6 +721,7 @@ files:
|
|
|
721
721
|
- lib/rubik/storage/vakanuvis/gate.rb
|
|
722
722
|
- lib/rubik/storage/vakanuvis/harness.rb
|
|
723
723
|
- lib/rubik/storage/vakanuvis/memory_steward.rb
|
|
724
|
+
- lib/rubik/storage/vakanuvis/orientation.rb
|
|
724
725
|
- lib/rubik/storage/vakanuvis/service.rb
|
|
725
726
|
- lib/rubik/storage/vakanuvis/vocabulary.rb
|
|
726
727
|
- lib/rubik/storage/vakanuvis/wiki_skeleton.rb
|