poetry-agent 0.1.1 → 0.1.3

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: b23fc776e813d4bc6418bfbdc982f06756c3b7cc12eeac3b36d210fd29e63aac
4
- data.tar.gz: 90ee7680d11bcc1fa12cdea0f7dd8543b972af0de5f491b0b364565eea672a05
3
+ metadata.gz: f94b26ba2c90475193b090c1deba25d33ffad8fbec0c3b4fdee0e22be1c7a977
4
+ data.tar.gz: 8d504ccbe9f96573ffe89dee1e7ed80ccefd1b5af93a9f9543aa70228ff5c059
5
5
  SHA512:
6
- metadata.gz: 428611c9a1fe1618a72f5ae5f3236890080f3b8a20dd34cedb80cfa8c8162c1ef571a12b8e1de0efb7d25703475f8096e3da8242bc521a9c7275e99699935388
7
- data.tar.gz: 910b7c98b3525cac9059f118b5199a638e360f10dfdc333b6d374255fd5174433a40fd6f50b330f6917b52e0a4b7aef8c030541aa6ec424d11eef2843912e991
6
+ metadata.gz: 9e565a75006736af0eae0d6ca7e7284c00c7450399abde58de0e9aafac1869a7f72fd77f256a3100e41525eeea2a92751ecb9b3b03a1324450453ab71402f2e3
7
+ data.tar.gz: 23e554fad8c091dca3ca66f7947452f870f88586d78901470024fd9d8f84b10a15129c35c8f2cba5b4f72044b9f4eb7af3f8d35e07dec630debba4de1b704d87
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.3] - 2026-09-13
4
+
5
+ ### Fixed
6
+
7
+ - A2UI's native catalog supplies the render-stable key as `id:` to a component that requires one (Field, MessageScroller), since an agent never sets ids. On 0.1.2 the values tier refused the missing option and the renderer swallowed the error as a warning, so a Field rendered nothing.
8
+ - The MCP `check` tool knows the app's own `poetry_*` helper methods (`app/helpers`, read boot-free), committed registry or not: the adapter `poetry:pagination` copies in no longer reads as an unknown component.
9
+
10
+ ## [0.1.2] - 2026-09-13
11
+
12
+ ### Changed
13
+
14
+ - A tool called without one of its declared required arguments (or with it blank) answers an error naming the argument; `check` in particular no longer reports PASS for a call that sent no `source`. `compose` and `build_page` keep answering a blank brief with their own guidance.
15
+
16
+ - `compose`, `build_page`, `list_components` and `describe_component` name an app component's helper from the registry, and an app component without one by the class to render, never an invented `poetry_` name. Once the app committed its registry that file is the truth and the component sources are not scanned; an invalid committed registry is not a root and does not crash the server.
17
+
18
+ - The WebMCP agent-focus styling (`:tool-form-active` on the form an agent fills, `:tool-submit-active` on its submit) moves here from poetry-ui's nine theme fragments, where it was identical in every theme: `app/assets/stylesheets/poetry-agent.css`, plain CSS on the theme's tokens, vendored by `poetry:install` into `layer(base)` when this gem is bundled. Hosts without poetry-agent no longer carry the rules or the two warnings the CSS optimizer prints for the origin-trial pseudo-classes on every minified build (the Rails Tailwind task minifies by default).
19
+ - Controllers manifests are registered by the same convention, boot-free: every bundled gem's and the app's own (`bin/rails poetry:stimulus:manifest`), so the `check` tool validates chart, agent and host controllers like core's.
20
+ - The server assembles from every published registry in the bundle plus the app's own committed one (`bin/rails poetry:registry`), by convention: no gem is named, a third-party engine that commits a registry is served, and an app's components describe, check and compose with full contracts under their declared helpers. `Server.from_registries` merges roots; `from_registry` remains for one.
21
+ - The MCP `check` tool knows the host application's own component helpers: a boot-free scan of the app directory's component files for `helper :name` declarations adds those names to the valid set, so the tool agrees with `bin/rails poetry:check` that the helper exists. Their option contracts stay with the booted check.
22
+
3
23
  ## [0.1.1] - 2026-09-08
4
24
 
5
25
  Lockstep release with the family; no changes in this gem.
@@ -0,0 +1,25 @@
1
+ /* poetry-agent: the WebMCP agent-focus states. The browser applies
2
+ :tool-form-active to the form an agent is filling through a declarative
3
+ tool and :tool-submit-active to its submit; these rules make that a
4
+ designed state on the theme's tokens instead of the browser's default.
5
+ :where() keeps both rules forgiving (zero specificity, and a browser
6
+ without the pseudo-classes drops the argument, not the rule), so a host
7
+ restyles them with any rule of its own. Plain CSS on purpose: the file
8
+ is vendored into the host's Tailwind entry (`poetry:install` when this
9
+ gem is bundled) and imported into layer(base) beside the theme.
10
+
11
+ The CSS optimizer that runs on every minified build (the Rails
12
+ Tailwind task minifies by default; TAILWINDCSS_DEBUG=1 skips it) does
13
+ not yet know these origin-trial pseudo-classes and warns twice; that is
14
+ the optimizer, not the CSS - the rules compile and ship. Hosts without
15
+ poetry-agent never carry them. */
16
+ :where(form:tool-form-active) {
17
+ border-radius: var(--radius);
18
+ outline: 2px dashed color-mix(in oklab, var(--ring) 60%, transparent);
19
+ outline-offset: 4px;
20
+ }
21
+
22
+ :where(:tool-submit-active) {
23
+ border-color: var(--ring);
24
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--ring) 50%, transparent);
25
+ }
@@ -97,6 +97,10 @@ module Poetry
97
97
  by_name[component["component"].to_s] || [nil, nil]
98
98
  end
99
99
 
100
+ def option_required?(entry, name)
101
+ Array(entry["options"]).any? { |option| option["name"] == name && option["required"] }
102
+ end
103
+
100
104
  def attributes(component, entry, scope, renderer)
101
105
  attributes = {}
102
106
  Array(entry["styles"]).each do |axis|
@@ -109,6 +113,10 @@ module Poetry
109
113
 
110
114
  attributes[name.to_sym] = renderer.resolve(component[name], scope)
111
115
  end
116
+ # An id the component requires (Field: the control its label
117
+ # names) is not the agent's to set, so the render-stable key
118
+ # stands in and the component builds instead of vanishing.
119
+ attributes[:id] = renderer.stable_key if option_required?(entry, "id") && !attributes.key?(:id)
112
120
  bound = BOUND_OPTIONS.find { |name| renderer.surface.binding?(component[name]) }
113
121
  if bound && option_names.include?("name") && !component.key?("name")
114
122
  attributes[:name] = renderer.input_name(component[bound]["path"], scope)
@@ -12,23 +12,37 @@ module Poetry
12
12
  module Bundled
13
13
  module_function
14
14
 
15
- # @param root [String, nil] a registry root; defaults to poetry-ui
16
- # @param app_root [String] the host app (build_page's probe/direct
15
+ # @param root [String, nil] one more registry root to serve (the
16
+ # bundle's published registries and the app's committed one are
17
+ # found on their own)
18
+ # @param app_root [String] the host app (its committed registry, when
19
+ # `bin/rails poetry:registry` wrote one; build_page's probe/direct
17
20
  # steps read its config/theme)
18
21
  # @return [Server]
19
22
  # @raise [ArgumentError] when no component registry is found
20
23
  def server(root: nil, app_root: Dir.pwd)
21
24
  ui = soft_require("poetry/ui")
22
- root ||= ui ? Poetry::Ui.root.to_s : Gem::Specification.find_all_by_name("poetry-ui").first&.gem_dir
23
- registry = root && File.join(root, Poetry::Core::Registry::RELATIVE_PATH)
24
- unless registry && File.exist?(registry)
25
- raise ArgumentError, "no component registry at #{registry || "(no poetry-ui found)"} - pass root:"
26
- end
25
+ roots = Poetry::Core::Registry.gem_roots(app_root: app_root).map(&:to_s)
26
+ # Controllers manifests by the same convention, boot-free: every
27
+ # bundled gem's and the app's own, so the check tool validates
28
+ # chart, agent and host controllers like core's.
29
+ manifest_roots = Poetry::Core::Registry.gem_roots(app_root: app_root, registry: false)
30
+ Poetry::Core::Stimulus::Manifest.register_roots(manifest_roots)
31
+ roots.unshift(root) if root && !roots.include?(root)
32
+ raise ArgumentError, "no published component registry in the bundle - pass a root" if roots.empty?
27
33
 
28
34
  skills, helpers, recipes =
29
- ui ? [Poetry::Ui.agent_skills, Poetry::Ui.helper_names, Poetry::Ui.recipe_items.summaries] : [{}, nil, []]
30
- Server.from_registry(root, icon_names: icon_names, helpers: helpers, skills: skills,
31
- app_root: app_root, recipes: recipes)
35
+ if ui
36
+ [Poetry::Ui.agent_skills(app_root: app_root), Poetry::Ui.helper_names, Poetry::Ui.recipe_items.summaries]
37
+ else
38
+ [{}, nil, []]
39
+ end
40
+ # The valid helper set: the registries' own sections carry every
41
+ # gem helper and the app's declared ones; poetry-ui's live names
42
+ # ride along when the gem is loaded, for a registry that predates
43
+ # the sections.
44
+ Server.from_registries(roots, icon_names: icon_names, helpers: helpers, skills: skills,
45
+ app_root: app_root, recipes: recipes)
32
46
  end
33
47
 
34
48
  # The lucide names, or nil for a host without poetry-lucide (check
@@ -27,6 +27,10 @@ module Poetry
27
27
  # The serverInfo payload returned by the initialize handshake.
28
28
  SERVER_INFO = { "name" => "poetry-agent", "version" => Poetry::Agent::VERSION }.freeze
29
29
 
30
+ # Tools that answer a blank brief with guidance of their own (the
31
+ # block catalog, what to send) rather than a pass.
32
+ TOLERATES_BLANK = %w[compose build_page].freeze
33
+
30
34
  # The tool roster the server advertises (tools/list): MCP Tool-shaped
31
35
  # definitions, read-only by construction.
32
36
  TOOLS = [
@@ -196,13 +200,47 @@ module Poetry
196
200
  # Lazy because the usage skill is generated from the registry on
197
201
  # first fetch - server boot stays instant.
198
202
  def self.from_registry(root, helpers: nil, icon_names: nil, skills: {}, app_root: nil, recipes: [])
199
- committed = Poetry::Core::Registry.committed(root)
203
+ from_registries([root], helpers: helpers, icon_names: icon_names, skills: skills,
204
+ app_root: app_root, recipes: recipes)
205
+ end
206
+
207
+ # Several registry roots as one catalog (Registry.merged: later
208
+ # roots win a path collision, block templates resolve to absolute
209
+ # paths): the gems' published registries and, when the app
210
+ # committed its own with `bin/rails poetry:registry`, the app's -
211
+ # its components then describe, check and compose like a gem's.
212
+ def self.from_registries(roots, helpers: nil, icon_names: nil, skills: {}, app_root: nil, recipes: [])
213
+ committed = Poetry::Core::Registry.merged(roots, source_root: roots.first)
214
+ # The app's own components declare their helpers in source
215
+ # (`helper :name`); a boot-free scan of app_root's component files
216
+ # adds those names to the valid set, so the check tool agrees with
217
+ # `bin/rails poetry:check` that the helper exists (their option
218
+ # contracts are only known to the booted check).
219
+ helper_entries = (committed.helpers || {}).dup
220
+ # Only for an app that never committed its registry: once the
221
+ # file exists it is the truth (helpers, contracts), not the scan.
222
+ if app_root && !Poetry::Core::Registry.published_at?(app_root)
223
+ Poetry::Core::HostComponents.declared_helpers(root: app_root).each { |name| helper_entries[name] ||= {} }
224
+ end
225
+ helpers = (helpers + mapped_helpers(committed.entries)).uniq if helpers
226
+ # The app's own poetry_* helper methods (app/helpers, boot-free):
227
+ # valid names with contracts of their own, whether or not the
228
+ # registry is committed - no registry lists a helper method.
229
+ host_helpers = app_root ? Poetry::Core::HostComponents.helper_methods(root: app_root) : nil
200
230
  catalog = Poetry::Core::Check::Catalog.new(committed.entries, helpers: helpers,
201
- helper_entries: committed.helpers,
231
+ helper_entries: helper_entries,
202
232
  icon_names: icon_names,
203
- helper_args: committed.helper_args)
233
+ helper_args: committed.helper_args,
234
+ host_helpers: host_helpers)
204
235
  new(entries: committed.entries, catalog: catalog, blocks: committed.blocks || {},
205
- root: root, skills: skills, app_root: app_root, recipes: recipes)
236
+ root: committed.source_root.to_s, skills: skills, app_root: app_root, recipes: recipes)
237
+ end
238
+
239
+ # The helper each entry names, so an explicit helper list never
240
+ # hides another root's components (an entry without one has no
241
+ # helper to map).
242
+ def self.mapped_helpers(entries)
243
+ entries.filter_map { |path, entry| Poetry::Core::Registry.helper_for(path, entry) }
206
244
  end
207
245
 
208
246
  # app_root: the HOST app directory (where `bundle exec poetry-agent`
@@ -295,6 +333,10 @@ module Poetry
295
333
  def call_tool(params)
296
334
  name = params["name"]
297
335
  arguments = params["arguments"] || {}
336
+ if !TOLERATES_BLANK.include?(name) && (missing = missing_arguments(name, arguments)).any?
337
+ return tool_content("#{name}: missing required argument#{"s" if missing.size > 1} " \
338
+ "#{missing.join(", ")} (see tools/list for the schema)", error: true)
339
+ end
298
340
  text =
299
341
  case name
300
342
  when "compose" then compose(arguments)
@@ -312,12 +354,21 @@ module Poetry
312
354
  tool_content(text)
313
355
  end
314
356
 
357
+ # The tool's declared required arguments that are absent or blank -
358
+ # a wrong key never reaches a tool as an empty value it would pass.
359
+ def missing_arguments(name, arguments)
360
+ tool = TOOLS.find { |candidate| candidate["name"] == name }
361
+ return [] unless tool
362
+
363
+ Array(tool.dig("inputSchema", "required")).select { |key| arguments[key].to_s.strip.empty? }
364
+ end
365
+
315
366
  # --- the tools (project the built surfaces) ---
316
367
 
317
368
  def list_components
318
369
  @entries.map do |path, entry|
319
370
  interactive = entry["controllers"]&.any? ? " [interactive]" : ""
320
- "- #{title(path)} (`#{helper(path)}`)#{interactive}: #{summary(entry)}"
371
+ "- #{title(path)} (#{helper_label(path)})#{interactive}: #{summary(entry)}"
321
372
  end.join("\n")
322
373
  end
323
374
 
@@ -327,7 +378,7 @@ module Poetry
327
378
 
328
379
  entry = @entries.fetch(path)
329
380
  detail = arguments.fetch("detail", "detailed")
330
- lines = ["# #{title(path)} (`#{helper(path)}`)", summary(entry)]
381
+ lines = ["# #{title(path)} (#{helper_label(path)})", summary(entry)]
331
382
  lines.concat(surface_lines(entry)) if %w[detailed full].include?(detail)
332
383
  lines.concat(full_lines(entry)) if detail == "full"
333
384
  # The block back-reference: a screen containing this
@@ -434,7 +485,9 @@ module Poetry
434
485
  def component_route(scored, components)
435
486
  lines = ["No block covers this brief - component-scale work."]
436
487
  lines << if components.any?
437
- "Matched components: #{components.map { |name| "#{name} (`poetry_#{name}`)" }.join(", ")} - " \
488
+ "Matched components: #{components.map do |name|
489
+ "#{name} (#{helper_label(path_for(name))})"
490
+ end.join(", ")} - " \
438
491
  "describe_component for the contracts."
439
492
  else
440
493
  "No component name matched either - list_components for the catalog."
@@ -860,7 +913,7 @@ module Poetry
860
913
  convention = args && slot["types"].all? { |type| args[type]&.zero? } ? " - options as keywords" : ""
861
914
  facets << "types #{slot["types"].join("|")}#{convention}"
862
915
  end
863
- facets << "takes #{helper(slot["component"])} props, not a block" if slot["component"]
916
+ facets << "takes #{helper_text(slot["component"])} props, not a block" if slot["component"]
864
917
  # The render-crash seams, stated where agents read them.
865
918
  if (yieldless = slot["yieldless"])
866
919
  setters = yieldless.map { |name| "with_#{name}" }.join("/")
@@ -936,14 +989,24 @@ module Poetry
936
989
  end
937
990
 
938
991
  # name may be the title (button, command_dialog) or the full path.
992
+ # A registry path, a title, or a helper name (the poetry_ convention
993
+ # or an app component's declared helper).
939
994
  def resolve(name)
940
995
  return name if @entries.key?(name)
941
996
 
942
- @entries.keys.find { |path| title(path) == name }
997
+ @entries.keys.find { |path| title(path) == name || helper(path) == name }
943
998
  end
944
999
 
945
- def title(path) = path.split("/").drop(2).join("_")
946
- def helper(path) = "poetry_#{title(path)}"
1000
+ # A gem path drops its poetry/<gem>/ prefix; an app path is its own.
1001
+ def title(path) = (path.start_with?("poetry/") ? path.split("/").drop(2) : path.split("/")).join("_")
1002
+ # The helper the registry names for the entry, nil for an app
1003
+ # component that declared none (it renders by class).
1004
+ def helper(path) = Poetry::Core::Registry.helper_for(path, @entries[path] || {})
1005
+ # The helper to render the entry by, or the class when it has none.
1006
+ def helper_text(path) = helper(path) || "render #{@entries.dig(path, "class_name") || path}"
1007
+ # The same, the helper in backticks.
1008
+ def helper_label(path) = (name = helper(path)) ? "`#{name}`" : helper_text(path)
1009
+ def path_for(name) = @entries.keys.find { |path| title(path) == name }
947
1010
 
948
1011
  def result(id, value) = { "jsonrpc" => "2.0", "id" => id, "result" => value }
949
1012
 
@@ -3,6 +3,6 @@
3
3
  module Poetry
4
4
  module Agent
5
5
  # The gem version (lockstep with the poetry family).
6
- VERSION = "0.1.1"
6
+ VERSION = "0.1.3"
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: poetry-agent
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Solt
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - '='
17
17
  - !ruby/object:Gem::Version
18
- version: 0.1.1
18
+ version: 0.1.3
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - '='
24
24
  - !ruby/object:Gem::Version
25
- version: 0.1.1
25
+ version: 0.1.3
26
26
  description: 'The agent-interop gem of Poetry, the AI-native UI component library.
27
27
  Five surfaces: the boot-free poetry-agent MCP server (the component contract over
28
28
  Model Context Protocol, with runtime skill delivery); the WebMCP runtime that registers
@@ -43,6 +43,7 @@ files:
43
43
  - CHANGELOG.md
44
44
  - LICENSE.txt
45
45
  - README.md
46
+ - app/assets/stylesheets/poetry-agent.css
46
47
  - app/javascript/poetry/agent/a2ui_surface_controller.js
47
48
  - app/javascript/poetry/agent/adapter.js
48
49
  - app/javascript/poetry/agent/agui_client_tool_controller.js