ask-session-protocol 0.2.0 → 0.2.1

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: 22436fe459ec2c7e91ed8d5c12db05a72d01daaa83794536882d094b084b9b08
4
- data.tar.gz: d512c1dea2ebdfbce2ba971d00d4430431b020ee79911fc26585d3ff2aefe0e9
3
+ metadata.gz: a04df0359177ab5a906a801cf56ea27438cf21eded5616221d01def2fffa68e1
4
+ data.tar.gz: 3d7b35424187f6662fecc811ea31cd6bcdbc58b8635646a60bbeaab2de77fbe1
5
5
  SHA512:
6
- metadata.gz: a4f4a7094797aa9acb3293ef0ab96b69dcdc238b93ed378f32090327605931780cfb3a7c96bdca45d4466fe6b2ba3d90214be360eb03204aa4480b3b93b85eca
7
- data.tar.gz: 60bfed82e8fd6a6c852e04afa5c0682c0bb2a4a7fd5e790a1d5e51bb204f6f9343e2d9f9de47f6717e4ede694e6410b559c1ebc0e640e9238393aecd3c33aa7a
6
+ metadata.gz: 30ea7ada3a75105590418634f716a459e88e2b6fdf937bf41e87d4885aa23734211cec9b25d83a6b953fdffc8acbb71566c5316355ee4ed5a38e373ee2bb8811
7
+ data.tar.gz: aa66d8826cb8c0017f0433feeb6c8ece2826872b6815a41087813c67ac4223cc29540c4ba03de5bbfb1a891707ece4c6ecaac8548e252d4e2c55f3d68c45f225
data/CHANGELOG.md CHANGED
@@ -5,6 +5,31 @@ the keep-a-changelog format.
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ### Changed
9
+
10
+ - **`turn.failed` payload** — `turnId` is now optional: the host only
11
+ knows a turn id once `turn.started` has fired, so a run that dies
12
+ before the turn starts still validates against the registry (matching
13
+ the host's emitter).
14
+
15
+ ### Fixed
16
+
17
+ - **Payload key handling** — Symbol payload keys are normalized to the
18
+ wire's String keys at the boundary (events and interactions), so
19
+ in-process emitters no longer fail required-field checks or serialize
20
+ ambiguous duplicate keys. Symbol/String collisions and non-String,
21
+ non-Symbol keys raise `ArgumentError`.
22
+ - **`from_h` input checks** — `Events.from_h` and `Interactions.from_h`
23
+ reject non-Hash input with `ArgumentError` before normalizing keys
24
+ (previously `NoMethodError`).
25
+
26
+ ### Added
27
+
28
+ - **`Events.filter_snapshot`** — consumes a replayed snapshot
29
+ (`session/subscribe`'s `snapshot`, `session/events`' `events`):
30
+ canonical entries rebuild as Events, host-internal or unknown entries
31
+ are filtered out.
32
+
8
33
  ## [0.2.0] - 2026-08-12
9
34
 
10
35
  ### Added
data/README.md CHANGED
@@ -112,6 +112,12 @@ Ask::SessionProtocol::Methods.validate_params!("session/send", { "sessionId" =>
112
112
  Unknown payload fields are allowed (forward compatibility); missing required
113
113
  fields, wrong types, and out-of-enum values raise `ArgumentError`.
114
114
 
115
+ Payloads are string-keyed on the wire. Ruby callers may pass Symbol keys —
116
+ they are normalized to Strings at the boundary, and a Symbol/String
117
+ collision on the same name raises. `Events.filter_snapshot` consumes a
118
+ replayed snapshot (`session/subscribe`, `session/events`): canonical
119
+ entries become Events, host-internal or unknown entries are filtered out.
120
+
115
121
  ## JSON Schema
116
122
 
117
123
  `docs/ask-session-protocol.schema.json` (draft 2020-12) is generated from the
@@ -164,7 +164,7 @@
164
164
  "type": "object",
165
165
  "properties": {
166
166
  "turnId": {
167
- "description": "The turn identifier.",
167
+ "description": "The turn identifier, when the failure is attributed to a started turn.",
168
168
  "type": "string"
169
169
  },
170
170
  "error": {
@@ -173,7 +173,6 @@
173
173
  }
174
174
  },
175
175
  "required": [
176
- "turnId",
177
176
  "error"
178
177
  ]
179
178
  }
@@ -67,7 +67,7 @@ module Ask
67
67
  description: "A turn failed; the session remains usable.",
68
68
  interaction: false,
69
69
  payload: {
70
- "turnId" => { type: :string, required: true, description: "The turn identifier." },
70
+ "turnId" => { type: :string, required: false, description: "The turn identifier, when the failure is attributed to a started turn." },
71
71
  "error" => { type: :string, required: true, description: "Human-readable failure reason." }
72
72
  }
73
73
  },
@@ -232,6 +232,7 @@ module Ask
232
232
  # @param payload [Hash] event body; validated against the registry
233
233
  # @return [Event]
234
234
  def self.event(type:, seq:, payload: {})
235
+ payload = normalize_payload_keys(payload, "event #{type} payload")
235
236
  validate_payload!(type, payload)
236
237
  Event.new(type: type, seq: seq, payload: payload)
237
238
  end
@@ -241,8 +242,9 @@ module Ask
241
242
  # @param hash [Hash] { "type" =>, "seq" =>, "payload" => }
242
243
  # @return [Event]
243
244
  def self.from_h(hash)
244
- hash = hash.transform_keys(&:to_s)
245
245
  raise ArgumentError, "event must be a Hash" unless hash.is_a?(Hash)
246
+
247
+ hash = hash.transform_keys(&:to_s)
246
248
  raise ArgumentError, "event missing type" if hash["type"].nil?
247
249
  raise ArgumentError, "event missing seq" if hash["seq"].nil?
248
250
 
@@ -250,6 +252,32 @@ module Ask
250
252
  event(type: hash["type"], seq: hash["seq"], payload: payload)
251
253
  end
252
254
 
255
+ # Rebuild the events of a replayed snapshot (the `snapshot` result of
256
+ # session/subscribe, the `events` result of session/events).
257
+ #
258
+ # Snapshots carry canonical events only: the host filters its
259
+ # internal detail out of the log before replaying. Entries that are
260
+ # not canonical — host-internal rows from an older host, foreign
261
+ # records, non-Hash junk — are dropped rather than raised, matching
262
+ # the client rule that unknown types are tolerated within a major
263
+ # protocol version. Entries whose type IS canonical must be
264
+ # well-formed; a malformed canonical entry raises.
265
+ #
266
+ # @param entries [Array<Event, Hash>] the raw snapshot
267
+ # @return [Array<Event>] canonical events, in order
268
+ def self.filter_snapshot(entries)
269
+ raise ArgumentError, "snapshot must be an Array, got: #{entries.class}" unless entries.is_a?(Array)
270
+
271
+ entries.filter_map do |entry|
272
+ case entry
273
+ when Event then entry
274
+ when Hash
275
+ type = entry[:type] || entry["type"]
276
+ from_h(entry) if known?(type)
277
+ end
278
+ end
279
+ end
280
+
253
281
  # Whether `type` is a canonical event type.
254
282
  def self.known?(type)
255
283
  TYPES.key?(type)
@@ -279,6 +307,7 @@ module Ask
279
307
  raise ArgumentError, "unknown session event type: #{type.inspect}" unless spec
280
308
  raise ArgumentError, "payload for #{type} must be a Hash" unless payload.is_a?(Hash)
281
309
 
310
+ payload = normalize_payload_keys(payload, "event #{type} payload")
282
311
  spec[:payload].each do |field, field_spec|
283
312
  value = payload[field]
284
313
  if field_spec[:required] && value.nil?
@@ -291,6 +320,32 @@ module Ask
291
320
  true
292
321
  end
293
322
 
323
+ # Normalize a payload's keys to the wire form (String). Ruby callers
324
+ # build payloads with Symbol keys all the time; JSON would stringify
325
+ # them on the way out anyway, so the contract accepts both at the
326
+ # boundary and stores String keys. Keys that cannot round-trip — a
327
+ # Symbol/String collision on the same name, or a key that is neither
328
+ # — raise, because serializing those would silently drop or
329
+ # duplicate data on the wire.
330
+ #
331
+ # @api private
332
+ def self.normalize_payload_keys(payload, label)
333
+ return payload unless payload.is_a?(Hash)
334
+ return payload if payload.keys.all?(String)
335
+
336
+ payload.each_with_object({}) do |(key, value), out|
337
+ unless key.is_a?(String) || key.is_a?(Symbol)
338
+ raise ArgumentError, "#{label} keys must be Strings or Symbols, got #{key.inspect}"
339
+ end
340
+
341
+ name = key.to_s
342
+ if out.key?(name)
343
+ raise ArgumentError, "#{label} has duplicate key #{name.inspect} after normalizing Symbols to Strings"
344
+ end
345
+ out[name] = value
346
+ end
347
+ end
348
+
294
349
  # Shared field validator, used by the events, interactions, and
295
350
  # methods registries. Public because sibling modules call it with an
296
351
  # explicit receiver.
@@ -92,6 +92,7 @@ module Ask
92
92
  # @param payload [Hash] kind-specific body
93
93
  # @return [Interaction]
94
94
  def self.interaction(id:, kind:, status: "pending", payload: {})
95
+ payload = Events.normalize_payload_keys(payload, "interaction #{kind} payload")
95
96
  validate_payload!(kind, payload)
96
97
  Interaction.new(id: id, kind: kind, status: status, payload: payload)
97
98
  end
@@ -101,9 +102,9 @@ module Ask
101
102
  # @param hash [Hash] { "id" =>, "kind" =>, "status" =>, "payload" => }
102
103
  # @return [Interaction]
103
104
  def self.from_h(hash)
104
- hash = hash.transform_keys(&:to_s)
105
105
  raise ArgumentError, "interaction must be a Hash" unless hash.is_a?(Hash)
106
106
 
107
+ hash = hash.transform_keys(&:to_s)
107
108
  interaction(
108
109
  id: hash["id"],
109
110
  kind: hash["kind"],
@@ -123,6 +124,7 @@ module Ask
123
124
  raise ArgumentError, "unknown interaction kind: #{kind.inspect}" unless spec
124
125
  raise ArgumentError, "payload for #{kind} must be a Hash" unless payload.is_a?(Hash)
125
126
 
127
+ payload = Events.normalize_payload_keys(payload, "interaction #{kind} payload")
126
128
  spec.each do |field, field_spec|
127
129
  value = payload[field]
128
130
  if field_spec[:required] && value.nil?
@@ -116,7 +116,7 @@ module Ask
116
116
  },
117
117
  result: {
118
118
  "subscription" => { type: :object, required: true, description: "{sessionId, deliveryKind}." },
119
- "snapshot" => { type: :array, required: false, description: "Events after afterSeq, when includeSnapshot." }
119
+ "snapshot" => { type: :array, required: false, description: "Canonical event envelopes after afterSeq (host-internal events are filtered out), when includeSnapshot." }
120
120
  }
121
121
  },
122
122
  "session/events" => {
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module SessionProtocol
5
- VERSION = "0.2.0"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-session-protocol
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto