diffbroker 0.10.0 → 0.11.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/lib/diffbroker/agents/base.rb +3 -0
- data/lib/diffbroker/agents/claude.rb +6 -1
- data/lib/diffbroker/session_ledger.rb +47 -0
- data/lib/diffbroker/task_runner.rb +32 -6
- data/lib/diffbroker/version.rb +1 -1
- data/lib/diffbroker.rb +1 -0
- data/plugin/.claude-plugin/plugin.json +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5edf0d3084adb3e7b1e8a9f55dda5062efa3a5207d8f4ca7d7860141acef3ec7
|
|
4
|
+
data.tar.gz: 85899028c0a4d827e3df60c530298f9713e4a33e4ccf5ec4397fae0d58a6c37a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b7ca4efd11472c69863a386ebf10336402eac96f998a8665457f20a7f6b395af3a3814925915e1881d0f018c906344a0eccd45cdd0d9557fc4f44fc0a3891be
|
|
7
|
+
data.tar.gz: d763d542d2a6c27adad2bf78cf3b6f0bb1488b0f384bb90fef6990b27c142af472078409f5886be43c92fe6df7b158e847547720018f28ce0efe57ab7060b4dc
|
|
@@ -16,6 +16,9 @@ module Diffbroker
|
|
|
16
16
|
# The run's lines for the user's log: what the model said and the tools it called. Subclasses override.
|
|
17
17
|
def parse_log(_line) = []
|
|
18
18
|
|
|
19
|
+
# Whether a chat message can continue this agent's earlier session (task["resume_session_id"]).
|
|
20
|
+
def resumes? = false
|
|
21
|
+
|
|
19
22
|
def env(token, task_id) = { TOKEN_ENV => token, "DIFFBROKER_TASK_ID" => task_id }
|
|
20
23
|
|
|
21
24
|
private
|
|
@@ -27,12 +27,17 @@ module Diffbroker
|
|
|
27
27
|
|
|
28
28
|
def name = "claude_code"
|
|
29
29
|
|
|
30
|
+
def resumes? = true
|
|
31
|
+
|
|
30
32
|
def autonomous_command(task, mcp_config:)
|
|
31
33
|
args = task["read_only"] ? READ_ONLY_ARGS : @config.claude_args
|
|
32
|
-
[ @config.claude_command, "-p", task.fetch("prompt"), "--output-format", "stream-json", "--verbose", *args,
|
|
34
|
+
[ @config.claude_command, "-p", task.fetch("prompt"), *resume(task), "--output-format", "stream-json", "--verbose", *args,
|
|
33
35
|
"--mcp-config", mcp_config, *strict(task), "--plugin-dir", @plugin_dir ]
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
# The permissions above are passed again: a resumed session keeps its conversation, not its sandbox.
|
|
39
|
+
def resume(task) = task["resume_session_id"] ? [ "--resume", task["resume_session_id"] ] : []
|
|
40
|
+
|
|
36
41
|
def terminal_command(task, mcp_config:)
|
|
37
42
|
[ @config.claude_command, "--mcp-config", mcp_config, *strict(task), "--plugin-dir", @plugin_dir ]
|
|
38
43
|
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Diffbroker
|
|
7
|
+
# The agent sessions this companion started itself. The server offers to resume a chat's earlier session by id, but it is
|
|
8
|
+
# only ever resumed here when this machine made it: a server (or anyone who could fake its message) cannot make an agent
|
|
9
|
+
# continue another conversation of the user's, such as an interactive session in their own terminal.
|
|
10
|
+
class SessionLedger
|
|
11
|
+
FILE = "sessions.json"
|
|
12
|
+
LIMIT = 500
|
|
13
|
+
ID = /\A\w[\w-]{0,99}\z/
|
|
14
|
+
|
|
15
|
+
def initialize(state_dir:)
|
|
16
|
+
@path = File.join(state_dir, FILE)
|
|
17
|
+
@lock = Mutex.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def remember(id)
|
|
21
|
+
return unless id.to_s.match?(ID)
|
|
22
|
+
|
|
23
|
+
@lock.synchronize do
|
|
24
|
+
ids = read
|
|
25
|
+
next if ids.include?(id)
|
|
26
|
+
|
|
27
|
+
write((ids + [ id ]).last(LIMIT))
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def known?(id) = id.to_s.match?(ID) && @lock.synchronize { read.include?(id) }
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def read
|
|
36
|
+
data = JSON.parse(File.read(@path))
|
|
37
|
+
data.is_a?(Array) ? data.grep(String) : []
|
|
38
|
+
rescue Errno::ENOENT, JSON::ParserError
|
|
39
|
+
[]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def write(ids)
|
|
43
|
+
FileUtils.mkdir_p(File.dirname(@path), mode: 0o700)
|
|
44
|
+
File.write(@path, JSON.generate(ids), perm: 0o600)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -12,12 +12,12 @@ module Diffbroker
|
|
|
12
12
|
PRINT_OUTPUT = 120
|
|
13
13
|
|
|
14
14
|
Handle = Struct.new(:pid, :cancelled)
|
|
15
|
-
Outcome = Struct.new(:status, :cost_usd, :error_code, :last_output)
|
|
15
|
+
Outcome = Struct.new(:status, :cost_usd, :error_code, :last_output, :no_result)
|
|
16
16
|
|
|
17
17
|
def initialize(config:, report:, worktree: Worktree.new(root: config.worktree_root), neutralizer: Neutralizer.new,
|
|
18
18
|
gh_check: GhCheck.new, launcher: Launcher.new(name: config.terminal), out: $stdout, grace: CANCEL_GRACE,
|
|
19
19
|
progress_interval: PROGRESS_INTERVAL, run_script: RunScript.new(state_dir: config.state_dir),
|
|
20
|
-
reaper: Reaper.new(root: config.worktree_root))
|
|
20
|
+
reaper: Reaper.new(root: config.worktree_root), sessions: SessionLedger.new(state_dir: config.state_dir))
|
|
21
21
|
@config = config
|
|
22
22
|
@report = report
|
|
23
23
|
@worktree = worktree
|
|
@@ -29,6 +29,7 @@ module Diffbroker
|
|
|
29
29
|
@progress_interval = progress_interval
|
|
30
30
|
@run_script = run_script
|
|
31
31
|
@reaper = reaper
|
|
32
|
+
@sessions = sessions
|
|
32
33
|
@handles = {}
|
|
33
34
|
@running = Set.new
|
|
34
35
|
@cancelled = Set.new
|
|
@@ -132,7 +133,7 @@ module Diffbroker
|
|
|
132
133
|
def run_autonomous(task, agent, mcp_config, dir)
|
|
133
134
|
id = task["task_id"]
|
|
134
135
|
@lock.synchronize { @running << id }
|
|
135
|
-
outcome =
|
|
136
|
+
outcome = run_with_resume(task, agent, mcp_config, dir)
|
|
136
137
|
outcome = verify_after_write(task, outcome, agent, mcp_config, dir) if ExplainVerifier.verified?(task) && outcome.status == "done"
|
|
137
138
|
outcome = publish_branch(task, outcome, dir) if outcome.status == "done"
|
|
138
139
|
finished(id, outcome.status, outcome.cost_usd, outcome.error_code, outcome.last_output)
|
|
@@ -143,6 +144,27 @@ module Diffbroker
|
|
|
143
144
|
end
|
|
144
145
|
end
|
|
145
146
|
|
|
147
|
+
# A chat message the server offered to resume (task["resume"]) continues the agent's earlier session, when this machine made it
|
|
148
|
+
# and this agent can: it gets only the new question and keeps what it read. A session that cannot be resumed (deleted, expired)
|
|
149
|
+
# makes the agent exit without a word: the message then runs again from scratch with the full prompt, which carries the chat.
|
|
150
|
+
def run_with_resume(task, agent, mcp_config, dir)
|
|
151
|
+
resume = resume_for(task, agent)
|
|
152
|
+
return run_process(task, agent, mcp_config, dir) unless resume
|
|
153
|
+
|
|
154
|
+
outcome = run_process(task.merge("prompt" => resume["prompt"], "resume_session_id" => resume["session_id"]), agent, mcp_config, dir)
|
|
155
|
+
return outcome unless outcome.status == "failed" && outcome.no_result
|
|
156
|
+
|
|
157
|
+
say(task["task_id"], "Could not resume the earlier session: starting this message from scratch")
|
|
158
|
+
run_process(task, agent, mcp_config, dir)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def resume_for(task, agent)
|
|
162
|
+
resume = task["resume"]
|
|
163
|
+
return unless resume.is_a?(Hash) && agent.resumes? && resume["agent"] == agent.name && resume["prompt"].to_s != ""
|
|
164
|
+
|
|
165
|
+
resume if @sessions.known?(resume["session_id"])
|
|
166
|
+
end
|
|
167
|
+
|
|
146
168
|
# An update_branch task ends with a rebase the agent may not push (see BranchPublisher): the companion does, here.
|
|
147
169
|
def publish_branch(task, outcome, dir)
|
|
148
170
|
return outcome unless publishes_branch?(task)
|
|
@@ -197,7 +219,7 @@ module Diffbroker
|
|
|
197
219
|
|
|
198
220
|
if handle.cancelled then Outcome.new("cancelled", result&.cost_usd, "cancelled", output)
|
|
199
221
|
elsif status.success? && !result&.error then Outcome.new("done", result&.cost_usd, nil, output)
|
|
200
|
-
else Outcome.new("failed", result&.cost_usd, "agent_failed", output)
|
|
222
|
+
else Outcome.new("failed", result&.cost_usd, "agent_failed", output, result.nil?)
|
|
201
223
|
end
|
|
202
224
|
ensure
|
|
203
225
|
writer.close if writer && !writer.closed?
|
|
@@ -221,6 +243,10 @@ module Diffbroker
|
|
|
221
243
|
case (event = agent.parse_stream(line))
|
|
222
244
|
when Agents::Result then result = event
|
|
223
245
|
when Agents::Progress
|
|
246
|
+
if event.session_id
|
|
247
|
+
@sessions.remember(event.session_id)
|
|
248
|
+
progress(id, event.step, nil, nil, event.session_id)
|
|
249
|
+
end
|
|
224
250
|
next if event.step == last_step || monotonic - last_sent < @progress_interval
|
|
225
251
|
|
|
226
252
|
progress(id, event.step)
|
|
@@ -256,8 +282,8 @@ module Diffbroker
|
|
|
256
282
|
end
|
|
257
283
|
end
|
|
258
284
|
|
|
259
|
-
def progress(id, step, agent = nil, phase = nil)
|
|
260
|
-
@report.call("progress", { "task_id" => id, "step" => step, "agent" => agent, "phase" => phase }.compact)
|
|
285
|
+
def progress(id, step, agent = nil, phase = nil, session_id = nil)
|
|
286
|
+
@report.call("progress", { "task_id" => id, "step" => step, "agent" => agent, "phase" => phase, "session_id" => session_id }.compact)
|
|
261
287
|
say(id, "progress: #{step}") unless step == "starting"
|
|
262
288
|
end
|
|
263
289
|
|
data/lib/diffbroker/version.rb
CHANGED
data/lib/diffbroker.rb
CHANGED
|
@@ -39,6 +39,7 @@ module Diffbroker
|
|
|
39
39
|
autoload :RunScript, File.expand_path("diffbroker/run_script", __dir__)
|
|
40
40
|
autoload :Runner, File.expand_path("diffbroker/runner", __dir__)
|
|
41
41
|
autoload :ServiceInstaller, File.expand_path("diffbroker/service_installer", __dir__)
|
|
42
|
+
autoload :SessionLedger, File.expand_path("diffbroker/session_ledger", __dir__)
|
|
42
43
|
autoload :SkillInstaller, File.expand_path("diffbroker/skill_installer", __dir__)
|
|
43
44
|
autoload :TaskFiles, File.expand_path("diffbroker/task_files", __dir__)
|
|
44
45
|
autoload :TaskRunner, File.expand_path("diffbroker/task_runner", __dir__)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: diffbroker
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yuri Sidorov (@newstler)
|
|
@@ -63,6 +63,7 @@ files:
|
|
|
63
63
|
- lib/diffbroker/run_script.rb
|
|
64
64
|
- lib/diffbroker/runner.rb
|
|
65
65
|
- lib/diffbroker/service_installer.rb
|
|
66
|
+
- lib/diffbroker/session_ledger.rb
|
|
66
67
|
- lib/diffbroker/skill_installer.rb
|
|
67
68
|
- lib/diffbroker/task_files.rb
|
|
68
69
|
- lib/diffbroker/task_runner.rb
|