simple_a2a 0.1.0 → 0.3.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 +4 -4
- data/CHANGELOG.md +67 -0
- data/README.md +78 -38
- data/compare_agent2agent.md +460 -0
- data/docs/api/client/index.md +19 -0
- data/docs/api/index.md +4 -3
- data/docs/api/models/index.md +13 -11
- data/docs/api/server/index.md +42 -10
- data/docs/api/storage/index.md +0 -1
- data/docs/architecture/index.md +17 -15
- data/docs/architecture/protocol.md +16 -1
- data/docs/assets/images/simple_a2a.jpg +0 -0
- data/docs/examples/agent-chaining.md +107 -0
- data/docs/examples/auth-headers.md +105 -0
- data/docs/examples/cancellation.md +105 -0
- data/docs/examples/index.md +123 -52
- data/docs/examples/interrupted-states.md +114 -0
- data/docs/examples/multipart.md +103 -0
- data/docs/examples/push-notifications.md +117 -0
- data/docs/examples/resubscribe.md +129 -0
- data/docs/examples/sqlite-storage.md +131 -0
- data/docs/examples/streaming.md +1 -4
- data/docs/guides/push-notifications.md +4 -1
- data/docs/guides/streaming.md +34 -5
- data/docs/index.md +55 -27
- data/examples/04_resubscribe/client.rb +140 -0
- data/examples/04_resubscribe/server.rb +75 -0
- data/examples/05_cancellation/client.rb +150 -0
- data/examples/05_cancellation/server.rb +77 -0
- data/examples/06_push_notifications/client.rb +192 -0
- data/examples/06_push_notifications/server.rb +123 -0
- data/examples/07_agent_chaining/client.rb +120 -0
- data/examples/07_agent_chaining/server.rb +150 -0
- data/examples/08_interrupted_states/client.rb +148 -0
- data/examples/08_interrupted_states/server.rb +142 -0
- data/examples/09_multipart/client.rb +117 -0
- data/examples/09_multipart/server.rb +97 -0
- data/examples/10_auth_headers/client.rb +92 -0
- data/examples/10_auth_headers/server.rb +98 -0
- data/examples/11_sqlite_storage/Brewfile +1 -0
- data/examples/11_sqlite_storage/Gemfile +9 -0
- data/examples/11_sqlite_storage/client.rb +114 -0
- data/examples/11_sqlite_storage/run +154 -0
- data/examples/11_sqlite_storage/server.rb +131 -0
- data/examples/README.md +384 -0
- data/lib/simple_a2a/client/sse.rb +15 -0
- data/lib/simple_a2a/server/app.rb +131 -45
- data/lib/simple_a2a/server/base.rb +19 -17
- data/lib/simple_a2a/server/broadcast_registry.rb +24 -0
- data/lib/simple_a2a/server/multi_agent.rb +1 -1
- data/lib/simple_a2a/server/push_config_store.rb +29 -0
- data/lib/simple_a2a/server/push_sender.rb +1 -0
- data/lib/simple_a2a/server/task_broadcast.rb +46 -0
- data/lib/simple_a2a/version.rb +1 -1
- metadata +38 -20
- data/lib/simple_a2a/server/event_router.rb +0 -50
|
@@ -0,0 +1 @@
|
|
|
1
|
+
brew "sqlite3"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Usage:
|
|
5
|
+
# bundle exec ruby examples/11_sqlite_storage/client.rb populate [ids_file]
|
|
6
|
+
# bundle exec ruby examples/11_sqlite_storage/client.rb verify [ids_file]
|
|
7
|
+
#
|
|
8
|
+
# populate — sends three tasks and writes their IDs to ids_file (default:
|
|
9
|
+
# /tmp/a2a_sqlite_demo_ids.json) so the verify phase can find them.
|
|
10
|
+
#
|
|
11
|
+
# verify — reads ids_file, retrieves each task from the (restarted) server,
|
|
12
|
+
# and confirms all tasks are still present with state "completed".
|
|
13
|
+
#
|
|
14
|
+
# The run script in this directory manages both phases automatically.
|
|
15
|
+
|
|
16
|
+
require_relative "../common_config"
|
|
17
|
+
require "json"
|
|
18
|
+
|
|
19
|
+
URL = "http://localhost:9292"
|
|
20
|
+
IDS_FILE = ARGV[1] || "/tmp/a2a_sqlite_demo_ids.json"
|
|
21
|
+
PHASE = ARGV[0]
|
|
22
|
+
|
|
23
|
+
unless %w[populate verify].include?(PHASE)
|
|
24
|
+
abort "Usage: client.rb <populate|verify> [ids_file]"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def divider = puts("─" * 60)
|
|
28
|
+
|
|
29
|
+
client = A2A.client(url: URL)
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# Phase: populate
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
if PHASE == "populate"
|
|
35
|
+
puts
|
|
36
|
+
puts "=== Phase 1: Populate — sending tasks to SQLite-backed server ==="
|
|
37
|
+
puts
|
|
38
|
+
|
|
39
|
+
card = client.agent_card
|
|
40
|
+
puts " Agent : #{card.name}"
|
|
41
|
+
puts " DB : shown in server output"
|
|
42
|
+
puts
|
|
43
|
+
|
|
44
|
+
messages = [
|
|
45
|
+
"alpha — first message",
|
|
46
|
+
"beta — second message",
|
|
47
|
+
"gamma — third message"
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
ids = []
|
|
51
|
+
messages.each do |text|
|
|
52
|
+
task = client.send_task(message: A2A::Models::Message.user(text))
|
|
53
|
+
ids << task.id
|
|
54
|
+
puts " sent : #{text.strip}"
|
|
55
|
+
puts " id : #{task.id}"
|
|
56
|
+
puts " state : #{task.status.state}"
|
|
57
|
+
puts " reply : #{task.artifacts.first&.parts&.first&.text}"
|
|
58
|
+
puts
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
File.write(IDS_FILE, JSON.generate(ids))
|
|
62
|
+
puts " IDs written to #{IDS_FILE}"
|
|
63
|
+
divider
|
|
64
|
+
puts
|
|
65
|
+
puts "Populate complete. The server will now be stopped and restarted."
|
|
66
|
+
puts "The same database file will be passed to the new server instance."
|
|
67
|
+
puts
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Phase: verify
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
else
|
|
73
|
+
puts
|
|
74
|
+
puts "=== Phase 2: Verify — confirming persistence after server restart ==="
|
|
75
|
+
puts
|
|
76
|
+
|
|
77
|
+
unless File.exist?(IDS_FILE)
|
|
78
|
+
abort "IDs file not found: #{IDS_FILE} — run the populate phase first."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
ids = JSON.parse(File.read(IDS_FILE))
|
|
82
|
+
puts " Reading #{ids.length} task IDs from #{IDS_FILE}"
|
|
83
|
+
puts
|
|
84
|
+
|
|
85
|
+
results = ids.map do |id|
|
|
86
|
+
task = client.get_task(id)
|
|
87
|
+
puts " id : #{id}"
|
|
88
|
+
puts " state : #{task.status.state}"
|
|
89
|
+
puts " reply : #{task.artifacts.first&.parts&.first&.text}"
|
|
90
|
+
puts
|
|
91
|
+
task
|
|
92
|
+
rescue A2A::Error => e
|
|
93
|
+
puts " id : #{id} MISSING — #{e.message}"
|
|
94
|
+
puts
|
|
95
|
+
nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
divider
|
|
99
|
+
|
|
100
|
+
all_present = results.none?(&:nil?)
|
|
101
|
+
all_completed = results.compact.all? { |t| t.status.state == "completed" }
|
|
102
|
+
count_ok = results.length == ids.length
|
|
103
|
+
|
|
104
|
+
puts
|
|
105
|
+
puts "=== Verification ==="
|
|
106
|
+
puts " All tasks present after restart : #{all_present ? 'PASS' : 'FAIL'}"
|
|
107
|
+
puts " All tasks in completed state : #{all_completed ? 'PASS' : 'FAIL'}"
|
|
108
|
+
puts " Task count matches (#{ids.length}) : #{count_ok ? 'PASS' : 'FAIL'}"
|
|
109
|
+
puts
|
|
110
|
+
|
|
111
|
+
all_ok = all_present && all_completed && count_ok
|
|
112
|
+
puts(all_ok ? "All assertions passed." : "One or more assertions failed.")
|
|
113
|
+
exit(all_ok ? 0 : 1)
|
|
114
|
+
end
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Custom run script for demo 11 — SQLite3 persistent storage.
|
|
5
|
+
#
|
|
6
|
+
# This demo proves *persistence across server restarts*, so it needs a
|
|
7
|
+
# two-phase lifecycle:
|
|
8
|
+
#
|
|
9
|
+
# Phase 1 — start server, populate tasks via client, stop server
|
|
10
|
+
# Phase 2 — restart server with the same DB file, verify tasks, stop server
|
|
11
|
+
#
|
|
12
|
+
# Dependencies are declared in the Gemfile and Brewfile alongside this script
|
|
13
|
+
# rather than in-lined into the application code:
|
|
14
|
+
#
|
|
15
|
+
# Brewfile — sqlite3 binary (macOS only; other platforms must provide it)
|
|
16
|
+
# Gemfile — sqlite3 gem + simple_a2a gem (via gemspec path)
|
|
17
|
+
#
|
|
18
|
+
# The top-level `examples/run` launcher detects this file and delegates here.
|
|
19
|
+
|
|
20
|
+
require "socket"
|
|
21
|
+
|
|
22
|
+
DEMO_DIR = File.expand_path(__dir__)
|
|
23
|
+
RUBY = RbConfig.ruby
|
|
24
|
+
SERVER_PORT = 9292
|
|
25
|
+
STARTUP_TIMEOUT = 20
|
|
26
|
+
DB_FILE = "/tmp/a2a_sqlite_demo_tasks.db"
|
|
27
|
+
IDS_FILE = "/tmp/a2a_sqlite_demo_ids.json"
|
|
28
|
+
|
|
29
|
+
BREWFILE = File.join(DEMO_DIR, "Brewfile")
|
|
30
|
+
GEMFILE = File.join(DEMO_DIR, "Gemfile")
|
|
31
|
+
SERVER_RB = File.join(DEMO_DIR, "server.rb")
|
|
32
|
+
CLIENT_RB = File.join(DEMO_DIR, "client.rb")
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Helpers
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
def banner(text)
|
|
38
|
+
bar = "─" * (text.length + 4)
|
|
39
|
+
puts "┌#{bar}┐"
|
|
40
|
+
puts "│ #{text} │"
|
|
41
|
+
puts "└#{bar}┘"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def server_ready?(port)
|
|
45
|
+
TCPSocket.new("localhost", port).close
|
|
46
|
+
true
|
|
47
|
+
rescue Errno::ECONNREFUSED, Errno::EADDRNOTAVAIL, Errno::ETIMEDOUT
|
|
48
|
+
false
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def wait_for_server(port, timeout)
|
|
52
|
+
deadline = Time.now + timeout
|
|
53
|
+
sleep 0.1 until server_ready?(port) || Time.now > deadline
|
|
54
|
+
server_ready?(port)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def bundle_env(gemfile)
|
|
58
|
+
{ "BUNDLE_GEMFILE" => gemfile }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def start_server(gemfile, db_path)
|
|
62
|
+
pid = spawn(bundle_env(gemfile), RUBY, SERVER_RB, db_path, out: $stdout, err: $stderr)
|
|
63
|
+
unless wait_for_server(SERVER_PORT, STARTUP_TIMEOUT)
|
|
64
|
+
Process.kill("TERM", pid) rescue nil
|
|
65
|
+
abort "\nServer did not become ready within #{STARTUP_TIMEOUT}s — aborting."
|
|
66
|
+
end
|
|
67
|
+
puts "\n(server ready on port #{SERVER_PORT})\n\n"
|
|
68
|
+
pid
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def stop_server(pid)
|
|
72
|
+
puts "\n(stopping server…)"
|
|
73
|
+
Process.kill("TERM", pid)
|
|
74
|
+
Process.wait(pid)
|
|
75
|
+
rescue Errno::ESRCH, Errno::ECHILD
|
|
76
|
+
# already gone
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Step 1 — ensure sqlite3 binary is available
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
banner "11_sqlite_storage — checking dependencies"
|
|
83
|
+
|
|
84
|
+
unless system("which sqlite3 > /dev/null 2>&1")
|
|
85
|
+
if RUBY_PLATFORM =~ /darwin/
|
|
86
|
+
puts "sqlite3 binary not found — installing via Homebrew (Brewfile)..."
|
|
87
|
+
if system("brew bundle check --file=#{BREWFILE} > /dev/null 2>&1")
|
|
88
|
+
puts "(already satisfied)"
|
|
89
|
+
else
|
|
90
|
+
system("brew bundle install --file=#{BREWFILE}") or
|
|
91
|
+
abort "brew bundle install failed. Check #{BREWFILE} and try again."
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
abort <<~MSG
|
|
95
|
+
|
|
96
|
+
sqlite3 binary not found.
|
|
97
|
+
Install sqlite3 for your platform (e.g. apt install sqlite3) and retry.
|
|
98
|
+
MSG
|
|
99
|
+
end
|
|
100
|
+
else
|
|
101
|
+
puts "(sqlite3 binary found)"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# Step 2 — bundle install using the local Gemfile (adds sqlite3 to the bundle)
|
|
106
|
+
# ---------------------------------------------------------------------------
|
|
107
|
+
puts "(running bundle install for demo 11 Gemfile...)"
|
|
108
|
+
system(bundle_env(GEMFILE), "bundle", "install", "--quiet") or
|
|
109
|
+
abort "bundle install failed. Check #{GEMFILE} and try again."
|
|
110
|
+
puts "(bundle ready)"
|
|
111
|
+
puts
|
|
112
|
+
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
# Clean up any leftover files from a previous run
|
|
115
|
+
# ---------------------------------------------------------------------------
|
|
116
|
+
File.delete(DB_FILE) if File.exist?(DB_FILE)
|
|
117
|
+
File.delete(IDS_FILE) if File.exist?(IDS_FILE)
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Phase 1 — populate
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
banner "11_sqlite_storage — Phase 1: populate"
|
|
123
|
+
server_pid = start_server(GEMFILE, DB_FILE)
|
|
124
|
+
|
|
125
|
+
banner "11_sqlite_storage — client (populate)"
|
|
126
|
+
populate_ok = system(bundle_env(GEMFILE), RUBY, CLIENT_RB, "populate", IDS_FILE)
|
|
127
|
+
|
|
128
|
+
stop_server(server_pid)
|
|
129
|
+
|
|
130
|
+
abort "\nPopulate phase failed — aborting." unless populate_ok
|
|
131
|
+
|
|
132
|
+
puts
|
|
133
|
+
puts "(database preserved at #{DB_FILE})"
|
|
134
|
+
puts "(sleeping 1s before restart...)"
|
|
135
|
+
sleep 1
|
|
136
|
+
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
# Phase 2 — verify (same DB file, brand-new server process)
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
banner "11_sqlite_storage — Phase 2: verify (server restarted)"
|
|
141
|
+
server_pid = start_server(GEMFILE, DB_FILE)
|
|
142
|
+
|
|
143
|
+
banner "11_sqlite_storage — client (verify)"
|
|
144
|
+
verify_ok = system(bundle_env(GEMFILE), RUBY, CLIENT_RB, "verify", IDS_FILE)
|
|
145
|
+
|
|
146
|
+
stop_server(server_pid)
|
|
147
|
+
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
# Clean up temp files
|
|
150
|
+
# ---------------------------------------------------------------------------
|
|
151
|
+
File.delete(DB_FILE) if File.exist?(DB_FILE)
|
|
152
|
+
File.delete(IDS_FILE) if File.exist?(IDS_FILE)
|
|
153
|
+
|
|
154
|
+
exit(verify_ok ? 0 : 1)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Usage: BUNDLE_GEMFILE=examples/11_sqlite_storage/Gemfile \
|
|
5
|
+
# bundle exec ruby examples/11_sqlite_storage/server.rb [db_path]
|
|
6
|
+
#
|
|
7
|
+
# The run script in this directory manages the full lifecycle automatically.
|
|
8
|
+
#
|
|
9
|
+
# Demonstrates injecting a custom Storage::Base implementation (SQLite3) into
|
|
10
|
+
# the simple_a2a server. The db_path argument lets two consecutive server
|
|
11
|
+
# instances share the same database file, proving that tasks created in the
|
|
12
|
+
# first run survive a full process restart.
|
|
13
|
+
|
|
14
|
+
require "sqlite3"
|
|
15
|
+
require_relative "../common_config"
|
|
16
|
+
require "json"
|
|
17
|
+
require "time"
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# SqliteStorage — A2A::Storage::Base backed by SQLite3
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
class SqliteStorage < A2A::Storage::Base
|
|
23
|
+
def initialize(path)
|
|
24
|
+
@db = SQLite3::Database.new(path)
|
|
25
|
+
@mutex = Mutex.new
|
|
26
|
+
@db.execute("PRAGMA journal_mode=WAL")
|
|
27
|
+
@db.execute("PRAGMA busy_timeout=5000")
|
|
28
|
+
setup_schema
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def save(task)
|
|
32
|
+
@mutex.synchronize do
|
|
33
|
+
now = Time.now.iso8601
|
|
34
|
+
@db.execute(
|
|
35
|
+
"INSERT INTO tasks (id, data, created_at, updated_at) VALUES (?, ?, ?, ?) " \
|
|
36
|
+
"ON CONFLICT(id) DO UPDATE SET data=excluded.data, updated_at=excluded.updated_at",
|
|
37
|
+
[task.id, task.to_h.to_json, now, now]
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
task
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def find(id)
|
|
44
|
+
row = @mutex.synchronize { @db.get_first_row("SELECT data FROM tasks WHERE id=?", [id]) }
|
|
45
|
+
return nil unless row
|
|
46
|
+
A2A::Models::Task.from_hash(JSON.parse(row[0]))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def find!(id)
|
|
50
|
+
find(id) or raise A2A::TaskNotFoundError, "Task #{id} not found"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def delete(id)
|
|
54
|
+
@mutex.synchronize { @db.execute("DELETE FROM tasks WHERE id=?", [id]) }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def list
|
|
58
|
+
rows = @mutex.synchronize { @db.execute("SELECT data FROM tasks ORDER BY rowid") }
|
|
59
|
+
rows.map { |row| A2A::Models::Task.from_hash(JSON.parse(row[0])) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def size
|
|
63
|
+
@mutex.synchronize { @db.get_first_value("SELECT COUNT(*) FROM tasks") }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def clear
|
|
67
|
+
@mutex.synchronize { @db.execute("DELETE FROM tasks") }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def setup_schema
|
|
73
|
+
@db.execute(<<~SQL)
|
|
74
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
75
|
+
id TEXT PRIMARY KEY,
|
|
76
|
+
data TEXT NOT NULL,
|
|
77
|
+
created_at TEXT NOT NULL,
|
|
78
|
+
updated_at TEXT NOT NULL
|
|
79
|
+
)
|
|
80
|
+
SQL
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
# Executor — simple echo with a completion timestamp
|
|
86
|
+
# ---------------------------------------------------------------------------
|
|
87
|
+
class EchoExecutor < A2A::Server::AgentExecutor
|
|
88
|
+
def call(ctx)
|
|
89
|
+
input = ctx.message.text_content.strip
|
|
90
|
+
ctx.task.complete!(artifacts: [
|
|
91
|
+
A2A::Models::Artifact.new(
|
|
92
|
+
name: "reply",
|
|
93
|
+
parts: [A2A::Models::Part.text("echo: #{input} (completed at #{Time.now.utc.iso8601})")]
|
|
94
|
+
)
|
|
95
|
+
])
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# ---------------------------------------------------------------------------
|
|
100
|
+
# Startup
|
|
101
|
+
# ---------------------------------------------------------------------------
|
|
102
|
+
db_path = ARGV.first || File.join(__dir__, "tasks.db")
|
|
103
|
+
storage = SqliteStorage.new(db_path)
|
|
104
|
+
|
|
105
|
+
card = A2A::Models::AgentCard.new(
|
|
106
|
+
name: "PersistentEchoAgent",
|
|
107
|
+
version: "1.0",
|
|
108
|
+
description: "Stores tasks in SQLite3; survives server restarts",
|
|
109
|
+
capabilities: A2A::Models::AgentCapabilities.new,
|
|
110
|
+
skills: [
|
|
111
|
+
A2A::Models::AgentSkill.new(
|
|
112
|
+
name: "echo",
|
|
113
|
+
description: "Echoes any text back with a completion timestamp"
|
|
114
|
+
)
|
|
115
|
+
],
|
|
116
|
+
interfaces: [
|
|
117
|
+
A2A::Models::AgentInterface.new(
|
|
118
|
+
type: "json-rpc",
|
|
119
|
+
url: "http://localhost:9292",
|
|
120
|
+
version: "1.0"
|
|
121
|
+
)
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
puts "Starting PersistentEchoAgent on http://localhost:9292"
|
|
126
|
+
puts " database: #{db_path}"
|
|
127
|
+
puts " existing tasks in DB: #{storage.size}"
|
|
128
|
+
puts "Press Ctrl-C to stop."
|
|
129
|
+
puts
|
|
130
|
+
|
|
131
|
+
A2A.server(agent_card: card, executor: EchoExecutor.new, storage: storage).run
|