codex-ruby 0.1.1 → 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 +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +17 -0
- data/lib/codex_sdk/agent_thread.rb +7 -7
- data/lib/codex_sdk/config_serializer.rb +1 -0
- data/lib/codex_sdk/events.rb +1 -3
- data/lib/codex_sdk/exec.rb +37 -5
- data/lib/codex_sdk/options.rb +36 -2
- data/lib/codex_sdk/rollout_context_snapshot_reader.rb +74 -0
- data/lib/codex_sdk/version.rb +1 -1
- data/lib/codex_sdk.rb +1 -0
- metadata +4 -30
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: db45d058e2d271fe579ab9e4a614b571e194496c70d60b36470ad5fb5a7ebcbc
|
|
4
|
+
data.tar.gz: e243fc97acdb98e1ee2aaf2554b5f13c51a98e41df256fcb9f476ce61f8bf7e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 00453f4fa7796cd76712465d9830264b57b79fc8a0261ad4092889ca3387a2db5bee8ffd23ba06349930666458789eff4d95c19761de97c1460ff11c5a046a3e
|
|
7
|
+
data.tar.gz: f28130158754e20ed7bb5452e14f43f453b436184fbe25ab804cfb8f02236edca1b1968b335f512dd329715e153fc7692f18e6b00953be2c00f425e6af0e3bd2
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
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
|
+
|
|
3
9
|
## 0.1.1 (2026-04-11)
|
|
4
10
|
|
|
5
11
|
- Patch 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
|
|
78
|
+
input.filter_map do |entry|
|
|
80
79
|
entry[:text] if entry[:type] == "text"
|
|
81
|
-
|
|
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
|
data/lib/codex_sdk/events.rb
CHANGED
data/lib/codex_sdk/exec.rb
CHANGED
|
@@ -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
|
|
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
|
data/lib/codex_sdk/options.rb
CHANGED
|
@@ -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
|
data/lib/codex_sdk/version.rb
CHANGED
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.
|
|
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
|