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,329 @@
|
|
|
1
|
+
# Multi-Turn Conversations
|
|
2
|
+
|
|
3
|
+
Multi-turn conversations allow agents to maintain context across multiple interactions, request additional input, and build stateful experiences.
|
|
4
|
+
|
|
5
|
+
## Using Sessions
|
|
6
|
+
|
|
7
|
+
Sessions maintain history and state:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
server.agent("chat") do |context|
|
|
11
|
+
# Access conversation history
|
|
12
|
+
history = context.history
|
|
13
|
+
|
|
14
|
+
# Build context from history
|
|
15
|
+
all_messages = history + context.input
|
|
16
|
+
|
|
17
|
+
# Generate response considering full context
|
|
18
|
+
response = generate_response(all_messages)
|
|
19
|
+
|
|
20
|
+
SimpleAcp::Models::Message.agent(response)
|
|
21
|
+
end
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Session History
|
|
25
|
+
|
|
26
|
+
History automatically accumulates:
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
# First interaction
|
|
30
|
+
# history: []
|
|
31
|
+
# input: [user: "Hello"]
|
|
32
|
+
# After: history becomes [user: "Hello", agent: "Hi!"]
|
|
33
|
+
|
|
34
|
+
# Second interaction
|
|
35
|
+
# history: [user: "Hello", agent: "Hi!"]
|
|
36
|
+
# input: [user: "How are you?"]
|
|
37
|
+
# After: history becomes [user: "Hello", agent: "Hi!", user: "How are you?", agent: "I'm good!"]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Session State
|
|
41
|
+
|
|
42
|
+
Custom state persists across turns:
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
server.agent("counter") do |context|
|
|
46
|
+
count = context.state || 0
|
|
47
|
+
count += 1
|
|
48
|
+
context.set_state(count)
|
|
49
|
+
|
|
50
|
+
SimpleAcp::Models::Message.agent("Turn #{count}")
|
|
51
|
+
end
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Complex State
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
server.agent("form") do |context|
|
|
58
|
+
state = context.state || { step: 1, data: {} }
|
|
59
|
+
|
|
60
|
+
case state[:step]
|
|
61
|
+
when 1
|
|
62
|
+
context.set_state(state.merge(step: 2))
|
|
63
|
+
SimpleAcp::Models::Message.agent("Enter your name:")
|
|
64
|
+
when 2
|
|
65
|
+
state[:data][:name] = context.input.first&.text_content
|
|
66
|
+
context.set_state(state.merge(step: 3))
|
|
67
|
+
SimpleAcp::Models::Message.agent("Enter your email:")
|
|
68
|
+
when 3
|
|
69
|
+
state[:data][:email] = context.input.first&.text_content
|
|
70
|
+
context.set_state(state.merge(step: :done))
|
|
71
|
+
SimpleAcp::Models::Message.agent(
|
|
72
|
+
"Thanks #{state[:data][:name]}! We'll contact you at #{state[:data][:email]}"
|
|
73
|
+
)
|
|
74
|
+
else
|
|
75
|
+
SimpleAcp::Models::Message.agent("Form complete!")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Awaiting Input
|
|
81
|
+
|
|
82
|
+
For synchronous input requests within a single run:
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
server.agent("questioner") do |context|
|
|
86
|
+
Enumerator.new do |yielder|
|
|
87
|
+
# Ask first question
|
|
88
|
+
result = context.await_message(
|
|
89
|
+
SimpleAcp::Models::Message.agent("What is your name?")
|
|
90
|
+
)
|
|
91
|
+
yielder << result
|
|
92
|
+
|
|
93
|
+
# After resume, get the answer
|
|
94
|
+
name = context.resume_message&.text_content
|
|
95
|
+
|
|
96
|
+
# Ask second question
|
|
97
|
+
result = context.await_message(
|
|
98
|
+
SimpleAcp::Models::Message.agent("What is your favorite color, #{name}?")
|
|
99
|
+
)
|
|
100
|
+
yielder << result
|
|
101
|
+
|
|
102
|
+
color = context.resume_message&.text_content
|
|
103
|
+
|
|
104
|
+
# Final response
|
|
105
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
106
|
+
SimpleAcp::Models::Message.agent("Great choice, #{name}! #{color} is nice.")
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Await Flow
|
|
113
|
+
|
|
114
|
+
```mermaid
|
|
115
|
+
sequenceDiagram
|
|
116
|
+
participant Client
|
|
117
|
+
participant Server
|
|
118
|
+
participant Agent
|
|
119
|
+
|
|
120
|
+
Client->>Server: run_sync (input)
|
|
121
|
+
Server->>Agent: execute
|
|
122
|
+
Agent-->>Server: await_message("Name?")
|
|
123
|
+
Server-->>Client: Run (awaiting)
|
|
124
|
+
|
|
125
|
+
Client->>Server: run_resume (name)
|
|
126
|
+
Server->>Agent: continue with resume
|
|
127
|
+
Agent-->>Server: await_message("Color?")
|
|
128
|
+
Server-->>Client: Run (awaiting)
|
|
129
|
+
|
|
130
|
+
Client->>Server: run_resume (color)
|
|
131
|
+
Server->>Agent: continue with resume
|
|
132
|
+
Agent-->>Server: final response
|
|
133
|
+
Server-->>Client: Run (completed)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Client-Side Await Handling
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
run = client.run_sync(agent: "questioner", input: [...])
|
|
140
|
+
|
|
141
|
+
while run.awaiting?
|
|
142
|
+
puts run.await_request.message.text_content
|
|
143
|
+
|
|
144
|
+
answer = gets.chomp
|
|
145
|
+
|
|
146
|
+
run = client.run_resume_sync(
|
|
147
|
+
run_id: run.run_id,
|
|
148
|
+
await_resume: SimpleAcp::Models::MessageAwaitResume.new(
|
|
149
|
+
message: SimpleAcp::Models::Message.user(answer)
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
puts "Final: #{run.output.last.text_content}"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Conversation Patterns
|
|
158
|
+
|
|
159
|
+
### Contextual Responses
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
server.agent("assistant") do |context|
|
|
163
|
+
# Build full conversation
|
|
164
|
+
conversation = context.history.map do |msg|
|
|
165
|
+
{ role: msg.role, content: msg.text_content }
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
conversation << {
|
|
169
|
+
role: "user",
|
|
170
|
+
content: context.input.first&.text_content
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Use LLM with full context
|
|
174
|
+
response = llm.chat(conversation)
|
|
175
|
+
|
|
176
|
+
SimpleAcp::Models::Message.agent(response)
|
|
177
|
+
end
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Memory Management
|
|
181
|
+
|
|
182
|
+
Limit history for efficiency:
|
|
183
|
+
|
|
184
|
+
```ruby
|
|
185
|
+
server.agent("bounded-chat") do |context|
|
|
186
|
+
# Only use last 10 messages
|
|
187
|
+
recent_history = context.history.last(10)
|
|
188
|
+
|
|
189
|
+
conversation = recent_history + context.input
|
|
190
|
+
|
|
191
|
+
response = generate_response(conversation)
|
|
192
|
+
|
|
193
|
+
SimpleAcp::Models::Message.agent(response)
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Summarization
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
server.agent("summarizing-chat") do |context|
|
|
201
|
+
state = context.state || { summary: nil }
|
|
202
|
+
|
|
203
|
+
if context.history.length > 20
|
|
204
|
+
# Summarize older history
|
|
205
|
+
old_messages = context.history[0..-11]
|
|
206
|
+
state[:summary] = summarize(old_messages)
|
|
207
|
+
context.set_state(state)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Use summary + recent history
|
|
211
|
+
conversation_context = [
|
|
212
|
+
state[:summary] ? "Previous context: #{state[:summary]}" : nil,
|
|
213
|
+
*context.history.last(10).map(&:text_content),
|
|
214
|
+
context.input.first&.text_content
|
|
215
|
+
].compact
|
|
216
|
+
|
|
217
|
+
response = generate_response(conversation_context)
|
|
218
|
+
|
|
219
|
+
SimpleAcp::Models::Message.agent(response)
|
|
220
|
+
end
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Conversation Reset
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
server.agent("resettable") do |context|
|
|
227
|
+
command = context.input.first&.text_content
|
|
228
|
+
|
|
229
|
+
if command&.downcase == "reset"
|
|
230
|
+
context.set_state(nil)
|
|
231
|
+
return SimpleAcp::Models::Message.agent("Conversation reset!")
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Normal processing...
|
|
235
|
+
end
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Interactive Workflows
|
|
239
|
+
|
|
240
|
+
### Quiz Game
|
|
241
|
+
|
|
242
|
+
```ruby
|
|
243
|
+
server.agent("quiz") do |context|
|
|
244
|
+
state = context.state || {
|
|
245
|
+
score: 0,
|
|
246
|
+
question_index: 0,
|
|
247
|
+
questions: load_questions
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if state[:question_index] > 0
|
|
251
|
+
# Check previous answer
|
|
252
|
+
answer = context.input.first&.text_content
|
|
253
|
+
correct = state[:questions][state[:question_index] - 1][:answer]
|
|
254
|
+
|
|
255
|
+
if answer&.downcase == correct.downcase
|
|
256
|
+
state[:score] += 1
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
if state[:question_index] >= state[:questions].length
|
|
261
|
+
context.set_state(nil)
|
|
262
|
+
return SimpleAcp::Models::Message.agent(
|
|
263
|
+
"Quiz complete! Score: #{state[:score]}/#{state[:questions].length}"
|
|
264
|
+
)
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
question = state[:questions][state[:question_index]]
|
|
268
|
+
state[:question_index] += 1
|
|
269
|
+
context.set_state(state)
|
|
270
|
+
|
|
271
|
+
SimpleAcp::Models::Message.agent(question[:text])
|
|
272
|
+
end
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Shopping Assistant
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
server.agent("shop") do |context|
|
|
279
|
+
cart = context.state || { items: [], total: 0.0 }
|
|
280
|
+
command = context.input.first&.text_content&.downcase
|
|
281
|
+
|
|
282
|
+
response = case command
|
|
283
|
+
when /^add (.+)/
|
|
284
|
+
item = find_product($1)
|
|
285
|
+
if item
|
|
286
|
+
cart[:items] << item
|
|
287
|
+
cart[:total] += item[:price]
|
|
288
|
+
"Added #{item[:name]} ($#{item[:price]})"
|
|
289
|
+
else
|
|
290
|
+
"Product not found"
|
|
291
|
+
end
|
|
292
|
+
when /^remove (.+)/
|
|
293
|
+
item = cart[:items].find { |i| i[:name].downcase.include?($1) }
|
|
294
|
+
if item
|
|
295
|
+
cart[:items].delete(item)
|
|
296
|
+
cart[:total] -= item[:price]
|
|
297
|
+
"Removed #{item[:name]}"
|
|
298
|
+
else
|
|
299
|
+
"Item not in cart"
|
|
300
|
+
end
|
|
301
|
+
when "cart"
|
|
302
|
+
items_list = cart[:items].map { |i| "- #{i[:name]}: $#{i[:price]}" }.join("\n")
|
|
303
|
+
"Cart:\n#{items_list}\nTotal: $#{cart[:total]}"
|
|
304
|
+
when "checkout"
|
|
305
|
+
total = cart[:total]
|
|
306
|
+
context.set_state(nil)
|
|
307
|
+
"Order placed! Total: $#{total}"
|
|
308
|
+
else
|
|
309
|
+
"Commands: add <item>, remove <item>, cart, checkout"
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
context.set_state(cart)
|
|
313
|
+
SimpleAcp::Models::Message.agent(response)
|
|
314
|
+
end
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Best Practices
|
|
318
|
+
|
|
319
|
+
1. **Limit history size** - Don't let history grow unbounded
|
|
320
|
+
2. **Use state wisely** - Store minimal necessary data
|
|
321
|
+
3. **Handle resets** - Allow users to start fresh
|
|
322
|
+
4. **Validate state** - Check for expected structure
|
|
323
|
+
5. **Test edge cases** - Empty history, missing state, etc.
|
|
324
|
+
|
|
325
|
+
## Next Steps
|
|
326
|
+
|
|
327
|
+
- Review [Sessions](../core-concepts/sessions.md) concept
|
|
328
|
+
- See [Client Sessions](../client/sessions.md) for client-side handling
|
|
329
|
+
- Explore [HTTP Endpoints](http-endpoints.md) for session APIs
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Streaming Responses
|
|
2
|
+
|
|
3
|
+
Streaming enables real-time delivery of agent responses via Server-Sent Events (SSE). This provides a better user experience for long-running operations.
|
|
4
|
+
|
|
5
|
+
## Why Streaming?
|
|
6
|
+
|
|
7
|
+
- **Immediate Feedback**: Users see responses as they're generated
|
|
8
|
+
- **Progress Visibility**: Track long operations in real-time
|
|
9
|
+
- **Resource Efficiency**: Process data incrementally
|
|
10
|
+
- **Better UX**: No waiting for complete responses
|
|
11
|
+
|
|
12
|
+
## Basic Streaming
|
|
13
|
+
|
|
14
|
+
Return an `Enumerator` to enable streaming:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
server.agent("streamer") do |context|
|
|
18
|
+
Enumerator.new do |yielder|
|
|
19
|
+
5.times do |i|
|
|
20
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
21
|
+
SimpleAcp::Models::Message.agent("Message #{i + 1}")
|
|
22
|
+
)
|
|
23
|
+
sleep 0.5
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## RunYield
|
|
30
|
+
|
|
31
|
+
`RunYield` wraps messages for streaming:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# Single message
|
|
35
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
36
|
+
SimpleAcp::Models::Message.agent("Hello")
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Multiple messages
|
|
40
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
41
|
+
[
|
|
42
|
+
SimpleAcp::Models::Message.agent("First"),
|
|
43
|
+
SimpleAcp::Models::Message.agent("Second")
|
|
44
|
+
]
|
|
45
|
+
)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Streaming Patterns
|
|
49
|
+
|
|
50
|
+
### Token-by-Token
|
|
51
|
+
|
|
52
|
+
Stream individual tokens for typing effect:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
server.agent("typer") do |context|
|
|
56
|
+
text = context.input.first&.text_content || "Hello World"
|
|
57
|
+
|
|
58
|
+
Enumerator.new do |yielder|
|
|
59
|
+
text.chars.each do |char|
|
|
60
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
61
|
+
SimpleAcp::Models::Message.agent(char)
|
|
62
|
+
)
|
|
63
|
+
sleep 0.05
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Word-by-Word
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
server.agent("word-stream") do |context|
|
|
73
|
+
text = context.input.first&.text_content || ""
|
|
74
|
+
|
|
75
|
+
Enumerator.new do |yielder|
|
|
76
|
+
text.split.each do |word|
|
|
77
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
78
|
+
SimpleAcp::Models::Message.agent(word + " ")
|
|
79
|
+
)
|
|
80
|
+
sleep 0.1
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Progress Updates
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
server.agent("processor") do |context|
|
|
90
|
+
items = parse_items(context.input)
|
|
91
|
+
total = items.length
|
|
92
|
+
|
|
93
|
+
Enumerator.new do |yielder|
|
|
94
|
+
items.each_with_index do |item, i|
|
|
95
|
+
# Process item
|
|
96
|
+
result = process(item)
|
|
97
|
+
|
|
98
|
+
# Report progress
|
|
99
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
100
|
+
SimpleAcp::Models::Message.agent(
|
|
101
|
+
"Processing #{i + 1}/#{total}: #{result}"
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
107
|
+
SimpleAcp::Models::Message.agent("Complete!")
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### LLM Integration
|
|
114
|
+
|
|
115
|
+
Stream responses from language models:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
server.agent("chat") do |context|
|
|
119
|
+
messages = context.history + context.input
|
|
120
|
+
|
|
121
|
+
Enumerator.new do |yielder|
|
|
122
|
+
llm.stream_chat(messages) do |chunk|
|
|
123
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
124
|
+
SimpleAcp::Models::Message.agent(chunk)
|
|
125
|
+
)
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Mixed Content
|
|
132
|
+
|
|
133
|
+
Stream different content types:
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
server.agent("analyzer") do |context|
|
|
137
|
+
data = parse_data(context.input)
|
|
138
|
+
|
|
139
|
+
Enumerator.new do |yielder|
|
|
140
|
+
# Text update
|
|
141
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
142
|
+
SimpleAcp::Models::Message.agent("Analyzing data...")
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
result = analyze(data)
|
|
146
|
+
|
|
147
|
+
# JSON result
|
|
148
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
149
|
+
SimpleAcp::Models::Message.agent(
|
|
150
|
+
SimpleAcp::Models::MessagePart.json(result)
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Summary text
|
|
155
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
156
|
+
SimpleAcp::Models::Message.agent("Analysis complete!")
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Event Types
|
|
163
|
+
|
|
164
|
+
Streaming produces these events:
|
|
165
|
+
|
|
166
|
+
```mermaid
|
|
167
|
+
sequenceDiagram
|
|
168
|
+
participant Client
|
|
169
|
+
participant Server
|
|
170
|
+
|
|
171
|
+
Server-->>Client: RunStartedEvent
|
|
172
|
+
Server-->>Client: MessageCreatedEvent
|
|
173
|
+
Server-->>Client: MessagePartEvent
|
|
174
|
+
Server-->>Client: MessageCompletedEvent
|
|
175
|
+
Note over Server,Client: Repeat for each message
|
|
176
|
+
Server-->>Client: RunCompletedEvent
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## HTTP Streaming
|
|
180
|
+
|
|
181
|
+
The server uses SSE for HTTP streaming:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
POST /runs HTTP/1.1
|
|
185
|
+
Accept: text/event-stream
|
|
186
|
+
Content-Type: application/json
|
|
187
|
+
|
|
188
|
+
{"agent_name": "chat", "input": [...]}
|
|
189
|
+
|
|
190
|
+
HTTP/1.1 200 OK
|
|
191
|
+
Content-Type: text/event-stream
|
|
192
|
+
|
|
193
|
+
event: run_started
|
|
194
|
+
data: {"run_id":"abc123"}
|
|
195
|
+
|
|
196
|
+
event: message_created
|
|
197
|
+
data: {"message":{"role":"agent","parts":[]}}
|
|
198
|
+
|
|
199
|
+
event: message_part
|
|
200
|
+
data: {"part":{"content_type":"text/plain","content":"Hello"}}
|
|
201
|
+
|
|
202
|
+
event: message_completed
|
|
203
|
+
data: {"message":{"role":"agent","parts":[...]}}
|
|
204
|
+
|
|
205
|
+
event: run_completed
|
|
206
|
+
data: {"run":{...}}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Server-Side Streaming
|
|
210
|
+
|
|
211
|
+
Execute streaming runs programmatically:
|
|
212
|
+
|
|
213
|
+
```ruby
|
|
214
|
+
server.run_stream(
|
|
215
|
+
agent_name: "chat",
|
|
216
|
+
input: messages
|
|
217
|
+
) do |event|
|
|
218
|
+
case event
|
|
219
|
+
when SimpleAcp::Models::MessagePartEvent
|
|
220
|
+
print event.part.content
|
|
221
|
+
when SimpleAcp::Models::RunCompletedEvent
|
|
222
|
+
puts "\nDone!"
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## Error Handling
|
|
228
|
+
|
|
229
|
+
Handle errors during streaming:
|
|
230
|
+
|
|
231
|
+
```ruby
|
|
232
|
+
server.agent("safe-streamer") do |context|
|
|
233
|
+
Enumerator.new do |yielder|
|
|
234
|
+
begin
|
|
235
|
+
process_stream(context.input) do |chunk|
|
|
236
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
237
|
+
SimpleAcp::Models::Message.agent(chunk)
|
|
238
|
+
)
|
|
239
|
+
end
|
|
240
|
+
rescue StreamError => e
|
|
241
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
242
|
+
SimpleAcp::Models::Message.agent("Stream error: #{e.message}")
|
|
243
|
+
)
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Buffering
|
|
250
|
+
|
|
251
|
+
For high-frequency updates, consider buffering:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
server.agent("buffered") do |context|
|
|
255
|
+
Enumerator.new do |yielder|
|
|
256
|
+
buffer = []
|
|
257
|
+
|
|
258
|
+
process_items(context.input) do |item|
|
|
259
|
+
buffer << item
|
|
260
|
+
|
|
261
|
+
# Flush every 10 items
|
|
262
|
+
if buffer.length >= 10
|
|
263
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
264
|
+
SimpleAcp::Models::Message.agent(buffer.join)
|
|
265
|
+
)
|
|
266
|
+
buffer.clear
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Flush remaining
|
|
271
|
+
unless buffer.empty?
|
|
272
|
+
yielder << SimpleAcp::Server::RunYield.new(
|
|
273
|
+
SimpleAcp::Models::Message.agent(buffer.join)
|
|
274
|
+
)
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Best Practices
|
|
281
|
+
|
|
282
|
+
1. **Yield frequently** - Don't wait too long between yields
|
|
283
|
+
2. **Handle errors** - Catch and report errors gracefully
|
|
284
|
+
3. **Use appropriate chunking** - Balance frequency with overhead
|
|
285
|
+
4. **Test streaming** - Verify event order and content
|
|
286
|
+
5. **Monitor memory** - Stream large responses to avoid memory issues
|
|
287
|
+
|
|
288
|
+
## Testing Streaming
|
|
289
|
+
|
|
290
|
+
```ruby
|
|
291
|
+
def test_streaming_agent
|
|
292
|
+
events = []
|
|
293
|
+
|
|
294
|
+
server.run_stream(
|
|
295
|
+
agent_name: "streamer",
|
|
296
|
+
input: [SimpleAcp::Models::Message.user("test")]
|
|
297
|
+
) do |event|
|
|
298
|
+
events << event
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Verify event sequence
|
|
302
|
+
assert events.first.is_a?(SimpleAcp::Models::RunStartedEvent)
|
|
303
|
+
assert events.last.is_a?(SimpleAcp::Models::RunCompletedEvent)
|
|
304
|
+
|
|
305
|
+
# Check message events
|
|
306
|
+
message_events = events.select { |e| e.is_a?(SimpleAcp::Models::MessagePartEvent) }
|
|
307
|
+
assert message_events.length > 0
|
|
308
|
+
end
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Next Steps
|
|
312
|
+
|
|
313
|
+
- Learn about [Multi-Turn Conversations](multi-turn.md)
|
|
314
|
+
- See [Client Streaming](../client/streaming.md) for consuming streams
|
|
315
|
+
- Review [Events](../core-concepts/events.md) for event details
|