agent2agent 1.0.8 → 1.1.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/lib/a2a/agent.rb +165 -117
  3. data/lib/a2a/client.rb +470 -51
  4. data/lib/a2a/errors/json_rpc_error.rb +71 -0
  5. data/lib/a2a/errors/rest_error.rb +68 -0
  6. data/lib/a2a/errors.rb +535 -0
  7. data/lib/a2a/faraday/middleware/json_rpc/request.rb +96 -0
  8. data/lib/a2a/faraday/middleware/json_rpc/response.rb +131 -0
  9. data/lib/a2a/faraday/middleware/rest/request.rb +166 -0
  10. data/lib/a2a/faraday/middleware/rest/response.rb +144 -0
  11. data/lib/a2a/faraday/middleware/schema_request.rb +69 -0
  12. data/lib/a2a/middleware/extract_message.rb +120 -0
  13. data/lib/a2a/middleware/fetch_task.rb +228 -0
  14. data/lib/a2a/middleware/limit_history_length.rb +123 -0
  15. data/lib/a2a/middleware/limit_pagination_size.rb +133 -0
  16. data/lib/a2a/middleware/sse_stream.rb +235 -0
  17. data/lib/a2a/middleware.rb +7 -0
  18. data/lib/a2a/schema/definition.rb +35 -1
  19. data/lib/a2a/schema.rb +126 -0
  20. data/lib/a2a/{bindings → server/bindings}/json_rpc.rb +12 -8
  21. data/lib/a2a/{bindings → server/bindings}/rest.rb +12 -8
  22. data/lib/a2a/server/dispatcher.rb +52 -54
  23. data/lib/a2a/server/env.rb +4 -6
  24. data/lib/a2a/server/triage.rb +1 -1
  25. data/lib/a2a/server.rb +10 -10
  26. data/lib/a2a/sse/event_parser.rb +202 -0
  27. data/lib/a2a/sse/json_rpc_stream.rb +27 -5
  28. data/lib/a2a/sse/rest_stream.rb +17 -5
  29. data/lib/a2a/sse/stream.rb +135 -7
  30. data/lib/a2a/sse.rb +1 -0
  31. data/lib/a2a/test_helpers.rb +89 -0
  32. data/lib/a2a/version.rb +1 -1
  33. data/lib/a2a.rb +6 -2
  34. data/lib/traces/provider/a2a/{bindings → server/bindings}/json_rpc.rb +2 -2
  35. data/lib/traces/provider/a2a/{bindings → server/bindings}/rest.rb +2 -2
  36. data/lib/traces/provider/a2a.rb +2 -2
  37. metadata +49 -22
  38. data/lib/a2a/server/cancel_task.rb +0 -14
  39. data/lib/a2a/server/create_task_push_notification_config.rb +0 -14
  40. data/lib/a2a/server/delete_task_push_notification_config.rb +0 -14
  41. data/lib/a2a/server/get_extended_agent_card.rb +0 -15
  42. data/lib/a2a/server/get_task.rb +0 -14
  43. data/lib/a2a/server/get_task_push_notification_config.rb +0 -14
  44. data/lib/a2a/server/list_task_push_notification_configs.rb +0 -14
  45. data/lib/a2a/server/list_tasks.rb +0 -14
  46. data/lib/a2a/server/send_message.rb +0 -14
  47. data/lib/a2a/server/send_streaming_message.rb +0 -14
  48. data/lib/a2a/server/subscribe_to_task.rb +0 -14
  49. data/lib/a2a/store/processor.rb +0 -136
  50. data/lib/a2a/store/pub_sub.rb +0 -149
  51. data/lib/a2a/store/sqlite.rb +0 -533
  52. data/lib/a2a/store/webhooks.rb +0 -94
  53. data/lib/a2a/store.rb +0 -6
  54. data/lib/a2a/task_store.rb +0 -315
@@ -1,533 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "sqlite3"
4
- require "json"
5
- require "securerandom"
6
- require "console"
7
-
8
- require_relative "pub_sub"
9
- require_relative "webhooks"
10
-
11
- module A2A
12
- module Store
13
- # SQLite-backed task store with async pub/sub and webhook delivery.
14
- #
15
- # This is the enlightened replacement for the in-memory TaskStore.
16
- # Follows the gospel:
17
- # - async-job's schema patterns (indexed by state, updated_at DESC)
18
- # - async-job's duck-typed delegate protocol (#call, #start, #stop)
19
- # - Async::Queue-based pub/sub (fiber-safe, no threads)
20
- # - Async::HTTP::Internet for webhook delivery (no Net::HTTP)
21
- #
22
- # The store composes three concerns:
23
- # 1. SQLite — persistent CRUD for tasks, push configs
24
- # 2. PubSub — in-process streaming subscriptions
25
- # 3. Webhooks — push notification delivery
26
- #
27
- # SQLite is safe for fiber-based concurrency within a single process
28
- # because Ruby fibers are cooperatively scheduled — only one fiber
29
- # runs at a time, so no concurrent writes can collide.
30
- #
31
- class SQLite
32
- TERMINAL_STATES = %w[
33
- TASK_STATE_COMPLETED TASK_STATE_FAILED
34
- TASK_STATE_CANCELED TASK_STATE_REJECTED
35
- ].freeze
36
-
37
- attr_reader :pub_sub, :webhooks
38
-
39
- # @param path [String] path to SQLite database file (":memory:" for in-memory)
40
- #
41
- def initialize(path: ":memory:")
42
- @db = ::SQLite3::Database.new(path)
43
- @db.results_as_hash = true
44
- @db.execute("PRAGMA journal_mode = WAL")
45
- @db.execute("PRAGMA synchronous = NORMAL")
46
- @db.execute("PRAGMA foreign_keys = ON")
47
-
48
- @pub_sub = PubSub.new
49
- @webhooks = Webhooks.new
50
-
51
- create_tables
52
- end
53
-
54
- # ── Task CRUD ──────────────────────────────────────────────────────
55
-
56
- def create(id, context_id, push_config = nil)
57
- now = now_ts
58
-
59
- @db.execute(<<~SQL, [id, context_id, "TASK_STATE_SUBMITTED", "[]", "[]", now, now])
60
- INSERT INTO tasks (id, context_id, state, artifacts, history, created_at, updated_at)
61
- VALUES (?, ?, ?, ?, ?, ?, ?)
62
- SQL
63
-
64
- if push_config
65
- cfg_id = push_config["id"] || SecureRandom.uuid
66
- push_config = push_config.merge("id" => cfg_id, "taskId" => id)
67
- insert_push_config(id, push_config)
68
- end
69
-
70
- get(id)
71
- end
72
-
73
- def get(id)
74
- row = @db.get_first_row("SELECT * FROM tasks WHERE id = ?", [id])
75
- return nil unless row
76
- row_to_task(row)
77
- end
78
-
79
- def update_state(id, state, message: nil)
80
- now = now_ts
81
- @db.execute("UPDATE tasks SET state = ?, updated_at = ? WHERE id = ?", [state, now, id])
82
-
83
- task = get(id)
84
- return nil unless task
85
-
86
- event = build_status_event(task, message)
87
- @pub_sub.notify(id, { type: :status, data: event })
88
- @webhooks.deliver(list_push_configs(id), { "statusUpdate" => event })
89
-
90
- if TERMINAL_STATES.include?(state)
91
- @pub_sub.close(id)
92
- end
93
-
94
- task
95
- end
96
-
97
- def complete(id, result)
98
- now = now_ts
99
- result_json = result ? JSON.generate(result) : nil
100
- @db.execute(
101
- "UPDATE tasks SET state = ?, result = ?, updated_at = ? WHERE id = ?",
102
- ["TASK_STATE_COMPLETED", result_json, now, id]
103
- )
104
-
105
- task = get(id)
106
- return nil unless task
107
-
108
- event = build_status_event(task)
109
- @pub_sub.notify(id, { type: :status, data: event })
110
- @webhooks.deliver(list_push_configs(id), { "statusUpdate" => event })
111
- @pub_sub.close(id)
112
-
113
- task
114
- end
115
-
116
- def fail(id, msg)
117
- now = now_ts
118
- result_json = msg ? JSON.generate(msg) : nil
119
- @db.execute(
120
- "UPDATE tasks SET state = ?, result = ?, updated_at = ? WHERE id = ?",
121
- ["TASK_STATE_FAILED", result_json, now, id]
122
- )
123
-
124
- task = get(id)
125
- return nil unless task
126
-
127
- event = build_status_event(task)
128
- @pub_sub.notify(id, { type: :status, data: event })
129
- @webhooks.deliver(list_push_configs(id), { "statusUpdate" => event })
130
- @pub_sub.close(id)
131
-
132
- task
133
- end
134
-
135
- def cancel(id)
136
- update_state(id, "TASK_STATE_CANCELED")
137
- end
138
-
139
- def terminal?(id)
140
- task = get(id)
141
- task && TERMINAL_STATES.include?(task[:state])
142
- end
143
-
144
- # ── Artifacts ──────────────────────────────────────────────────────
145
-
146
- def add_artifact(id, artifact)
147
- task = get(id)
148
- return nil unless task
149
-
150
- artifacts = task[:artifacts]
151
- artifacts << artifact
152
- now = now_ts
153
-
154
- @db.execute(
155
- "UPDATE tasks SET artifacts = ?, updated_at = ? WHERE id = ?",
156
- [JSON.generate(artifacts), now, id]
157
- )
158
-
159
- event = {
160
- "taskId" => id,
161
- "contextId" => task[:context_id],
162
- "artifact" => artifact,
163
- "append" => false,
164
- "lastChunk" => true,
165
- }
166
- @pub_sub.notify(id, { type: :artifact, data: event })
167
- @webhooks.deliver(list_push_configs(id), { "artifactUpdate" => event })
168
-
169
- get(id)
170
- end
171
-
172
- # ── History ────────────────────────────────────────────────────────
173
-
174
- def add_message(id, msg)
175
- task = get(id)
176
- return nil unless task
177
-
178
- history = task[:history]
179
- history << msg
180
-
181
- @db.execute(
182
- "UPDATE tasks SET history = ?, updated_at = ? WHERE id = ?",
183
- [JSON.generate(history), now_ts, id]
184
- )
185
-
186
- get(id)
187
- end
188
-
189
- # ── Listing ────────────────────────────────────────────────────────
190
-
191
- def list(context_id: nil, state: nil)
192
- conditions = []
193
- params = []
194
-
195
- if context_id
196
- conditions << "context_id = ?"
197
- params << context_id
198
- end
199
-
200
- if state
201
- conditions << "state = ?"
202
- params << state
203
- end
204
-
205
- where = conditions.empty? ? "" : "WHERE #{conditions.join(" AND ")}"
206
- sql = "SELECT * FROM tasks #{where} ORDER BY updated_at DESC"
207
-
208
- @db.execute(sql, params).map { |row| row_to_task(row) }
209
- end
210
-
211
- # ── Push Notification Config CRUD ──────────────────────────────────
212
-
213
- def create_push_config(task_id, config)
214
- task = get(task_id)
215
- return nil unless task
216
-
217
- cfg_id = config["id"] || SecureRandom.uuid
218
- config = config.merge("id" => cfg_id, "taskId" => task_id)
219
- insert_push_config(task_id, config)
220
- config
221
- end
222
-
223
- def get_push_config(task_id, config_id)
224
- row = @db.get_first_row(
225
- "SELECT * FROM push_configs WHERE task_id = ? AND id = ?",
226
- [task_id, config_id]
227
- )
228
- return nil unless row
229
- row_to_push_config(row)
230
- end
231
-
232
- def list_push_configs(task_id)
233
- @db.execute(
234
- "SELECT * FROM push_configs WHERE task_id = ? ORDER BY created_at",
235
- [task_id]
236
- ).map { |row| row_to_push_config(row) }
237
- end
238
-
239
- def delete_push_config(task_id, config_id)
240
- @db.execute(
241
- "DELETE FROM push_configs WHERE task_id = ? AND id = ?",
242
- [task_id, config_id]
243
- )
244
- end
245
-
246
- # ── Streaming / Pub-Sub ────────────────────────────────────────────
247
-
248
- def subscribe(task_id)
249
- task = get(task_id)
250
- return nil unless task
251
- @pub_sub.subscribe(task_id)
252
- end
253
-
254
- def unsubscribe(task_id, queue)
255
- @pub_sub.unsubscribe(task_id, queue)
256
- end
257
-
258
- # ── Lifecycle (async-job duck-type) ────────────────────────────────
259
-
260
- def start
261
- # No-op for now. Could start background cleanup tasks.
262
- end
263
-
264
- def stop
265
- @db.close if @db
266
- end
267
-
268
- private
269
-
270
- def create_tables
271
- @db.execute_batch(<<~SQL)
272
- CREATE TABLE IF NOT EXISTS tasks (
273
- id TEXT PRIMARY KEY,
274
- context_id TEXT NOT NULL,
275
- state TEXT NOT NULL DEFAULT 'TASK_STATE_SUBMITTED',
276
- result TEXT,
277
- artifacts TEXT NOT NULL DEFAULT '[]',
278
- history TEXT NOT NULL DEFAULT '[]',
279
- created_at TEXT NOT NULL,
280
- updated_at TEXT NOT NULL
281
- );
282
-
283
- CREATE INDEX IF NOT EXISTS idx_tasks_context
284
- ON tasks(context_id);
285
- CREATE INDEX IF NOT EXISTS idx_tasks_state
286
- ON tasks(state);
287
- CREATE INDEX IF NOT EXISTS idx_tasks_updated
288
- ON tasks(updated_at DESC);
289
-
290
- CREATE TABLE IF NOT EXISTS push_configs (
291
- id TEXT PRIMARY KEY,
292
- task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
293
- url TEXT NOT NULL,
294
- token TEXT,
295
- auth_scheme TEXT,
296
- auth_credentials TEXT,
297
- created_at TEXT NOT NULL
298
- );
299
-
300
- CREATE INDEX IF NOT EXISTS idx_push_configs_task
301
- ON push_configs(task_id);
302
- SQL
303
- end
304
-
305
- def now_ts
306
- Time.now.utc.strftime("%Y-%m-%dT%H:%M:%S.%3NZ")
307
- end
308
-
309
- def row_to_task(row)
310
- {
311
- id: row["id"],
312
- context_id: row["context_id"],
313
- state: row["state"],
314
- result: row["result"] ? JSON.parse(row["result"]) : nil,
315
- artifacts: JSON.parse(row["artifacts"] || "[]"),
316
- history: JSON.parse(row["history"] || "[]"),
317
- created_at: row["created_at"],
318
- updated_at: row["updated_at"],
319
- }
320
- end
321
-
322
- def row_to_push_config(row)
323
- config = {
324
- "id" => row["id"],
325
- "taskId" => row["task_id"],
326
- "url" => row["url"],
327
- }
328
- config["token"] = row["token"] if row["token"]
329
- if row["auth_scheme"]
330
- config["authentication"] = {
331
- "scheme" => row["auth_scheme"],
332
- "credentials" => row["auth_credentials"],
333
- }
334
- end
335
- config
336
- end
337
-
338
- def insert_push_config(task_id, config)
339
- auth = config["authentication"] || {}
340
- params = [
341
- config["id"], task_id, config["url"],
342
- config["token"],
343
- auth["scheme"], auth["credentials"],
344
- now_ts
345
- ]
346
- @db.execute(<<~SQL, params)
347
- INSERT INTO push_configs (id, task_id, url, token, auth_scheme, auth_credentials, created_at)
348
- VALUES (?, ?, ?, ?, ?, ?, ?)
349
- SQL
350
- end
351
-
352
- def build_status_event(task, message = nil)
353
- event = {
354
- "taskId" => task[:id],
355
- "contextId" => task[:context_id],
356
- "status" => {
357
- "state" => task[:state],
358
- "timestamp" => task[:updated_at],
359
- },
360
- }
361
- event["status"]["message"] = message if message
362
- event
363
- end
364
- end
365
- end
366
- end
367
-
368
- test do
369
- describe "A2A::Store::SQLite" do
370
- it "creates and retrieves tasks" do
371
- store = A2A::Store::SQLite.new
372
- task = store.create("t1", "ctx1")
373
-
374
- task[:id].should == "t1"
375
- task[:context_id].should == "ctx1"
376
- task[:state].should == "TASK_STATE_SUBMITTED"
377
- task[:artifacts].should == []
378
- task[:history].should == []
379
-
380
- fetched = store.get("t1")
381
- fetched[:id].should == "t1"
382
- end
383
-
384
- it "returns nil for missing tasks" do
385
- store = A2A::Store::SQLite.new
386
- store.get("nonexistent").should.be.nil
387
- end
388
-
389
- it "updates task state" do
390
- store = A2A::Store::SQLite.new
391
- store.create("t1", "ctx1")
392
-
393
- store.update_state("t1", "TASK_STATE_WORKING")
394
- task = store.get("t1")
395
- task[:state].should == "TASK_STATE_WORKING"
396
- end
397
-
398
- it "completes tasks" do
399
- store = A2A::Store::SQLite.new
400
- store.create("t1", "ctx1")
401
-
402
- store.complete("t1", { "answer" => 42 })
403
- task = store.get("t1")
404
- task[:state].should == "TASK_STATE_COMPLETED"
405
- task[:result].should == { "answer" => 42 }
406
- end
407
-
408
- it "fails tasks" do
409
- store = A2A::Store::SQLite.new
410
- store.create("t1", "ctx1")
411
-
412
- store.fail("t1", "something broke")
413
- task = store.get("t1")
414
- task[:state].should == "TASK_STATE_FAILED"
415
- end
416
-
417
- it "cancels tasks" do
418
- store = A2A::Store::SQLite.new
419
- store.create("t1", "ctx1")
420
-
421
- store.cancel("t1")
422
- task = store.get("t1")
423
- task[:state].should == "TASK_STATE_CANCELED"
424
- end
425
-
426
- it "detects terminal states" do
427
- store = A2A::Store::SQLite.new
428
- store.create("t1", "ctx1")
429
-
430
- store.terminal?("t1").should == false
431
- store.complete("t1", nil)
432
- store.terminal?("t1").should == true
433
- end
434
-
435
- it "adds artifacts" do
436
- store = A2A::Store::SQLite.new
437
- store.create("t1", "ctx1")
438
-
439
- artifact = { "artifactId" => "a1", "parts" => [{ "text" => "hello" }] }
440
- store.add_artifact("t1", artifact)
441
-
442
- task = store.get("t1")
443
- task[:artifacts].length.should == 1
444
- task[:artifacts].first["artifactId"].should == "a1"
445
- end
446
-
447
- it "adds messages to history" do
448
- store = A2A::Store::SQLite.new
449
- store.create("t1", "ctx1")
450
-
451
- msg = { "messageId" => "m1", "role" => "ROLE_USER", "parts" => [{ "text" => "hi" }] }
452
- store.add_message("t1", msg)
453
-
454
- task = store.get("t1")
455
- task[:history].length.should == 1
456
- task[:history].first["messageId"].should == "m1"
457
- end
458
-
459
- it "lists tasks with filtering" do
460
- store = A2A::Store::SQLite.new
461
- store.create("t1", "ctx1")
462
- store.create("t2", "ctx1")
463
- store.create("t3", "ctx2")
464
- store.complete("t2", nil)
465
-
466
- # All tasks
467
- store.list.length.should == 3
468
-
469
- # By context
470
- store.list(context_id: "ctx1").length.should == 2
471
-
472
- # By state
473
- store.list(state: "TASK_STATE_COMPLETED").length.should == 1
474
- store.list(state: "TASK_STATE_COMPLETED").first[:id].should == "t2"
475
-
476
- # Both filters
477
- store.list(context_id: "ctx1", state: "TASK_STATE_SUBMITTED").length.should == 1
478
- end
479
-
480
- it "manages push notification configs" do
481
- store = A2A::Store::SQLite.new
482
- store.create("t1", "ctx1")
483
-
484
- config = store.create_push_config("t1", {
485
- "url" => "https://example.com/hook",
486
- "token" => "secret",
487
- "authentication" => { "scheme" => "Bearer", "credentials" => "abc123" },
488
- })
489
-
490
- config["url"].should == "https://example.com/hook"
491
- config["token"].should == "secret"
492
- config["id"].should.not.be.nil
493
-
494
- # Get
495
- fetched = store.get_push_config("t1", config["id"])
496
- fetched["url"].should == "https://example.com/hook"
497
- fetched["authentication"]["scheme"].should == "Bearer"
498
-
499
- # List
500
- store.list_push_configs("t1").length.should == 1
501
-
502
- # Delete
503
- store.delete_push_config("t1", config["id"])
504
- store.list_push_configs("t1").length.should == 0
505
- end
506
-
507
- it "creates tasks with inline push config" do
508
- store = A2A::Store::SQLite.new
509
- store.create("t1", "ctx1", {
510
- "url" => "https://example.com/hook",
511
- "token" => "tok",
512
- })
513
-
514
- configs = store.list_push_configs("t1")
515
- configs.length.should == 1
516
- configs.first["url"].should == "https://example.com/hook"
517
- end
518
-
519
- it "provides pub/sub subscriptions" do
520
- store = A2A::Store::SQLite.new
521
- store.create("t1", "ctx1")
522
-
523
- queue = store.subscribe("t1")
524
- queue.should.not.be.nil
525
- queue.is_a?(Async::Queue).should == true
526
- end
527
-
528
- it "returns nil subscribing to nonexistent task" do
529
- store = A2A::Store::SQLite.new
530
- store.subscribe("nonexistent").should.be.nil
531
- end
532
- end
533
- end
@@ -1,94 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "async"
4
- require "async/http/internet"
5
- require "json"
6
- require "console"
7
-
8
- module A2A
9
- module Store
10
- # Async webhook delivery for A2A push notifications.
11
- #
12
- # Following the gospel (async-http):
13
- # - Uses Async::HTTP::Internet for non-blocking HTTP requests
14
- # - Each delivery runs in its own Async task (fiber, not thread)
15
- # - Failures are logged but do not propagate
16
- #
17
- # The A2A spec says:
18
- # - Push notification payloads use StreamResponse format
19
- # - Content-Type: application/a2a+json
20
- # - Authentication via scheme + credentials from the config
21
- # - Token header: X-A2A-Notification-Token
22
- # - Delivery is at-least-once with possible retries
23
- #
24
- class Webhooks
25
- def initialize
26
- @internet = nil
27
- end
28
-
29
- # Deliver a payload to all push notification configs for a task.
30
- #
31
- # @parameter configs [Array(Hash)] push notification configs
32
- # @parameter payload [Hash] the StreamResponse payload
33
- #
34
- def deliver(configs, payload)
35
- return if configs.nil? || configs.empty?
36
-
37
- configs.each do |config|
38
- Async do
39
- deliver_single(config, payload)
40
- rescue => e
41
- Console.error(self, "Webhook delivery failed for #{config["url"]}", e)
42
- end
43
- end
44
- end
45
-
46
- private
47
-
48
- def internet
49
- @internet ||= Async::HTTP::Internet.new
50
- end
51
-
52
- def deliver_single(config, payload)
53
- url = config["url"]
54
- return unless url && !url.empty?
55
-
56
- headers = {
57
- "content-type" => "application/a2a+json",
58
- }
59
-
60
- # Authentication
61
- if (auth = config["authentication"])
62
- scheme = auth["scheme"] || "Bearer"
63
- creds = auth["credentials"] || ""
64
- headers["authorization"] = "#{scheme} #{creds}"
65
- end
66
-
67
- # Token header
68
- if (token = config["token"])
69
- headers["x-a2a-notification-token"] = token
70
- end
71
-
72
- body = JSON.generate(payload)
73
-
74
- internet.post(url, headers, [body])
75
- end
76
- end
77
- end
78
- end
79
-
80
- test do
81
- describe "A2A::Store::Webhooks" do
82
- it "can be instantiated" do
83
- wh = A2A::Store::Webhooks.new
84
- wh.should.not.be.nil
85
- end
86
-
87
- it "silently skips empty config lists" do
88
- wh = A2A::Store::Webhooks.new
89
- # Should not raise — returns nil for empty/nil configs
90
- wh.deliver([], { "statusUpdate" => {} }).should.be.nil
91
- wh.deliver(nil, { "statusUpdate" => {} }).should.be.nil
92
- end
93
- end
94
- end
data/lib/a2a/store.rb DELETED
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "store/pub_sub"
4
- require_relative "store/webhooks"
5
- require_relative "store/sqlite"
6
- require_relative "store/processor"