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 +4 -4
- data/CHANGELOG.md +20 -0
- data/app/assets/stylesheets/poetry-agent.css +25 -0
- data/lib/poetry/agent/a2ui/catalogs/native.rb +8 -0
- data/lib/poetry/agent/mcp/bundled.rb +24 -10
- data/lib/poetry/agent/mcp/server.rb +74 -11
- data/lib/poetry/agent/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f94b26ba2c90475193b090c1deba25d33ffad8fbec0c3b4fdee0e22be1c7a977
|
|
4
|
+
data.tar.gz: 8d504ccbe9f96573ffe89dee1e7ed80ccefd1b5af93a9f9543aa70228ff5c059
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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]
|
|
16
|
-
#
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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)} (
|
|
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)} (
|
|
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
|
|
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 #{
|
|
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
|
-
|
|
946
|
-
def
|
|
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
|
|
data/lib/poetry/agent/version.rb
CHANGED
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.
|
|
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.
|
|
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.
|
|
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
|