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,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAcp
4
+ module Server
5
+ # Request context passed to agent handlers during execution.
6
+ #
7
+ # Provides access to input messages, session state, conversation history,
8
+ # and methods for controlling execution flow.
9
+ #
10
+ # @example Accessing input
11
+ # server.agent("echo") do |context|
12
+ # text = context.input.first.text_content
13
+ # Models::Message.agent("You said: #{text}")
14
+ # end
15
+ #
16
+ # @example Using session state
17
+ # server.agent("counter") do |context|
18
+ # count = (context.state || 0) + 1
19
+ # context.set_state(count)
20
+ # Models::Message.agent("Count: #{count}")
21
+ # end
22
+ class Context
23
+ # @return [Models::Run] the current run being executed
24
+ attr_reader :run
25
+
26
+ # @return [Models::Session, nil] the session (if any)
27
+ attr_reader :session
28
+
29
+ # @return [Array<Models::Message>] input messages for this run
30
+ attr_reader :input
31
+
32
+ # @return [Server::Base] reference to the server
33
+ attr_reader :server
34
+
35
+ # Initialize a new context.
36
+ #
37
+ # @param run [Models::Run] the run being executed
38
+ # @param session [Models::Session, nil] optional session
39
+ # @param input [Array<Models::Message>] input messages
40
+ # @param server [Server::Base] the server instance
41
+ def initialize(run:, session:, input:, server:)
42
+ @run = run
43
+ @session = session
44
+ @input = input
45
+ @server = server
46
+ @cancelled = false
47
+ end
48
+
49
+ # @return [String] the name of the agent being executed
50
+ def agent_name
51
+ @run.agent_name
52
+ end
53
+
54
+ # @return [String] the unique run ID
55
+ def run_id
56
+ @run.run_id
57
+ end
58
+
59
+ # @return [String, nil] the session ID if a session is active
60
+ def session_id
61
+ @session&.id
62
+ end
63
+
64
+ # Check if the run has been cancelled.
65
+ #
66
+ # @return [Boolean] true if cancelled
67
+ def cancelled?
68
+ @cancelled
69
+ end
70
+
71
+ # Mark the run as cancelled.
72
+ #
73
+ # @return [void]
74
+ def cancel!
75
+ @cancelled = true
76
+ end
77
+
78
+ # Get conversation history from the session.
79
+ #
80
+ # @return [Array<Models::Message>] previous messages in the session
81
+ def history
82
+ @session&.history || []
83
+ end
84
+
85
+ # Get arbitrary state data from the session.
86
+ #
87
+ # @return [Object, nil] the stored state data
88
+ def state
89
+ @session&.state
90
+ end
91
+
92
+ # Update the session state with new data.
93
+ #
94
+ # @param new_state [Object] the state data to store
95
+ # @return [void]
96
+ def set_state(new_state)
97
+ @session&.set_state(new_state)
98
+ @server.storage.save_session(@session) if @session
99
+ end
100
+
101
+ # Request additional input from the client, pausing execution.
102
+ #
103
+ # This puts the run into an "awaiting" state until the client
104
+ # calls resume with the requested input.
105
+ #
106
+ # @param prompt_message [Models::Message] message to display to the client
107
+ # @return [RunYieldAwait] yield this from your agent to pause
108
+ #
109
+ # @example Multi-turn conversation
110
+ # server.agent("questioner") do |context|
111
+ # Enumerator.new do |yielder|
112
+ # result = context.await_message(Models::Message.agent("What is your name?"))
113
+ # yielder << result
114
+ # name = context.resume_message&.text_content
115
+ # yielder << RunYield.new(Models::Message.agent("Hello, #{name}!"))
116
+ # end
117
+ # end
118
+ def await_message(prompt_message)
119
+ request = Models::MessageAwaitRequest.new(message: prompt_message)
120
+ @run.await!(request)
121
+ @server.storage.save_run(@run)
122
+
123
+ # Return a result that indicates the run is awaiting
124
+ RunYieldAwait.new(request: request)
125
+ end
126
+
127
+ # Log a message for debugging or trajectory tracking.
128
+ #
129
+ # @param message [String] the message to log
130
+ # @return [void]
131
+ def log(message)
132
+ SimpleAcp.logger&.info("[#{agent_name}] #{message}")
133
+ end
134
+
135
+ # Get the resume message (only available during resume).
136
+ #
137
+ # This method returns nil for initial contexts. Override in
138
+ # ResumeContext to return the actual resume message.
139
+ #
140
+ # @return [Models::Message, nil] nil for initial context
141
+ def resume_message
142
+ nil
143
+ end
144
+ end
145
+
146
+ # Wrapper for yielding output messages from an agent.
147
+ #
148
+ # Use this when streaming responses from an Enumerator-based agent handler.
149
+ #
150
+ # @example
151
+ # Enumerator.new do |yielder|
152
+ # yielder << RunYield.new(Models::Message.agent("First message"))
153
+ # yielder << RunYield.new(Models::Message.agent("Second message"))
154
+ # end
155
+ class RunYield
156
+ # @return [Models::Message] the message to output
157
+ attr_reader :message
158
+
159
+ # Create a new message yield.
160
+ #
161
+ # @param message [Models::Message, Hash] the message to yield
162
+ def initialize(message)
163
+ @message = message.is_a?(Models::Message) ? message : Models::Message.from_hash(message)
164
+ end
165
+ end
166
+
167
+ # Wrapper for pausing execution and awaiting client input.
168
+ #
169
+ # Yield this from an agent to pause execution until the client
170
+ # resumes with the requested input.
171
+ class RunYieldAwait
172
+ # @return [Models::AwaitRequest] the await request details
173
+ attr_reader :request
174
+
175
+ # Create a new await yield.
176
+ #
177
+ # @param request [Models::AwaitRequest] the request for client input
178
+ def initialize(request:)
179
+ @request = request
180
+ end
181
+ end
182
+
183
+ # Extended context for resuming an awaited run.
184
+ #
185
+ # Contains the client's response to the await request.
186
+ class ResumeContext < Context
187
+ # @return [Models::AwaitResume] the client's resume payload
188
+ attr_reader :await_resume
189
+
190
+ # Initialize a resume context.
191
+ #
192
+ # @param run [Models::Run] the run being resumed
193
+ # @param session [Models::Session, nil] optional session
194
+ # @param input [Array<Models::Message>] original input messages
195
+ # @param server [Server::Base] the server instance
196
+ # @param await_resume [Models::AwaitResume] the client's response
197
+ def initialize(run:, session:, input:, server:, await_resume:)
198
+ super(run: run, session: session, input: input, server: server)
199
+ @await_resume = await_resume
200
+ end
201
+
202
+ # Get the message from the client's resume payload.
203
+ #
204
+ # @return [Models::Message, nil] the client's response message
205
+ def resume_message
206
+ @await_resume&.message
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "async"
4
+ require "falcon"
5
+ require "falcon/server"
6
+ require "protocol/rack"
7
+ require "io/endpoint"
8
+ require "io/endpoint/address_endpoint"
9
+
10
+ module SimpleAcp
11
+ module Server
12
+ # Falcon-based server runner using fiber concurrency.
13
+ #
14
+ # Provides efficient handling of SSE streams and long-lived
15
+ # connections through Async's fiber scheduler.
16
+ #
17
+ # @example Starting a server
18
+ # app = server.to_app
19
+ # FalconRunner.run(app, port: 8000)
20
+ module FalconRunner
21
+ # Start the server using Falcon.
22
+ #
23
+ # @param app [#call] the Rack application
24
+ # @param port [Integer] port to bind to (default: 8000)
25
+ # @param host [String] host to bind to (default: "0.0.0.0")
26
+ # @param options [Hash] additional options
27
+ # @option options [Integer] :count number of worker processes
28
+ # @return [void]
29
+ def self.run(app, port: 8000, host: "0.0.0.0", **options)
30
+ endpoint = IO::Endpoint::AddressEndpoint.new(Addrinfo.tcp(host, port))
31
+
32
+ puts "ACP Server (Falcon) running on http://#{host}:#{port}"
33
+
34
+ server_task = nil
35
+
36
+ # Handle interrupt for graceful shutdown
37
+ trap("INT") do
38
+ puts "\nShutting down..."
39
+ Thread.main.raise(Interrupt)
40
+ end
41
+
42
+ trap("TERM") do
43
+ puts "\nShutting down..."
44
+ Thread.main.raise(Interrupt)
45
+ end
46
+
47
+ begin
48
+ Sync do |task|
49
+ rack_app = Protocol::Rack::Adapter.new(app)
50
+ server = Falcon::Server.new(rack_app, endpoint, protocol: Async::HTTP::Protocol::HTTP1, scheme: "http")
51
+
52
+ server_task = server.run
53
+ server_task.wait
54
+ end
55
+ rescue Interrupt
56
+ # Graceful shutdown
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SimpleAcp
4
+ module Storage
5
+ # Abstract base class for storage backends.
6
+ #
7
+ # Defines the interface for storing runs, sessions, and events.
8
+ # Implementations include Memory, Redis, and PostgreSQL.
9
+ #
10
+ # @abstract Subclass and implement all methods
11
+ #
12
+ # @example Custom storage backend
13
+ # class MyStorage < SimpleAcp::Storage::Base
14
+ # def get_run(run_id)
15
+ # # ...
16
+ # end
17
+ # # ... implement other methods
18
+ # end
19
+ class Base
20
+ # Initialize the storage backend.
21
+ #
22
+ # @param options [Hash] backend-specific options
23
+ def initialize(options = {})
24
+ @options = options
25
+ end
26
+
27
+ # Retrieve a run by ID.
28
+ #
29
+ # @param run_id [String] the run ID
30
+ # @return [Models::Run, nil] the run or nil if not found
31
+ # @raise [NotImplementedError] if not implemented
32
+ def get_run(run_id)
33
+ raise NotImplementedError, "#{self.class}#get_run must be implemented"
34
+ end
35
+
36
+ # Save a run.
37
+ #
38
+ # @param run [Models::Run] the run to save
39
+ # @return [Models::Run] the saved run
40
+ # @raise [NotImplementedError] if not implemented
41
+ def save_run(run)
42
+ raise NotImplementedError, "#{self.class}#save_run must be implemented"
43
+ end
44
+
45
+ # Delete a run.
46
+ #
47
+ # @param run_id [String] the run ID to delete
48
+ # @return [void]
49
+ # @raise [NotImplementedError] if not implemented
50
+ def delete_run(run_id)
51
+ raise NotImplementedError, "#{self.class}#delete_run must be implemented"
52
+ end
53
+
54
+ # List runs with optional filtering.
55
+ #
56
+ # @param agent_name [String, nil] filter by agent name
57
+ # @param session_id [String, nil] filter by session ID
58
+ # @param limit [Integer] maximum number to return (default: 10)
59
+ # @param offset [Integer] number to skip (default: 0)
60
+ # @return [Hash] with :runs and :total keys
61
+ # @raise [NotImplementedError] if not implemented
62
+ def list_runs(agent_name: nil, session_id: nil, limit: 10, offset: 0)
63
+ raise NotImplementedError, "#{self.class}#list_runs must be implemented"
64
+ end
65
+
66
+ # Retrieve a session by ID.
67
+ #
68
+ # @param session_id [String] the session ID
69
+ # @return [Models::Session, nil] the session or nil if not found
70
+ # @raise [NotImplementedError] if not implemented
71
+ def get_session(session_id)
72
+ raise NotImplementedError, "#{self.class}#get_session must be implemented"
73
+ end
74
+
75
+ # Save a session.
76
+ #
77
+ # @param session [Models::Session] the session to save
78
+ # @return [Models::Session] the saved session
79
+ # @raise [NotImplementedError] if not implemented
80
+ def save_session(session)
81
+ raise NotImplementedError, "#{self.class}#save_session must be implemented"
82
+ end
83
+
84
+ # Delete a session.
85
+ #
86
+ # @param session_id [String] the session ID to delete
87
+ # @return [void]
88
+ # @raise [NotImplementedError] if not implemented
89
+ def delete_session(session_id)
90
+ raise NotImplementedError, "#{self.class}#delete_session must be implemented"
91
+ end
92
+
93
+ # Add an event to a run.
94
+ #
95
+ # @param run_id [String] the run ID
96
+ # @param event [Models::Event] the event to add
97
+ # @return [Models::Event] the added event
98
+ # @raise [NotImplementedError] if not implemented
99
+ def add_event(run_id, event)
100
+ raise NotImplementedError, "#{self.class}#add_event must be implemented"
101
+ end
102
+
103
+ # Get events for a run.
104
+ #
105
+ # @param run_id [String] the run ID
106
+ # @param limit [Integer] maximum number to return (default: 100)
107
+ # @param offset [Integer] number to skip (default: 0)
108
+ # @return [Array<Models::Event>] list of events
109
+ # @raise [NotImplementedError] if not implemented
110
+ def get_events(run_id, limit: 100, offset: 0)
111
+ raise NotImplementedError, "#{self.class}#get_events must be implemented"
112
+ end
113
+
114
+ # Close the storage connection.
115
+ #
116
+ # @return [void]
117
+ def close
118
+ # Override in subclasses if needed
119
+ end
120
+
121
+ # Check if the storage is accessible.
122
+ #
123
+ # @return [Boolean] true if accessible
124
+ def ping
125
+ true
126
+ end
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "concurrent"
4
+
5
+ module SimpleAcp
6
+ module Storage
7
+ # Thread-safe in-memory storage backend.
8
+ #
9
+ # This is the default storage backend. Data is lost when the process exits.
10
+ # Uses concurrent-ruby for thread safety.
11
+ #
12
+ # @example
13
+ # storage = SimpleAcp::Storage::Memory.new
14
+ # server = SimpleAcp::Server::Base.new(storage: storage)
15
+ class Memory < Base
16
+ def initialize(options = {})
17
+ super
18
+ @runs = Concurrent::Map.new
19
+ @sessions = Concurrent::Map.new
20
+ @events = Concurrent::Map.new
21
+ @mutex = Mutex.new
22
+ end
23
+
24
+ # @see Base#get_run
25
+ def get_run(run_id)
26
+ @runs[run_id]
27
+ end
28
+
29
+ # @see Base#save_run
30
+ def save_run(run)
31
+ @runs[run.run_id] = run
32
+ run
33
+ end
34
+
35
+ # @see Base#delete_run
36
+ def delete_run(run_id)
37
+ @events.delete(run_id)
38
+ @runs.delete(run_id)
39
+ end
40
+
41
+ # @see Base#list_runs
42
+ def list_runs(agent_name: nil, session_id: nil, limit: 10, offset: 0)
43
+ runs = @runs.values
44
+
45
+ runs = runs.select { |r| r.agent_name == agent_name } if agent_name
46
+ runs = runs.select { |r| r.session_id == session_id } if session_id
47
+
48
+ runs = runs.sort_by { |r| r.created_at || Time.at(0) }.reverse
49
+
50
+ {
51
+ runs: runs.drop(offset).take(limit),
52
+ total: runs.length
53
+ }
54
+ end
55
+
56
+ # @see Base#get_session
57
+ def get_session(session_id)
58
+ @sessions[session_id]
59
+ end
60
+
61
+ # @see Base#save_session
62
+ def save_session(session)
63
+ @sessions[session.id] = session
64
+ session
65
+ end
66
+
67
+ # @see Base#delete_session
68
+ def delete_session(session_id)
69
+ @sessions.delete(session_id)
70
+ end
71
+
72
+ # @see Base#add_event
73
+ def add_event(run_id, event)
74
+ @mutex.synchronize do
75
+ @events[run_id] ||= []
76
+ @events[run_id] << event
77
+ end
78
+ event
79
+ end
80
+
81
+ # @see Base#get_events
82
+ def get_events(run_id, limit: 100, offset: 0)
83
+ events = @events[run_id] || []
84
+ events.drop(offset).take(limit)
85
+ end
86
+
87
+ # Clear all stored data.
88
+ #
89
+ # @return [void]
90
+ def clear!
91
+ @runs.clear
92
+ @sessions.clear
93
+ @events.clear
94
+ end
95
+
96
+ # Get storage statistics.
97
+ #
98
+ # @return [Hash] counts of runs, sessions, and events
99
+ def stats
100
+ {
101
+ runs: @runs.size,
102
+ sessions: @sessions.size,
103
+ events: @events.values.sum(&:length)
104
+ }
105
+ end
106
+ end
107
+ end
108
+ end