codex-ruby 0.1.0 → 0.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 03d9e3beac47546980e8b63c3051c2d5100a3f1db197cf85a43c8d7b9eaf8f56
4
- data.tar.gz: 879f569d1dfadc2451c7bf2ccb6a6cc36e897d3c1644e327bc8bb3d477014e72
3
+ metadata.gz: db45d058e2d271fe579ab9e4a614b571e194496c70d60b36470ad5fb5a7ebcbc
4
+ data.tar.gz: e243fc97acdb98e1ee2aaf2554b5f13c51a98e41df256fcb9f476ce61f8bf7e3
5
5
  SHA512:
6
- metadata.gz: c353524c5d986caea98150081c3efacaff18a918a643e2bf75af34f06e1b7d2c91627b433dc463d90c6f406cc20c68e8603430f2063ed6a1299ec881e19528bd
7
- data.tar.gz: ad57c8fd2dd7de943f0b541930125219342fffab87ce42890e643d6590abc09deb4d03ed7876f29802d05cb73bb66fc91f0a692ec2cf902a9a1bd29264d2c36b
6
+ metadata.gz: 00453f4fa7796cd76712465d9830264b57b79fc8a0261ad4092889ca3387a2db5bee8ffd23ba06349930666458789eff4d95c19761de97c1460ff11c5a046a3e
7
+ data.tar.gz: f28130158754e20ed7bb5452e14f43f453b436184fbe25ab804cfb8f02236edca1b1968b335f512dd329715e153fc7692f18e6b00953be2c00f425e6af0e3bd2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.2 (2026-04-19)
4
+
5
+ - Expose rollout-derived `context_snapshot` data on `Exec`, `AgentThread`, and blocking `Turn` results
6
+ - Add `TokenUsage` and `ContextSnapshot` types plus rollout log parsing for Codex `token_count` events
7
+ - Add RuboCop to the repository with a dedicated CI lint job and baseline configuration
8
+
9
+ ## 0.1.1 (2026-04-11)
10
+
11
+ - Patch release
12
+
3
13
  ## 0.1.0 (2026-04-10)
4
14
 
5
15
  - Initial release
data/README.md CHANGED
@@ -43,6 +43,7 @@ thread = client.start_thread(
43
43
  turn = thread.run("Explain this codebase")
44
44
  puts turn.final_response
45
45
  puts "Tokens used: #{turn.usage.input_tokens} in, #{turn.usage.output_tokens} out"
46
+ puts "Final context: #{turn.context_snapshot.context_tokens} / #{turn.context_snapshot.model_context_window}"
46
47
 
47
48
  # Streaming run - yields events as they arrive
48
49
  thread.run_streamed("Fix the failing tests") do |event|
@@ -62,6 +63,8 @@ thread.run_streamed("Fix the failing tests") do |event|
62
63
  puts "Error: #{event.error_message}"
63
64
  end
64
65
  end
66
+
67
+ puts "Final context: #{thread.context_snapshot.context_tokens} / #{thread.context_snapshot.model_context_window}"
65
68
  ```
66
69
 
67
70
  ### Resume a thread
@@ -120,6 +123,20 @@ client = CodexSDK::Client.new(
120
123
  | `Events::ItemCompleted` | Item finished, provides typed `item` |
121
124
  | `Events::Error` | Stream-level error, provides `message` |
122
125
 
126
+ ## Context snapshots
127
+
128
+ Codex CLI writes richer rollout logs under `~/.codex/sessions` (or `CODEX_HOME/sessions`). After a run completes, `Thread#run` and `Thread#run_streamed` expose a final `context_snapshot` derived from the latest `token_count` entry in those rollout files.
129
+
130
+ ```ruby
131
+ snapshot = thread.context_snapshot
132
+ snapshot.context_tokens # => current prompt/context footprint
133
+ snapshot.model_context_window # => model max context window
134
+ snapshot.last_token_usage.total_tokens
135
+ snapshot.total_token_usage.total_tokens
136
+ ```
137
+
138
+ This is separate from `turn.completed.usage`, which is still the per-turn API usage reported by the JSON event stream.
139
+
123
140
  ## Item types
124
141
 
125
142
  | Item | Fields |
@@ -4,7 +4,7 @@ require "fileutils"
4
4
 
5
5
  module CodexSDK
6
6
  class AgentThread
7
- attr_reader :id
7
+ attr_reader :id, :context_snapshot
8
8
 
9
9
  def initialize(options, thread_options:, resume_id: nil)
10
10
  @options = options
@@ -33,7 +33,7 @@ module CodexSDK
33
33
  end
34
34
  end
35
35
 
36
- Turn.new(items: items, final_response: final_response, usage: usage)
36
+ Turn.new(items: items, final_response: final_response, usage: usage, context_snapshot: @context_snapshot)
37
37
  end
38
38
 
39
39
  # Streaming run: yields each event to the block as it arrives.
@@ -41,9 +41,7 @@ module CodexSDK
41
41
  prompt = normalize_input(input)
42
42
 
43
43
  output_schema_path = nil
44
- if turn_options.output_schema
45
- output_schema_path = write_output_schema(turn_options.output_schema)
46
- end
44
+ output_schema_path = write_output_schema(turn_options.output_schema) if turn_options.output_schema
47
45
 
48
46
  @exec = Exec.new(
49
47
  @options,
@@ -61,6 +59,7 @@ module CodexSDK
61
59
  block.call(event)
62
60
  end
63
61
  ensure
62
+ @context_snapshot = @exec&.context_snapshot
64
63
  cleanup_output_schema(output_schema_path)
65
64
  end
66
65
 
@@ -76,9 +75,9 @@ module CodexSDK
76
75
  when String
77
76
  input
78
77
  when Array
79
- input.filter_map { |entry|
78
+ input.filter_map do |entry|
80
79
  entry[:text] if entry[:type] == "text"
81
- }.join("\n\n")
80
+ end.join("\n\n")
82
81
  else
83
82
  input.to_s
84
83
  end
@@ -93,6 +92,7 @@ module CodexSDK
93
92
 
94
93
  def cleanup_output_schema(path)
95
94
  return unless path
95
+
96
96
  dir = File.dirname(path)
97
97
  FileUtils.rm_rf(dir)
98
98
  rescue StandardError
@@ -38,6 +38,7 @@ module CodexSDK
38
38
  value.to_json
39
39
  when Integer, Float
40
40
  raise ArgumentError, "cannot serialize non-finite number" unless value.to_f.finite?
41
+
41
42
  value.to_s
42
43
  when true, false
43
44
  value.to_s
@@ -19,9 +19,7 @@ module CodexSDK
19
19
 
20
20
  ThreadStarted = Data.define(:thread_id)
21
21
 
22
- TurnStarted = Data.define do
23
- def initialize; super(); end
24
- end
22
+ TurnStarted = Data.define
25
23
 
26
24
  TurnCompleted = Data.define(:usage) do
27
25
  def self.from_json(data)
@@ -10,7 +10,7 @@ module CodexSDK
10
10
  class Exec
11
11
  SHUTDOWN_TIMEOUT = 10 # seconds to wait after SIGTERM before SIGKILL
12
12
 
13
- attr_reader :pid
13
+ attr_reader :pid, :context_snapshot
14
14
 
15
15
  def initialize(options, thread_options: ThreadOptions.new)
16
16
  @options = options
@@ -27,6 +27,9 @@ module CodexSDK
27
27
  def run(prompt, resume_thread_id: nil, images: [], output_schema_path: nil, &block)
28
28
  args = build_args(resume_thread_id: resume_thread_id, images: images, output_schema_path: output_schema_path)
29
29
  env = build_env
30
+ sessions_root = codex_sessions_root(env)
31
+ started_at = Time.now
32
+ @context_snapshot = nil
30
33
 
31
34
  @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(env, *args)
32
35
 
@@ -35,7 +38,11 @@ module CodexSDK
35
38
  @stdin.close
36
39
 
37
40
  # Read stderr in background thread
38
- stderr_reader = ::Thread.new { @stderr.read rescue "" }
41
+ stderr_reader = ::Thread.new do
42
+ @stderr.read
43
+ rescue StandardError
44
+ ""
45
+ end
39
46
 
40
47
  # Read JSONL from stdout line by line
41
48
  @stdout.each_line do |line|
@@ -64,6 +71,11 @@ module CodexSDK
64
71
  stderr: stderr_buf
65
72
  )
66
73
  end
74
+
75
+ @context_snapshot = read_context_snapshot(
76
+ sessions_root: sessions_root,
77
+ started_at: started_at
78
+ )
67
79
  ensure
68
80
  cleanup
69
81
  end
@@ -122,9 +134,7 @@ module CodexSDK
122
134
  args.concat(["--config", "sandbox_workspace_write.network_access=#{to.network_access}"])
123
135
  end
124
136
 
125
- if to.web_search
126
- args.concat(["--config", "web_search=#{ConfigSerializer.to_toml_value(to.web_search)}"])
127
- end
137
+ args.concat(["--config", "web_search=#{ConfigSerializer.to_toml_value(to.web_search)}"]) if to.web_search
128
138
 
129
139
  if to.approval_policy
130
140
  args.concat(["--config", "approval_policy=#{ConfigSerializer.to_toml_value(to.approval_policy)}"])
@@ -153,15 +163,37 @@ module CodexSDK
153
163
  def find_codex_path
154
164
  path = `which codex 2>/dev/null`.strip
155
165
  raise Error, "codex binary not found in PATH" if path.empty?
166
+
156
167
  path
157
168
  end
158
169
 
170
+ def codex_sessions_root(env)
171
+ return File.join(env["CODEX_HOME"], "sessions") if env["CODEX_HOME"] && !env["CODEX_HOME"].empty?
172
+
173
+ return unless env["HOME"] && !env["HOME"].empty?
174
+
175
+ File.join(env["HOME"], ".codex", "sessions")
176
+ end
177
+
178
+ def read_context_snapshot(sessions_root:, started_at:)
179
+ return unless sessions_root
180
+
181
+ RolloutContextSnapshotReader.new(
182
+ sessions_root: sessions_root,
183
+ started_at: started_at
184
+ ).read
185
+ rescue StandardError
186
+ nil
187
+ end
188
+
159
189
  def wait_for_exit(timeout)
160
190
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
161
191
  loop do
162
192
  return true unless @wait_thread&.alive?
193
+
163
194
  remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
164
195
  return false if remaining <= 0
196
+
165
197
  sleep([0.1, remaining].min)
166
198
  end
167
199
  end
@@ -63,9 +63,43 @@ module CodexSDK
63
63
  end
64
64
  end
65
65
 
66
+ # Detailed token usage from rollout token_count snapshots.
67
+ TokenUsage = Data.define(
68
+ :input_tokens,
69
+ :cached_input_tokens,
70
+ :output_tokens,
71
+ :reasoning_output_tokens,
72
+ :total_tokens
73
+ ) do
74
+ def initialize(
75
+ input_tokens: 0,
76
+ cached_input_tokens: 0,
77
+ output_tokens: 0,
78
+ reasoning_output_tokens: 0,
79
+ total_tokens: 0
80
+ )
81
+ super
82
+ end
83
+ end
84
+
85
+ # Final context snapshot derived from Codex rollout logs.
86
+ ContextSnapshot = Data.define(:model_context_window, :last_token_usage, :total_token_usage) do
87
+ def initialize(
88
+ model_context_window: 0,
89
+ last_token_usage: TokenUsage.new,
90
+ total_token_usage: TokenUsage.new
91
+ )
92
+ super
93
+ end
94
+
95
+ def context_tokens
96
+ last_token_usage.total_tokens
97
+ end
98
+ end
99
+
66
100
  # Result of a blocking Thread#run call.
67
- Turn = Data.define(:items, :final_response, :usage) do
68
- def initialize(items: [], final_response: "", usage: nil)
101
+ Turn = Data.define(:items, :final_response, :usage, :context_snapshot) do
102
+ def initialize(items: [], final_response: "", usage: nil, context_snapshot: nil)
69
103
  super
70
104
  end
71
105
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module CodexSDK
6
+ class RolloutContextSnapshotReader
7
+ def initialize(sessions_root:, started_at:)
8
+ @sessions_root = sessions_root
9
+ @started_at = started_at
10
+ end
11
+
12
+ def read
13
+ candidate_rollouts.reverse_each do |path|
14
+ snapshot = read_rollout(path)
15
+ return snapshot if snapshot
16
+ end
17
+
18
+ nil
19
+ end
20
+
21
+ private
22
+
23
+ def candidate_rollouts
24
+ return [] unless Dir.exist?(@sessions_root)
25
+
26
+ Dir.glob(File.join(@sessions_root, "**", "rollout-*.jsonl"))
27
+ .select { |path| candidate_rollout?(path) }
28
+ .sort_by { |path| File.mtime(path) }
29
+ end
30
+
31
+ def candidate_rollout?(path)
32
+ return true unless @started_at
33
+
34
+ File.mtime(path) >= (@started_at - 1)
35
+ end
36
+
37
+ def read_rollout(path)
38
+ snapshot = nil
39
+
40
+ File.foreach(path) do |line|
41
+ event = JSON.parse(line)
42
+ next unless event["type"] == "event_msg"
43
+
44
+ payload = event["payload"]
45
+ next unless payload.is_a?(Hash) && payload["type"] == "token_count"
46
+
47
+ info = payload["info"]
48
+ next unless info.is_a?(Hash)
49
+
50
+ snapshot = ContextSnapshot.new(
51
+ model_context_window: info["model_context_window"].to_i,
52
+ last_token_usage: parse_usage(info["last_token_usage"]),
53
+ total_token_usage: parse_usage(info["total_token_usage"])
54
+ )
55
+ end
56
+
57
+ snapshot
58
+ rescue Errno::ENOENT, JSON::ParserError
59
+ nil
60
+ end
61
+
62
+ def parse_usage(data)
63
+ return TokenUsage.new unless data.is_a?(Hash)
64
+
65
+ TokenUsage.new(
66
+ input_tokens: data["input_tokens"].to_i,
67
+ cached_input_tokens: data["cached_input_tokens"].to_i,
68
+ output_tokens: data["output_tokens"].to_i,
69
+ reasoning_output_tokens: data["reasoning_output_tokens"].to_i,
70
+ total_tokens: data["total_tokens"].to_i
71
+ )
72
+ end
73
+ end
74
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module CodexSDK
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/codex_sdk.rb CHANGED
@@ -29,6 +29,7 @@ require_relative "codex_sdk/options"
29
29
  require_relative "codex_sdk/config_serializer"
30
30
  require_relative "codex_sdk/items"
31
31
  require_relative "codex_sdk/events"
32
+ require_relative "codex_sdk/rollout_context_snapshot_reader"
32
33
  require_relative "codex_sdk/exec"
33
34
  require_relative "codex_sdk/agent_thread"
34
35
  require_relative "codex_sdk/client"
metadata CHANGED
@@ -1,42 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: codex-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Kopylov
8
8
  bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies:
12
- - !ruby/object:Gem::Dependency
13
- name: rake
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - "~>"
17
- - !ruby/object:Gem::Version
18
- version: '13.0'
19
- type: :development
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - "~>"
24
- - !ruby/object:Gem::Version
25
- version: '13.0'
26
- - !ruby/object:Gem::Dependency
27
- name: rspec
28
- requirement: !ruby/object:Gem::Requirement
29
- requirements:
30
- - - "~>"
31
- - !ruby/object:Gem::Version
32
- version: '3.0'
33
- type: :development
34
- prerelease: false
35
- version_requirements: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '3.0'
11
+ dependencies: []
40
12
  description: A Ruby client for the Codex CLI, providing subprocess management, JSONL
41
13
  event parsing, and a clean API for building AI-powered applications.
42
14
  email:
@@ -56,6 +28,7 @@ files:
56
28
  - lib/codex_sdk/exec.rb
57
29
  - lib/codex_sdk/items.rb
58
30
  - lib/codex_sdk/options.rb
31
+ - lib/codex_sdk/rollout_context_snapshot_reader.rb
59
32
  - lib/codex_sdk/version.rb
60
33
  homepage: https://github.com/tonic20/codex-ruby
61
34
  licenses:
@@ -64,6 +37,7 @@ metadata:
64
37
  homepage_uri: https://github.com/tonic20/codex-ruby
65
38
  source_code_uri: https://github.com/tonic20/codex-ruby
66
39
  changelog_uri: https://github.com/tonic20/codex-ruby/blob/main/CHANGELOG.md
40
+ rubygems_mfa_required: 'true'
67
41
  rdoc_options: []
68
42
  require_paths:
69
43
  - lib