simple_acp 0.0.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.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/CHANGELOG.md +5 -0
  4. data/COMMITS.md +196 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +385 -0
  7. data/Rakefile +13 -0
  8. data/docs/api/client-base.md +383 -0
  9. data/docs/api/index.md +159 -0
  10. data/docs/api/models.md +286 -0
  11. data/docs/api/server-base.md +379 -0
  12. data/docs/api/storage.md +347 -0
  13. data/docs/assets/images/simple_acp.jpg +0 -0
  14. data/docs/client/index.md +279 -0
  15. data/docs/client/sessions.md +324 -0
  16. data/docs/client/streaming.md +345 -0
  17. data/docs/client/sync-async.md +308 -0
  18. data/docs/core-concepts/agents.md +253 -0
  19. data/docs/core-concepts/events.md +337 -0
  20. data/docs/core-concepts/index.md +147 -0
  21. data/docs/core-concepts/messages.md +211 -0
  22. data/docs/core-concepts/runs.md +278 -0
  23. data/docs/core-concepts/sessions.md +281 -0
  24. data/docs/examples.md +659 -0
  25. data/docs/getting-started/configuration.md +166 -0
  26. data/docs/getting-started/index.md +62 -0
  27. data/docs/getting-started/installation.md +95 -0
  28. data/docs/getting-started/quick-start.md +189 -0
  29. data/docs/index.md +119 -0
  30. data/docs/server/creating-agents.md +360 -0
  31. data/docs/server/http-endpoints.md +411 -0
  32. data/docs/server/index.md +218 -0
  33. data/docs/server/multi-turn.md +329 -0
  34. data/docs/server/streaming.md +315 -0
  35. data/docs/storage/custom.md +414 -0
  36. data/docs/storage/index.md +176 -0
  37. data/docs/storage/memory.md +198 -0
  38. data/docs/storage/postgresql.md +350 -0
  39. data/docs/storage/redis.md +287 -0
  40. data/examples/01_basic/client.rb +88 -0
  41. data/examples/01_basic/server.rb +100 -0
  42. data/examples/02_async_execution/client.rb +107 -0
  43. data/examples/02_async_execution/server.rb +56 -0
  44. data/examples/03_run_management/client.rb +115 -0
  45. data/examples/03_run_management/server.rb +84 -0
  46. data/examples/04_rich_messages/client.rb +160 -0
  47. data/examples/04_rich_messages/server.rb +180 -0
  48. data/examples/05_await_resume/client.rb +164 -0
  49. data/examples/05_await_resume/server.rb +114 -0
  50. data/examples/06_agent_metadata/client.rb +188 -0
  51. data/examples/06_agent_metadata/server.rb +192 -0
  52. data/examples/README.md +252 -0
  53. data/examples/run_demo.sh +137 -0
  54. data/lib/simple_acp/client/base.rb +448 -0
  55. data/lib/simple_acp/client/sse.rb +141 -0
  56. data/lib/simple_acp/models/agent_manifest.rb +129 -0
  57. data/lib/simple_acp/models/await.rb +123 -0
  58. data/lib/simple_acp/models/base.rb +147 -0
  59. data/lib/simple_acp/models/errors.rb +102 -0
  60. data/lib/simple_acp/models/events.rb +256 -0
  61. data/lib/simple_acp/models/message.rb +235 -0
  62. data/lib/simple_acp/models/message_part.rb +225 -0
  63. data/lib/simple_acp/models/metadata.rb +161 -0
  64. data/lib/simple_acp/models/run.rb +298 -0
  65. data/lib/simple_acp/models/session.rb +137 -0
  66. data/lib/simple_acp/models/types.rb +210 -0
  67. data/lib/simple_acp/server/agent.rb +116 -0
  68. data/lib/simple_acp/server/app.rb +264 -0
  69. data/lib/simple_acp/server/base.rb +510 -0
  70. data/lib/simple_acp/server/context.rb +210 -0
  71. data/lib/simple_acp/server/falcon_runner.rb +61 -0
  72. data/lib/simple_acp/storage/base.rb +129 -0
  73. data/lib/simple_acp/storage/memory.rb +108 -0
  74. data/lib/simple_acp/storage/postgresql.rb +233 -0
  75. data/lib/simple_acp/storage/redis.rb +178 -0
  76. data/lib/simple_acp/version.rb +5 -0
  77. data/lib/simple_acp.rb +91 -0
  78. data/mkdocs.yml +152 -0
  79. data/sig/simple_acp.rbs +4 -0
  80. metadata +418 -0
@@ -0,0 +1,414 @@
1
+ # Custom Storage Backends
2
+
3
+ Build your own storage backend to integrate with any data store or implement special requirements.
4
+
5
+ ## Interface
6
+
7
+ All storage backends extend `SimpleAcp::Storage::Base`:
8
+
9
+ ```ruby
10
+ module SimpleAcp
11
+ module Storage
12
+ class Base
13
+ def initialize(options = {})
14
+ def get_run(run_id)
15
+ def save_run(run)
16
+ def delete_run(run_id)
17
+ def list_runs(agent_name: nil, session_id: nil, limit: 10, offset: 0)
18
+
19
+ def get_session(session_id)
20
+ def save_session(session)
21
+ def delete_session(session_id)
22
+
23
+ def add_event(run_id, event)
24
+ def get_events(run_id, limit: 100, offset: 0)
25
+
26
+ def close
27
+ def ping
28
+ end
29
+ end
30
+ end
31
+ ```
32
+
33
+ ## Minimal Implementation
34
+
35
+ ```ruby
36
+ class MyStorage < SimpleAcp::Storage::Base
37
+ def initialize(options = {})
38
+ super
39
+ @runs = {}
40
+ @sessions = {}
41
+ @events = {}
42
+ end
43
+
44
+ # Runs
45
+ def get_run(run_id)
46
+ @runs[run_id]
47
+ end
48
+
49
+ def save_run(run)
50
+ @runs[run.run_id] = run
51
+ run
52
+ end
53
+
54
+ def delete_run(run_id)
55
+ @events.delete(run_id)
56
+ @runs.delete(run_id)
57
+ end
58
+
59
+ def list_runs(agent_name: nil, session_id: nil, limit: 10, offset: 0)
60
+ runs = @runs.values
61
+ runs = runs.select { |r| r.agent_name == agent_name } if agent_name
62
+ runs = runs.select { |r| r.session_id == session_id } if session_id
63
+ runs = runs.sort_by { |r| r.created_at || Time.at(0) }.reverse
64
+
65
+ {
66
+ runs: runs.drop(offset).take(limit),
67
+ total: runs.length
68
+ }
69
+ end
70
+
71
+ # Sessions
72
+ def get_session(session_id)
73
+ @sessions[session_id]
74
+ end
75
+
76
+ def save_session(session)
77
+ @sessions[session.id] = session
78
+ session
79
+ end
80
+
81
+ def delete_session(session_id)
82
+ @sessions.delete(session_id)
83
+ end
84
+
85
+ # Events
86
+ def add_event(run_id, event)
87
+ @events[run_id] ||= []
88
+ @events[run_id] << event
89
+ event
90
+ end
91
+
92
+ def get_events(run_id, limit: 100, offset: 0)
93
+ events = @events[run_id] || []
94
+ events.drop(offset).take(limit)
95
+ end
96
+
97
+ # Lifecycle
98
+ def close
99
+ # Cleanup if needed
100
+ end
101
+
102
+ def ping
103
+ true
104
+ end
105
+ end
106
+ ```
107
+
108
+ ## Example: SQLite Backend
109
+
110
+ ```ruby
111
+ require 'sequel'
112
+
113
+ class SQLiteStorage < SimpleAcp::Storage::Base
114
+ def initialize(options = {})
115
+ super
116
+ @db = Sequel.sqlite(options[:path] || ':memory:')
117
+ setup_tables
118
+ end
119
+
120
+ def get_run(run_id)
121
+ row = @db[:runs].where(run_id: run_id).first
122
+ return nil unless row
123
+ deserialize_run(row)
124
+ end
125
+
126
+ def save_run(run)
127
+ data = serialize_run(run)
128
+
129
+ if @db[:runs].where(run_id: run.run_id).count > 0
130
+ @db[:runs].where(run_id: run.run_id).update(data)
131
+ else
132
+ @db[:runs].insert(data)
133
+ end
134
+
135
+ run
136
+ end
137
+
138
+ def delete_run(run_id)
139
+ @db[:events].where(run_id: run_id).delete
140
+ @db[:runs].where(run_id: run_id).delete
141
+ end
142
+
143
+ def list_runs(agent_name: nil, session_id: nil, limit: 10, offset: 0)
144
+ dataset = @db[:runs]
145
+ dataset = dataset.where(agent_name: agent_name) if agent_name
146
+ dataset = dataset.where(session_id: session_id) if session_id
147
+
148
+ total = dataset.count
149
+ rows = dataset
150
+ .order(Sequel.desc(:created_at))
151
+ .limit(limit)
152
+ .offset(offset)
153
+ .all
154
+
155
+ {
156
+ runs: rows.map { |r| deserialize_run(r) },
157
+ total: total
158
+ }
159
+ end
160
+
161
+ # ... similar for sessions and events
162
+
163
+ private
164
+
165
+ def setup_tables
166
+ @db.create_table?(:runs) do
167
+ String :run_id, primary_key: true
168
+ String :agent_name
169
+ String :session_id
170
+ String :status
171
+ Text :output
172
+ Text :error
173
+ DateTime :created_at
174
+ DateTime :finished_at
175
+ end
176
+
177
+ @db.create_table?(:sessions) do
178
+ String :id, primary_key: true
179
+ Text :history
180
+ Text :state
181
+ end
182
+
183
+ @db.create_table?(:events) do
184
+ primary_key :id
185
+ String :run_id
186
+ String :event_type
187
+ Text :data
188
+ DateTime :created_at
189
+ end
190
+ end
191
+
192
+ def serialize_run(run)
193
+ {
194
+ run_id: run.run_id,
195
+ agent_name: run.agent_name,
196
+ session_id: run.session_id,
197
+ status: run.status,
198
+ output: run.output.to_json,
199
+ error: run.error&.to_json,
200
+ created_at: run.created_at,
201
+ finished_at: run.finished_at
202
+ }
203
+ end
204
+
205
+ def deserialize_run(row)
206
+ output = row[:output] ? JSON.parse(row[:output]).map { |m|
207
+ SimpleAcp::Models::Message.from_hash(m)
208
+ } : []
209
+
210
+ run = SimpleAcp::Models::Run.new(
211
+ run_id: row[:run_id],
212
+ agent_name: row[:agent_name],
213
+ session_id: row[:session_id],
214
+ status: row[:status]
215
+ )
216
+ run.instance_variable_set(:@output, output)
217
+ run.instance_variable_set(:@created_at, row[:created_at])
218
+ run.instance_variable_set(:@finished_at, row[:finished_at])
219
+ run
220
+ end
221
+ end
222
+ ```
223
+
224
+ ## Example: S3 Backend
225
+
226
+ For archival storage:
227
+
228
+ ```ruby
229
+ require 'aws-sdk-s3'
230
+
231
+ class S3Storage < SimpleAcp::Storage::Base
232
+ def initialize(options = {})
233
+ super
234
+ @s3 = Aws::S3::Client.new(
235
+ region: options[:region] || 'us-east-1'
236
+ )
237
+ @bucket = options[:bucket]
238
+ @prefix = options[:prefix] || 'acp/'
239
+
240
+ # Use memory for active data, S3 for archive
241
+ @active = SimpleAcp::Storage::Memory.new
242
+ end
243
+
244
+ def get_run(run_id)
245
+ # Check active first
246
+ run = @active.get_run(run_id)
247
+ return run if run
248
+
249
+ # Fall back to S3
250
+ load_from_s3("runs/#{run_id}")
251
+ end
252
+
253
+ def save_run(run)
254
+ @active.save_run(run)
255
+
256
+ # Archive completed runs
257
+ if run.terminal?
258
+ save_to_s3("runs/#{run.run_id}", run.to_json)
259
+ end
260
+
261
+ run
262
+ end
263
+
264
+ # ... implement other methods
265
+
266
+ private
267
+
268
+ def save_to_s3(key, data)
269
+ @s3.put_object(
270
+ bucket: @bucket,
271
+ key: "#{@prefix}#{key}",
272
+ body: data
273
+ )
274
+ end
275
+
276
+ def load_from_s3(key)
277
+ response = @s3.get_object(
278
+ bucket: @bucket,
279
+ key: "#{@prefix}#{key}"
280
+ )
281
+ JSON.parse(response.body.read)
282
+ rescue Aws::S3::Errors::NoSuchKey
283
+ nil
284
+ end
285
+ end
286
+ ```
287
+
288
+ ## Example: Hybrid Backend
289
+
290
+ Combine multiple backends:
291
+
292
+ ```ruby
293
+ class HybridStorage < SimpleAcp::Storage::Base
294
+ def initialize(options = {})
295
+ super
296
+ @cache = SimpleAcp::Storage::Redis.new(
297
+ url: options[:redis_url],
298
+ ttl: 3600 # 1 hour cache
299
+ )
300
+ @persistent = SimpleAcp::Storage::PostgreSQL.new(
301
+ url: options[:database_url]
302
+ )
303
+ end
304
+
305
+ def get_run(run_id)
306
+ # Try cache first
307
+ run = @cache.get_run(run_id)
308
+ return run if run
309
+
310
+ # Fall back to database
311
+ run = @persistent.get_run(run_id)
312
+
313
+ # Populate cache
314
+ @cache.save_run(run) if run
315
+
316
+ run
317
+ end
318
+
319
+ def save_run(run)
320
+ # Write to both
321
+ @cache.save_run(run)
322
+ @persistent.save_run(run)
323
+ run
324
+ end
325
+
326
+ def delete_run(run_id)
327
+ @cache.delete_run(run_id)
328
+ @persistent.delete_run(run_id)
329
+ end
330
+
331
+ # ... implement other methods delegating appropriately
332
+ end
333
+ ```
334
+
335
+ ## Testing Custom Backends
336
+
337
+ ```ruby
338
+ class CustomStorageTest < Minitest::Test
339
+ def setup
340
+ @storage = MyStorage.new
341
+ end
342
+
343
+ def teardown
344
+ @storage.close
345
+ end
346
+
347
+ def test_save_and_get_run
348
+ run = create_test_run
349
+ @storage.save_run(run)
350
+
351
+ loaded = @storage.get_run(run.run_id)
352
+ assert_equal run.run_id, loaded.run_id
353
+ assert_equal run.agent_name, loaded.agent_name
354
+ end
355
+
356
+ def test_list_runs_filters
357
+ @storage.save_run(create_test_run(agent_name: "a"))
358
+ @storage.save_run(create_test_run(agent_name: "b"))
359
+
360
+ result = @storage.list_runs(agent_name: "a")
361
+ assert_equal 1, result[:total]
362
+ assert_equal "a", result[:runs].first.agent_name
363
+ end
364
+
365
+ def test_session_persistence
366
+ session = create_test_session
367
+ @storage.save_session(session)
368
+
369
+ loaded = @storage.get_session(session.id)
370
+ assert_equal session.id, loaded.id
371
+ end
372
+
373
+ def test_events_ordering
374
+ events = 3.times.map { |i| create_test_event(i) }
375
+ events.each { |e| @storage.add_event("run-1", e) }
376
+
377
+ loaded = @storage.get_events("run-1")
378
+ assert_equal 3, loaded.length
379
+ end
380
+
381
+ private
382
+
383
+ def create_test_run(overrides = {})
384
+ SimpleAcp::Models::Run.new({
385
+ run_id: SecureRandom.uuid,
386
+ agent_name: "test",
387
+ status: "completed"
388
+ }.merge(overrides))
389
+ end
390
+
391
+ def create_test_session
392
+ SimpleAcp::Models::Session.new(id: SecureRandom.uuid)
393
+ end
394
+
395
+ def create_test_event(index)
396
+ SimpleAcp::Models::MessagePartEvent.new(
397
+ part: SimpleAcp::Models::MessagePart.text("Event #{index}")
398
+ )
399
+ end
400
+ end
401
+ ```
402
+
403
+ ## Best Practices
404
+
405
+ 1. **Implement all methods** - Don't leave `NotImplementedError`
406
+ 2. **Handle missing data** - Return `nil` for not found
407
+ 3. **Be thread-safe** - Use appropriate locking
408
+ 4. **Test thoroughly** - Cover edge cases
409
+ 5. **Document limitations** - Note any constraints
410
+
411
+ ## Next Steps
412
+
413
+ - Review built-in [Memory](memory.md), [Redis](redis.md), and [PostgreSQL](postgresql.md) implementations
414
+ - See [API Reference](../api/storage.md) for interface details
@@ -0,0 +1,176 @@
1
+ # Storage Backends
2
+
3
+ Storage backends persist runs, sessions, and events. Choose the right backend for your deployment scenario.
4
+
5
+ ## Overview
6
+
7
+ ```mermaid
8
+ graph TB
9
+ S[Server] --> ST{Storage}
10
+ ST --> M[Memory]
11
+ ST --> R[Redis]
12
+ ST --> P[PostgreSQL]
13
+ ST --> C[Custom]
14
+ ```
15
+
16
+ ## Available Backends
17
+
18
+ | Backend | Use Case | Persistence | Scaling |
19
+ |---------|----------|-------------|---------|
20
+ | [Memory](memory.md) | Development, testing | None | Single process |
21
+ | [Redis](redis.md) | Production, distributed | TTL-based | Horizontal |
22
+ | [PostgreSQL](postgresql.md) | Production, audit | Permanent | Vertical |
23
+ | [Custom](custom.md) | Special requirements | Variable | Variable |
24
+
25
+ ## Quick Comparison
26
+
27
+ ### Memory
28
+
29
+ ```ruby
30
+ storage = SimpleAcp::Storage::Memory.new
31
+ server = SimpleAcp::Server::Base.new(storage: storage)
32
+ ```
33
+
34
+ - No dependencies
35
+ - Fast, in-process
36
+ - Data lost on restart
37
+ - Single process only
38
+
39
+ ### Redis
40
+
41
+ ```ruby
42
+ require 'simple_acp/storage/redis'
43
+
44
+ storage = SimpleAcp::Storage::Redis.new(
45
+ url: "redis://localhost:6379",
46
+ ttl: 86400
47
+ )
48
+ server = SimpleAcp::Server::Base.new(storage: storage)
49
+ ```
50
+
51
+ - Requires Redis server
52
+ - Automatic expiration
53
+ - Multi-process support
54
+ - Good for scaling
55
+
56
+ ### PostgreSQL
57
+
58
+ ```ruby
59
+ require 'simple_acp/storage/postgresql'
60
+
61
+ storage = SimpleAcp::Storage::PostgreSQL.new(
62
+ url: "postgres://localhost/acp"
63
+ )
64
+ server = SimpleAcp::Server::Base.new(storage: storage)
65
+ ```
66
+
67
+ - Requires PostgreSQL
68
+ - Permanent storage
69
+ - Full query capabilities
70
+ - Good for auditing
71
+
72
+ ## Choosing a Backend
73
+
74
+ ### Use Memory When
75
+
76
+ - Developing locally
77
+ - Running tests
78
+ - Single-process deployment
79
+ - Data persistence not needed
80
+
81
+ ### Use Redis When
82
+
83
+ - Multiple server processes
84
+ - Horizontal scaling needed
85
+ - TTL-based cleanup acceptable
86
+ - Real-time performance critical
87
+
88
+ ### Use PostgreSQL When
89
+
90
+ - Audit trail required
91
+ - Complex queries needed
92
+ - Long-term data retention
93
+ - Compliance requirements
94
+
95
+ ## Storage Interface
96
+
97
+ All backends implement the same interface:
98
+
99
+ ```ruby
100
+ class Storage::Base
101
+ def get_run(run_id)
102
+ def save_run(run)
103
+ def delete_run(run_id)
104
+ def list_runs(agent_name:, session_id:, limit:, offset:)
105
+
106
+ def get_session(session_id)
107
+ def save_session(session)
108
+ def delete_session(session_id)
109
+
110
+ def add_event(run_id, event)
111
+ def get_events(run_id, limit:, offset:)
112
+
113
+ def close
114
+ def ping
115
+ end
116
+ ```
117
+
118
+ ## Configuration
119
+
120
+ ### Environment Variables
121
+
122
+ ```bash
123
+ # Redis
124
+ export REDIS_URL=redis://localhost:6379
125
+
126
+ # PostgreSQL
127
+ export DATABASE_URL=postgres://user:pass@host/db
128
+ ```
129
+
130
+ ### Connection Options
131
+
132
+ Each backend accepts specific connection options. See individual backend documentation for details.
133
+
134
+ ## In This Section
135
+
136
+ <div class="grid cards" markdown>
137
+
138
+ - :material-memory:{ .lg .middle } **Memory**
139
+
140
+ ---
141
+
142
+ In-process storage for development
143
+
144
+ [:octicons-arrow-right-24: Memory](memory.md)
145
+
146
+ - :material-database-clock:{ .lg .middle } **Redis**
147
+
148
+ ---
149
+
150
+ Distributed storage with TTL
151
+
152
+ [:octicons-arrow-right-24: Redis](redis.md)
153
+
154
+ - :material-elephant:{ .lg .middle } **PostgreSQL**
155
+
156
+ ---
157
+
158
+ Persistent relational storage
159
+
160
+ [:octicons-arrow-right-24: PostgreSQL](postgresql.md)
161
+
162
+ - :material-cog:{ .lg .middle } **Custom Backends**
163
+
164
+ ---
165
+
166
+ Build your own storage
167
+
168
+ [:octicons-arrow-right-24: Custom](custom.md)
169
+
170
+ </div>
171
+
172
+ ## Next Steps
173
+
174
+ - Start with [Memory](memory.md) for development
175
+ - Move to [Redis](redis.md) or [PostgreSQL](postgresql.md) for production
176
+ - Build a [Custom Backend](custom.md) for special needs