ace-sim 0.13.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 +7 -0
- data/.ace-defaults/nav/protocols/wfi-sources/ace-sim.yml +19 -0
- data/.ace-defaults/sim/config.yml +9 -0
- data/.ace-defaults/sim/presets/validate-idea.yml +10 -0
- data/.ace-defaults/sim/presets/validate-task.yml +9 -0
- data/.ace-defaults/sim/steps/draft.md +41 -0
- data/.ace-defaults/sim/steps/plan.md +54 -0
- data/.ace-defaults/sim/steps/work.md +54 -0
- data/CHANGELOG.md +266 -0
- data/LICENSE +21 -0
- data/README.md +40 -0
- data/Rakefile +12 -0
- data/docs/demo/ace-sim-run-4x.gif +0 -0
- data/docs/demo/ace-sim-run.gif +0 -0
- data/docs/demo/ace-sim-run.tape.yml +24 -0
- data/docs/getting-started.md +95 -0
- data/docs/handbook.md +24 -0
- data/docs/usage.md +127 -0
- data/exe/ace-sim +15 -0
- data/handbook/skills/as-sim-run/SKILL.md +29 -0
- data/handbook/workflow-instructions/sim/run.wf.md +155 -0
- data/lib/ace/sim/cli/commands/run.rb +139 -0
- data/lib/ace/sim/cli.rb +48 -0
- data/lib/ace/sim/models/simulation_session.rb +85 -0
- data/lib/ace/sim/molecules/final_synthesis_executor.rb +231 -0
- data/lib/ace/sim/molecules/session_store.rb +66 -0
- data/lib/ace/sim/molecules/source_bundler.rb +70 -0
- data/lib/ace/sim/molecules/stage_executor.rb +106 -0
- data/lib/ace/sim/molecules/synthesis_builder.rb +41 -0
- data/lib/ace/sim/organisms/simulation_runner.rb +172 -0
- data/lib/ace/sim/version.rb +7 -0
- data/lib/ace/sim.rb +132 -0
- metadata +177 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Sim
|
|
7
|
+
module Molecules
|
|
8
|
+
class FinalSynthesisExecutor
|
|
9
|
+
class CommandRunner
|
|
10
|
+
def call(args)
|
|
11
|
+
command = Ace::Core::Atoms::CommandExecutor.build_command(args[0], *args[1..])
|
|
12
|
+
result = Ace::Core::Atoms::CommandExecutor.execute(command)
|
|
13
|
+
{
|
|
14
|
+
success: result[:success],
|
|
15
|
+
stdout: result[:stdout].to_s,
|
|
16
|
+
stderr: result[:stderr].to_s,
|
|
17
|
+
exit_code: result[:exit_code]
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(command_runner: nil)
|
|
23
|
+
@command_runner = command_runner || CommandRunner.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def execute(run_dir:, session:, chains:, source_original_input_path: nil)
|
|
27
|
+
final_dir = File.join(run_dir, "final")
|
|
28
|
+
FileUtils.mkdir_p(final_dir)
|
|
29
|
+
|
|
30
|
+
source_original_path = File.join(final_dir, "source.original.md")
|
|
31
|
+
input_path = File.join(final_dir, "input.md")
|
|
32
|
+
bundle_path = File.join(final_dir, "user.bundle.md")
|
|
33
|
+
prompt_path = File.join(final_dir, "user.prompt.md")
|
|
34
|
+
raw_output_path = File.join(final_dir, "output.sequence.md")
|
|
35
|
+
report_path = File.join(final_dir, "suggestions.report.md")
|
|
36
|
+
revised_source_path = File.join(final_dir, "source.revised.md")
|
|
37
|
+
|
|
38
|
+
copy_source(source_original_path, session: session, source_original_input_path: source_original_input_path)
|
|
39
|
+
write_input(input_path, session: session, chains: chains)
|
|
40
|
+
write_bundle(bundle_path, workflow_ref: session.synthesis_workflow)
|
|
41
|
+
|
|
42
|
+
bundle_result = command_runner.call(["ace-bundle", bundle_path, "--output", prompt_path])
|
|
43
|
+
unless bundle_result[:success]
|
|
44
|
+
return failure("ace-bundle failed", bundle_result, report_path: report_path, raw_output_path: raw_output_path,
|
|
45
|
+
revised_source_path: revised_source_path)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
provider = session.synthesis_provider.to_s.strip.empty? ? session.providers.first : session.synthesis_provider
|
|
49
|
+
llm_result = command_runner.call(["ace-llm", provider, "--prompt", prompt_path, "--output", raw_output_path])
|
|
50
|
+
unless llm_result[:success]
|
|
51
|
+
return failure("ace-llm failed", llm_result, report_path: report_path, raw_output_path: raw_output_path,
|
|
52
|
+
revised_source_path: revised_source_path)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sequence = read_non_empty(raw_output_path)
|
|
56
|
+
if sequence.nil?
|
|
57
|
+
return missing_output_failure("Final synthesis sequence missing or empty", provider, report_path, raw_output_path,
|
|
58
|
+
revised_source_path)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
parsed = parse_sequence(sequence)
|
|
62
|
+
unless parsed
|
|
63
|
+
return {
|
|
64
|
+
"status" => "failed",
|
|
65
|
+
"provider" => provider,
|
|
66
|
+
"error" => "Final synthesis output missing required tags: <suggestions-report> and <source-revised>",
|
|
67
|
+
"source_original_path" => source_original_path,
|
|
68
|
+
"input_path" => input_path,
|
|
69
|
+
"bundle_path" => bundle_path,
|
|
70
|
+
"prompt_path" => prompt_path,
|
|
71
|
+
"raw_output_path" => raw_output_path,
|
|
72
|
+
"report_path" => report_path,
|
|
73
|
+
"revised_source_path" => revised_source_path,
|
|
74
|
+
"output_path" => report_path
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
File.write(report_path, parsed.fetch("suggestions_report"))
|
|
79
|
+
File.write(revised_source_path, parsed.fetch("source_revised"))
|
|
80
|
+
|
|
81
|
+
{
|
|
82
|
+
"status" => "ok",
|
|
83
|
+
"provider" => provider,
|
|
84
|
+
"source_original_path" => source_original_path,
|
|
85
|
+
"input_path" => input_path,
|
|
86
|
+
"bundle_path" => bundle_path,
|
|
87
|
+
"prompt_path" => prompt_path,
|
|
88
|
+
"raw_output_path" => raw_output_path,
|
|
89
|
+
"report_path" => report_path,
|
|
90
|
+
"revised_source_path" => revised_source_path,
|
|
91
|
+
"output_path" => report_path
|
|
92
|
+
}
|
|
93
|
+
rescue => e
|
|
94
|
+
{
|
|
95
|
+
"status" => "failed",
|
|
96
|
+
"error" => e.message,
|
|
97
|
+
"report_path" => report_path,
|
|
98
|
+
"raw_output_path" => raw_output_path,
|
|
99
|
+
"revised_source_path" => revised_source_path,
|
|
100
|
+
"output_path" => report_path
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
attr_reader :command_runner
|
|
107
|
+
|
|
108
|
+
def copy_source(path, session:, source_original_input_path:)
|
|
109
|
+
origin_path = source_original_input_path || session.source.first
|
|
110
|
+
FileUtils.cp(origin_path, path)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def write_input(path, session:, chains:)
|
|
114
|
+
sources_display = session.source.is_a?(Array) ? session.source.join(", ") : session.source.to_s
|
|
115
|
+
content = +"# ace-sim final synthesis input\n\n"
|
|
116
|
+
content << "Source file(s): #{sources_display}\n\n"
|
|
117
|
+
content << "## Chain outputs\n\n"
|
|
118
|
+
|
|
119
|
+
chains.each do |chain|
|
|
120
|
+
content << "### Chain #{chain["provider"]}##{chain["iteration"]} (#{chain["status"]})\n\n"
|
|
121
|
+
chain.fetch("steps", []).each do |step|
|
|
122
|
+
content << "#### Step #{step["step"]} (#{step["status"]})\n\n"
|
|
123
|
+
if step["output_path"] && File.exist?(step["output_path"])
|
|
124
|
+
content << "```markdown\n"
|
|
125
|
+
content << File.read(step["output_path"])
|
|
126
|
+
content << "\n```\n\n"
|
|
127
|
+
else
|
|
128
|
+
content << "_No output captured_\n\n"
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
File.write(path, content)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def write_bundle(path, workflow_ref:)
|
|
137
|
+
content = <<~MD
|
|
138
|
+
---
|
|
139
|
+
description: "ace-sim final suggestions synthesis bundle"
|
|
140
|
+
bundle:
|
|
141
|
+
embed_document_source: true
|
|
142
|
+
sections:
|
|
143
|
+
synthesis_workflow:
|
|
144
|
+
files:
|
|
145
|
+
- #{workflow_ref}
|
|
146
|
+
source_original:
|
|
147
|
+
files:
|
|
148
|
+
- ./source.original.md
|
|
149
|
+
simulation_outputs:
|
|
150
|
+
files:
|
|
151
|
+
- ./input.md
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
# Goal
|
|
155
|
+
|
|
156
|
+
Produce final actionable synthesis from simulation outputs and the original source.
|
|
157
|
+
|
|
158
|
+
## Instructions
|
|
159
|
+
|
|
160
|
+
1. Read `<source_original>` completely.
|
|
161
|
+
2. Read `<simulation_outputs>` completely.
|
|
162
|
+
3. Follow `<synthesis_workflow>` for evaluation structure.
|
|
163
|
+
4. Return markdown only with both required tags:
|
|
164
|
+
- `<suggestions-report>...</suggestions-report>`
|
|
165
|
+
- `<source-revised>...</source-revised>`
|
|
166
|
+
5. The revised source must be directly usable as the next source file iteration.
|
|
167
|
+
MD
|
|
168
|
+
|
|
169
|
+
File.write(path, content)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def read_non_empty(path)
|
|
173
|
+
return nil unless File.exist?(path)
|
|
174
|
+
|
|
175
|
+
content = File.read(path)
|
|
176
|
+
return nil if content.strip.empty?
|
|
177
|
+
|
|
178
|
+
content
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def parse_sequence(content)
|
|
182
|
+
report = extract_tag(content, "suggestions-report")
|
|
183
|
+
revised = extract_tag(content, "source-revised")
|
|
184
|
+
return nil if report.nil? || revised.nil?
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
"suggestions_report" => report,
|
|
188
|
+
"source_revised" => revised
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def extract_tag(content, tag_name)
|
|
193
|
+
match = content.match(%r{<#{Regexp.escape(tag_name)}>(.*?)</#{Regexp.escape(tag_name)}>}m)
|
|
194
|
+
return nil unless match
|
|
195
|
+
|
|
196
|
+
extracted = match[1].to_s.strip
|
|
197
|
+
return nil if extracted.empty?
|
|
198
|
+
|
|
199
|
+
extracted + "\n"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def missing_output_failure(message, provider, report_path, raw_output_path, revised_source_path)
|
|
203
|
+
{
|
|
204
|
+
"status" => "failed",
|
|
205
|
+
"provider" => provider,
|
|
206
|
+
"error" => "#{message}: #{raw_output_path}",
|
|
207
|
+
"report_path" => report_path,
|
|
208
|
+
"raw_output_path" => raw_output_path,
|
|
209
|
+
"revised_source_path" => revised_source_path,
|
|
210
|
+
"output_path" => report_path
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def failure(prefix, result, report_path:, raw_output_path:, revised_source_path:)
|
|
215
|
+
detail = result[:stderr].to_s.strip
|
|
216
|
+
detail = result[:stdout].to_s.strip if detail.empty?
|
|
217
|
+
detail = "command failed" if detail.empty?
|
|
218
|
+
|
|
219
|
+
{
|
|
220
|
+
"status" => "failed",
|
|
221
|
+
"error" => "#{prefix}: #{detail}",
|
|
222
|
+
"report_path" => report_path,
|
|
223
|
+
"raw_output_path" => raw_output_path,
|
|
224
|
+
"revised_source_path" => revised_source_path,
|
|
225
|
+
"output_path" => report_path
|
|
226
|
+
}
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Sim
|
|
8
|
+
module Molecules
|
|
9
|
+
class SessionStore
|
|
10
|
+
class RunDirectoryExistsError < StandardError; end
|
|
11
|
+
|
|
12
|
+
attr_reader :cache_root
|
|
13
|
+
|
|
14
|
+
def initialize(cache_root: nil)
|
|
15
|
+
@cache_root = cache_root || Ace::Sim.get("sim", "cache_root") || ".ace-local/sim"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run_dir_for(run_id)
|
|
19
|
+
File.join(cache_root, "simulations", run_id)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def prepare_run(run_id)
|
|
23
|
+
run_dir = run_dir_for(run_id)
|
|
24
|
+
raise RunDirectoryExistsError, "Run directory already exists: #{run_dir}" if Dir.exist?(run_dir)
|
|
25
|
+
|
|
26
|
+
FileUtils.mkdir_p(File.join(run_dir, "chains"))
|
|
27
|
+
run_dir
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def chain_dir(run_dir, provider, iteration)
|
|
31
|
+
provider_slug = provider.to_s.gsub(/[^a-zA-Z0-9_-]/, "-")
|
|
32
|
+
File.join(run_dir, "chains", "#{provider_slug}-#{iteration}")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def final_dir(run_dir)
|
|
36
|
+
File.join(run_dir, "final")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def prepare_step_dir(run_dir, provider, iteration, step_index, step_name)
|
|
40
|
+
dirname = format("%02d-%s", step_index, step_name)
|
|
41
|
+
dir = File.join(chain_dir(run_dir, provider, iteration), dirname)
|
|
42
|
+
FileUtils.mkdir_p(dir)
|
|
43
|
+
dir
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def write_session(run_dir, payload)
|
|
47
|
+
write_yaml(File.join(run_dir, "session.yml"), payload)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def write_synthesis(run_dir, payload)
|
|
51
|
+
write_yaml(File.join(run_dir, "synthesis.yml"), payload)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def write_yaml(path, payload)
|
|
55
|
+
write_text(path, YAML.dump(payload))
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def write_text(path, content)
|
|
59
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
60
|
+
File.write(path, content)
|
|
61
|
+
path
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Sim
|
|
7
|
+
module Molecules
|
|
8
|
+
class SourceBundler
|
|
9
|
+
class CommandRunner
|
|
10
|
+
def call(args)
|
|
11
|
+
command = Ace::Core::Atoms::CommandExecutor.build_command(args[0], *args[1..])
|
|
12
|
+
result = Ace::Core::Atoms::CommandExecutor.execute(command)
|
|
13
|
+
{
|
|
14
|
+
success: result[:success],
|
|
15
|
+
stdout: result[:stdout].to_s,
|
|
16
|
+
stderr: result[:stderr].to_s,
|
|
17
|
+
exit_code: result[:exit_code]
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(command_runner: nil)
|
|
23
|
+
@command_runner = command_runner || CommandRunner.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def bundle(sources:, output_path:)
|
|
27
|
+
raise Ace::Sim::ValidationError, "source cannot be empty" if sources.nil? || sources.empty?
|
|
28
|
+
|
|
29
|
+
run_dir = File.dirname(output_path)
|
|
30
|
+
bundle_path = File.join(run_dir, "input.bundle.md")
|
|
31
|
+
content_path = File.join(run_dir, "input.md")
|
|
32
|
+
|
|
33
|
+
FileUtils.mkdir_p(run_dir)
|
|
34
|
+
File.write(bundle_path, build_bundle_yaml(sources))
|
|
35
|
+
|
|
36
|
+
result = command_runner.call(["ace-bundle", bundle_path, "--output", content_path])
|
|
37
|
+
return content_path if result[:success]
|
|
38
|
+
|
|
39
|
+
detail = result[:stderr].to_s.strip
|
|
40
|
+
detail = result[:stdout].to_s.strip if detail.empty?
|
|
41
|
+
detail = "command failed" if detail.empty?
|
|
42
|
+
raise Ace::Sim::ValidationError, "ace-bundle failed: #{detail}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
attr_reader :command_runner
|
|
48
|
+
|
|
49
|
+
def build_bundle_yaml(sources)
|
|
50
|
+
files_yaml = sources.map { |s| " - #{s}" }.join("\n")
|
|
51
|
+
<<~YAML
|
|
52
|
+
---
|
|
53
|
+
description: "ace-sim source bundle"
|
|
54
|
+
bundle:
|
|
55
|
+
embed_document_source: true
|
|
56
|
+
sections:
|
|
57
|
+
source:
|
|
58
|
+
files:
|
|
59
|
+
#{files_yaml}
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
# Source Files
|
|
63
|
+
|
|
64
|
+
Combined input from #{sources.length} source(s).
|
|
65
|
+
YAML
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Sim
|
|
7
|
+
module Molecules
|
|
8
|
+
class StageExecutor
|
|
9
|
+
class CommandRunner
|
|
10
|
+
def call(args)
|
|
11
|
+
command = Ace::Core::Atoms::CommandExecutor.build_command(args[0], *args[1..])
|
|
12
|
+
result = Ace::Core::Atoms::CommandExecutor.execute(command)
|
|
13
|
+
{
|
|
14
|
+
success: result[:success],
|
|
15
|
+
stdout: result[:stdout].to_s,
|
|
16
|
+
stderr: result[:stderr].to_s,
|
|
17
|
+
exit_code: result[:exit_code]
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(command_runner: nil)
|
|
23
|
+
@command_runner = command_runner || CommandRunner.new
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def execute(step:, provider:, iteration:, step_dir:, step_bundle_path:, input_source_path:)
|
|
27
|
+
input_path = File.join(step_dir, "input.md")
|
|
28
|
+
bundle_path = File.join(step_dir, "user.bundle.md")
|
|
29
|
+
prompt_path = File.join(step_dir, "user.prompt.md")
|
|
30
|
+
output_path = File.join(step_dir, "output.md")
|
|
31
|
+
|
|
32
|
+
FileUtils.cp(input_source_path, input_path)
|
|
33
|
+
FileUtils.cp(step_bundle_path, bundle_path)
|
|
34
|
+
|
|
35
|
+
bundle_result = command_runner.call(["ace-bundle", bundle_path, "--output", prompt_path])
|
|
36
|
+
return failure(step, provider, iteration, output_path, "ace-bundle failed", bundle_result) unless bundle_result[:success]
|
|
37
|
+
|
|
38
|
+
llm_result = command_runner.call(["ace-llm", provider, "--prompt", prompt_path, "--output", output_path])
|
|
39
|
+
return failure(step, provider, iteration, output_path, "ace-llm failed", llm_result) unless llm_result[:success]
|
|
40
|
+
|
|
41
|
+
unless valid_output?(output_path)
|
|
42
|
+
return {
|
|
43
|
+
"step" => step,
|
|
44
|
+
"provider" => provider,
|
|
45
|
+
"iteration" => iteration,
|
|
46
|
+
"status" => "failed",
|
|
47
|
+
"input_path" => input_path,
|
|
48
|
+
"bundle_path" => bundle_path,
|
|
49
|
+
"prompt_path" => prompt_path,
|
|
50
|
+
"output_path" => output_path,
|
|
51
|
+
"error" => "Step output missing or empty: #{output_path}"
|
|
52
|
+
}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
{
|
|
56
|
+
"step" => step,
|
|
57
|
+
"provider" => provider,
|
|
58
|
+
"iteration" => iteration,
|
|
59
|
+
"status" => "ok",
|
|
60
|
+
"input_path" => input_path,
|
|
61
|
+
"bundle_path" => bundle_path,
|
|
62
|
+
"prompt_path" => prompt_path,
|
|
63
|
+
"output_path" => output_path
|
|
64
|
+
}
|
|
65
|
+
rescue => e
|
|
66
|
+
{
|
|
67
|
+
"step" => step,
|
|
68
|
+
"provider" => provider,
|
|
69
|
+
"iteration" => iteration,
|
|
70
|
+
"status" => "failed",
|
|
71
|
+
"input_path" => input_path,
|
|
72
|
+
"bundle_path" => bundle_path,
|
|
73
|
+
"prompt_path" => prompt_path,
|
|
74
|
+
"output_path" => output_path,
|
|
75
|
+
"error" => e.message
|
|
76
|
+
}
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
attr_reader :command_runner
|
|
82
|
+
|
|
83
|
+
def valid_output?(output_path)
|
|
84
|
+
return false unless File.exist?(output_path)
|
|
85
|
+
|
|
86
|
+
!File.read(output_path).strip.empty?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def failure(step, provider, iteration, output_path, prefix, result)
|
|
90
|
+
detail = result[:stderr].to_s.strip
|
|
91
|
+
detail = result[:stdout].to_s.strip if detail.empty?
|
|
92
|
+
detail = "command failed" if detail.empty?
|
|
93
|
+
|
|
94
|
+
{
|
|
95
|
+
"step" => step,
|
|
96
|
+
"provider" => provider,
|
|
97
|
+
"iteration" => iteration,
|
|
98
|
+
"status" => "failed",
|
|
99
|
+
"output_path" => output_path,
|
|
100
|
+
"error" => "#{prefix}: #{detail}"
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Sim
|
|
5
|
+
module Molecules
|
|
6
|
+
class SynthesisBuilder
|
|
7
|
+
def build(session:, sources:, chains:, final_stage: nil)
|
|
8
|
+
{
|
|
9
|
+
"run_id" => session.run_id,
|
|
10
|
+
"preset" => session.preset,
|
|
11
|
+
"sources" => sources,
|
|
12
|
+
"providers" => session.providers,
|
|
13
|
+
"repeat" => session.repeat,
|
|
14
|
+
"dry_run" => session.dry_run?,
|
|
15
|
+
"writeback" => session.writeback,
|
|
16
|
+
"synthesis_workflow" => session.synthesis_workflow,
|
|
17
|
+
"synthesis_provider" => session.synthesis_provider,
|
|
18
|
+
"chains" => chains,
|
|
19
|
+
"final_stage" => final_stage,
|
|
20
|
+
"status" => overall_status(chains, final_stage: final_stage)
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def chain_status(step_results)
|
|
25
|
+
(step_results.any? { |step| step["status"] == "failed" }) ? "failed" : "ok"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def overall_status(chains, final_stage: nil)
|
|
31
|
+
return "failed" if final_stage && final_stage["status"] == "failed"
|
|
32
|
+
return "failed" if chains.empty?
|
|
33
|
+
return "failed" if chains.all? { |chain| chain["status"] == "failed" }
|
|
34
|
+
return "partial" if chains.any? { |chain| chain["status"] == "failed" }
|
|
35
|
+
|
|
36
|
+
"ok"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Sim
|
|
5
|
+
module Organisms
|
|
6
|
+
class SimulationRunner
|
|
7
|
+
def initialize(session_store: nil, stage_executor: nil, synthesis_builder: nil,
|
|
8
|
+
final_synthesis_executor: nil, source_bundler: nil)
|
|
9
|
+
@session_store = session_store || Molecules::SessionStore.new
|
|
10
|
+
@stage_executor = stage_executor || Molecules::StageExecutor.new
|
|
11
|
+
@synthesis_builder = synthesis_builder || Molecules::SynthesisBuilder.new
|
|
12
|
+
@final_synthesis_executor = final_synthesis_executor || Molecules::FinalSynthesisExecutor.new
|
|
13
|
+
@source_bundler = source_bundler || Molecules::SourceBundler.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def run(session)
|
|
17
|
+
sources = session.source
|
|
18
|
+
if session.writeback && sources.length > 1
|
|
19
|
+
raise Ace::Sim::ValidationError, "writeback requires a single source file"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
run_id, run_dir = prepare_unique_run(session)
|
|
23
|
+
bundled_input_path = source_bundler.bundle(
|
|
24
|
+
sources: sources,
|
|
25
|
+
output_path: File.join(run_dir, "input.md")
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
chains = []
|
|
29
|
+
session.providers.each do |provider|
|
|
30
|
+
1.upto(session.repeat) do |iteration|
|
|
31
|
+
chains << run_chain(
|
|
32
|
+
session: session,
|
|
33
|
+
run_dir: run_dir,
|
|
34
|
+
provider: provider,
|
|
35
|
+
iteration: iteration,
|
|
36
|
+
bundled_input_path: bundled_input_path
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
final_stage = nil
|
|
42
|
+
if session.synthesis_enabled?
|
|
43
|
+
final_stage = final_synthesis_executor.execute(
|
|
44
|
+
run_dir: run_dir,
|
|
45
|
+
session: session,
|
|
46
|
+
chains: chains,
|
|
47
|
+
source_original_input_path: bundled_input_path
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
synthesis = synthesis_builder.build(
|
|
52
|
+
session: session,
|
|
53
|
+
sources: sources,
|
|
54
|
+
chains: chains,
|
|
55
|
+
final_stage: final_stage
|
|
56
|
+
)
|
|
57
|
+
session_store.write_synthesis(run_dir, synthesis)
|
|
58
|
+
|
|
59
|
+
session_store.write_session(
|
|
60
|
+
run_dir,
|
|
61
|
+
session.to_h.merge(
|
|
62
|
+
"run_id" => run_id,
|
|
63
|
+
"sources" => sources,
|
|
64
|
+
"status" => synthesis["status"],
|
|
65
|
+
"chain_count" => chains.length,
|
|
66
|
+
"writeback_applied" => false,
|
|
67
|
+
"synthesis_report_path" => final_stage && final_stage["report_path"],
|
|
68
|
+
"synthesis_revised_source_path" => final_stage && final_stage["revised_source_path"]
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
{
|
|
73
|
+
success: synthesis["status"] != "failed",
|
|
74
|
+
status: synthesis["status"],
|
|
75
|
+
run_id: run_id,
|
|
76
|
+
run_dir: run_dir,
|
|
77
|
+
chains: chains,
|
|
78
|
+
final_stage: final_stage,
|
|
79
|
+
synthesis: synthesis,
|
|
80
|
+
error: (synthesis["status"] == "failed") ? failure_reason(chains, final_stage) : nil
|
|
81
|
+
}
|
|
82
|
+
rescue Ace::Sim::ValidationError => e
|
|
83
|
+
{
|
|
84
|
+
success: false,
|
|
85
|
+
status: "failed",
|
|
86
|
+
run_id: session.run_id,
|
|
87
|
+
run_dir: nil,
|
|
88
|
+
chains: [],
|
|
89
|
+
synthesis: nil,
|
|
90
|
+
error: e.message
|
|
91
|
+
}
|
|
92
|
+
rescue => e
|
|
93
|
+
{
|
|
94
|
+
success: false,
|
|
95
|
+
status: "failed",
|
|
96
|
+
run_id: session.run_id,
|
|
97
|
+
run_dir: nil,
|
|
98
|
+
chains: [],
|
|
99
|
+
synthesis: nil,
|
|
100
|
+
error: "#{e.class}: #{e.message}"
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
attr_reader :session_store, :stage_executor, :synthesis_builder, :final_synthesis_executor,
|
|
107
|
+
:source_bundler
|
|
108
|
+
|
|
109
|
+
def run_chain(session:, run_dir:, provider:, iteration:, bundled_input_path:)
|
|
110
|
+
current_input_path = bundled_input_path
|
|
111
|
+
step_results = []
|
|
112
|
+
|
|
113
|
+
session.steps.each_with_index do |step, index|
|
|
114
|
+
step_index = case step
|
|
115
|
+
when "draft"
|
|
116
|
+
1
|
|
117
|
+
when "plan"
|
|
118
|
+
2
|
|
119
|
+
when "work"
|
|
120
|
+
3
|
|
121
|
+
else
|
|
122
|
+
index + 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
step_dir = session_store.prepare_step_dir(run_dir, provider, iteration, step_index, step)
|
|
126
|
+
result = stage_executor.execute(
|
|
127
|
+
step: step,
|
|
128
|
+
provider: provider,
|
|
129
|
+
iteration: iteration,
|
|
130
|
+
step_dir: step_dir,
|
|
131
|
+
step_bundle_path: session.bundle_path_for(step),
|
|
132
|
+
input_source_path: current_input_path
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
step_results << result
|
|
136
|
+
break if result["status"] == "failed"
|
|
137
|
+
|
|
138
|
+
current_input_path = result["output_path"]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
{
|
|
142
|
+
"provider" => provider,
|
|
143
|
+
"iteration" => iteration,
|
|
144
|
+
"status" => synthesis_builder.chain_status(step_results),
|
|
145
|
+
"steps" => step_results
|
|
146
|
+
}
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def prepare_unique_run(session)
|
|
150
|
+
attempts = 0
|
|
151
|
+
begin
|
|
152
|
+
attempts += 1
|
|
153
|
+
run_dir = session_store.prepare_run(session.run_id)
|
|
154
|
+
[session.run_id, run_dir]
|
|
155
|
+
rescue Molecules::SessionStore::RunDirectoryExistsError
|
|
156
|
+
raise Ace::Sim::ValidationError, "Could not allocate unique run id" if attempts >= 5
|
|
157
|
+
|
|
158
|
+
session.regenerate_run_id!
|
|
159
|
+
retry
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def failure_reason(chains, final_stage)
|
|
164
|
+
return "Final synthesis failed" if final_stage && final_stage["status"] == "failed"
|
|
165
|
+
return "All chains failed" if chains.all? { |chain| chain["status"] == "failed" }
|
|
166
|
+
|
|
167
|
+
"Simulation failed"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|