diffbroker 0.14.0 → 0.15.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/claude.rb +14 -2
- data/lib/diffbroker/agents/codex.rb +36 -2
- data/lib/diffbroker/agents.rb +4 -1
- data/lib/diffbroker/explain_verifier.rb +2 -1
- data/lib/diffbroker/task_runner.rb +13 -11
- data/lib/diffbroker/version.rb +1 -1
- data/plugin/.claude-plugin/plugin.json +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1d00b5898842a32e456087716d26bdb5b1c28e35f04e44b9d73acf8726d78f7
|
|
4
|
+
data.tar.gz: 25464398637a5bcf58c4e85f8babb57134cd1d3463e0325bb445b983328c7e09
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 306e519a17b426111cfe62d5ea845abd4b2c1e77534749864cb6b44cb9dd802126d85ab1025a1a89e8ba26f0ff08591f701f13c7c05aba75de2985ea629c14f6
|
|
7
|
+
data.tar.gz: e7e2f33022fcbc9c670821bff1025709c86cd5900ba52e229150ce4847ff8403fc304888117d942e0c616256af1a6616060dd45d5fe4f0ddf041e963a3a47ed8
|
|
@@ -51,12 +51,16 @@ module Diffbroker
|
|
|
51
51
|
event = parse_json(line) or return nil
|
|
52
52
|
|
|
53
53
|
case event["type"]
|
|
54
|
-
when "system"
|
|
54
|
+
when "system"
|
|
55
|
+
return unless event["subtype"] == "init"
|
|
56
|
+
|
|
57
|
+
@billing = billing_for(event["apiKeySource"])
|
|
58
|
+
Progress.new(step: "starting", session_id: event["session_id"])
|
|
55
59
|
when "assistant"
|
|
56
60
|
tool = Array(event.dig("message", "content")).reverse.find { _1.is_a?(Hash) && _1["type"] == "tool_use" }
|
|
57
61
|
tool && Progress.new(step: step_for(tool["name"].to_s, tool["input"].is_a?(Hash) ? tool["input"] : {}), session_id: nil)
|
|
58
62
|
when "result"
|
|
59
|
-
Result.new(cost_usd: event["total_cost_usd"], text: event["result"].to_s, error: event["is_error"] == true)
|
|
63
|
+
Result.new(cost_usd: event["total_cost_usd"], text: event["result"].to_s, error: event["is_error"] == true, billing: @billing)
|
|
60
64
|
end
|
|
61
65
|
end
|
|
62
66
|
|
|
@@ -74,6 +78,14 @@ module Diffbroker
|
|
|
74
78
|
|
|
75
79
|
private
|
|
76
80
|
|
|
81
|
+
# The init event names where the credentials come from: "none" is the OAuth login of a Claude subscription, anything
|
|
82
|
+
# else (ANTHROPIC_API_KEY, apiKeyHelper, …) is pay-per-token. An event without the key says nothing.
|
|
83
|
+
def billing_for(source)
|
|
84
|
+
return unless source.is_a?(String) && !source.empty?
|
|
85
|
+
|
|
86
|
+
source == "none" ? "subscription" : "api"
|
|
87
|
+
end
|
|
88
|
+
|
|
77
89
|
def log_block(block)
|
|
78
90
|
return unless block.is_a?(Hash)
|
|
79
91
|
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "open3"
|
|
4
|
+
require "timeout"
|
|
5
|
+
|
|
3
6
|
module Diffbroker
|
|
4
7
|
module Agents
|
|
5
8
|
# Codex. Autonomous: `codex exec --json` in a workspace-write sandbox with network (spec §4.2 step 5); the MCP
|
|
@@ -9,6 +12,35 @@ module Diffbroker
|
|
|
9
12
|
# TaskRunner never runs Codex headless on an untrusted head.
|
|
10
13
|
class Codex < Base
|
|
11
14
|
READ_ONLY_ARGS = %w[--sandbox read-only].freeze
|
|
15
|
+
STATUS_TIMEOUT = 10
|
|
16
|
+
@billing_cache = {}
|
|
17
|
+
@billing_lock = Mutex.new
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# `codex login status` once per process and command: "Logged in using ChatGPT" is a subscription, "using an API key" is
|
|
21
|
+
# pay-per-token. Anything else, or a failure, is unknown.
|
|
22
|
+
def billing_for(command)
|
|
23
|
+
@billing_lock.synchronize { @billing_cache.fetch(command) { @billing_cache[command] = detect_billing(command) } }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def parse_login_status(text)
|
|
27
|
+
case text.to_s
|
|
28
|
+
when /logged in using chatgpt/i then "subscription"
|
|
29
|
+
when /logged in using an api key/i then "api"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def reset_billing_cache! = @billing_lock.synchronize { @billing_cache.clear }
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def detect_billing(command)
|
|
38
|
+
out, = Timeout.timeout(STATUS_TIMEOUT) { Open3.capture2e(command, "login", "status", stdin_data: "") }
|
|
39
|
+
parse_login_status(out)
|
|
40
|
+
rescue StandardError, Timeout::Error
|
|
41
|
+
nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
12
44
|
|
|
13
45
|
def name = "codex"
|
|
14
46
|
|
|
@@ -25,8 +57,8 @@ module Diffbroker
|
|
|
25
57
|
case event["type"]
|
|
26
58
|
when "thread.started" then Progress.new(step: "starting", session_id: event["thread_id"])
|
|
27
59
|
when "item.started", "item.completed" then item(event["item"].is_a?(Hash) ? event["item"] : {})
|
|
28
|
-
when "turn.completed" then Result.new(cost_usd: nil, text: @last_message.to_s, error: false)
|
|
29
|
-
when "turn.failed", "error" then Result.new(cost_usd: nil, text: @last_message.to_s, error: true)
|
|
60
|
+
when "turn.completed" then Result.new(cost_usd: nil, text: @last_message.to_s, error: false, billing: billing)
|
|
61
|
+
when "turn.failed", "error" then Result.new(cost_usd: nil, text: @last_message.to_s, error: true, billing: billing)
|
|
30
62
|
end
|
|
31
63
|
end
|
|
32
64
|
|
|
@@ -47,6 +79,8 @@ module Diffbroker
|
|
|
47
79
|
|
|
48
80
|
private
|
|
49
81
|
|
|
82
|
+
def billing = self.class.billing_for(@config.codex_command)
|
|
83
|
+
|
|
50
84
|
def item(item)
|
|
51
85
|
case item["type"]
|
|
52
86
|
when "command_execution" then Progress.new(step: "running a command", session_id: nil)
|
data/lib/diffbroker/agents.rb
CHANGED
|
@@ -4,7 +4,10 @@ module Diffbroker
|
|
|
4
4
|
# The user's own coding agents, run headless (autonomous) or interactively (terminal). Spec §4.2 steps 5–7.
|
|
5
5
|
module Agents
|
|
6
6
|
Progress = Data.define(:step, :session_id)
|
|
7
|
-
|
|
7
|
+
# billing: "subscription" or "api" when the agent's login says how the run was paid, else nil.
|
|
8
|
+
Result = Data.define(:cost_usd, :text, :error, :billing) do
|
|
9
|
+
def initialize(cost_usd:, text:, error:, billing: nil) = super
|
|
10
|
+
end
|
|
8
11
|
# One line of the run for the user's own log (kind: text, thinking, tool, error or result). Tool results (file
|
|
9
12
|
# contents) are never an entry.
|
|
10
13
|
Entry = Data.define(:kind, :text)
|
|
@@ -49,7 +49,8 @@ module Diffbroker
|
|
|
49
49
|
when "cancelled" then [ "cancelled", "cancelled" ]
|
|
50
50
|
else [ "failed", verify.error_code || "agent_failed" ]
|
|
51
51
|
end
|
|
52
|
-
TaskRunner::Outcome.new(status, costs.empty? ? nil : costs.sum.round(6), code, verify.last_output || write.last_output
|
|
52
|
+
TaskRunner::Outcome.new(status, costs.empty? ? nil : costs.sum.round(6), code, verify.last_output || write.last_output, false,
|
|
53
|
+
[ write.billing, verify.billing ].include?("api") ? "api" : write.billing || verify.billing)
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
end
|
|
@@ -12,7 +12,7 @@ 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, :no_result)
|
|
15
|
+
Outcome = Struct.new(:status, :cost_usd, :error_code, :last_output, :no_result, :billing)
|
|
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,
|
|
@@ -146,7 +146,7 @@ module Diffbroker
|
|
|
146
146
|
outcome = run_with_resume(task, agent, mcp_config, dir)
|
|
147
147
|
outcome = verify_after_write(task, outcome, agent, mcp_config, dir) if ExplainVerifier.verified?(task) && outcome.status == "done"
|
|
148
148
|
outcome = publish_branch(task, outcome, dir) if outcome.status == "done"
|
|
149
|
-
finished(id, outcome.status, outcome.cost_usd, outcome.error_code, outcome.last_output)
|
|
149
|
+
finished(id, outcome.status, outcome.cost_usd, outcome.error_code, outcome.last_output, outcome.billing)
|
|
150
150
|
ensure
|
|
151
151
|
@lock.synchronize do
|
|
152
152
|
@running.delete(id)
|
|
@@ -184,7 +184,7 @@ module Diffbroker
|
|
|
184
184
|
say(task["task_id"], result.message)
|
|
185
185
|
return outcome unless result.failed?
|
|
186
186
|
|
|
187
|
-
Outcome.new("failed", outcome.cost_usd, "agent_failed", result.message)
|
|
187
|
+
Outcome.new("failed", outcome.cost_usd, "agent_failed", result.message, false, outcome.billing)
|
|
188
188
|
end
|
|
189
189
|
|
|
190
190
|
# The same push for a terminal run, executed by its script after the agent exits (`diffbroker publish-branch`).
|
|
@@ -204,9 +204,9 @@ module Diffbroker
|
|
|
204
204
|
rescue Error => e
|
|
205
205
|
# The explanation is written; keep its cost and let the server show it as not verified.
|
|
206
206
|
say(id, "Verifier skipped: #{e.message}")
|
|
207
|
-
return Outcome.new("done", write.cost_usd, nil, "Verifier skipped: #{e.message}")
|
|
207
|
+
return Outcome.new("done", write.cost_usd, nil, "Verifier skipped: #{e.message}", false, write.billing)
|
|
208
208
|
end
|
|
209
|
-
return Outcome.new("cancelled", write.cost_usd, "cancelled", write.last_output) if @lock.synchronize { @cancelled.include?(id) }
|
|
209
|
+
return Outcome.new("cancelled", write.cost_usd, "cancelled", write.last_output, false, write.billing) if @lock.synchronize { @cancelled.include?(id) }
|
|
210
210
|
|
|
211
211
|
verify = run_process(task.merge("prompt" => prompt), agent, mcp_config, dir, step: "verifying", phase: "verifier")
|
|
212
212
|
ExplainVerifier.combine(write, verify)
|
|
@@ -227,9 +227,9 @@ module Diffbroker
|
|
|
227
227
|
# Only the agent's own result text: a raw stream-json line can carry file contents (spec §5.2).
|
|
228
228
|
output = result&.text
|
|
229
229
|
|
|
230
|
-
if handle.cancelled then Outcome.new("cancelled", result&.cost_usd, "cancelled", output)
|
|
231
|
-
elsif status.success? && !result&.error then Outcome.new("done", result&.cost_usd, nil, output)
|
|
232
|
-
else Outcome.new("failed", result&.cost_usd, "agent_failed", output, result.nil
|
|
230
|
+
if handle.cancelled then Outcome.new("cancelled", result&.cost_usd, "cancelled", output, false, result&.billing)
|
|
231
|
+
elsif status.success? && !result&.error then Outcome.new("done", result&.cost_usd, nil, output, false, result&.billing)
|
|
232
|
+
else Outcome.new("failed", result&.cost_usd, "agent_failed", output, result.nil?, result&.billing)
|
|
233
233
|
end
|
|
234
234
|
ensure
|
|
235
235
|
writer.close if writer && !writer.closed?
|
|
@@ -297,12 +297,14 @@ module Diffbroker
|
|
|
297
297
|
say(id, "progress: #{step}") unless step == "starting"
|
|
298
298
|
end
|
|
299
299
|
|
|
300
|
-
def finished(id, status, cost_usd, error_code, last_output)
|
|
300
|
+
def finished(id, status, cost_usd, error_code, last_output, billing = nil)
|
|
301
301
|
return if @lock.synchronize { @interrupted }
|
|
302
302
|
|
|
303
303
|
last = last_output.to_s.lines.last.to_s.strip
|
|
304
|
-
|
|
305
|
-
|
|
304
|
+
payload = { "task_id" => id, "status" => status, "cost_usd" => cost_usd, "error_code" => error_code,
|
|
305
|
+
"last_output" => last[0, MAX_OUTPUT] }
|
|
306
|
+
payload["billing"] = billing if %w[subscription api].include?(billing)
|
|
307
|
+
@report.call("finished", payload)
|
|
306
308
|
summary = [ (error_code && error_code != status ? "#{status} (#{error_code})" : status), (format("$%.2f", cost_usd) if cost_usd&.positive?),
|
|
307
309
|
(last[0, PRINT_OUTPUT] unless last.empty?) ].compact.join(" · ")
|
|
308
310
|
say(id, summary)
|
data/lib/diffbroker/version.rb
CHANGED