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 +4 -4
- data/CHANGELOG.md +25 -0
- data/README.md +6 -0
- data/docs/ask-session-protocol.schema.json +1 -2
- data/lib/ask/session_protocol/events.rb +57 -2
- data/lib/ask/session_protocol/interactions.rb +3 -1
- data/lib/ask/session_protocol/methods.rb +1 -1
- data/lib/ask/session_protocol/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a04df0359177ab5a906a801cf56ea27438cf21eded5616221d01def2fffa68e1
|
|
4
|
+
data.tar.gz: 3d7b35424187f6662fecc811ea31cd6bcdbc58b8635646a60bbeaab2de77fbe1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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:
|
|
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: "
|
|
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" => {
|