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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/CHANGELOG.md +5 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +385 -0
- data/Rakefile +13 -0
- data/docs/api/client-base.md +383 -0
- data/docs/api/index.md +159 -0
- data/docs/api/models.md +286 -0
- data/docs/api/server-base.md +379 -0
- data/docs/api/storage.md +347 -0
- data/docs/assets/images/simple_acp.jpg +0 -0
- data/docs/client/index.md +279 -0
- data/docs/client/sessions.md +324 -0
- data/docs/client/streaming.md +345 -0
- data/docs/client/sync-async.md +308 -0
- data/docs/core-concepts/agents.md +253 -0
- data/docs/core-concepts/events.md +337 -0
- data/docs/core-concepts/index.md +147 -0
- data/docs/core-concepts/messages.md +211 -0
- data/docs/core-concepts/runs.md +278 -0
- data/docs/core-concepts/sessions.md +281 -0
- data/docs/examples.md +659 -0
- data/docs/getting-started/configuration.md +166 -0
- data/docs/getting-started/index.md +62 -0
- data/docs/getting-started/installation.md +95 -0
- data/docs/getting-started/quick-start.md +189 -0
- data/docs/index.md +119 -0
- data/docs/server/creating-agents.md +360 -0
- data/docs/server/http-endpoints.md +411 -0
- data/docs/server/index.md +218 -0
- data/docs/server/multi-turn.md +329 -0
- data/docs/server/streaming.md +315 -0
- data/docs/storage/custom.md +414 -0
- data/docs/storage/index.md +176 -0
- data/docs/storage/memory.md +198 -0
- data/docs/storage/postgresql.md +350 -0
- data/docs/storage/redis.md +287 -0
- data/examples/01_basic/client.rb +88 -0
- data/examples/01_basic/server.rb +100 -0
- data/examples/02_async_execution/client.rb +107 -0
- data/examples/02_async_execution/server.rb +56 -0
- data/examples/03_run_management/client.rb +115 -0
- data/examples/03_run_management/server.rb +84 -0
- data/examples/04_rich_messages/client.rb +160 -0
- data/examples/04_rich_messages/server.rb +180 -0
- data/examples/05_await_resume/client.rb +164 -0
- data/examples/05_await_resume/server.rb +114 -0
- data/examples/06_agent_metadata/client.rb +188 -0
- data/examples/06_agent_metadata/server.rb +192 -0
- data/examples/README.md +252 -0
- data/examples/run_demo.sh +137 -0
- data/lib/simple_acp/client/base.rb +448 -0
- data/lib/simple_acp/client/sse.rb +141 -0
- data/lib/simple_acp/models/agent_manifest.rb +129 -0
- data/lib/simple_acp/models/await.rb +123 -0
- data/lib/simple_acp/models/base.rb +147 -0
- data/lib/simple_acp/models/errors.rb +102 -0
- data/lib/simple_acp/models/events.rb +256 -0
- data/lib/simple_acp/models/message.rb +235 -0
- data/lib/simple_acp/models/message_part.rb +225 -0
- data/lib/simple_acp/models/metadata.rb +161 -0
- data/lib/simple_acp/models/run.rb +298 -0
- data/lib/simple_acp/models/session.rb +137 -0
- data/lib/simple_acp/models/types.rb +210 -0
- data/lib/simple_acp/server/agent.rb +116 -0
- data/lib/simple_acp/server/app.rb +264 -0
- data/lib/simple_acp/server/base.rb +510 -0
- data/lib/simple_acp/server/context.rb +210 -0
- data/lib/simple_acp/server/falcon_runner.rb +61 -0
- data/lib/simple_acp/storage/base.rb +129 -0
- data/lib/simple_acp/storage/memory.rb +108 -0
- data/lib/simple_acp/storage/postgresql.rb +233 -0
- data/lib/simple_acp/storage/redis.rb +178 -0
- data/lib/simple_acp/version.rb +5 -0
- data/lib/simple_acp.rb +91 -0
- data/mkdocs.yml +152 -0
- data/sig/simple_acp.rbs +4 -0
- metadata +418 -0
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SimpleAcp
|
|
4
|
+
module Server
|
|
5
|
+
# Main ACP Server class for hosting agents and handling requests.
|
|
6
|
+
#
|
|
7
|
+
# The server manages agent registration, run execution (sync, async, stream),
|
|
8
|
+
# session state, and exposes an HTTP API via Roda/Falcon.
|
|
9
|
+
#
|
|
10
|
+
# @example Creating and running a server
|
|
11
|
+
# server = SimpleAcp::Server::Base.new
|
|
12
|
+
# server.agent("echo", description: "Echoes input") do |context|
|
|
13
|
+
# SimpleAcp::Models::Message.agent(context.input.first.text_content)
|
|
14
|
+
# end
|
|
15
|
+
# server.run(port: 8000)
|
|
16
|
+
#
|
|
17
|
+
# @example Using custom storage
|
|
18
|
+
# storage = SimpleAcp::Storage::Redis.new(url: "redis://localhost:6379")
|
|
19
|
+
# server = SimpleAcp::Server::Base.new(storage: storage)
|
|
20
|
+
class Base
|
|
21
|
+
# @return [Hash<String, Agent>] registered agents indexed by name
|
|
22
|
+
attr_reader :agents
|
|
23
|
+
|
|
24
|
+
# @return [Storage::Base] storage backend for runs, sessions, and events
|
|
25
|
+
attr_reader :storage
|
|
26
|
+
|
|
27
|
+
# @return [Hash] additional configuration options
|
|
28
|
+
attr_reader :options
|
|
29
|
+
|
|
30
|
+
# Initialize a new ACP server.
|
|
31
|
+
#
|
|
32
|
+
# @param storage [Storage::Base, nil] storage backend (defaults to Memory)
|
|
33
|
+
# @param options [Hash] additional configuration options
|
|
34
|
+
def initialize(storage: nil, **options)
|
|
35
|
+
@agents = {}
|
|
36
|
+
@storage = storage || SimpleAcp::Storage::Memory.new
|
|
37
|
+
@options = options
|
|
38
|
+
@running_contexts = Concurrent::Map.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Register an agent using block syntax or decorator-style.
|
|
42
|
+
#
|
|
43
|
+
# @param name [String, nil] agent name (must follow RFC 1123 DNS label format)
|
|
44
|
+
# @param description [String, nil] human-readable description
|
|
45
|
+
# @param options [Hash] additional options
|
|
46
|
+
# @option options [Array<String>] :input_content_types accepted MIME types
|
|
47
|
+
# @option options [Array<String>] :output_content_types produced MIME types
|
|
48
|
+
# @option options [Hash] :metadata agent metadata
|
|
49
|
+
# @yield [Context] block that handles agent requests
|
|
50
|
+
# @return [Agent, Proc] the registered agent or a decorator lambda
|
|
51
|
+
#
|
|
52
|
+
# @example Block syntax
|
|
53
|
+
# server.agent("greeter", description: "Greets users") do |context|
|
|
54
|
+
# name = context.input.first&.text_content || "World"
|
|
55
|
+
# SimpleAcp::Models::Message.agent("Hello, #{name}!")
|
|
56
|
+
# end
|
|
57
|
+
#
|
|
58
|
+
# @example Streaming agent
|
|
59
|
+
# server.agent("counter") do |context|
|
|
60
|
+
# Enumerator.new do |yielder|
|
|
61
|
+
# 3.times { |i| yielder << SimpleAcp::Models::Message.agent("Count: #{i}") }
|
|
62
|
+
# end
|
|
63
|
+
# end
|
|
64
|
+
def agent(name = nil, description: nil, **options, &block)
|
|
65
|
+
if block_given?
|
|
66
|
+
# Direct registration with block
|
|
67
|
+
agent_obj = AgentDSL.define(
|
|
68
|
+
name: name,
|
|
69
|
+
description: description,
|
|
70
|
+
**options,
|
|
71
|
+
&block
|
|
72
|
+
)
|
|
73
|
+
register(agent_obj)
|
|
74
|
+
agent_obj
|
|
75
|
+
else
|
|
76
|
+
# Return a lambda for decorator-style usage
|
|
77
|
+
->(handler) do
|
|
78
|
+
agent_name = name || handler_name(handler)
|
|
79
|
+
agent_obj = Agent.new(
|
|
80
|
+
manifest: Models::AgentManifest.new(
|
|
81
|
+
name: agent_name,
|
|
82
|
+
description: description,
|
|
83
|
+
**options
|
|
84
|
+
),
|
|
85
|
+
handler: handler
|
|
86
|
+
)
|
|
87
|
+
register(agent_obj)
|
|
88
|
+
handler
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Register an agent instance directly.
|
|
94
|
+
#
|
|
95
|
+
# @param agent [Agent] the agent to register
|
|
96
|
+
# @return [Agent] the registered agent
|
|
97
|
+
# @raise [ValidationError] if the agent is invalid
|
|
98
|
+
# @raise [ConfigurationError] if an agent with the same name exists
|
|
99
|
+
def register(agent)
|
|
100
|
+
raise SimpleAcp::ValidationError, "Invalid agent" unless agent.valid?
|
|
101
|
+
raise SimpleAcp::ConfigurationError, "Agent '#{agent.name}' already registered" if @agents.key?(agent.name)
|
|
102
|
+
|
|
103
|
+
@agents[agent.name] = agent
|
|
104
|
+
agent
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Unregister an agent by name.
|
|
108
|
+
#
|
|
109
|
+
# @param name [String] the agent name to remove
|
|
110
|
+
# @return [Agent, nil] the removed agent or nil if not found
|
|
111
|
+
def unregister(name)
|
|
112
|
+
@agents.delete(name)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Run an agent synchronously, blocking until completion.
|
|
116
|
+
#
|
|
117
|
+
# @param agent_name [String] name of the agent to run
|
|
118
|
+
# @param input [Array<Models::Message>, Models::Message, String] input messages
|
|
119
|
+
# @param session_id [String, nil] optional session ID for stateful interactions
|
|
120
|
+
# @param session [Models::Session, Hash, nil] optional session data
|
|
121
|
+
# @return [Models::Run] the completed run with output
|
|
122
|
+
# @raise [NotFoundError] if the agent is not found
|
|
123
|
+
def run_sync(agent_name:, input:, session_id: nil, session: nil)
|
|
124
|
+
run, context = prepare_run(agent_name, input, session_id, session)
|
|
125
|
+
|
|
126
|
+
begin
|
|
127
|
+
run.start!
|
|
128
|
+
@storage.save_run(run)
|
|
129
|
+
|
|
130
|
+
output_messages = []
|
|
131
|
+
execute_agent(context) do |yielded|
|
|
132
|
+
case yielded
|
|
133
|
+
when RunYield
|
|
134
|
+
output_messages << yielded.message
|
|
135
|
+
when RunYieldAwait
|
|
136
|
+
# Agent is awaiting - save state and return
|
|
137
|
+
return run
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
run.complete!(output_messages)
|
|
142
|
+
update_session_history(context.session, input, output_messages)
|
|
143
|
+
rescue StandardError => e
|
|
144
|
+
run.fail!(e.message)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
@storage.save_run(run)
|
|
148
|
+
run
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Run an agent asynchronously, returning immediately with a run ID.
|
|
152
|
+
#
|
|
153
|
+
# The agent executes in a background thread. Use {#cancel_run} to stop
|
|
154
|
+
# or poll the storage to check status.
|
|
155
|
+
#
|
|
156
|
+
# @param agent_name [String] name of the agent to run
|
|
157
|
+
# @param input [Array<Models::Message>, Models::Message, String] input messages
|
|
158
|
+
# @param session_id [String, nil] optional session ID
|
|
159
|
+
# @param session [Models::Session, Hash, nil] optional session data
|
|
160
|
+
# @return [Models::Run] the run (status will be :created or :in_progress)
|
|
161
|
+
# @raise [NotFoundError] if the agent is not found
|
|
162
|
+
def run_async(agent_name:, input:, session_id: nil, session: nil)
|
|
163
|
+
run, context = prepare_run(agent_name, input, session_id, session)
|
|
164
|
+
|
|
165
|
+
Thread.new do
|
|
166
|
+
begin
|
|
167
|
+
run.start!
|
|
168
|
+
@storage.save_run(run)
|
|
169
|
+
|
|
170
|
+
output_messages = []
|
|
171
|
+
awaiting = false
|
|
172
|
+
|
|
173
|
+
execute_agent(context) do |yielded|
|
|
174
|
+
case yielded
|
|
175
|
+
when RunYield
|
|
176
|
+
output_messages << yielded.message
|
|
177
|
+
when RunYieldAwait
|
|
178
|
+
awaiting = true
|
|
179
|
+
break
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Check if cancelled or awaiting before completing
|
|
184
|
+
if context.cancelled?
|
|
185
|
+
run.cancelled!
|
|
186
|
+
elsif !awaiting
|
|
187
|
+
run.complete!(output_messages)
|
|
188
|
+
update_session_history(context.session, input, output_messages)
|
|
189
|
+
end
|
|
190
|
+
# If awaiting, run is already in awaiting state from await_message
|
|
191
|
+
rescue StandardError => e
|
|
192
|
+
run.fail!(e.message)
|
|
193
|
+
ensure
|
|
194
|
+
@storage.save_run(run)
|
|
195
|
+
@running_contexts.delete(run.run_id)
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
run
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Run an agent with streaming output via Server-Sent Events.
|
|
203
|
+
#
|
|
204
|
+
# Yields events as the agent executes, enabling real-time response streaming.
|
|
205
|
+
#
|
|
206
|
+
# @param agent_name [String] name of the agent to run
|
|
207
|
+
# @param input [Array<Models::Message>, Models::Message, String] input messages
|
|
208
|
+
# @param session_id [String, nil] optional session ID
|
|
209
|
+
# @param session [Models::Session, Hash, nil] optional session data
|
|
210
|
+
# @yield [Models::Event] events as they occur during execution
|
|
211
|
+
# @yieldparam event [Models::RunCreatedEvent, Models::MessagePartEvent, Models::RunCompletedEvent, etc.]
|
|
212
|
+
# @return [void]
|
|
213
|
+
# @raise [NotFoundError] if the agent is not found
|
|
214
|
+
#
|
|
215
|
+
# @example
|
|
216
|
+
# server.run_stream(agent_name: "echo", input: "Hello") do |event|
|
|
217
|
+
# case event
|
|
218
|
+
# when Models::MessagePartEvent
|
|
219
|
+
# print event.part.content
|
|
220
|
+
# when Models::RunCompletedEvent
|
|
221
|
+
# puts "\nDone!"
|
|
222
|
+
# end
|
|
223
|
+
# end
|
|
224
|
+
def run_stream(agent_name:, input:, session_id: nil, session: nil)
|
|
225
|
+
run, context = prepare_run(agent_name, input, session_id, session)
|
|
226
|
+
|
|
227
|
+
begin
|
|
228
|
+
yield Models::RunCreatedEvent.new(run: run)
|
|
229
|
+
@storage.add_event(run.run_id, Models::RunCreatedEvent.new(run: run))
|
|
230
|
+
|
|
231
|
+
run.start!
|
|
232
|
+
@storage.save_run(run)
|
|
233
|
+
yield Models::RunInProgressEvent.new(run_id: run.run_id)
|
|
234
|
+
@storage.add_event(run.run_id, Models::RunInProgressEvent.new(run_id: run.run_id))
|
|
235
|
+
|
|
236
|
+
output_messages = []
|
|
237
|
+
|
|
238
|
+
execute_agent(context) do |yielded|
|
|
239
|
+
case yielded
|
|
240
|
+
when RunYield
|
|
241
|
+
message = yielded.message
|
|
242
|
+
output_messages << message
|
|
243
|
+
|
|
244
|
+
yield Models::MessageCreatedEvent.new(message: message)
|
|
245
|
+
@storage.add_event(run.run_id, Models::MessageCreatedEvent.new(message: message))
|
|
246
|
+
|
|
247
|
+
message.parts.each do |part|
|
|
248
|
+
yield Models::MessagePartEvent.new(part: part)
|
|
249
|
+
@storage.add_event(run.run_id, Models::MessagePartEvent.new(part: part))
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
yield Models::MessageCompletedEvent.new(message: message)
|
|
253
|
+
@storage.add_event(run.run_id, Models::MessageCompletedEvent.new(message: message))
|
|
254
|
+
when RunYieldAwait
|
|
255
|
+
yield Models::RunAwaitingEvent.new(run_id: run.run_id, await_request: yielded.request)
|
|
256
|
+
@storage.add_event(run.run_id, Models::RunAwaitingEvent.new(run_id: run.run_id, await_request: yielded.request))
|
|
257
|
+
return
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
run.complete!(output_messages)
|
|
262
|
+
update_session_history(context.session, input, output_messages)
|
|
263
|
+
|
|
264
|
+
yield Models::RunCompletedEvent.new(run: run)
|
|
265
|
+
@storage.add_event(run.run_id, Models::RunCompletedEvent.new(run: run))
|
|
266
|
+
rescue StandardError => e
|
|
267
|
+
run.fail!(e.message)
|
|
268
|
+
yield Models::RunFailedEvent.new(run_id: run.run_id, error: run.error)
|
|
269
|
+
@storage.add_event(run.run_id, Models::RunFailedEvent.new(run_id: run.run_id, error: run.error))
|
|
270
|
+
ensure
|
|
271
|
+
@storage.save_run(run)
|
|
272
|
+
@running_contexts.delete(run.run_id)
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Resume an awaited run synchronously.
|
|
277
|
+
#
|
|
278
|
+
# When an agent yields a {RunYieldAwait}, the run enters an "awaiting" state.
|
|
279
|
+
# Use this method to provide the requested input and continue execution.
|
|
280
|
+
#
|
|
281
|
+
# @param run_id [String] the run ID to resume
|
|
282
|
+
# @param await_resume [Models::AwaitResume] the resume payload with client response
|
|
283
|
+
# @return [Models::Run] the completed run
|
|
284
|
+
# @raise [NotFoundError] if the run is not found
|
|
285
|
+
# @raise [ValidationError] if the run is not in awaiting state
|
|
286
|
+
def resume_sync(run_id:, await_resume:)
|
|
287
|
+
run, context = prepare_resume(run_id, await_resume)
|
|
288
|
+
|
|
289
|
+
begin
|
|
290
|
+
run.start!
|
|
291
|
+
@storage.save_run(run)
|
|
292
|
+
|
|
293
|
+
output_messages = run.output.dup
|
|
294
|
+
|
|
295
|
+
execute_agent(context) do |yielded|
|
|
296
|
+
case yielded
|
|
297
|
+
when RunYield
|
|
298
|
+
output_messages << yielded.message
|
|
299
|
+
when RunYieldAwait
|
|
300
|
+
return run
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
run.complete!(output_messages)
|
|
305
|
+
update_session_history(context.session, [await_resume.message].compact, output_messages)
|
|
306
|
+
rescue StandardError => e
|
|
307
|
+
run.fail!(e.message)
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
@storage.save_run(run)
|
|
311
|
+
run
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Resume an awaited run with streaming output.
|
|
315
|
+
#
|
|
316
|
+
# @param run_id [String] the run ID to resume
|
|
317
|
+
# @param await_resume [Models::AwaitResume] the resume payload with client response
|
|
318
|
+
# @yield [Models::Event] events as they occur during execution
|
|
319
|
+
# @return [void]
|
|
320
|
+
# @raise [NotFoundError] if the run is not found
|
|
321
|
+
# @raise [ValidationError] if the run is not in awaiting state
|
|
322
|
+
def resume_stream(run_id:, await_resume:)
|
|
323
|
+
run, context = prepare_resume(run_id, await_resume)
|
|
324
|
+
|
|
325
|
+
begin
|
|
326
|
+
run.start!
|
|
327
|
+
@storage.save_run(run)
|
|
328
|
+
yield Models::RunInProgressEvent.new(run_id: run.run_id)
|
|
329
|
+
@storage.add_event(run.run_id, Models::RunInProgressEvent.new(run_id: run.run_id))
|
|
330
|
+
|
|
331
|
+
output_messages = run.output.dup
|
|
332
|
+
|
|
333
|
+
execute_agent(context) do |yielded|
|
|
334
|
+
case yielded
|
|
335
|
+
when RunYield
|
|
336
|
+
message = yielded.message
|
|
337
|
+
output_messages << message
|
|
338
|
+
|
|
339
|
+
yield Models::MessageCreatedEvent.new(message: message)
|
|
340
|
+
@storage.add_event(run.run_id, Models::MessageCreatedEvent.new(message: message))
|
|
341
|
+
|
|
342
|
+
yield Models::MessageCompletedEvent.new(message: message)
|
|
343
|
+
@storage.add_event(run.run_id, Models::MessageCompletedEvent.new(message: message))
|
|
344
|
+
when RunYieldAwait
|
|
345
|
+
yield Models::RunAwaitingEvent.new(run_id: run.run_id, await_request: yielded.request)
|
|
346
|
+
@storage.add_event(run.run_id, Models::RunAwaitingEvent.new(run_id: run.run_id, await_request: yielded.request))
|
|
347
|
+
return
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
run.complete!(output_messages)
|
|
352
|
+
yield Models::RunCompletedEvent.new(run: run)
|
|
353
|
+
@storage.add_event(run.run_id, Models::RunCompletedEvent.new(run: run))
|
|
354
|
+
rescue StandardError => e
|
|
355
|
+
run.fail!(e.message)
|
|
356
|
+
yield Models::RunFailedEvent.new(run_id: run.run_id, error: run.error)
|
|
357
|
+
@storage.add_event(run.run_id, Models::RunFailedEvent.new(run_id: run.run_id, error: run.error))
|
|
358
|
+
ensure
|
|
359
|
+
@storage.save_run(run)
|
|
360
|
+
@running_contexts.delete(run.run_id)
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Cancel a running agent execution.
|
|
365
|
+
#
|
|
366
|
+
# @param run_id [String] the run ID to cancel
|
|
367
|
+
# @return [Models::Run] the cancelled run
|
|
368
|
+
# @raise [NotFoundError] if the run is not found
|
|
369
|
+
def cancel_run(run_id)
|
|
370
|
+
run = @storage.get_run(run_id)
|
|
371
|
+
raise SimpleAcp::NotFoundError, "Run '#{run_id}' not found" unless run
|
|
372
|
+
|
|
373
|
+
context = @running_contexts[run_id]
|
|
374
|
+
context&.cancel!
|
|
375
|
+
|
|
376
|
+
run.cancelled!
|
|
377
|
+
@storage.save_run(run)
|
|
378
|
+
run
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Create a Rack-compatible application.
|
|
382
|
+
#
|
|
383
|
+
# @return [Roda] the Rack application
|
|
384
|
+
def to_app
|
|
385
|
+
# Create a subclass to avoid freezing the base App class
|
|
386
|
+
app_class = Class.new(App)
|
|
387
|
+
app_class.configure(self)
|
|
388
|
+
app_class.freeze.app
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Start the HTTP server using Falcon.
|
|
392
|
+
#
|
|
393
|
+
# Falcon provides fiber-based concurrency for efficient handling
|
|
394
|
+
# of SSE streams and long-lived connections.
|
|
395
|
+
#
|
|
396
|
+
# @param port [Integer] port to listen on (default: 8000)
|
|
397
|
+
# @param host [String] host to bind to (default: "0.0.0.0")
|
|
398
|
+
# @param options [Hash] additional Falcon configuration options
|
|
399
|
+
# @return [void]
|
|
400
|
+
def run(port: 8000, host: "0.0.0.0", **options)
|
|
401
|
+
require_relative "falcon_runner"
|
|
402
|
+
|
|
403
|
+
app = to_app
|
|
404
|
+
|
|
405
|
+
puts "Registered agents: #{@agents.keys.join(', ')}"
|
|
406
|
+
|
|
407
|
+
FalconRunner.run(app, port: port, host: host, **options)
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
private
|
|
411
|
+
|
|
412
|
+
def prepare_run(agent_name, input, session_id, session_data)
|
|
413
|
+
agent = @agents[agent_name]
|
|
414
|
+
raise SimpleAcp::NotFoundError, "Agent '#{agent_name}' not found" unless agent
|
|
415
|
+
|
|
416
|
+
session = resolve_session(session_id, session_data)
|
|
417
|
+
|
|
418
|
+
run = Models::Run.new(
|
|
419
|
+
agent_name: agent_name,
|
|
420
|
+
session_id: session&.id
|
|
421
|
+
)
|
|
422
|
+
@storage.save_run(run)
|
|
423
|
+
|
|
424
|
+
context = Context.new(
|
|
425
|
+
run: run,
|
|
426
|
+
session: session,
|
|
427
|
+
input: input,
|
|
428
|
+
server: self
|
|
429
|
+
)
|
|
430
|
+
@running_contexts[run.run_id] = context
|
|
431
|
+
|
|
432
|
+
[run, context]
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def prepare_resume(run_id, await_resume)
|
|
436
|
+
run = @storage.get_run(run_id)
|
|
437
|
+
raise SimpleAcp::NotFoundError, "Run '#{run_id}' not found" unless run
|
|
438
|
+
raise SimpleAcp::ValidationError, "Run is not awaiting" unless run.awaiting?
|
|
439
|
+
|
|
440
|
+
session = run.session_id ? @storage.get_session(run.session_id) : nil
|
|
441
|
+
|
|
442
|
+
context = ResumeContext.new(
|
|
443
|
+
run: run,
|
|
444
|
+
session: session,
|
|
445
|
+
input: [],
|
|
446
|
+
server: self,
|
|
447
|
+
await_resume: await_resume
|
|
448
|
+
)
|
|
449
|
+
@running_contexts[run.run_id] = context
|
|
450
|
+
|
|
451
|
+
[run, context]
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
def resolve_session(session_id, session_data)
|
|
455
|
+
if session_data
|
|
456
|
+
session = Models::Session.from_hash(session_data.is_a?(Hash) ? session_data : session_data.to_h)
|
|
457
|
+
@storage.save_session(session)
|
|
458
|
+
session
|
|
459
|
+
elsif session_id
|
|
460
|
+
@storage.get_session(session_id) || begin
|
|
461
|
+
session = Models::Session.new(id: session_id)
|
|
462
|
+
@storage.save_session(session)
|
|
463
|
+
session
|
|
464
|
+
end
|
|
465
|
+
end
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def execute_agent(context)
|
|
469
|
+
agent = @agents[context.agent_name]
|
|
470
|
+
return unless agent
|
|
471
|
+
|
|
472
|
+
result = agent.call(context)
|
|
473
|
+
|
|
474
|
+
if result.respond_to?(:each)
|
|
475
|
+
result.each do |item|
|
|
476
|
+
break if context.cancelled?
|
|
477
|
+
|
|
478
|
+
case item
|
|
479
|
+
when RunYield, RunYieldAwait
|
|
480
|
+
yield item
|
|
481
|
+
when Models::Message
|
|
482
|
+
yield RunYield.new(item)
|
|
483
|
+
when String
|
|
484
|
+
yield RunYield.new(Models::Message.agent(item))
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def update_session_history(session, input, output)
|
|
491
|
+
return unless session
|
|
492
|
+
|
|
493
|
+
Array(input).each { |msg| session.add_to_history(msg) }
|
|
494
|
+
Array(output).each { |msg| session.add_to_history(msg) }
|
|
495
|
+
@storage.save_session(session)
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def handler_name(handler)
|
|
499
|
+
case handler
|
|
500
|
+
when Method
|
|
501
|
+
handler.name.to_s
|
|
502
|
+
when Proc
|
|
503
|
+
"anonymous_agent"
|
|
504
|
+
else
|
|
505
|
+
handler.class.name.downcase.gsub("::", "_")
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|