rubik 0.1.6 → 0.3.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 +5 -1
- data/CHANGELOG.md +169 -0
- data/README.md +137 -255
- data/bin/rubik +9 -0
- data/lib/rubik/ast/extraction_result.rb +37 -0
- data/lib/rubik/ast/extractor.rb +35 -9
- data/lib/rubik/ast/graph.rb +27 -1
- data/lib/rubik/ast/treesitter/c_walker.rb +136 -28
- data/lib/rubik/ast/treesitter_extractor.rb +45 -17
- data/lib/rubik/buffer/apply_contract.rb +78 -0
- data/lib/rubik/buffer/manager.rb +61 -5
- data/lib/rubik/cli/workspace_trust.rb +78 -0
- data/lib/rubik/concurrency/event_loop.rb +56 -44
- data/lib/rubik/concurrency/scheduler.rb +7 -24
- data/lib/rubik/context/git_lister.rb +6 -1
- data/lib/rubik/context/scanner.rb +15 -3
- data/lib/rubik/kernel/init.rb +15 -0
- data/lib/rubik/mcp/transport/stdio.rb +14 -11
- 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 +152 -3
- data/lib/rubik/plugin/manager.rb +148 -71
- data/lib/rubik/plugin/package_inventory.rb +84 -0
- data/lib/rubik/plugin/requires_dag.rb +40 -0
- data/lib/rubik/plugin/storage.rb +100 -14
- 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/repl/commands/buffer.rb +28 -18
- data/lib/rubik/runtime/repair_probe.rb +98 -0
- 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/blocked_guidance.rb +19 -0
- data/lib/rubik/security/guard.rb +116 -7
- data/lib/rubik/storage/store.rb +52 -11
- data/lib/rubik/tools/builtins/define_subagent.rb +69 -0
- data/lib/rubik/tools/builtins/lifecycle.rb +129 -136
- 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/version.rb +1 -1
- metadata +15 -1
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
module Subagent
|
|
6
|
+
# Owns the transactional boundary for one subagent spawn. Kept separate
|
|
7
|
+
# from the root supervisor's query and lifecycle APIs.
|
|
8
|
+
module SpawnSupervisor
|
|
9
|
+
RESERVED_ROLE_PROFILES = {
|
|
10
|
+
"vakanüvis distiller" => "vakanuvis",
|
|
11
|
+
"context pruner" => "pruner"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
CHILD_CONTEXT_COEFFECTS = [
|
|
15
|
+
Rubik::Coeffects::SUBAGENT_QUESTION_ASKER,
|
|
16
|
+
Rubik::Coeffects::SUBAGENT_SENDER_ID,
|
|
17
|
+
Rubik::Coeffects::TASK_CANCELLATION
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def spawn(role:, prompt:, type_name: "research", system_prompt: nil,
|
|
21
|
+
parent_id: ConversationId::ROOT_ID, workspace: "inherit", model: "inherit",
|
|
22
|
+
max_steps: nil, provider: nil, depends_on: nil, reserved_id: nil, owner: nil)
|
|
23
|
+
role_str = role.to_s.strip
|
|
24
|
+
prompt_str = prompt.to_s.strip
|
|
25
|
+
raise ArgumentError, "Subagent role cannot be empty" if role_str.empty?
|
|
26
|
+
raise ArgumentError, "Subagent prompt cannot be empty" if prompt_str.empty?
|
|
27
|
+
|
|
28
|
+
# Issue #125: the owner's task was cancelled — refuse before any
|
|
29
|
+
# reservation, slot or context work happens.
|
|
30
|
+
refute_cancelled_owner!(owner)
|
|
31
|
+
|
|
32
|
+
# Bağımlı spec doğrudan spawn edilmez: kuyruğa girer (FS/SS
|
|
33
|
+
# kısıtı), bırakma süpürmesi gerçek koreografiyi oynatır.
|
|
34
|
+
if depends_on
|
|
35
|
+
return spawn_batch([{ role: role_str, prompt: prompt_str, type_name: type_name,
|
|
36
|
+
system_prompt: system_prompt, parent_id: parent_id, workspace: workspace,
|
|
37
|
+
model: model, max_steps: max_steps, provider: provider, depends_on: depends_on }],
|
|
38
|
+
owner: owner).first[1]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
created = @monitor.synchronize do
|
|
42
|
+
validate_concurrency!
|
|
43
|
+
validate_depth!(parent_id)
|
|
44
|
+
|
|
45
|
+
profile_snapshot = resolve_profile_snapshot(type_name: type_name, role: role_str,
|
|
46
|
+
system_prompt: system_prompt)
|
|
47
|
+
effective_sys_prompt = profile_snapshot.effective_system_prompt
|
|
48
|
+
# A profile update must not redirect rollback to a newly provided
|
|
49
|
+
# init supervisor after this spawn registered its unit.
|
|
50
|
+
activation_init = init_for_activation
|
|
51
|
+
# Transcript persistence needs the full custom prompt, whereas an
|
|
52
|
+
# unchanged built-in prompt can be reconstructed from its type.
|
|
53
|
+
prompt_reconstructible = system_prompt.to_s.strip.empty? && !profile_snapshot.custom_prompt
|
|
54
|
+
|
|
55
|
+
# Queued specs already reserved this id; fresh spawns reserve the
|
|
56
|
+
# next sequence number here and return it if activation rolls back.
|
|
57
|
+
generated_id = reserved_id.nil?
|
|
58
|
+
subagent_id = generated_id ? "subagent-#{@counter + 1}" : reserved_id.to_s
|
|
59
|
+
id_in_use = @instances.key?(subagent_id) || (@finished || {}).key?(subagent_id)
|
|
60
|
+
raise ArgumentError, "Subagent id '#{subagent_id}' is already in use." if id_in_use
|
|
61
|
+
|
|
62
|
+
@counter += 1 if generated_id
|
|
63
|
+
|
|
64
|
+
child_ctx = nil
|
|
65
|
+
inst = nil
|
|
66
|
+
begin
|
|
67
|
+
child_ctx = extend_root_context!(:spawn)
|
|
68
|
+
effective_buffer = branch_or_inherit_buffer(workspace)
|
|
69
|
+
scoped_registry = build_scoped_registry(profile_snapshot, child_ctx, effective_buffer)
|
|
70
|
+
prov, effective_model = resolve_provider_and_model(model)
|
|
71
|
+
prov = provider if provider
|
|
72
|
+
|
|
73
|
+
inst = Instance.new(
|
|
74
|
+
id: subagent_id, parent_id: parent_id, role: role_str, type_name: profile_snapshot.name,
|
|
75
|
+
prompt: prompt_str, system_prompt: effective_sys_prompt, workspace: workspace,
|
|
76
|
+
model: effective_model, context: child_ctx, tool_registry: scoped_registry,
|
|
77
|
+
provider: prov, buffer_manager: effective_buffer, max_steps: (max_steps || @default_max_steps).to_i,
|
|
78
|
+
provider_gate: @provider_gate, root_dir: @root_dir, config: @config,
|
|
79
|
+
context_limit: (@config.respond_to?(:fetch) ? @config.fetch("context.max_tokens", 32_768) : 32_768),
|
|
80
|
+
event_bus: @event_bus, transcript_storage: absorb_storage, owner_task: owner,
|
|
81
|
+
prompt_reconstructible: prompt_reconstructible, profile_snapshot: profile_snapshot
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
@instances[subagent_id] = inst
|
|
85
|
+
activate_instance!(inst, child_ctx: child_ctx, owner_token: owner,
|
|
86
|
+
init_supervisor: activation_init, defer_start: true)
|
|
87
|
+
raise "Subagent worker start gate could not be released." unless inst.release_start!
|
|
88
|
+
|
|
89
|
+
inst
|
|
90
|
+
rescue StandardError
|
|
91
|
+
rollback_spawn!(subagent_id: subagent_id, inst: inst, child_ctx: child_ctx,
|
|
92
|
+
init_supervisor: activation_init, generated_id: generated_id)
|
|
93
|
+
raise
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
# SS bırakma kancası: yeni başlayan instance, onu bekleyen kuyruk
|
|
97
|
+
# spesifikasyonlarını serbest bırakır (kilit DIŞINDA — emit kuralı).
|
|
98
|
+
begin
|
|
99
|
+
release_ready!
|
|
100
|
+
rescue StandardError => e
|
|
101
|
+
Rubik::Log.warn(:orchestrator, "subagent dependency release failed after successful spawn (#{e.class}: #{e.message})")
|
|
102
|
+
end
|
|
103
|
+
created
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private
|
|
107
|
+
|
|
108
|
+
def resolve_profile_snapshot(type_name:, role:, system_prompt:)
|
|
109
|
+
reserved_name = RESERVED_ROLE_PROFILES[role.to_s.downcase.strip]
|
|
110
|
+
if reserved_name
|
|
111
|
+
requested = type_name.to_s.downcase.strip
|
|
112
|
+
unless requested.empty? || requested == "research" || requested == reserved_name
|
|
113
|
+
raise ReservedProfileError,
|
|
114
|
+
"Reserved subagent role '#{role}' must use built-in archetype '#{reserved_name}', not '#{requested}'."
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
type_def = @type_registry.builtins.find { |candidate| candidate.name.to_s.downcase == reserved_name }
|
|
118
|
+
unless type_def
|
|
119
|
+
raise ReservedProfileError,
|
|
120
|
+
"Reserved subagent role '#{role}' requires built-in archetype '#{reserved_name}', which is unavailable."
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if !system_prompt.to_s.strip.empty? && system_prompt.to_s != type_def.system_prompt.to_s
|
|
124
|
+
raise ReservedProfileError, "Reserved subagent role '#{role}' cannot override its built-in system prompt."
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
effective_prompt = type_def.system_prompt
|
|
128
|
+
else
|
|
129
|
+
type_def = @type_registry.get(type_name) || @type_registry.get("research")
|
|
130
|
+
raise ReservedProfileError, "No subagent archetype is available for '#{type_name}'." unless type_def
|
|
131
|
+
|
|
132
|
+
effective_prompt = system_prompt.nil? ? type_def.system_prompt : system_prompt
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
snapshot = ProfileSnapshot.capture(type_def, effective_system_prompt: effective_prompt)
|
|
136
|
+
missing = snapshot.requires.reject do |name|
|
|
137
|
+
CHILD_CONTEXT_COEFFECTS.include?(name) || !Rubik::Coeffects.get(@root_context, name).nil?
|
|
138
|
+
end
|
|
139
|
+
raise UnsatisfiedCoeffectsError.new(snapshot.name, missing) unless missing.empty?
|
|
140
|
+
|
|
141
|
+
snapshot
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def rollback_spawn!(subagent_id:, inst:, child_ctx:, init_supervisor:, generated_id:)
|
|
145
|
+
@instances.delete(subagent_id) if inst && @instances[subagent_id].equal?(inst)
|
|
146
|
+
|
|
147
|
+
begin
|
|
148
|
+
inst&.abort_start!
|
|
149
|
+
rescue StandardError => e
|
|
150
|
+
Rubik::Log.warn(:orchestrator, "subagent spawn thread rollback failed (#{e.class}: #{e.message})")
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
begin
|
|
154
|
+
init_supervisor&.unregister(inst.unit) if inst&.unit
|
|
155
|
+
rescue StandardError => e
|
|
156
|
+
Rubik::Log.warn(:orchestrator, "subagent kernel unit rollback failed (#{e.class}: #{e.message})")
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
begin
|
|
160
|
+
child_ctx&.dispose
|
|
161
|
+
rescue StandardError => e
|
|
162
|
+
Rubik::Log.warn(:orchestrator, "subagent context rollback failed (#{e.class}: #{e.message})")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
@counter -= 1 if generated_id && subagent_id == "subagent-#{@counter}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
module Subagent
|
|
6
|
+
# Owns the parked worker gate used to make subagent startup reversible.
|
|
7
|
+
module StartGate
|
|
8
|
+
def start_async(defer_start: false, &on_complete)
|
|
9
|
+
run_gate = Queue.new
|
|
10
|
+
@monitor.synchronize do
|
|
11
|
+
return false if @thread&.alive?
|
|
12
|
+
|
|
13
|
+
@started_at = Time.now
|
|
14
|
+
@run_started_monotonic = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
15
|
+
@run_started_step = @budget.current_step
|
|
16
|
+
@run_seq += 1
|
|
17
|
+
@start_aborted = false
|
|
18
|
+
@start_gate = run_gate
|
|
19
|
+
transition!(:running)
|
|
20
|
+
@unit&.sync_state!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ready = Queue.new
|
|
24
|
+
thread = Thread.new do
|
|
25
|
+
Thread.handle_interrupt(Object => :never) do
|
|
26
|
+
ready << true # cleanup is armed before cancellation is enabled
|
|
27
|
+
Thread.handle_interrupt(Object => :immediate) do
|
|
28
|
+
run_execution_cycle if run_gate.pop == :start
|
|
29
|
+
end
|
|
30
|
+
ensure
|
|
31
|
+
aborted = @monitor.synchronize { @start_aborted }
|
|
32
|
+
@ended_at ||= Time.now
|
|
33
|
+
if aborted
|
|
34
|
+
cleanup_context
|
|
35
|
+
else
|
|
36
|
+
profile_name = @profile_snapshot&.name || @type_name
|
|
37
|
+
profile_version = @profile_snapshot ? @profile_snapshot.profile_version.to_s : "unknown"
|
|
38
|
+
@journal&.record_run_finished(run: @run_seq, state: @state.to_s, result: @result,
|
|
39
|
+
result_ref: (result_ref if settled?), outcome: @state.to_s,
|
|
40
|
+
error_summary: @last_error,
|
|
41
|
+
duration_ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) -
|
|
42
|
+
@run_started_monotonic) * 1000).round,
|
|
43
|
+
step_count: [@budget.current_step - @run_started_step, 0].max,
|
|
44
|
+
profile_name: profile_name, profile_version: profile_version)
|
|
45
|
+
save_transcript! if @transcript_path.nil?
|
|
46
|
+
@cancelled_by_operator ? cleanup : cleanup_context
|
|
47
|
+
on_complete&.call(self)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
@monitor.synchronize { @thread = thread }
|
|
52
|
+
ready.pop
|
|
53
|
+
release_start! unless defer_start
|
|
54
|
+
true
|
|
55
|
+
rescue StandardError
|
|
56
|
+
abort_start!
|
|
57
|
+
raise
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# The spawn transaction releases the worker only after every
|
|
61
|
+
# fallible registration step has completed.
|
|
62
|
+
def release_start!
|
|
63
|
+
@monitor.synchronize do
|
|
64
|
+
return false unless @start_gate && !@start_aborted
|
|
65
|
+
|
|
66
|
+
profile_name = @profile_snapshot&.name || @type_name
|
|
67
|
+
profile_version = @profile_snapshot ? @profile_snapshot.profile_version.to_s : "unknown"
|
|
68
|
+
@journal&.record_run_started(run: @run_seq, role: @role, type: @type_name, prompt: @prompt,
|
|
69
|
+
profile_name: profile_name, profile_version: profile_version)
|
|
70
|
+
@start_gate << :start
|
|
71
|
+
@start_gate = nil
|
|
72
|
+
end
|
|
73
|
+
true
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def abort_start!
|
|
77
|
+
thread = @monitor.synchronize do
|
|
78
|
+
@start_aborted = true
|
|
79
|
+
gate = @start_gate
|
|
80
|
+
@start_gate = nil
|
|
81
|
+
gate << :abort if gate
|
|
82
|
+
@thread
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if thread && thread != Thread.current
|
|
86
|
+
thread.join(0.5)
|
|
87
|
+
if thread.alive?
|
|
88
|
+
thread.kill
|
|
89
|
+
thread.join
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
cleanup_context
|
|
93
|
+
true
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -4,6 +4,8 @@ require "monitor"
|
|
|
4
4
|
require_relative "../../platform"
|
|
5
5
|
require_relative "../task_cancellation"
|
|
6
6
|
require_relative "type_registry"
|
|
7
|
+
require_relative "profile_snapshot"
|
|
8
|
+
require_relative "spawn_transaction"
|
|
7
9
|
require_relative "instance"
|
|
8
10
|
require_relative "result_archive"
|
|
9
11
|
require_relative "execution_reads"
|
|
@@ -44,6 +46,7 @@ module Rubik
|
|
|
44
46
|
include DependencySupervisor
|
|
45
47
|
include MessagingSupervisor
|
|
46
48
|
include ActivationSupervisor
|
|
49
|
+
include SpawnSupervisor
|
|
47
50
|
include ExecutionReads
|
|
48
51
|
include TaskOwnershipSupervisor
|
|
49
52
|
include WaitSupervisor
|
|
@@ -60,7 +63,7 @@ module Rubik
|
|
|
60
63
|
@event_bus = event_bus
|
|
61
64
|
@init = init
|
|
62
65
|
|
|
63
|
-
@type_registry = TypeRegistry.new
|
|
66
|
+
@type_registry = TypeRegistry.new(default_context: @root_context)
|
|
64
67
|
@instances = {}
|
|
65
68
|
@deferred = {}
|
|
66
69
|
@monitor = Monitor.new
|
|
@@ -78,72 +81,6 @@ module Rubik
|
|
|
78
81
|
@root_context.register_disposable { shutdown_all } if @root_context.respond_to?(:register_disposable)
|
|
79
82
|
end
|
|
80
83
|
|
|
81
|
-
def spawn(role:, prompt:, type_name: "research", system_prompt: nil,
|
|
82
|
-
parent_id: ConversationId::ROOT_ID, workspace: "inherit", model: "inherit",
|
|
83
|
-
max_steps: nil, provider: nil, depends_on: nil, reserved_id: nil, owner: nil)
|
|
84
|
-
role_str = role.to_s.strip
|
|
85
|
-
prompt_str = prompt.to_s.strip
|
|
86
|
-
raise ArgumentError, "Subagent role cannot be empty" if role_str.empty?
|
|
87
|
-
raise ArgumentError, "Subagent prompt cannot be empty" if prompt_str.empty?
|
|
88
|
-
|
|
89
|
-
# Issue #125: the owner's task was cancelled — refuse before any
|
|
90
|
-
# reservation, slot or context work happens.
|
|
91
|
-
refute_cancelled_owner!(owner)
|
|
92
|
-
|
|
93
|
-
# Bağımlı spec doğrudan spawn edilmez: kuyruğa girer (FS/SS
|
|
94
|
-
# kısıtı), bırakma süpürmesi gerçek koreografiyi oynatır.
|
|
95
|
-
if depends_on
|
|
96
|
-
return spawn_batch([{ role: role_str, prompt: prompt_str, type_name: type_name,
|
|
97
|
-
system_prompt: system_prompt, parent_id: parent_id, workspace: workspace,
|
|
98
|
-
model: model, max_steps: max_steps, provider: provider, depends_on: depends_on }],
|
|
99
|
-
owner: owner).first[1]
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
created = @monitor.synchronize do
|
|
103
|
-
validate_concurrency!
|
|
104
|
-
validate_depth!(parent_id)
|
|
105
|
-
|
|
106
|
-
type_def = @type_registry.get(type_name) || @type_registry.get("research")
|
|
107
|
-
effective_sys_prompt = system_prompt || type_def.system_prompt
|
|
108
|
-
# Transcript sağlık paketi (2026-10-10): etkili prompt yalnızca
|
|
109
|
-
# builtin archetype default'undan türemişse kurulabilirdir →
|
|
110
|
-
# kalıcı dosyada meta stub yeterli. Çağıranın kendi system_prompt'i
|
|
111
|
-
# veya custom tip prompt'u tek kayıttır → bire bir kopya kalır.
|
|
112
|
-
prompt_reconstructible = system_prompt.to_s.strip.empty? && !type_def.custom_prompt
|
|
113
|
-
|
|
114
|
-
child_ctx = extend_root_context!(:spawn)
|
|
115
|
-
effective_buffer = branch_or_inherit_buffer(workspace)
|
|
116
|
-
scoped_registry = build_scoped_registry(type_def, child_ctx, effective_buffer)
|
|
117
|
-
prov, effective_model = resolve_provider_and_model(model)
|
|
118
|
-
prov = provider if provider
|
|
119
|
-
|
|
120
|
-
# Kuyruktan bırakılan spec kendi REZERVE id'siyle doğar — modelin
|
|
121
|
-
# depends_on/wait referansı kopmaz. Taze spawn yalnız sayaç ilerletir.
|
|
122
|
-
subagent_id = reserved_id || "subagent-#{@counter += 1}"
|
|
123
|
-
|
|
124
|
-
inst = Instance.new(
|
|
125
|
-
id: subagent_id, parent_id: parent_id, role: role_str, type_name: type_def.name,
|
|
126
|
-
prompt: prompt_str, system_prompt: effective_sys_prompt, workspace: workspace,
|
|
127
|
-
model: effective_model, context: child_ctx, tool_registry: scoped_registry,
|
|
128
|
-
provider: prov, buffer_manager: effective_buffer, max_steps: (max_steps || @default_max_steps).to_i,
|
|
129
|
-
provider_gate: @provider_gate, root_dir: @root_dir, config: @config,
|
|
130
|
-
context_limit: (@config.respond_to?(:fetch) ? @config.fetch("context.max_tokens", 32_768) : 32_768),
|
|
131
|
-
event_bus: @event_bus, transcript_storage: absorb_storage, owner_task: owner,
|
|
132
|
-
prompt_reconstructible: prompt_reconstructible
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
@instances[subagent_id] = inst
|
|
136
|
-
activate_instance!(inst, child_ctx: child_ctx, owner_token: owner)
|
|
137
|
-
inst
|
|
138
|
-
end
|
|
139
|
-
# SS bırakma kancası: yeni başlayan instance, onu bekleyen kuyruk
|
|
140
|
-
# spesifikasyonlarını serbest bırakır (kilit DIŞINDA — emit kuralı).
|
|
141
|
-
release_ready!
|
|
142
|
-
created
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Tombstone + kuyruk dahil arama: reap/rezervasyon sonrası da
|
|
146
|
-
# status/transcript/list sorgulanır.
|
|
147
84
|
def get(id) = @monitor.synchronize { @instances[id.to_s] || (@finished ||= {})[id.to_s] || queued_spec(id.to_s) }
|
|
148
85
|
|
|
149
86
|
def extend_budget(id, steps = Budget::DEFAULT_GRANT_STEPS)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rubik
|
|
4
|
+
module Orchestrator
|
|
5
|
+
module Subagent
|
|
6
|
+
# Registered archetype representing a subagent role profile, baseline
|
|
7
|
+
# system prompt, capability filtering policies, and lifecycle revision.
|
|
8
|
+
TypeDefinition = Struct.new(
|
|
9
|
+
:name,
|
|
10
|
+
:description,
|
|
11
|
+
:system_prompt,
|
|
12
|
+
:requires, :grants,
|
|
13
|
+
:tool_filter,
|
|
14
|
+
:enable_write_tools, :enable_subagent_tools, :custom_prompt,
|
|
15
|
+
:revision, :context, :handle,
|
|
16
|
+
keyword_init: true
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Internal tracking struct for a single archetype registration revision.
|
|
20
|
+
Registration = Struct.new(:revision, :name, :type_def, :handle, :context, :active, keyword_init: true)
|
|
21
|
+
|
|
22
|
+
# Handle returned by dynamic archetype registration, allowing explicit
|
|
23
|
+
# reversible cleanup and exposing lifecycle revision metadata. Delegates
|
|
24
|
+
# all profile properties to the underlying TypeDefinition.
|
|
25
|
+
class RegistrationHandle
|
|
26
|
+
attr_reader :revision, :type_def, :context, :registry, :name
|
|
27
|
+
|
|
28
|
+
def initialize(revision:, type_def:, context:, registry:)
|
|
29
|
+
@revision = revision
|
|
30
|
+
@type_def = type_def
|
|
31
|
+
@context = context
|
|
32
|
+
@registry = registry
|
|
33
|
+
@name = type_def.name
|
|
34
|
+
@active = true
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def active?
|
|
38
|
+
@active
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def undo
|
|
42
|
+
return false unless @active
|
|
43
|
+
|
|
44
|
+
@registry.unregister_dynamic(self)
|
|
45
|
+
end
|
|
46
|
+
alias unregister undo
|
|
47
|
+
|
|
48
|
+
def to_type_def
|
|
49
|
+
@type_def
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def handle
|
|
53
|
+
self
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def description = @type_def.description
|
|
57
|
+
def system_prompt = @type_def.system_prompt
|
|
58
|
+
def tool_filter = @type_def.tool_filter
|
|
59
|
+
def requires = @type_def.requires
|
|
60
|
+
def grants = @type_def.grants
|
|
61
|
+
def enable_write_tools = @type_def.enable_write_tools
|
|
62
|
+
def enable_subagent_tools = @type_def.enable_subagent_tools
|
|
63
|
+
def custom_prompt = @type_def.custom_prompt
|
|
64
|
+
|
|
65
|
+
def is_a?(klass)
|
|
66
|
+
klass == TypeDefinition || klass == RegistrationHandle || super
|
|
67
|
+
end
|
|
68
|
+
alias kind_of? is_a?
|
|
69
|
+
|
|
70
|
+
def respond_to_missing?(mid, include_private = false)
|
|
71
|
+
@type_def.respond_to?(mid, include_private) || super
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def method_missing(mid, *, &)
|
|
75
|
+
if @type_def.respond_to?(mid)
|
|
76
|
+
@type_def.public_send(mid, *, &)
|
|
77
|
+
else
|
|
78
|
+
super
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|