robot_lab-a2a 0.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 (41) hide show
  1. checksums.yaml +7 -0
  2. data/.envrc +1 -0
  3. data/.github/workflows/deploy-github-pages.yml +52 -0
  4. data/.rubocop.yml +26 -0
  5. data/CHANGELOG.md +24 -0
  6. data/COMMITS.md +196 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +106 -0
  9. data/Rakefile +104 -0
  10. data/docs/assets/images/architecture.png +0 -0
  11. data/docs/assets/images/architecture.svg +258 -0
  12. data/docs/examples.md +116 -0
  13. data/docs/getting-started.md +103 -0
  14. data/docs/index.md +23 -0
  15. data/docs/interactive-modes.md +104 -0
  16. data/docs/server-api.md +118 -0
  17. data/examples/01_sync_robot/client.rb +94 -0
  18. data/examples/01_sync_robot/server.rb +45 -0
  19. data/examples/02_interactive_a2a_tool/client.rb +144 -0
  20. data/examples/02_interactive_a2a_tool/server.rb +78 -0
  21. data/examples/03_robot_network/client.rb +83 -0
  22. data/examples/03_robot_network/server.rb +77 -0
  23. data/examples/04_io_bridge/client.rb +140 -0
  24. data/examples/04_io_bridge/server.rb +64 -0
  25. data/examples/05_multi_agent/client.rb +97 -0
  26. data/examples/05_multi_agent/server.rb +76 -0
  27. data/examples/06_rack_mount/client.rb +90 -0
  28. data/examples/06_rack_mount/config.ru +44 -0
  29. data/examples/06_rack_mount/server.rb +72 -0
  30. data/examples/common_config.rb +9 -0
  31. data/examples/run +112 -0
  32. data/lib/robot_lab/a2a/ask_user_tool.rb +43 -0
  33. data/lib/robot_lab/a2a/io_bridge.rb +75 -0
  34. data/lib/robot_lab/a2a/network_adapter.rb +38 -0
  35. data/lib/robot_lab/a2a/registry.rb +36 -0
  36. data/lib/robot_lab/a2a/robot_adapter.rb +183 -0
  37. data/lib/robot_lab/a2a/server.rb +128 -0
  38. data/lib/robot_lab/a2a/version.rb +7 -0
  39. data/lib/robot_lab/a2a.rb +39 -0
  40. data/mkdocs.yml +153 -0
  41. metadata +128 -0
@@ -0,0 +1,258 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600" width="900" height="600">
2
+ <rect width="900" height="600" fill="#0e0e14"/>
3
+
4
+ <!-- ── Title ── -->
5
+ <text x="450" y="36" text-anchor="middle" fill="#e0e0f0" font-family="Roboto Mono, Consolas, monospace" font-size="18" font-weight="bold">robot_lab-a2a — Architecture</text>
6
+
7
+ <!-- ══════════════════════════════════════════════════════════════
8
+ Column labels
9
+ ══════════════════════════════════════════════════════════════ -->
10
+ <text x="68" y="68" text-anchor="middle" fill="#888" font-family="Roboto Mono, monospace" font-size="10">CLIENT</text>
11
+ <text x="230" y="68" text-anchor="middle" fill="#888" font-family="Roboto Mono, monospace" font-size="10">HTTP LAYER</text>
12
+ <text x="450" y="68" text-anchor="middle" fill="#888" font-family="Roboto Mono, monospace" font-size="10">ADAPTER LAYER</text>
13
+ <text x="700" y="68" text-anchor="middle" fill="#888" font-family="Roboto Mono, monospace" font-size="10">ROBOT LAYER</text>
14
+
15
+ <!-- ══════════════════════════════════════════════════════════════
16
+ A2A CLIENT (col 1)
17
+ ══════════════════════════════════════════════════════════════ -->
18
+ <rect x="14" y="80" width="108" height="440" rx="8" fill="#1a1030" stroke="#6633bb" stroke-width="1.5"/>
19
+ <text x="68" y="100" text-anchor="middle" fill="#bb88ff" font-family="Roboto Mono, monospace" font-size="11" font-weight="bold">A2A Client</text>
20
+
21
+ <!-- SDK box -->
22
+ <rect x="24" y="110" width="88" height="34" rx="5" fill="#2a1f50" stroke="#7755cc" stroke-width="1"/>
23
+ <text x="68" y="124" text-anchor="middle" fill="#ccaaff" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">Ruby SDK</text>
24
+ <text x="68" y="137" text-anchor="middle" fill="#9977cc" font-family="Roboto Mono, monospace" font-size="8">A2A.client(url:)</text>
25
+
26
+ <!-- curl box -->
27
+ <rect x="24" y="154" width="88" height="26" rx="5" fill="#2a1f50" stroke="#7755cc" stroke-width="1"/>
28
+ <text x="68" y="171" text-anchor="middle" fill="#ccaaff" font-family="Roboto Mono, monospace" font-size="9">curl / HTTP</text>
29
+
30
+ <!-- tasks/send -->
31
+ <rect x="24" y="200" width="88" height="50" rx="5" fill="#1e1440" stroke="#5533aa" stroke-width="1" stroke-dasharray="4,2"/>
32
+ <text x="68" y="215" text-anchor="middle" fill="#aa88ee" font-family="Roboto Mono, monospace" font-size="8">tasks/send</text>
33
+ <text x="68" y="228" text-anchor="middle" fill="#8866cc" font-family="Roboto Mono, monospace" font-size="8">tasks/get</text>
34
+ <text x="68" y="241" text-anchor="middle" fill="#8866cc" font-family="Roboto Mono, monospace" font-size="8">tasks/cancel</text>
35
+
36
+ <!-- input_required round-trip note -->
37
+ <rect x="24" y="268" width="88" height="44" rx="5" fill="#2a1040" stroke="#cc44bb" stroke-width="1" stroke-dasharray="4,2"/>
38
+ <text x="68" y="283" text-anchor="middle" fill="#ee88dd" font-family="Roboto Mono, monospace" font-size="8">input_required</text>
39
+ <text x="68" y="296" text-anchor="middle" fill="#cc66bb" font-family="Roboto Mono, monospace" font-size="8">→ send reply</text>
40
+ <text x="68" y="308" text-anchor="middle" fill="#cc66bb" font-family="Roboto Mono, monospace" font-size="8"> with task_id</text>
41
+
42
+ <!-- agentCard -->
43
+ <rect x="24" y="328" width="88" height="26" rx="5" fill="#1e1440" stroke="#5533aa" stroke-width="1" stroke-dasharray="4,2"/>
44
+ <text x="68" y="345" text-anchor="middle" fill="#aa88ee" font-family="Roboto Mono, monospace" font-size="8">GET agentCard</text>
45
+
46
+ <!-- ══════════════════════════════════════════════════════════════
47
+ HTTP LAYER — simple_a2a (col 2)
48
+ ══════════════════════════════════════════════════════════════ -->
49
+ <rect x="140" y="80" width="180" height="440" rx="8" fill="#0e1e30" stroke="#3388cc" stroke-width="1.5"/>
50
+ <text x="230" y="100" text-anchor="middle" fill="#66bbff" font-family="Roboto Mono, monospace" font-size="11" font-weight="bold">simple_a2a</text>
51
+
52
+ <!-- Falcon/Rack -->
53
+ <rect x="152" y="110" width="156" height="30" rx="5" fill="#0d2a40" stroke="#2277aa" stroke-width="1"/>
54
+ <text x="230" y="121" text-anchor="middle" fill="#55aaee" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">Falcon HTTP server</text>
55
+ <text x="230" y="133" text-anchor="middle" fill="#3388bb" font-family="Roboto Mono, monospace" font-size="8">JSON-RPC 2.0 / SSE</text>
56
+
57
+ <!-- App router -->
58
+ <rect x="152" y="150" width="156" height="50" rx="5" fill="#0d2a40" stroke="#2277aa" stroke-width="1"/>
59
+ <text x="230" y="165" text-anchor="middle" fill="#55aaee" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">App (Roda)</text>
60
+ <text x="230" y="178" text-anchor="middle" fill="#3388bb" font-family="Roboto Mono, monospace" font-size="8">dispatch → handle_send</text>
61
+ <text x="230" y="191" text-anchor="middle" fill="#3388bb" font-family="Roboto Mono, monospace" font-size="8">handle_get / cancel / list</text>
62
+
63
+ <!-- Context / ResumeContext -->
64
+ <rect x="152" y="212" width="72" height="44" rx="5" fill="#0d2233" stroke="#1a5577" stroke-width="1"/>
65
+ <text x="188" y="227" text-anchor="middle" fill="#44aacc" font-family="Roboto Mono, monospace" font-size="8" font-weight="bold">Context</text>
66
+ <text x="188" y="239" text-anchor="middle" fill="#2288aa" font-family="Roboto Mono, monospace" font-size="7">task, message</text>
67
+ <text x="188" y="250" text-anchor="middle" fill="#2288aa" font-family="Roboto Mono, monospace" font-size="7">storage, events</text>
68
+
69
+ <rect x="234" y="212" width="74" height="44" rx="5" fill="#1a1040" stroke="#cc44bb" stroke-width="1"/>
70
+ <text x="271" y="227" text-anchor="middle" fill="#ee88dd" font-family="Roboto Mono, monospace" font-size="8" font-weight="bold">ResumeContext</text>
71
+ <text x="271" y="239" text-anchor="middle" fill="#cc66bb" font-family="Roboto Mono, monospace" font-size="7">resume_message</text>
72
+ <text x="271" y="250" text-anchor="middle" fill="#cc66bb" font-family="Roboto Mono, monospace" font-size="7">task_id routing</text>
73
+
74
+ <!-- Storage -->
75
+ <rect x="152" y="268" width="156" height="34" rx="5" fill="#0d2a40" stroke="#2277aa" stroke-width="1"/>
76
+ <text x="230" y="281" text-anchor="middle" fill="#55aaee" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">Storage::Memory</text>
77
+ <text x="230" y="294" text-anchor="middle" fill="#3388bb" font-family="Roboto Mono, monospace" font-size="8">task persistence (in-process)</text>
78
+
79
+ <!-- AgentCard -->
80
+ <rect x="152" y="314" width="156" height="30" rx="5" fill="#0d2a40" stroke="#2277aa" stroke-width="1"/>
81
+ <text x="230" y="326" text-anchor="middle" fill="#55aaee" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">AgentCard</text>
82
+ <text x="230" y="339" text-anchor="middle" fill="#3388bb" font-family="Roboto Mono, monospace" font-size="8">name, skills, capabilities</text>
83
+
84
+ <!-- ══════════════════════════════════════════════════════════════
85
+ ADAPTER LAYER (col 3)
86
+ ══════════════════════════════════════════════════════════════ -->
87
+ <rect x="338" y="80" width="224" height="440" rx="8" fill="#0e1e16" stroke="#33aa66" stroke-width="1.5"/>
88
+ <text x="450" y="100" text-anchor="middle" fill="#66ee99" font-family="Roboto Mono, monospace" font-size="11" font-weight="bold">RobotLab::A2A</text>
89
+
90
+ <!-- Server builder -->
91
+ <rect x="350" y="110" width="200" height="34" rx="5" fill="#0a2818" stroke="#228855" stroke-width="1"/>
92
+ <text x="450" y="125" text-anchor="middle" fill="#44dd88" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">Server</text>
93
+ <text x="450" y="138" text-anchor="middle" fill="#229955" font-family="Roboto Mono, monospace" font-size="8">add_robot / add_network / run / to_app</text>
94
+
95
+ <!-- RobotAdapter -->
96
+ <rect x="350" y="156" width="200" height="118" rx="5" fill="#0a2818" stroke="#228855" stroke-width="1"/>
97
+ <text x="450" y="172" text-anchor="middle" fill="#44dd88" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">RobotAdapter</text>
98
+
99
+ <!-- :none -->
100
+ <rect x="360" y="180" width="56" height="22" rx="3" fill="#062010" stroke="#115533" stroke-width="1"/>
101
+ <text x="388" y="195" text-anchor="middle" fill="#33bb77" font-family="Roboto Mono, monospace" font-size="8">:none</text>
102
+
103
+ <!-- :a2a_tool -->
104
+ <rect x="424" y="180" width="60" height="22" rx="3" fill="#1a1040" stroke="#cc44bb" stroke-width="1"/>
105
+ <text x="454" y="195" text-anchor="middle" fill="#ee88dd" font-family="Roboto Mono, monospace" font-size="8">:a2a_tool</text>
106
+
107
+ <!-- :io_bridge -->
108
+ <rect x="492" y="180" width="52" height="22" rx="3" fill="#0a1e30" stroke="#3388cc" stroke-width="1"/>
109
+ <text x="518" y="195" text-anchor="middle" fill="#66bbff" font-family="Roboto Mono, monospace" font-size="8">:io_bridge</text>
110
+
111
+ <!-- AskUserTool -->
112
+ <rect x="360" y="212" width="86" height="52" rx="3" fill="#1a1040" stroke="#cc44bb" stroke-width="1" stroke-dasharray="3,2"/>
113
+ <text x="403" y="227" text-anchor="middle" fill="#ee88dd" font-family="Roboto Mono, monospace" font-size="8" font-weight="bold">AskUserTool</text>
114
+ <text x="403" y="240" text-anchor="middle" fill="#cc66bb" font-family="Roboto Mono, monospace" font-size="7">push event_queue</text>
115
+ <text x="403" y="252" text-anchor="middle" fill="#cc66bb" font-family="Roboto Mono, monospace" font-size="7">block answer_queue</text>
116
+
117
+ <!-- IoBridge -->
118
+ <rect x="454" y="212" width="88" height="52" rx="3" fill="#0a1e30" stroke="#3388cc" stroke-width="1" stroke-dasharray="3,2"/>
119
+ <text x="498" y="227" text-anchor="middle" fill="#66bbff" font-family="Roboto Mono, monospace" font-size="8" font-weight="bold">IoBridge</text>
120
+ <text x="498" y="240" text-anchor="middle" fill="#3388aa" font-family="Roboto Mono, monospace" font-size="7">buffer write/puts</text>
121
+ <text x="498" y="252" text-anchor="middle" fill="#3388aa" font-family="Roboto Mono, monospace" font-size="7">gets → block queue</text>
122
+
123
+ <!-- Thread box -->
124
+ <rect x="360" y="274" width="88" height="26" rx="3" fill="#0a1a10" stroke="#115533" stroke-width="1"/>
125
+ <text x="404" y="287" text-anchor="middle" fill="#44cc77" font-family="Roboto Mono, monospace" font-size="8">Thread (per call)</text>
126
+ <text x="404" y="299" text-anchor="middle" fill="#228844" font-family="Roboto Mono, monospace" font-size="7">robot.run(input)</text>
127
+
128
+ <!-- NetworkAdapter -->
129
+ <rect x="350" y="312" width="200" height="50" rx="5" fill="#0a2818" stroke="#228855" stroke-width="1"/>
130
+ <text x="450" y="328" text-anchor="middle" fill="#44dd88" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">NetworkAdapter (:none)</text>
131
+ <text x="450" y="341" text-anchor="middle" fill="#229955" font-family="Roboto Mono, monospace" font-size="8">network.run(message:)</text>
132
+ <text x="450" y="353" text-anchor="middle" fill="#229955" font-family="Roboto Mono, monospace" font-size="8">.last_text_content</text>
133
+
134
+ <!-- Registry -->
135
+ <rect x="350" y="376" width="200" height="58" rx="5" fill="#1e1a00" stroke="#ccaa00" stroke-width="1.5"/>
136
+ <text x="450" y="393" text-anchor="middle" fill="#ffdd44" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">Registry (Mutex)</text>
137
+ <text x="450" y="407" text-anchor="middle" fill="#cc9900" font-family="Roboto Mono, monospace" font-size="8">task_id → Entry</text>
138
+ <text x="450" y="419" text-anchor="middle" fill="#cc9900" font-family="Roboto Mono, monospace" font-size="8">Entry: thread | event_queue</text>
139
+ <text x="450" y="431" text-anchor="middle" fill="#cc9900" font-family="Roboto Mono, monospace" font-size="8"> | answer_queue</text>
140
+
141
+ <!-- ══════════════════════════════════════════════════════════════
142
+ ROBOT LAYER (col 4)
143
+ ══════════════════════════════════════════════════════════════ -->
144
+ <rect x="580" y="80" width="306" height="440" rx="8" fill="#1e1205" stroke="#dd8822" stroke-width="1.5"/>
145
+ <text x="733" y="100" text-anchor="middle" fill="#ffaa44" font-family="Roboto Mono, monospace" font-size="11" font-weight="bold">RobotLab</text>
146
+
147
+ <!-- Robot box -->
148
+ <rect x="592" y="110" width="138" height="160" rx="5" fill="#281600" stroke="#aa6600" stroke-width="1"/>
149
+ <text x="661" y="128" text-anchor="middle" fill="#ffaa44" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">Robot</text>
150
+ <text x="661" y="143" text-anchor="middle" fill="#cc8833" font-family="Roboto Mono, monospace" font-size="8">robot.name</text>
151
+ <text x="661" y="156" text-anchor="middle" fill="#cc8833" font-family="Roboto Mono, monospace" font-size="8">robot.description</text>
152
+ <text x="661" y="169" text-anchor="middle" fill="#cc8833" font-family="Roboto Mono, monospace" font-size="8">robot.run(text) → reply</text>
153
+
154
+ <!-- AskUser tool (inside robot) -->
155
+ <rect x="602" y="182" width="118" height="36" rx="3" fill="#1e0e00" stroke="#cc6600" stroke-width="1" stroke-dasharray="3,2"/>
156
+ <text x="661" y="197" text-anchor="middle" fill="#ffaa55" font-family="Roboto Mono, monospace" font-size="8">RobotLab::AskUser</text>
157
+ <text x="661" y="209" text-anchor="middle" fill="#aa6633" font-family="Roboto Mono, monospace" font-size="7">→ replaced by AskUserTool</text>
158
+
159
+ <!-- local_tools / io note -->
160
+ <rect x="602" y="228" width="118" height="36" rx="3" fill="#1e0e00" stroke="#7755aa" stroke-width="1" stroke-dasharray="3,2"/>
161
+ <text x="661" y="243" text-anchor="middle" fill="#bb88ff" font-family="Roboto Mono, monospace" font-size="8">local_tools (Array)</text>
162
+ <text x="661" y="255" text-anchor="middle" fill="#9966cc" font-family="Roboto Mono, monospace" font-size="7">input= / output= (io_bridge)</text>
163
+
164
+ <!-- Network box -->
165
+ <rect x="748" y="110" width="128" height="100" rx="5" fill="#200a28" stroke="#cc55dd" stroke-width="1"/>
166
+ <text x="812" y="128" text-anchor="middle" fill="#ee88ff" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">Network</text>
167
+ <text x="812" y="143" text-anchor="middle" fill="#bb66dd" font-family="Roboto Mono, monospace" font-size="8">network.run(message:)</text>
168
+ <text x="812" y="157" text-anchor="middle" fill="#bb66dd" font-family="Roboto Mono, monospace" font-size="8">.last_text_content</text>
169
+
170
+ <!-- Pipeline stages -->
171
+ <rect x="758" y="168" width="108" height="34" rx="3" fill="#180820" stroke="#aa44cc" stroke-width="1" stroke-dasharray="3,2"/>
172
+ <text x="812" y="183" text-anchor="middle" fill="#dd88ff" font-family="Roboto Mono, monospace" font-size="8">Step A → Step B</text>
173
+ <text x="812" y="196" text-anchor="middle" fill="#9944bb" font-family="Roboto Mono, monospace" font-size="7">→ Step C (pipeline)</text>
174
+
175
+ <!-- LLM note -->
176
+ <rect x="592" y="290" width="274" height="46" rx="5" fill="#1a1000" stroke="#886622" stroke-width="1"/>
177
+ <text x="729" y="308" text-anchor="middle" fill="#ddaa44" font-family="Roboto Mono, monospace" font-size="9" font-weight="bold">LLM / External Services</text>
178
+ <text x="729" y="323" text-anchor="middle" fill="#886633" font-family="Roboto Mono, monospace" font-size="8">ruby_llm, openai, anthropic, etc. (optional, robot-internal)</text>
179
+
180
+ <!-- ══════════════════════════════════════════════════════════════
181
+ ARROWS
182
+ ══════════════════════════════════════════════════════════════ -->
183
+ <defs>
184
+ <marker id="a1" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
185
+ <path d="M0,0 L8,4 L0,8 Z" fill="#6633bb"/>
186
+ </marker>
187
+ <marker id="a2" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
188
+ <path d="M0,0 L8,4 L0,8 Z" fill="#3388cc"/>
189
+ </marker>
190
+ <marker id="a3" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
191
+ <path d="M0,0 L8,4 L0,8 Z" fill="#33aa66"/>
192
+ </marker>
193
+ <marker id="a4" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
194
+ <path d="M0,0 L8,4 L0,8 Z" fill="#dd8822"/>
195
+ </marker>
196
+ <marker id="a5" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
197
+ <path d="M0,0 L8,4 L0,8 Z" fill="#cc44bb"/>
198
+ </marker>
199
+ <marker id="a6" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
200
+ <path d="M0,0 L8,4 L0,8 Z" fill="#ccaa00"/>
201
+ </marker>
202
+ </defs>
203
+
204
+ <!-- Client → simple_a2a (HTTP POST) -->
205
+ <line x1="122" y1="127" x2="140" y2="127" stroke="#6633bb" stroke-width="2" marker-end="url(#a1)"/>
206
+ <text x="131" y="122" text-anchor="middle" fill="#9966cc" font-family="Roboto Mono, monospace" font-size="7">POST</text>
207
+
208
+ <!-- simple_a2a → executor.call() -->
209
+ <line x1="320" y1="185" x2="338" y2="185" stroke="#3388cc" stroke-width="2" marker-end="url(#a2)"/>
210
+ <text x="329" y="180" text-anchor="middle" fill="#4499bb" font-family="Roboto Mono, monospace" font-size="7">call</text>
211
+
212
+ <!-- RobotAdapter → Robot -->
213
+ <line x1="562" y1="200" x2="580" y2="200" stroke="#33aa66" stroke-width="2" marker-end="url(#a3)"/>
214
+ <text x="571" y="195" text-anchor="middle" fill="#55cc88" font-family="Roboto Mono, monospace" font-size="7">run</text>
215
+
216
+ <!-- NetworkAdapter → Network -->
217
+ <line x1="562" y1="335" x2="748" y2="165" stroke="#33aa66" stroke-width="1.5" stroke-dasharray="5,3" marker-end="url(#a3)"/>
218
+
219
+ <!-- input_required return arrow (dashed, upward) -->
220
+ <path d="M 403 264 Q 271 360 271 258" fill="none" stroke="#cc44bb" stroke-width="1.5" stroke-dasharray="5,3" marker-end="url(#a5)"/>
221
+ <text x="300" y="320" text-anchor="middle" fill="#cc44bb" font-family="Roboto Mono, monospace" font-size="8">input_required</text>
222
+
223
+ <!-- Resume arrow: ResumeContext → answer_queue -->
224
+ <path d="M 271 212 Q 271 170 403 212" fill="none" stroke="#cc44bb" stroke-width="1.5" stroke-dasharray="5,3" marker-end="url(#a5)"/>
225
+ <text x="330" y="168" text-anchor="middle" fill="#cc44bb" font-family="Roboto Mono, monospace" font-size="8">resume</text>
226
+
227
+ <!-- Registry ↔ RobotAdapter (dashed) -->
228
+ <line x1="404" y1="274" x2="404" y2="376" stroke="#ccaa00" stroke-width="1.5" stroke-dasharray="4,3" marker-end="url(#a6)"/>
229
+ <text x="416" y="332" fill="#998800" font-family="Roboto Mono, monospace" font-size="7">register/</text>
230
+ <text x="416" y="342" fill="#998800" font-family="Roboto Mono, monospace" font-size="7">fetch</text>
231
+
232
+ <!-- ══════════════════════════════════════════════════════════════
233
+ LEGEND
234
+ ══════════════════════════════════════════════════════════════ -->
235
+ <rect x="14" y="540" width="872" height="46" rx="6" fill="#111118" stroke="#333344" stroke-width="1"/>
236
+ <text x="28" y="556" fill="#888" font-family="Roboto Mono, monospace" font-size="9">Legend:</text>
237
+
238
+ <rect x="75" y="546" width="30" height="10" rx="2" fill="#1a1030" stroke="#6633bb" stroke-width="1"/>
239
+ <text x="111" y="556" fill="#aaa" font-family="Roboto Mono, monospace" font-size="9">A2A protocol</text>
240
+
241
+ <rect x="190" y="546" width="30" height="10" rx="2" fill="#0e1e30" stroke="#3388cc" stroke-width="1"/>
242
+ <text x="226" y="556" fill="#aaa" font-family="Roboto Mono, monospace" font-size="9">HTTP layer</text>
243
+
244
+ <rect x="300" y="546" width="30" height="10" rx="2" fill="#0e1e16" stroke="#33aa66" stroke-width="1"/>
245
+ <text x="336" y="556" fill="#aaa" font-family="Roboto Mono, monospace" font-size="9">Adapter layer</text>
246
+
247
+ <rect x="420" y="546" width="30" height="10" rx="2" fill="#1e1205" stroke="#dd8822" stroke-width="1"/>
248
+ <text x="456" y="556" fill="#aaa" font-family="Roboto Mono, monospace" font-size="9">Robot layer</text>
249
+
250
+ <line x1="535" y1="551" x2="565" y2="551" stroke="#cc44bb" stroke-width="1.5" stroke-dasharray="4,2"/>
251
+ <text x="572" y="556" fill="#aaa" font-family="Roboto Mono, monospace" font-size="9">interactive flow</text>
252
+
253
+ <line x1="665" y1="551" x2="695" y2="551" stroke="#ccaa00" stroke-width="1.5" stroke-dasharray="4,2"/>
254
+ <text x="702" y="556" fill="#aaa" font-family="Roboto Mono, monospace" font-size="9">Registry lookup</text>
255
+
256
+ <!-- second legend row -->
257
+ <text x="28" y="578" fill="#666" font-family="Roboto Mono, monospace" font-size="8">Dashed borders = injected/optional component | Solid borders = always-present component</text>
258
+ </svg>
data/docs/examples.md ADDED
@@ -0,0 +1,116 @@
1
+ # Examples
2
+
3
+ ## Layout
4
+
5
+ ```
6
+ examples/
7
+ run # launcher script
8
+ 01_sync_robot/
9
+ server.rb # starts the A2A server
10
+ client.rb # sends a task and prints the result
11
+ 02_interactive_a2a_tool/
12
+ server.rb
13
+ client.rb
14
+ 03_robot_network/
15
+ server.rb
16
+ client.rb
17
+ ```
18
+
19
+ ## How to run
20
+
21
+ From the repo root, pass the example directory name to `examples/run`:
22
+
23
+ ```bash
24
+ bundle exec ruby examples/run 01_sync_robot
25
+ ```
26
+
27
+ From inside `examples/` directly:
28
+
29
+ ```bash
30
+ cd examples
31
+ ./run 01_sync_robot
32
+ ```
33
+
34
+ The launcher starts `server.rb` in the background, waits for it to bind, then runs `client.rb` in the foreground so you see the output. Press Ctrl-C to stop.
35
+
36
+ To run server and client separately (useful for inspecting raw SSE output):
37
+
38
+ ```bash
39
+ # Terminal 1
40
+ bundle exec ruby examples/01_sync_robot/server.rb
41
+
42
+ # Terminal 2
43
+ bundle exec ruby examples/01_sync_robot/client.rb
44
+ ```
45
+
46
+ ## 01_sync_robot
47
+
48
+ **Demonstrates:** The simplest possible integration. A robot with no user prompts is registered in `:none` mode and invoked with a single text message.
49
+
50
+ **What it shows:**
51
+
52
+ - `Server.new.add_robot(...).run(port:)` pattern
53
+ - A single `tasks/send` POST
54
+ - Parsing the SSE stream to extract `task_complete` payload
55
+
56
+ **Expected output (client):**
57
+
58
+ ```
59
+ Sending task...
60
+ [event: task_started]
61
+ [event: task_complete]
62
+ Reply: The answer is 42.
63
+ Done.
64
+ ```
65
+
66
+ ## 02_interactive_a2a_tool
67
+
68
+ **Demonstrates:** A robot that calls `RobotLab::AskUser` during execution. The gem injects `AskUserTool`, converts the blocking call to an A2A `input_required` event, and the client resumes with the answer.
69
+
70
+ **Two-turn flow:**
71
+
72
+ 1. Client sends initial task → server starts robot thread → robot calls `AskUser("What is your name?")` → SSE delivers `input_required` event with prompt and task ID.
73
+ 2. Client sends a second `tasks/send` with the task ID and the user's answer → `AskUserTool` unblocks → robot continues → SSE delivers `task_complete`.
74
+
75
+ **What it shows:**
76
+
77
+ - `interactive: :a2a_tool` server setup
78
+ - How to extract `task_id` and `input_required` prompt from the first SSE stream
79
+ - How to construct the resume request with `task_id`
80
+
81
+ **Expected output (client):**
82
+
83
+ ```
84
+ Turn 1 — sending initial task...
85
+ [event: task_started]
86
+ [event: input_required] prompt: "What is your name?"
87
+ task_id: abc-123-def
88
+
89
+ Turn 2 — resuming with answer...
90
+ [event: task_started]
91
+ [event: task_complete]
92
+ Reply: Hello, Alice! Nice to meet you.
93
+ Done.
94
+ ```
95
+
96
+ ## 03_robot_network
97
+
98
+ **Demonstrates:** A `RobotLab::Network` (multi-stage pipeline) exposed as a single A2A agent via `NetworkAdapter` in `:none` mode.
99
+
100
+ **Pipeline stages:** The example network typically chains two or three robots — for example, a research robot that gathers context, followed by a synthesis robot that produces a final answer. Each stage's output feeds the next.
101
+
102
+ **What it shows:**
103
+
104
+ - `add_network` registration
105
+ - That the caller sees only one A2A endpoint regardless of how many internal stages the network has
106
+ - `network.run(message: text).last_text_content` as the final reply
107
+
108
+ **Expected output (client):**
109
+
110
+ ```
111
+ Sending task to network...
112
+ [event: task_started]
113
+ [event: task_complete]
114
+ Reply: [synthesised answer from the final pipeline stage]
115
+ Done.
116
+ ```
@@ -0,0 +1,103 @@
1
+ # Getting Started
2
+
3
+ ## Requirements
4
+
5
+ - Ruby >= 3.2
6
+ - The `robot_lab` gem (provides `RobotLab::Robot` and `RobotLab::Network`)
7
+ - Bundler
8
+
9
+ ## Installation
10
+
11
+ ### As a gem dependency
12
+
13
+ Add to your `Gemfile`:
14
+
15
+ ```ruby
16
+ gem "robot_lab-a2a"
17
+ ```
18
+
19
+ Then run:
20
+
21
+ ```bash
22
+ bundle install
23
+ ```
24
+
25
+ ### As a local path dependency (for development)
26
+
27
+ If you are working directly in this repo or have a local checkout of `robot_lab-a2a`:
28
+
29
+ ```ruby
30
+ # Gemfile
31
+ gem "robot_lab-a2a", path: "../robot_lab-a2a"
32
+ ```
33
+
34
+ The gem also depends on `simple_a2a`. For interactive resume support (`input_required`/resume lifecycle), you need a build of `simple_a2a` that includes `ResumeContext`. If your local copy is newer than the released gem, use a path dependency for that too:
35
+
36
+ ```ruby
37
+ gem "simple_a2a", path: "../simple_a2a"
38
+ ```
39
+
40
+ ## Sync Robot (5 minutes)
41
+
42
+ ### 1. Define your robot
43
+
44
+ ```ruby
45
+ # my_robot.rb
46
+ require "robot_lab"
47
+
48
+ class GreeterRobot < RobotLab::Robot
49
+ def initialize
50
+ super(
51
+ name: "Greeter",
52
+ description: "Returns a greeting for any input",
53
+ model: "gpt-4o-mini"
54
+ )
55
+ end
56
+
57
+ # robot_lab calls run(text) and expects a result with a .reply method
58
+ end
59
+ ```
60
+
61
+ ### 2. Start the server
62
+
63
+ ```ruby
64
+ # server.rb
65
+ require "robot_lab/a2a"
66
+ require_relative "my_robot"
67
+
68
+ robot = GreeterRobot.new
69
+
70
+ RobotLab::A2A::Server.new
71
+ .add_robot(robot, name: "Greeter", description: "Returns a greeting")
72
+ .run(port: 7000)
73
+ ```
74
+
75
+ ```bash
76
+ bundle exec ruby server.rb
77
+ # => Listening on http://0.0.0.0:7000
78
+ ```
79
+
80
+ ### 3. Send a task
81
+
82
+ The server exposes each robot at its DNS-label path. `GreeterRobot` registered as `"Greeter"` becomes `/greeter`.
83
+
84
+ ```bash
85
+ curl -X POST http://localhost:7000/greeter/tasks/send \
86
+ -H "Content-Type: application/json" \
87
+ -d '{"message": {"role": "user", "parts": [{"text": "Hello!"}]}}'
88
+ ```
89
+
90
+ The response is a stream of SSE events ending with a `task_complete` event whose payload contains the robot's reply.
91
+
92
+ ### 4. Inspect the agent card
93
+
94
+ Every registered agent exposes its capabilities at `/.well-known/agent.json` (via `simple_a2a`):
95
+
96
+ ```bash
97
+ curl http://localhost:7000/greeter/.well-known/agent.json
98
+ ```
99
+
100
+ ## What's next
101
+
102
+ - [Interactive Modes](interactive-modes.md) — enable multi-turn conversations with `:a2a_tool` or `:io_bridge`
103
+ - [Server API](server-api.md) — full reference for `Server.new`, `add_robot`, `add_network`, `run`, and `to_app`
data/docs/index.md ADDED
@@ -0,0 +1,23 @@
1
+ # robot_lab-a2a
2
+
3
+ `robot_lab-a2a` is an Agent2Agent (A2A) protocol adapter for RobotLab. It wraps RobotLab robots and networks in an HTTP+SSE server so any A2A-compliant client can invoke them, receive streaming events, and participate in multi-turn conversations — all without a terminal.
4
+
5
+ ## How it fits together
6
+
7
+ ![Architecture diagram](assets/images/architecture.png)
8
+
9
+ ## Key concepts
10
+
11
+ - **Server** (`RobotLab::A2A::Server`) — fluent builder that registers robots and networks as A2A agents, then starts an HTTP+SSE server via `simple_a2a`.
12
+ - **RobotAdapter** — wraps any `RobotLab::Robot`. Supports three modes: `:none` (synchronous), `:a2a_tool` (injects `AskUserTool` for multi-turn via Thread+Queue), and `:io_bridge` (replaces robot I/O streams for robots that use `gets`/`puts` directly).
13
+ - **NetworkAdapter** — wraps a `RobotLab::Network`. Runs the full pipeline in `:none` mode and returns the last text content.
14
+ - **AskUserTool** — drop-in replacement for `RobotLab::AskUser`. Converts a blocking terminal prompt into an A2A `input_required` event and waits for a client resume.
15
+ - **IoBridge** — IO-compatible object that buffers robot output and converts `gets` calls into `input_required` events, enabling interactive mode for robots that talk to raw streams.
16
+ - **Registry** — thread-safe singleton (Mutex-protected) keyed by A2A task ID. Holds `Entry(thread, event_queue, answer_queue)` so HTTP resume requests can find and unblock the correct robot thread.
17
+
18
+ ## Documentation pages
19
+
20
+ - [Getting Started](getting-started.md) — installation, first server, client usage
21
+ - [Interactive Modes](interactive-modes.md) — `:none`, `:a2a_tool`, `:io_bridge` in depth
22
+ - [Server API](server-api.md) — full constructor and method reference
23
+ - [Examples](examples.md) — walkthrough of the bundled example scripts
@@ -0,0 +1,104 @@
1
+ # Interactive Modes
2
+
3
+ ## Why interactive matters
4
+
5
+ RobotLab's `AskUser` tool blocks the robot thread and reads from a terminal. That works fine for CLI scripts but breaks completely in an HTTP server: there is no terminal, and the HTTP request has already returned by the time the robot needs an answer.
6
+
7
+ `robot_lab-a2a` solves this by converting blocking terminal prompts into A2A `input_required` events. The robot thread pauses on a Ruby `Queue`. The HTTP layer serialises the prompt as an SSE event to the A2A client. When the client sends a resume request, the answer is placed onto the queue and the robot thread continues — all without any terminal.
8
+
9
+ Three modes are available. Choose based on how your robot is implemented.
10
+
11
+ ## `:none` mode
12
+
13
+ The default. The robot receives the initial text message, runs to completion, and returns a single reply. No user interaction occurs during execution.
14
+
15
+ ```ruby
16
+ RobotLab::A2A::Server.new # interactive: :none is the default
17
+ .add_robot(robot, name: "Summariser", description: "Summarises text")
18
+ .run(port: 7000)
19
+ ```
20
+
21
+ **Robot interface required:** `robot.run(text)` returns a result responding to `.reply`.
22
+
23
+ Use `:none` when your robot never calls `AskUser` or reads from stdin.
24
+
25
+ ## `:a2a_tool` mode
26
+
27
+ The server injects an `AskUserTool` instance into `robot.local_tools` before the robot thread starts, and removes it on teardown. `AskUserTool` is a drop-in replacement for `RobotLab::AskUser` that uses `Queue` instead of `$stdin`.
28
+
29
+ **Turn 1 — initial request:**
30
+
31
+ ```bash
32
+ curl -X POST http://localhost:7000/my-robot/tasks/send \
33
+ -H "Content-Type: application/json" \
34
+ -d '{"message": {"role": "user", "parts": [{"text": "Plan a trip"}]}}'
35
+ ```
36
+
37
+ The server starts the robot on a new Thread, keyed in `Registry` by the A2A task ID. When the robot calls `AskUser`, `AskUserTool#execute`:
38
+
39
+ 1. Pushes `{type: :ask, prompt: "Where do you want to go?"}` onto `event_queue`.
40
+ 2. Blocks on `answer_queue`.
41
+
42
+ The SSE stream delivers an `input_required` event to the client containing the prompt text and the task ID.
43
+
44
+ **Turn 2 — resume:**
45
+
46
+ ```bash
47
+ curl -X POST http://localhost:7000/my-robot/tasks/send \
48
+ -H "Content-Type: application/json" \
49
+ -d '{
50
+ "task_id": "<task-id-from-turn-1>",
51
+ "message": {"role": "user", "parts": [{"text": "Paris"}]}
52
+ }'
53
+ ```
54
+
55
+ `simple_a2a` routes this to the resume handler. The server looks up the task in `Registry`, places `"Paris"` onto `answer_queue`, and the blocked `AskUserTool#execute` returns `"Paris"` to the robot. The robot continues. If it calls `AskUser` again, the cycle repeats. When the robot finishes, a `task_complete` event closes the SSE stream.
56
+
57
+ **Setup:**
58
+
59
+ ```ruby
60
+ RobotLab::A2A::Server.new(interactive: :a2a_tool)
61
+ .add_robot(robot, name: "Planner", description: "Plans trips interactively")
62
+ .run(port: 7000)
63
+ ```
64
+
65
+ **Robot interface required:**
66
+
67
+ - `robot.run(text)` — entry point.
68
+ - `robot.local_tools` — mutable `Array` that the server prepends `AskUserTool` to. The instance variable is `@local_tools`.
69
+
70
+ **Important:** Interactive resume requires a build of `simple_a2a` that includes `ResumeContext` support. See [Getting Started — Installation](getting-started.md#installation) for the path dependency pattern.
71
+
72
+ ## `:io_bridge` mode
73
+
74
+ For robots that communicate via raw Ruby IO (`puts`/`gets`) rather than the `AskUser` tool. The server replaces `robot.input` and `robot.output` with an `IoBridge` instance before starting the robot thread, and restores the originals on teardown.
75
+
76
+ `IoBridge` behaviour:
77
+
78
+ - **Writes** (`write`, `puts`, `print`) — accumulate into an internal buffer.
79
+ - **`gets`** — flushes the buffer as an `input_required` SSE event (the buffered text becomes the prompt), then blocks on `answer_queue` until the A2A client resumes. The answer is returned from `gets` to the robot.
80
+
81
+ The two-turn client pattern is identical to `:a2a_tool`.
82
+
83
+ ```ruby
84
+ RobotLab::A2A::Server.new(interactive: :io_bridge)
85
+ .add_robot(robot, name: "IO Robot", description: "Uses gets/puts for interaction")
86
+ .run(port: 7000)
87
+ ```
88
+
89
+ **Robot interface required:**
90
+
91
+ - `robot.input=` / `robot.output=` — settable IO attributes.
92
+ - The robot must use `@input.gets` and `@output.puts` (or equivalent) rather than `$stdin`/`$stdout` directly.
93
+
94
+ **Important:** Interactive resume requires `simple_a2a` with `ResumeContext` support.
95
+
96
+ ## Choosing a mode
97
+
98
+ | Mode | Robot interface required | Typical use case |
99
+ |---|---|---|
100
+ | `:none` | `robot.run(text) → result.reply` | Fully autonomous robots with no user prompts |
101
+ | `:a2a_tool` | `robot.run(text)` + mutable `robot.local_tools` | Robots that use `RobotLab::AskUser` as a tool |
102
+ | `:io_bridge` | `robot.run(text)` + settable `robot.input=` / `robot.output=` | Robots that interact via raw stdin/stdout streams |
103
+
104
+ When in doubt, start with `:none`. Upgrade to `:a2a_tool` if your robot uses `AskUser`, or `:io_bridge` if it uses raw IO.