robot_lab-a2a 0.2.1 → 0.2.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 06fea066d630fe600e2f5e4aeebb95ef5d36ce1003a7808cf5a2bb6d2abbe52a
4
- data.tar.gz: a1825a21bb7ad7fda768df531f376cb877a7b668429ef97a164e168b6cabb599
3
+ metadata.gz: 508cfed94ac09af2dc3aef1bdce245e1ec12e561dc1af58e1a11c37c362c3e9a
4
+ data.tar.gz: 7014425d58923f5cf00f74e96242f0f77890e4342ea1cd06022cbab8d13dbdad
5
5
  SHA512:
6
- metadata.gz: 0c387fb5ab2a628f94327ca5d86b96d1f16413c9aa4d76dfd22ed587a467c2ce9af25f530858ab51de6c1a62b83854f800cd7901a7a8c6d5d87b3d186a2ce469
7
- data.tar.gz: a8a70420bdb2702311677af2b7ff7e231d6d97013e15d94129b9e48b57038664864cc7c95ed0cfe4b2ba082dfdf640edb37dbf6d7d17ff82ebf6dcdb08f4a06f
6
+ metadata.gz: 3604a8078469267732f02a7bd7d5bd52d00e1980a2bac92c38c7c495947d11404fe1dc1614f166589e3be6a75afa98d5889485ad01121fc87e14e1107911ada1
7
+ data.tar.gz: c87c24bf66e2d1490d3e51b4d6155e753d5f1e24083fa2a3c93b1341506c8927f813b509c72d82224d319931fdea3c4b3026c5a1109343478645a9830fb3356a
data/.envrc CHANGED
@@ -1 +1,3 @@
1
+ source_up
1
2
  export RR=`pwd`
3
+ export BUNDLE_GEMFILE=Gemfile
data/.loki ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ # robot_lab-a2a — Agent2Agent protocol adapter over HTTP+SSE.
3
+
4
+ import_up "repo_dev.loki"
5
+
6
+ class Tasks
7
+ @@gem_name ||= "robot_lab-a2a".freeze
8
+
9
+ header "robot_lab-a2a v#{gem_version} — A2A protocol adapter"
10
+ end
@@ -0,0 +1,6 @@
1
+ 2 lib/robot_lab/a2a/ask_user_tool.rb
2
+ 1 lib/robot_lab/a2a/io_bridge.rb
3
+ 2 lib/robot_lab/a2a/network_adapter.rb
4
+ 2 lib/robot_lab/a2a/registry.rb
5
+ 7 lib/robot_lab/a2a/robot_adapter.rb
6
+ 8 lib/robot_lab/a2a/server.rb
data/.rubocop.yml CHANGED
@@ -1,173 +1 @@
1
- AllCops:
2
- NewCops: enable
3
- SuggestExtensions: false
4
- TargetRubyVersion: 4.0
5
- Exclude:
6
- - 'examples/**/*'
7
- - 'vendor/**/*'
8
- - 'dead_code/**/*'
9
-
10
- # ── Style: disabled cops ───────────────────────────────────────────────────
11
- Style/StringLiterals:
12
- Enabled: false
13
-
14
- Style/StringLiteralsInInterpolation:
15
- Enabled: false
16
-
17
- Style/Documentation:
18
- Enabled: false
19
-
20
- # Ruby 4.0 freezes string literals by default
21
- Style/FrozenStringLiteralComment:
22
- Enabled: false
23
-
24
- Style/IfUnlessModifier:
25
- Enabled: false
26
-
27
- Style/RescueModifier:
28
- Enabled: false
29
-
30
- Style/TrivialAccessors:
31
- Enabled: false
32
-
33
- Style/MultilineTernaryOperator:
34
- Enabled: false
35
-
36
- Style/SafeNavigation:
37
- Enabled: false
38
-
39
- Style/EmptyClassDefinition:
40
- Enabled: false
41
-
42
- Style/ClassAndModuleChildren:
43
- Enabled: false
44
-
45
- Style/RescueStandardError:
46
- Enabled: false
47
-
48
- Style/OneClassPerFile:
49
- Enabled: false
50
-
51
- # Both % and format/sprintf are acceptable
52
- Style/FormatString:
53
- Enabled: false
54
-
55
- # String concatenation and interpolation are both acceptable
56
- Style/StringConcatenation:
57
- Enabled: false
58
-
59
- # ── Layout ─────────────────────────────────────────────────────────────────
60
- Layout/LineLength:
61
- Max: 140
62
-
63
- Layout/ExtraSpacing:
64
- Enabled: false
65
-
66
- Layout/HashAlignment:
67
- Enabled: false
68
-
69
- Layout/FirstHashElementIndentation:
70
- Enabled: false
71
-
72
- Layout/EmptyLineAfterGuardClause:
73
- Enabled: false
74
-
75
- # ── Naming ─────────────────────────────────────────────────────────────────
76
- # Single-char params (c, e, n) are acceptable throughout
77
- Naming/MethodParameterName:
78
- Enabled: false
79
-
80
- Naming/VariableNumber:
81
- Exclude:
82
- - 'test/**/*'
83
-
84
- Naming/RescuedExceptionsVariableName:
85
- Enabled: false
86
-
87
- # set_results and similar explicit setters are clear and conventional
88
- Naming/AccessorMethodName:
89
- Enabled: false
90
-
91
-
92
- # has_tool_calls? and similar are clear and conventional
93
- Naming/PredicatePrefix:
94
- Enabled: false
95
-
96
- # Test helper methods don't need to follow predicate naming rules
97
- Naming/PredicateMethod:
98
- Exclude:
99
- - 'test/**/*'
100
-
101
- # ── Lint: relax noisy cops on intentional patterns ─────────────────────────
102
- # Library and framework methods commonly accept args for API/documentation purposes
103
- Lint/UnusedMethodArgument:
104
- Enabled: false
105
-
106
-
107
- Lint/EmptyBlock:
108
- Exclude:
109
- - 'test/**/*'
110
-
111
- Lint/ConstantDefinitionInBlock:
112
- Exclude:
113
- - 'Rakefile'
114
- - 'test/**/*'
115
-
116
- # ── Gemspec ────────────────────────────────────────────────────────────────
117
- Gemspec/DevelopmentDependencies:
118
- EnforcedStyle: Gemfile
119
-
120
- Gemspec/RequiredRubyVersion:
121
- Enabled: false
122
-
123
- Gemspec/OrderedDependencies:
124
- Enabled: false
125
-
126
- # ── Metrics ────────────────────────────────────────────────────────────────
127
- # Framework-level code (routers, parsers, orchestrators) is inherently complex.
128
- # Flog is the primary complexity gate — these RuboCop thresholds catch only
129
- # egregious outliers without false-positiving every dispatch method.
130
-
131
- Metrics/MethodLength:
132
- Max: 35
133
- CountAsOne:
134
- - heredoc
135
- - array
136
- - hash
137
- Exclude:
138
- - 'test/**/*'
139
-
140
- Metrics/AbcSize:
141
- Max: 40
142
- Exclude:
143
- - 'test/**/*'
144
-
145
- Metrics/ClassLength:
146
- Max: 600
147
- Exclude:
148
- - 'test/**/*'
149
-
150
- Metrics/ModuleLength:
151
- Max: 200
152
- Exclude:
153
- - 'test/**/*'
154
-
155
- Metrics/CyclomaticComplexity:
156
- Max: 20
157
- Exclude:
158
- - 'test/**/*'
159
-
160
- Metrics/PerceivedComplexity:
161
- Max: 20
162
- Exclude:
163
- - 'test/**/*'
164
-
165
- # Long method signatures with keyword args are a Ruby framework idiom
166
- Metrics/ParameterLists:
167
- Enabled: false
168
-
169
- Metrics/BlockLength:
170
- Exclude:
171
- - 'Rakefile'
172
- - '*.gemspec'
173
- - 'test/**/*'
1
+ inherit_from: ../.rubocop-base.yml
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ### Added
4
+ - `.loki` Asgard task file: `test`, `rubocop`, `rubocop_fix`, `flog`, `flay`, `quality`, `build`, `install`, `release`, and `console` tasks via the Asgard task runner
5
+ - `flay_check` Rake task: structural code duplication gate (mass threshold 50); integrated into the `quality` Rake task
6
+ - `flay` and `minitest-reporters` gems added to development dependencies
7
+ - `test_output.txt`, `flay_output.txt`, `flog_output.txt`, and `rubocop_output.txt` added to `.gitignore`
8
+
9
+ ### Changed
10
+ - `test/test_helper.rb`: test output redirected to `test_output.txt` via `$stdout` reassignment; `TerminalSummaryReporter` prints a single PASS/FAIL summary line to the terminal
11
+ - `Rakefile`: `rubocop` and `rubocop_fix` tasks removed (now owned by Asgard); `flay_check` integrated into the `quality` gate
12
+
3
13
  ## [0.2.1] - 2026-05-19
4
14
 
5
15
  ### Added
data/CLAUDE.md CHANGED
@@ -33,7 +33,7 @@ All source lives under `lib/robot_lab/a2a/`. The gem uses an adapter pattern —
33
33
 
34
34
  **RobotAdapter** (`robot_adapter.rb`) — Wraps a `RobotLab::Robot` as an executor. Supports three interactive modes:
35
35
  - `:none` — Synchronous, robot runs to completion with no input prompts.
36
- - `:acp_tool` — Injects `AskUserTool` into the robot's `@local_tools`, converting terminal blocking to Queue signaling. Restores original tools on teardown.
36
+ - `:a2a_tool` — Injects `AskUserTool` into the robot's `@local_tools`, converting terminal blocking to Queue signaling. Restores original tools on teardown.
37
37
  - `:io_bridge` — Replaces the robot's I/O streams with an `IoBridge` instance; works with robots that use `puts`/`gets` directly.
38
38
 
39
39
  **AskUserTool** (`ask_user_tool.rb`) — Drop-in replacement for `RobotLab::AskUser`. On `call`, pushes `{type: :ask, prompt:}` to the event_queue then blocks on answer_queue until the A2A client sends a resume.
data/Rakefile CHANGED
@@ -24,16 +24,6 @@ task :test_file, [:file] do |_t, args|
24
24
  ruby "test/#{args[:file]}"
25
25
  end
26
26
 
27
- desc 'Check code style with RuboCop'
28
- task :rubocop do
29
- sh 'bundle exec rubocop'
30
- end
31
-
32
- desc 'Auto-correct RuboCop offenses'
33
- task :rubocop_fix do
34
- sh 'bundle exec rubocop -a'
35
- end
36
-
37
27
  desc 'Check code complexity with Flog (warn >=20, fail >=50)'
38
28
  task :flog_check do
39
29
  require 'flog'
@@ -71,36 +61,62 @@ task :flog_check do
71
61
  end
72
62
  end
73
63
 
74
- desc 'Run all quality checks: tests (with coverage), RuboCop, and Flog'
75
- task :quality do
76
- results = {}
64
+ desc 'Check for structural code duplication with Flay (mass >= 50)'
65
+ task :flay_check do
66
+ require 'flay'
77
67
 
78
- puts "\n#{'=' * 60}"
79
- puts 'Quality Gate: Tests + Coverage'
80
- puts '=' * 60
81
- results[:tests] = system('bundle exec rake test') ? :pass : :fail
68
+ mass_threshold = 50
82
69
 
83
- puts "\n#{'=' * 60}"
84
- puts 'Quality Gate: RuboCop'
85
- puts '=' * 60
86
- results[:rubocop] = system('bundle exec rubocop') ? :pass : :fail
70
+ flay = Flay.new({ mass: mass_threshold, diff: false, verbose: false, summary: false, timeout: 60 })
71
+ flay.process(*Dir.glob('lib/**/*.rb'))
72
+ flay.analyze
87
73
 
88
- puts "\n#{'=' * 60}"
89
- puts 'Quality Gate: Flog Complexity'
90
- puts '=' * 60
91
- results[:flog] = system('bundle exec rake flog_check') ? :pass : :fail
74
+ if flay.hashes.empty?
75
+ puts "\nFlay: no structural duplication detected (mass >= #{mass_threshold})"
76
+ else
77
+ puts "\nFlay found structural duplication (mass >= #{mass_threshold}):"
78
+ flay.report
79
+ abort "\nFlay quality gate failed: #{flay.hashes.length} pattern(s) detected"
80
+ end
81
+ end
82
+
83
+ desc 'Run all quality checks: tests (with coverage), RuboCop, Flog, and Flay'
84
+ task :quality do
85
+ gates = [
86
+ ['Tests + Coverage', 'bundle exec rake test'],
87
+ ['RuboCop', 'bundle exec rubocop'],
88
+ ['Flog Complexity', 'bundle exec rake flog_check'],
89
+ ['Flay Duplication', 'bundle exec rake flay_check']
90
+ ]
91
+
92
+ results = gates.map do |label, command|
93
+ puts "\n#{'=' * 60}"
94
+ puts "Quality Gate: #{label}"
95
+ puts '=' * 60
96
+ [label, system(command) ? :pass : :fail]
97
+ end
98
+
99
+ green = ->(s) { "\e[32m#{s}\e[0m" }
100
+ red = ->(s) { "\e[31m#{s}\e[0m" }
101
+ width = results.map { |label, _| label.length }.max
92
102
 
93
103
  puts "\n#{'=' * 60}"
94
- puts 'Quality Summary'
104
+ puts 'Quality Gate Summary'
95
105
  puts '=' * 60
96
- results.each do |gate, status|
97
- icon = status == :pass ? 'PASS' : 'FAIL'
98
- puts " [#{icon}] #{gate}"
106
+ results.each do |label, status|
107
+ badge = status == :pass ? green.call('PASS') : red.call('FAIL')
108
+ puts " [#{badge}] #{label.ljust(width)}"
99
109
  end
110
+ puts '-' * 60
111
+
112
+ passed = results.count { |_, s| s == :pass }
113
+ failed = results.count { |_, s| s == :fail }
114
+ tally = "#{passed} passed, #{failed} failed"
115
+ puts " #{failed.zero? ? green.call(tally) : red.call(tally)}"
100
116
  puts '=' * 60
101
117
 
102
- abort "\nQuality gate failed" if results.values.any?(:fail)
103
- puts "\nAll quality gates passed."
118
+ abort "\n#{red.call('Quality gate failed.')}" unless failed.zero?
119
+ puts "\n#{green.call('All quality gates passed.')}"
104
120
  end
105
121
 
106
122
  namespace :docs do
data/docs/examples.md CHANGED
@@ -5,6 +5,7 @@
5
5
  ```
6
6
  examples/
7
7
  run # launcher script
8
+ common_config.rb # shared $LOAD_PATH + require, loaded by every example
8
9
  01_sync_robot/
9
10
  server.rb # starts the A2A server
10
11
  client.rb # sends a task and prints the result
@@ -14,6 +15,16 @@ examples/
14
15
  03_robot_network/
15
16
  server.rb
16
17
  client.rb
18
+ 04_io_bridge/
19
+ server.rb
20
+ client.rb
21
+ 05_multi_agent/
22
+ server.rb
23
+ client.rb
24
+ 06_rack_mount/
25
+ server.rb
26
+ config.ru # standalone Rack/Rails mount reference (not used by run)
27
+ client.rb
17
28
  ```
18
29
 
19
30
  ## How to run
@@ -114,3 +125,46 @@ Sending task to network...
114
125
  Reply: [synthesised answer from the final pipeline stage]
115
126
  Done.
116
127
  ```
128
+
129
+ ## 04_io_bridge
130
+
131
+ **Demonstrates:** `:io_bridge` interactive mode. `QuoteRobot` has no knowledge of A2A at all — it just calls `@output.puts` and `@input.gets` (falling back to `$stdout`/`$stdin` when not injected), so the same class also runs fine interactively in a terminal. The server injects an `IoBridge` as `robot.input`/`robot.output` before each run.
132
+
133
+ **Two-turn flow:**
134
+
135
+ 1. Client sends a topic (`"stoicism"`) → `QuoteRobot` writes its question to the buffered output, then calls `gets` → `IoBridge` flushes the buffer as the `input_required` prompt and blocks.
136
+ 2. Client resumes with the same `task_id` and an answer (`"Ada"`) → `IoBridge` unblocks `gets` → the robot completes and returns a quote addressed to that name.
137
+
138
+ **What it shows:**
139
+
140
+ - `interactive: :io_bridge` server setup
141
+ - A robot written against plain Ruby IO, with no A2A-specific code
142
+ - The same two-turn `task_id` resume pattern as `:a2a_tool`
143
+
144
+ **Note:** Turn 2 requires a `simple_a2a` build with `ResumeContext` support (>= 0.3.1). On older versions the client still demonstrates Turn 1's `input_required` suspension.
145
+
146
+ ## 05_multi_agent
147
+
148
+ **Demonstrates:** Multiple independent A2A agents served from a single process via the fluent builder API with explicit `path:` overrides — `HeadlineRobot` at `/headline` and `TagRobot` at `/tags`.
149
+
150
+ **What it shows:**
151
+
152
+ - Chaining `add_robot` calls with explicit `path:` values on one `Server` instance
153
+ - Two separate `A2A.client` instances, one per agent path, each with its own agent card
154
+ - That co-located agents are indistinguishable from independently hosted ones at the protocol level
155
+
156
+ **Expected output (client):** both clients discover their agent card, send the same input text, and print each robot's distinct reply (a capitalised headline vs. a list of `#hashtag` keywords) alongside pass/fail assertions.
157
+
158
+ ## 06_rack_mount
159
+
160
+ **Demonstrates:** `server.to_app`, which returns a `Rack::URLMap` instead of starting a dedicated server — for embedding A2A agents inside a larger Rack application (Rails, Sinatra, Puma, etc.) rather than running `server.run` standalone.
161
+
162
+ The demo composes the A2A agent (`EchoRobot`, mounted via `to_app`) with a plain `/health` JSON Rack endpoint on the same combined app, run via `A2A::Server::FalconRunner`. `config.ru` in this directory is a copy-paste reference for mounting the same `to_app` result under Rackup or a Rails `config/routes.rb` — it is not used by `examples/run`.
163
+
164
+ **What it shows:**
165
+
166
+ - `add_robot(...).to_app` returning a `Rack::URLMap`
167
+ - Composing that map with unrelated Rack routes
168
+ - That both A2A and non-A2A routes work correctly side by side on the composed app
169
+
170
+ **Expected output (client):** a passing health check against `/health` followed by a normal A2A `send_task` round trip against `/echo-robot`, each reported with a pass/fail assertion.
@@ -102,3 +102,7 @@ RobotLab::A2A::Server.new(interactive: :io_bridge)
102
102
  | `:io_bridge` | `robot.run(text)` + settable `robot.input=` / `robot.output=` | Robots that interact via raw stdin/stdout streams |
103
103
 
104
104
  When in doubt, start with `:none`. Upgrade to `:a2a_tool` if your robot uses `AskUser`, or `:io_bridge` if it uses raw IO.
105
+
106
+ ## Cancelling an in-progress task
107
+
108
+ While a robot is suspended waiting on `answer_queue` (i.e. between an `input_required` event and the client's resume), the A2A client may send `tasks/cancel` instead of resuming. `RobotAdapter#cancel` looks the task up in `Registry` by task ID, kills the robot thread with `Thread#kill`, and removes the entry from `Registry` before delegating to the default `AgentExecutor#cancel` behaviour. If no entry is found (e.g. the task already completed or was never interactive), `cancel` is a no-op aside from the default behaviour.
data/docs/server-api.md CHANGED
@@ -27,8 +27,8 @@ Valid `interactive` values: `:none`, `:a2a_tool`, `:io_bridge`. See [Interactive
27
27
  ```ruby
28
28
  server.add_robot(
29
29
  robot,
30
- name: required, # String — human-readable agent name
31
- description: required, # String — agent capability description
30
+ name: nil, # String — human-readable agent name; defaults to robot.name
31
+ description: nil, # String — agent capability description; defaults to robot.description
32
32
  path: nil # String — URL path segment; derived from name if nil
33
33
  ) → self
34
34
  ```
@@ -51,12 +51,12 @@ Wraps `robot` in a `RobotAdapter` using the server's `interactive` setting and r
51
51
  server.add_network(
52
52
  network,
53
53
  name: required, # String — human-readable agent name
54
- description: required, # String — agent capability description
54
+ description: nil, # String — agent capability description; defaults to name
55
55
  path: nil # String — URL path segment; derived from name if nil
56
56
  ) → self
57
57
  ```
58
58
 
59
- Wraps `network` in a `NetworkAdapter`. Only `:none` mode is supported for networks; the `interactive` setting is ignored.
59
+ Wraps `network` in a `NetworkAdapter`, always constructed with `interactive: :none`. If the server's own `interactive` setting is anything other than `:none`, `add_network` raises `ArgumentError` instead of silently ignoring it — networks do not support interactive modes. Wrap individual robots with `RobotAdapter` (via `add_robot`) for interactive flows.
60
60
 
61
61
  **`network` must respond to:**
62
62
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RobotLab
4
4
  module A2A
5
- VERSION = '0.2.1'
5
+ VERSION = '0.2.6'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: robot_lab-a2a
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dewayne VanHoozer
@@ -63,6 +63,8 @@ extra_rdoc_files: []
63
63
  files:
64
64
  - ".envrc"
65
65
  - ".github/workflows/deploy-github-pages.yml"
66
+ - ".loki"
67
+ - ".quality/reek_baseline.txt"
66
68
  - ".rubocop.yml"
67
69
  - CHANGELOG.md
68
70
  - CLAUDE.md
@@ -123,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
125
  - !ruby/object:Gem::Version
124
126
  version: '0'
125
127
  requirements: []
126
- rubygems_version: 4.0.11
128
+ rubygems_version: 4.0.17
127
129
  specification_version: 4
128
130
  summary: Agent2Agent (A2A) protocol adapter for RobotLab
129
131
  test_files: []