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.
- checksums.yaml +7 -0
- data/.envrc +1 -0
- data/.github/workflows/deploy-github-pages.yml +52 -0
- data/.rubocop.yml +26 -0
- data/CHANGELOG.md +24 -0
- data/COMMITS.md +196 -0
- data/LICENSE.txt +21 -0
- data/README.md +106 -0
- data/Rakefile +104 -0
- data/docs/assets/images/architecture.png +0 -0
- data/docs/assets/images/architecture.svg +258 -0
- data/docs/examples.md +116 -0
- data/docs/getting-started.md +103 -0
- data/docs/index.md +23 -0
- data/docs/interactive-modes.md +104 -0
- data/docs/server-api.md +118 -0
- data/examples/01_sync_robot/client.rb +94 -0
- data/examples/01_sync_robot/server.rb +45 -0
- data/examples/02_interactive_a2a_tool/client.rb +144 -0
- data/examples/02_interactive_a2a_tool/server.rb +78 -0
- data/examples/03_robot_network/client.rb +83 -0
- data/examples/03_robot_network/server.rb +77 -0
- data/examples/04_io_bridge/client.rb +140 -0
- data/examples/04_io_bridge/server.rb +64 -0
- data/examples/05_multi_agent/client.rb +97 -0
- data/examples/05_multi_agent/server.rb +76 -0
- data/examples/06_rack_mount/client.rb +90 -0
- data/examples/06_rack_mount/config.ru +44 -0
- data/examples/06_rack_mount/server.rb +72 -0
- data/examples/common_config.rb +9 -0
- data/examples/run +112 -0
- data/lib/robot_lab/a2a/ask_user_tool.rb +43 -0
- data/lib/robot_lab/a2a/io_bridge.rb +75 -0
- data/lib/robot_lab/a2a/network_adapter.rb +38 -0
- data/lib/robot_lab/a2a/registry.rb +36 -0
- data/lib/robot_lab/a2a/robot_adapter.rb +183 -0
- data/lib/robot_lab/a2a/server.rb +128 -0
- data/lib/robot_lab/a2a/version.rb +7 -0
- data/lib/robot_lab/a2a.rb +39 -0
- data/mkdocs.yml +153 -0
- 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
|
+

|
|
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.
|