ask-session 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e3b79e3300f8b708fcf59dc44f073a75cafb1f1991d9944c8d2fab673cb18a7e
4
- data.tar.gz: 5c79377d3098b7ca243198b3bb54ee1e017c69546787562b7e532f21455aa523
3
+ metadata.gz: 431ac519e177e903d981fd5b8909e0f61d0f3f87701e0da24404b7eff44ba3fc
4
+ data.tar.gz: 80c72aebcc96b0fd52e183e703732b8f0a804583728c4ffc28f5badb361fdccd
5
5
  SHA512:
6
- metadata.gz: b87ab80fb5d0f68a613dcc5351d8f632354365673ec46f2b215d43b661be8b06e21fd487b37b9c5f69153aa146868a89072d661cfbfb07032480326ce6e2a78e
7
- data.tar.gz: 4fd5f4b12677acb8b3b83c8875f5c8f645959a0133d9dc5b1fb675492f2928bbff685341539e1e277e7a51fa7622e301b6c602514dbd6ebbb1b17bc1c71c738a
6
+ metadata.gz: 216739303b1cc6ee78a9a6801ae4afcd715dc1c365383b42088870c2c025436106c5c43da4bab23e1c7a19269af658f50962a4fa1f50edfd6aa0f1f1a9b413f8
7
+ data.tar.gz: 76300dd1e7e90a752a1acad1ba53af2a06655890182a5469daecb2016aa011413a32c395b2209bd6ce8eca4df2843eae5144b170c27c0b03fcb6862580f6393a
data/CHANGELOG.md CHANGED
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Task guides in `docs/` — in-memory sessions, durable SQLite sessions via the optional ask-state-providers integration, subscriptions and replay, tool lifecycle through `Host#sink`, and export/import — with a `docs/index.md` hub.
13
+ - README restructured to an overview, an in-memory quick start, and links to all guides.
14
+
15
+ ### Changed
16
+
17
+ - Clarify that `Host#sink` records provider-neutral runtime lifecycle events, while protocol-facing tool events are translated by integration adapters; avoid attaching both producers to the same host for one execution.
18
+
10
19
  ## [0.1.0] - 2026-09-22
11
20
 
12
21
  ### Added
data/README.md CHANGED
@@ -2,210 +2,53 @@
2
2
 
3
3
  Event-sourced session state for the [ask-rb](https://github.com/ask-rb) ecosystem.
4
4
 
5
- ## Installation
6
-
7
- Requires Ruby 3.2+.
8
-
9
- ```ruby
10
- gem "ask-session"
11
- ```
12
-
13
5
  ## Overview
14
6
 
15
- Ask::Session provides the foundational value objects and stores for event-sourced session management in ask-rb. It defines immutable records, event envelopes, a concurrency-safe in-memory store, a durable provider-backed store for restart-safe sessions, a replayable host with subscriptions, and a state reducer.
16
-
17
- ### Core Types
18
-
19
- - **`Record`** immutable snapshot of a session: `id`, `status`, `metadata`, `created_at`, `updated_at`, `version`.
20
- - **`Event`** immutable event envelope: `session_id`, `seq`, `type`, `payload`, `trace_id`, `causation_id`, `created_at`.
21
-
22
- ### Serialization
7
+ Ask::Session provides the foundational value objects and stores for event-sourced
8
+ session management in ask-rb: immutable `Record` and `Event` envelopes, an
9
+ in-memory `Store` with concurrency guardrails, a durable `ProviderStore` for
10
+ restart-safe sessions, a replayable `Host` with subscriptions, a
11
+ `State` reducer, and a `Sink` that records ask-runtime tool lifecycle through
12
+ `Host#sink`. It ships with zero runtime dependencies durability through
13
+ [ask-state-providers](https://github.com/ask-rb/ask-state-providers) is an
14
+ optional integration.
23
15
 
24
- `Record` and `Event` support portable JSON serialization via `to_h`/`from_h` and the `Codec` module:
25
-
26
- ```ruby
27
- # Hash round-trip
28
- record = Ask::Session::Record.create(id: "s1", status: :active, metadata: { key: "val" })
29
- hash = record.to_h # => { id: "s1", status: :active, ..., created_at: <ISO8601>, version: 0 }
30
- restored = Ask::Session::Record.from_h(hash)
31
-
32
- # JSON via Codec
33
- json = Ask::Session::Codec.dump_record(record)
34
- record = Ask::Session::Codec.load_record(json)
35
-
36
- # Events work the same way
37
- event = Ask::Session::Event.create(session_id: "s1", seq: 1, type: "session.created")
38
- json = Ask::Session::Codec.dump_event(event)
39
- event = Ask::Session::Codec.load_event(json)
40
- ```
16
+ Task guides live in [docs/](https://github.com/ask-rb/ask-session/blob/master/docs/index.md).
41
17
 
42
- Malformed JSON or missing required fields raise `Ask::Session::SerializationError`.
43
-
44
- ### Store
18
+ ## Installation
45
19
 
46
- In-memory event store with optimistic concurrency control:
20
+ Requires Ruby 3.2+.
47
21
 
48
22
  ```ruby
49
- store = Ask::Session::Store.new
50
-
51
- # Create a session
52
- store.create(id: "sess_001")
53
-
54
- # Append events with expected sequence
55
- event = Ask::Session::Event.new(
56
- session_id: "sess_001", seq: 1, type: "session.created",
57
- payload: { status: :active }, created_at: Time.now
58
- )
59
- store.append_event(event, expected_sequence: 0)
60
-
61
- # Load session events
62
- events = store.events_after("sess_001", after_seq: 0)
63
-
64
- # Get all events for a session (frozen)
65
- events = store.events("sess_001")
66
-
67
- # Export/import for portability (all sessions)
68
- data = store.export
69
- new_store = Ask::Session::Store.new
70
- new_store.import(data)
71
-
72
- # Export a single session into a fresh store
73
- single = store.export("sess_001")
74
- fresh = Ask::Session::Store.new
75
- fresh.import(single)
76
-
77
- # Rebuild session state from events
78
- record = store.state("sess_001")
79
- record.status # => :active
80
- record.version # => 1
23
+ gem "ask-session"
81
24
  ```
82
25
 
83
- ### ProviderStore (durable)
26
+ ## Quick start
84
27
 
85
- `ProviderStore` is the durable counterpart to `Store`: same API (`create`, `load`, `load!`, `list`, `append_event`, `events`, `events_after`, `current_sequence`, `state`, `export`/`import`), but persisted through any adapter that responds to `get`, `set`, and `delete`. The [ask-state-providers](https://github.com/ask-rb/ask-state-providers) adapters (SQLite, Redis, Postgres, MySQL) work out of the box:
28
+ In-memory, single process:
86
29
 
87
30
  ```ruby
88
- require "ask-state-providers"
31
+ require "ask-session"
89
32
 
90
- adapter = Ask::State::Providers::SQLite.new(path: "sessions.db")
91
- host = Ask::Session::Host.new(store: Ask::Session::ProviderStore.new(adapter: adapter))
33
+ host = Ask::Session::Host.new
92
34
 
93
35
  host.create(id: "s1", metadata: { user: "alice" })
94
36
  host.send_message("s1", content: "hello")
37
+ host.session("s1").version # => 2
95
38
  host.close("s1", reason: "done")
96
- adapter.close
97
-
98
- # Later — same file, new process:
99
- adapter = Ask::State::Providers::SQLite.new(path: "sessions.db")
100
- host = Ask::Session::Host.new(store: Ask::Session::ProviderStore.new(adapter: adapter))
101
- host.session("s1") # => reduced Record with status: :closed, version: 3
102
- host.events("s1") # full history survives the restart
103
- ```
104
-
105
- Each session's record and event list persist under namespaced keys (`ask.session:record:*`, `ask.session:events:*`) alongside a session index, using JSON with symbol-safe encoding so statuses and payload symbols round-trip exactly.
106
-
107
- When the adapter exposes the provider lock API (`acquire_lock` / `release_lock`), every operation also runs under a cross-process store lock so read-modify-write sequences stay atomic across processes; exhausting the lock wait budget raises `Ask::Session::ConcurrencyError`. Adapters without lock methods fall back to an in-process mutex. Stale `expected_sequence` writers still fail with `ConcurrencyError` — locks serialize, they do not merge.
108
-
109
- ask-session keeps zero runtime dependencies: `ProviderStore` duck-types the adapter, so `ask-state-providers` is an optional integration, not a dependency of this gem.
110
-
111
- ### State Reducer
112
-
113
- Rebuild session state from event history:
114
-
115
- ```ruby
116
- events = [
117
- Ask::Session::Event.new(session_id: "s1", seq: 1, type: "session.created",
118
- payload: { status: :active }, created_at: Time.now),
119
- Ask::Session::Event.new(session_id: "s1", seq: 2, type: "message.added",
120
- payload: { role: :user, content: "hello" }, created_at: Time.now)
121
- ]
122
-
123
- record = Ask::Session::State.reduce("s1", events)
124
- record.status # => :active
125
- record.version # => 2
126
- ```
127
-
128
- ### Host
129
-
130
- Replayable session host with publish-subscribe:
131
-
132
- ```ruby
133
- store = Ask::Session::Store.new
134
- host = Ask::Session::Host.new(store: store)
135
-
136
- # Create a session
137
- record = host.create(id: "s1", metadata: { user: "alice" })
138
- record.status # => :active
139
-
140
- # Send messages
141
- event = host.send_message("s1", content: "hello")
142
- event.type # => "message.added"
143
-
144
- # Query
145
- host.session("s1") # => reduced Record
146
- host.list # => [Record, ...]
147
- host.events("s1") # => [Event, ...]
148
-
149
- # Subscribe with replay
150
- sub = host.subscribe("s1")
151
- event = sub.next(timeout: 1.0) # returns event or nil on timeout
152
- sub.each { |e| puts e.type } # yields until closed
153
- sub.close
154
-
155
- # Close or abort
156
- host.close("s1", reason: "done")
157
- host.abort("s1", reason: "error")
158
39
  ```
159
40
 
160
- Invalid transitions (send to closed/aborted, close twice) raise `Ask::Session::InvalidTransitionError`.
161
-
162
- #### Generic event append
41
+ Invalid transitions (sending to a closed session, closing twice) raise
42
+ `Ask::Session::InvalidTransitionError`.
163
43
 
164
- `Host#append` lets adapters record arbitrary event types (tool calls, vendor webhooks, custom lifecycle events) without coupling ask-session to any protocol or agent gem:
44
+ ## Guides
165
45
 
166
- ```ruby
167
- # Append a tool event
168
- event = host.append("s1", type: "tool.started", payload: { tool: "search" })
169
-
170
- # Append with trace correlation
171
- event = host.append("s1",
172
- type: "vendor.webhook.received",
173
- payload: { raw: body },
174
- trace_id: "trace_abc",
175
- causation_id: originating_event.trace_id
176
- )
177
- ```
178
-
179
- Every appended event advances the session's reduced `version` and `updated_at`, regardless of event type. Appends to closed or aborted sessions raise `InvalidTransitionError`. Adapters use `append` instead of `send_message` when the event type is not `message.added` — this keeps ask-session free of protocol and agent dependencies.
180
-
181
- #### Runtime tool-lifecycle sink
182
-
183
- `Ask::Session::Sink` is the boundary to ask-runtime's event-sink contract. Executors in ask-agent, ask-mcp, and ask-sandbox-providers report tool lifecycle through `ExecutionContext#event_sink` by calling `emit(event_type, event:)` — point that sink at a session host and tool history becomes part of the event-sourced session:
184
-
185
- ```ruby
186
- host = Ask::Session::Host.new
187
- host.create(id: "s1")
188
-
189
- sink = host.sink("s1", trace_id: "trace_abc")
190
- # pass `sink` as ExecutionContext's event_sink; runtime emits then record:
191
- # :tool_started -> tool.started
192
- # :tool_completed -> tool.completed
193
- # :tool_failed -> tool.failed
194
- # :tool_cancelled -> tool.cancelled
195
- # :tool_timed_out -> tool.timed_out
196
-
197
- host.events("s1").last.payload
198
- # => { tool_name: "bash", tool_call_id: "tc_1", input: { "cmd" => "ls" }, turn: 3 }
199
- ```
200
-
201
- Terminal events carry `outcome` (`:completed`/`:failed`/`:cancelled`/`:timed_out`), `duration` in seconds, `error` when present, and `output` when the runtime result exposes one. The sink duck-types the runtime events' public readers, so ask-session keeps zero runtime dependencies.
202
-
203
- Guards: an event correlated to a different session raises `Ask::Session::SessionMismatchError`; appends to closed/aborted sessions are dropped (terminal sessions stop recording without failing an in-flight tool run); missing sessions raise `NotFoundError`; unknown event types are ignored.
204
-
205
- ```ruby
206
- sink = Ask::Session::Sink.new(host: host, session_id: "s1", causation_id: originating.trace_id)
207
- sink.listening?(:tool_started) # => true
208
- ```
46
+ - [Guides index](https://github.com/ask-rb/ask-session/blob/master/docs/index.md)
47
+ - [In-memory sessions](https://github.com/ask-rb/ask-session/blob/master/docs/in_memory_sessions.md)
48
+ - [Durable sessions with SQLite (optional ask-state-providers)](https://github.com/ask-rb/ask-session/blob/master/docs/durable_sessions.md)
49
+ - [Subscriptions and replay](https://github.com/ask-rb/ask-session/blob/master/docs/subscriptions_and_replay.md)
50
+ - [Tool lifecycle through Host#sink](https://github.com/ask-rb/ask-session/blob/master/docs/tool_lifecycle.md)
51
+ - [Export and import](https://github.com/ask-rb/ask-session/blob/master/docs/export_import.md)
209
52
 
210
53
  ## Contributing
211
54
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Session
5
- VERSION = "0.1.1"
5
+ VERSION = "0.1.2"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-session
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto