agent_session_context 1.0.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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +298 -0
  5. data/exe/agent-session-context +6 -0
  6. data/lib/agent/session_context/builder.rb +150 -0
  7. data/lib/agent/session_context/cli/options.rb +214 -0
  8. data/lib/agent/session_context/cli.rb +254 -0
  9. data/lib/agent/session_context/config.rb +206 -0
  10. data/lib/agent/session_context/errors.rb +15 -0
  11. data/lib/agent/session_context/evidence_collector.rb +227 -0
  12. data/lib/agent/session_context/evidence_packet.rb +271 -0
  13. data/lib/agent/session_context/immutable_value.rb +71 -0
  14. data/lib/agent/session_context/injected_context.rb +92 -0
  15. data/lib/agent/session_context/injected_context_collector.rb +53 -0
  16. data/lib/agent/session_context/item.rb +54 -0
  17. data/lib/agent/session_context/loop.rb +122 -0
  18. data/lib/agent/session_context/loop_view.rb +248 -0
  19. data/lib/agent/session_context/prompt.rb +40 -0
  20. data/lib/agent/session_context/prompt_extractor.rb +31 -0
  21. data/lib/agent/session_context/renderers/human_display.rb +113 -0
  22. data/lib/agent/session_context/renderers/json.rb +21 -0
  23. data/lib/agent/session_context/renderers/json_lines.rb +25 -0
  24. data/lib/agent/session_context/renderers/markdown.rb +130 -0
  25. data/lib/agent/session_context/renderers/serializer.rb +124 -0
  26. data/lib/agent/session_context/renderers/text.rb +128 -0
  27. data/lib/agent/session_context/semantic_categories.rb +89 -0
  28. data/lib/agent/session_context/semantic_pipeline.rb +151 -0
  29. data/lib/agent/session_context/semantic_schema.rb +75 -0
  30. data/lib/agent/session_context/session_resolver.rb +147 -0
  31. data/lib/agent/session_context/snapshot.rb +128 -0
  32. data/lib/agent/session_context/source_ref.rb +46 -0
  33. data/lib/agent/session_context/subprocess_runner.rb +362 -0
  34. data/lib/agent/session_context/summarizers/claude.rb +126 -0
  35. data/lib/agent/session_context/summarizers/codex.rb +132 -0
  36. data/lib/agent/session_context/summarizers/command_execution_policy.rb +134 -0
  37. data/lib/agent/session_context/summarizers.rb +35 -0
  38. data/lib/agent/session_context/summary_parser.rb +219 -0
  39. data/lib/agent/session_context/tool_call.rb +21 -0
  40. data/lib/agent/session_context/transcript.rb +236 -0
  41. data/lib/agent/session_context/version.rb +7 -0
  42. data/lib/agent/session_context.rb +60 -0
  43. metadata +115 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e25d4c855e914bc804c30b9505d6983513b7465c37832eca6af1aec37cdafb63
4
+ data.tar.gz: '079e33a4af4f74b9cb23d6aaa07c8c5e8193da29aca3e12c9ea542c52a497c6c'
5
+ SHA512:
6
+ metadata.gz: c73b78d0a6ff85c7ff84e088e5b68308d4175628c56e9ea201eeffe9ff9a47aad8fd75004537659860922f864dff11fb5aa3eaad8e21b1891345821387ae1e04
7
+ data.tar.gz: 2647f8a4aad1fa59d5689c7fdfa5af5692af993a33ae00a3474601045a7add3c34453e2587382bf02255a294970fcb7a63fa6f187871241ab21c3afe796d7528
data/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ ## [Unreleased]
2
+
3
+ ## [1.0.0] - 2026-09-10
4
+
5
+ - Require `agent_sessions` 0.4, which adds the round-trip grouping the `loop` command is built on.
6
+ - **BREAKING**: the gem is now distributed as `agent_session_context`. The
7
+ namespace, require path, and executable are unchanged; users change only
8
+ the Gemfile line.
9
+ - Add the `loop` command and `Agent::SessionContext.loop`, showing a session
10
+ as the agent loop: prompts, model round trips, tool calls paired with
11
+ their results, and where it stopped. Moved from `agent_sessions` before it
12
+ shipped there, since this gem interprets what a store recorded rather than
13
+ normalizing it. Prints byte sizes and tool names, never prompt or
14
+ tool-result bodies; renders the same session the same way regardless of
15
+ machine or time zone; and always labels the ending inferred, since no
16
+ on-disk transcript records why a session stopped.
17
+
18
+ ## [0.1.0] - 2026-09-01
19
+
20
+ - Use the `agent-session_context` distribution, `Agent::SessionContext`
21
+ namespace, and `agent-session-context` executable to avoid collisions with
22
+ the existing `agent-context` gem and `Agent::Context` namespace.
23
+ - Let `--current` choose the uniquely latest session metadata timestamp only
24
+ when no supported session environment identifier is present. Present
25
+ identifiers keep their validation and errors; `--agent` narrows disk
26
+ fallback, empty stores raise `CurrentSessionUnavailable`, the CLI warns with
27
+ the selected UID, and exact ties refuse with explicit-session guidance.
28
+ - Make `show` include exact user prompts and a deduplicated injected-context
29
+ inventory, with explicit `--include-injected`/`include_injected: true` access
30
+ to full injected text and documented raw-transcript exclusions.
31
+ - Add real Claude Code and Codex CLI JSONL fixtures for the public `show`,
32
+ `prompts`, and `summarize` contract.
33
+ - Verify prompt filtering against injected environment, AGENTS, slash-command,
34
+ and tool-result records while keeping observed file/tool evidence grounded to
35
+ source refs.
36
+ - Bound summarizer subprocess stdout/stderr streaming to independent `1 MiB`
37
+ caps, add per-call timeout controls across config/API/CLI, and package the
38
+ shared subprocess/config support.
39
+ - Verify the Codex summarization backend against a live recorded session.
40
+ - Document the recorded-evidence model, CLI/API surface, privacy boundaries,
41
+ and provider boundaries.
42
+ - Include source, changelog, and issue-tracker links for the public GitHub
43
+ repository in the gem metadata.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Lucian Ghinda
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,298 @@
1
+ # Agent Session Context
2
+
3
+ [![Build](https://github.com/lucianghinda/agent_session_context/actions/workflows/main.yml/badge.svg)](https://github.com/lucianghinda/agent_session_context/actions/workflows/main.yml)
4
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%203.2-red.svg)](https://www.ruby-lang.org/)
5
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
6
+
7
+ Inspect and summarize recorded Claude Code and Codex sessions.
8
+
9
+ ## Installation
10
+
11
+ Use Ruby 3.2 or newer.
12
+
13
+ Add this line to your application's **Gemfile**:
14
+
15
+ ```ruby
16
+ gem "agent_session_context"
17
+ ```
18
+
19
+ Then run:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ Show the latest recorded session:
28
+
29
+ ```bash
30
+ agent-session-context show --current
31
+ ```
32
+
33
+ ## Usage
34
+
35
+ ### Basic Usage
36
+
37
+ Show an exact session:
38
+
39
+ ```bash
40
+ agent-session-context show codex:SESSION_ID
41
+ ```
42
+
43
+ Disambiguate a bare identifier:
44
+
45
+ ```bash
46
+ agent-session-context show SESSION_ID --agent codex
47
+ ```
48
+
49
+ List exact user prompts:
50
+
51
+ ```bash
52
+ agent-session-context prompts --current
53
+ ```
54
+
55
+ Render prompts as JSON Lines:
56
+
57
+ ```bash
58
+ agent-session-context prompts --current --format jsonl
59
+ ```
60
+
61
+ ### Current Sessions
62
+
63
+ Use these variables for `--current`, in order:
64
+
65
+ 1. `AGENT_SESSION_ID` with `AGENT_NAME`
66
+ 2. `CLAUDE_CODE_SESSION_ID`
67
+ 3. `CODEX_SESSION_ID`
68
+ 4. `CODEX_THREAD_ID`
69
+
70
+ Without variables, let the command select the unique newest session metadata.
71
+
72
+ Pass `--agent` to restrict that disk search.
73
+
74
+ Keep present identifiers authoritative.
75
+
76
+ Never trigger fallback after validating a present identifier.
77
+
78
+ Clear conflicting Claude and Codex identifiers before retrying.
79
+
80
+ Expect missing targets, empty stores, and timestamp ties to fail.
81
+
82
+ Read the selected UID from the CLI warning.
83
+
84
+ Treat disk selection as recency, not live context.
85
+
86
+ ### Local Context
87
+
88
+ Include deduplicated injected text explicitly:
89
+
90
+ ```bash
91
+ agent-session-context show --current --include-injected
92
+ ```
93
+
94
+ Review exact prompts and injected text before sharing them.
95
+
96
+ Expect `show` to exclude assistant messages, thinking, tool-result bodies, and raw envelopes.
97
+
98
+ Use `show` and `prompts` without starting a model.
99
+
100
+ ### Agent Loop
101
+
102
+ Show a session as the agent loop:
103
+
104
+ ```bash
105
+ agent-session-context loop --current
106
+ ```
107
+
108
+ Print prompts, model round trips, tool calls paired with their results, and where the session stopped.
109
+
110
+ Print byte sizes and tool names only, never prompt or tool-result bodies.
111
+
112
+ Expect the ending to always read as inferred: no store on disk records why a session stopped.
113
+
114
+ Expect deterministic output: render the same session the same way regardless of machine or time zone.
115
+
116
+ Render `loop` as text, Markdown, JSON, or JSON Lines with `--format`.
117
+
118
+ ### Summaries
119
+
120
+ Create a grounded summary:
121
+
122
+ ```bash
123
+ agent-session-context summarize --current
124
+ ```
125
+
126
+ Choose a backend and timeout:
127
+
128
+ ```bash
129
+ agent-session-context summarize --current --using codex --timeout 45
130
+ ```
131
+
132
+ Codex summarization was tested successfully against a live recorded session.
133
+
134
+ Expect summaries to cite recorded source references.
135
+
136
+ Expect summaries to exclude thinking, tool results, injected blocks, and raw records.
137
+
138
+ Treat Codex filesystem access as read-only, not hermetic.
139
+
140
+ ### Ruby API
141
+
142
+ Resolve and inspect a session:
143
+
144
+ ```ruby
145
+ require "agent/session_context"
146
+
147
+ session = Agent::SessionContext.resolve("codex:SESSION_ID")
148
+ snapshot = Agent::SessionContext.show(session)
149
+ ```
150
+
151
+ Inspect the newest recorded Codex session:
152
+
153
+ ```ruby
154
+ session = Agent::SessionContext.current(agent: :codex, env: {})
155
+ prompts = Agent::SessionContext.prompts(session)
156
+ ```
157
+
158
+ Read a session as the agent loop:
159
+
160
+ ```ruby
161
+ loop = Agent::SessionContext.loop(session)
162
+ loop.ending #=> :answered, :stopped_in_the_loop, :not_a_model_record, or :empty
163
+ ```
164
+
165
+ Create a summary with built-in settings:
166
+
167
+ ```ruby
168
+ summary = Agent::SessionContext.summarize(session, using: :codex, timeout: 45)
169
+ ```
170
+
171
+ Use a custom summarizer:
172
+
173
+ ```ruby
174
+ summary = Agent::SessionContext.summarize(
175
+ session,
176
+ summarizer: ->(prompt:, schema:) { call_your_model(prompt, schema) }
177
+ )
178
+ ```
179
+
180
+ Replace `call_your_model` with your adapter.
181
+
182
+ Return a JSON string matching the provided schema.
183
+
184
+ Pass either `summarizer:` or `timeout:`, never both.
185
+
186
+ ### Supported Public Ruby API
187
+
188
+ `Agent::SessionContext.resolve` and `Agent::SessionContext.current` return `Agent::Sessions::Session`.
189
+
190
+ `Agent::SessionContext.show` returns an `Agent::SessionContext::Snapshot` whose collections contain `Agent::SessionContext::Prompt`, `Agent::SessionContext::InjectedContext`, `Agent::SessionContext::Item`, and `Agent::SessionContext::SourceRef` values as applicable.
191
+
192
+ `Agent::SessionContext.prompts` returns an array of `Agent::SessionContext::Prompt` values.
193
+
194
+ `Agent::SessionContext.loop` returns an `Agent::SessionContext::Loop`.
195
+
196
+ `Agent::SessionContext.summarize` returns an `Agent::SessionContext::Snapshot` populated with summary `Agent::SessionContext::Item` values and summary metadata.
197
+
198
+ `Agent::SessionContext::Snapshot`, `Agent::SessionContext::Prompt`, `Agent::SessionContext::InjectedContext`, `Agent::SessionContext::Item`, and `Agent::SessionContext::SourceRef` are part of the supported public data model.
199
+
200
+ `Agent::SessionContext::Loop` and `Agent::SessionContext::ToolCall` are part of the supported public data model. `Agent::SessionContext::LoopView` is internal.
201
+
202
+ `Agent::SessionContext::VERSION` is public.
203
+
204
+ `Agent::SessionContext::CLI::FORMATS` is the supported frozen list of CLI output format names.
205
+
206
+ The public error classes listed in [Errors](#errors) are part of the compatibility contract.
207
+
208
+ ### Internal Architecture
209
+
210
+ `resolve -> capture -> extract/collect -> optionally summarize -> build snapshot -> render`.
211
+
212
+ `show` can expose injected text only when you opt into `include_injected`, while `summarize` keeps injected blocks and tool-result bodies out of the model prompt.
213
+
214
+ Except for `Agent::SessionContext::CLI::FORMATS`, the CLI implementation, builders, collectors, parsers, runners, renderers, and built-in summarizer adapters are internal details without compatibility guarantees.
215
+
216
+ ## Options
217
+
218
+ | Option | Description |
219
+ |---|---|
220
+ | `--current` | Use environment identity, then the newest disk metadata. |
221
+ | `--agent claude\|codex` | Restrict explicit lookup or disk fallback. |
222
+ | `--format FORMAT` | Choose text, Markdown, JSON, or JSON Lines when supported. |
223
+ | `--include-injected` | Include deduplicated injected text with `show`. |
224
+ | `--using BACKEND` | Choose `auto`, `claude`, or `codex` for summaries. |
225
+ | `--timeout SECONDS` | Set each provider call timeout from 1 through 3600 seconds. |
226
+
227
+ Run `agent-session-context help` for command details.
228
+
229
+ ### Configuration
230
+
231
+ Expect only `summarize` to load configuration.
232
+
233
+ Configuration paths retain the original `agent-context` name for compatibility.
234
+
235
+ Create `.agent-context.yml` in the recorded project:
236
+
237
+ ```yaml
238
+ summarize:
239
+ timeout_seconds: 300
240
+ ```
241
+
242
+ Set user defaults in `$XDG_CONFIG_HOME/agent_context/config.yml`.
243
+
244
+ Otherwise, use `$HOME/.config/agent_context/config.yml`.
245
+
246
+ Use `XDG_CONFIG_HOME` exclusively when it contains an absolute path.
247
+
248
+ Let project configuration override user defaults.
249
+
250
+ Pass `--timeout` to override both files.
251
+
252
+ Use finite numbers from `1` through `3600`.
253
+
254
+ Apply the timeout to each provider call.
255
+
256
+ ### Output Formats
257
+
258
+ | Command | Formats |
259
+ |---|---|
260
+ | `show` | `text`, `markdown`, `json` |
261
+ | `prompts` | `text`, `markdown`, `json`, `jsonl` |
262
+ | `loop` | `text`, `markdown`, `json`, `jsonl` |
263
+ | `summarize` | `text`, `markdown`, `json` |
264
+
265
+ ### Errors
266
+
267
+ Handle these public errors:
268
+
269
+ - `Agent::SessionContext::SessionNotFound`
270
+ - `Agent::SessionContext::AmbiguousSession`
271
+ - `Agent::SessionContext::CurrentSessionUnavailable`
272
+ - `Agent::SessionContext::UnsupportedAgent`
273
+ - `Agent::SessionContext::ConfigurationError`
274
+ - `Agent::SessionContext::SummarizerUnavailable`
275
+ - `Agent::SessionContext::SummarizerFailed`
276
+ - `Agent::SessionContext::InvalidSummary`
277
+
278
+ ## Contributing
279
+
280
+ Fork the repository and create a branch.
281
+
282
+ Run the test suite:
283
+
284
+ ```bash
285
+ bundle exec rake test
286
+ ```
287
+
288
+ Open a pull request with tests and documentation.
289
+
290
+ Follow [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
291
+
292
+ Report bugs through [GitHub Issues](https://github.com/lucianghinda/agent_session_context/issues).
293
+
294
+ ## License
295
+
296
+ Use the gem under the MIT License.
297
+
298
+ See [LICENSE.txt](LICENSE.txt).
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "agent/session_context"
5
+
6
+ exit Agent::SessionContext::CLI.new(ARGV).run
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Agent
4
+ module SessionContext
5
+ class Builder
6
+ PromptsResult = Data.define(:prompts, :reader_warnings) do
7
+ def partial_capture?
8
+ !reader_warnings.empty?
9
+ end
10
+ end
11
+
12
+ def initialize(
13
+ catalog: Agent::Sessions,
14
+ now: Time.now,
15
+ collector: EvidenceCollector.new,
16
+ prompt_extractor: PromptExtractor.new,
17
+ injected_context_collector: InjectedContextCollector.new
18
+ )
19
+ @catalog = catalog
20
+ @now = now
21
+ @collector = collector
22
+ @prompt_extractor = prompt_extractor
23
+ @injected_context_collector = injected_context_collector
24
+ end
25
+
26
+ def show(session, include_injected: false)
27
+ validate_include_injected!(include_injected)
28
+ transcript = capture(session)
29
+ observed = @collector.call(transcript)
30
+
31
+ build_snapshot(
32
+ session:,
33
+ transcript:,
34
+ observed:,
35
+ prompts: @prompt_extractor.call(transcript),
36
+ injected_context: @injected_context_collector.call(transcript, include_text: include_injected),
37
+ warnings: transcript.warnings,
38
+ summary_metadata: base_metadata(transcript)
39
+ )
40
+ end
41
+
42
+ def prompts(session)
43
+ prompts_result(session).prompts
44
+ end
45
+
46
+ def prompts_result(session)
47
+ transcript = capture(session)
48
+ PromptsResult.new(
49
+ prompts: @prompt_extractor.call(transcript),
50
+ reader_warnings: transcript.warnings
51
+ )
52
+ end
53
+
54
+ def loop(session)
55
+ Loop.for(@catalog.read(session))
56
+ end
57
+
58
+ def summarize(session, summarizer:)
59
+ transcript = capture(session)
60
+ observed = @collector.call(transcript)
61
+ semantic = SemanticPipeline.new(backend: summarizer).call(transcript:, observed:)
62
+ semantic_collections, unknown_warnings = collect_semantic_items(semantic.items)
63
+
64
+ build_snapshot(
65
+ session: session,
66
+ transcript: transcript,
67
+ observed: observed,
68
+ semantic_collections: semantic_collections,
69
+ warnings: transcript.warnings + semantic.warnings + unknown_warnings,
70
+ summary_metadata: semantic.metadata.merge(base_metadata(transcript))
71
+ )
72
+ end
73
+
74
+ private
75
+
76
+ def validate_include_injected!(value)
77
+ return if value.equal?(true) || value.equal?(false)
78
+
79
+ raise ArgumentError, "include_injected must be true or false"
80
+ end
81
+
82
+ def capture(session)
83
+ Transcript.capture(session, reader: @catalog.read(session), now: @now)
84
+ end
85
+
86
+ def collect_semantic_items(items)
87
+ collections = empty_semantic_collections
88
+ warnings = []
89
+
90
+ items.each do |item|
91
+ field = SemanticCategories.lookup(item.kind)&.snapshot_field
92
+ unless field
93
+ warnings << "Dropped unknown semantic kind #{item.kind.inspect}"
94
+ next
95
+ end
96
+
97
+ collections[field] << item
98
+ end
99
+
100
+ [collections.transform_values(&:freeze).freeze, warnings.freeze]
101
+ end
102
+
103
+ def empty_semantic_collections
104
+ SemanticCategories.snapshot_fields.to_h do |field|
105
+ [field, []]
106
+ end
107
+ end
108
+
109
+ def build_snapshot(
110
+ session:,
111
+ transcript:,
112
+ observed:,
113
+ warnings:,
114
+ summary_metadata:,
115
+ prompts: [],
116
+ injected_context: [],
117
+ semantic_collections: empty_semantic_collections.transform_values(&:freeze).freeze
118
+ )
119
+ Snapshot.new(
120
+ session_uid: session.uid,
121
+ agent: session.agent,
122
+ project_path: session.project_path,
123
+ captured_at: transcript.captured_at,
124
+ message_count: transcript.entries.length,
125
+ prompts:,
126
+ injected_context:,
127
+ files: observed.files,
128
+ documents: observed.documents,
129
+ tool_activity: observed.tool_activity,
130
+ **semantic_snapshot_attributes(semantic_collections),
131
+ warnings: warnings,
132
+ summary_metadata: summary_metadata
133
+ )
134
+ end
135
+
136
+ def semantic_snapshot_attributes(semantic_collections)
137
+ SemanticCategories.snapshot_fields.to_h do |field|
138
+ [field, semantic_collections.fetch(field)]
139
+ end
140
+ end
141
+
142
+ def base_metadata(transcript)
143
+ metadata = { injected_parts_filtered: transcript.entries.sum { |entry| entry.parts.count(&:injected) } }
144
+ reader_warning_count = transcript.warnings.length
145
+ metadata[:reader_warning_count] = reader_warning_count if reader_warning_count.positive?
146
+ metadata
147
+ end
148
+ end
149
+ end
150
+ end