diff --git a/README.md b/README.md index 76b024d..5df095f 100644 --- a/README.md +++ b/README.md @@ -84,14 +84,15 @@ environment: ```python import agent_plugins as ap -plugin = ap.locate("agent-plugins") -consumer_skill = plugin.skill("agent-plugins") -packaging_skill = plugin.skill("package-agent-plugin") - -print(consumer_skill.source) -print(packaging_skill.source) +print(ap.read("agent-plugins")) +print(ap.read("agent-plugins", skill="package-agent-plugin")) ``` +`read()` returns a briefing for the same-name skill by default. Pass `skill=` +for another workflow. The CLI reads all packaged skills unless `--skill` is +provided. `uvx` uses an isolated tool environment, so read through the target +interpreter when working with an existing installation. + Pass your library's distribution name to `locate()` to inspect its plugin. Use [`Plugin.from_project()`](https://peter-gy.github.io/agent-plugins/guide/inspect-project) to inspect the selected source files before building. ## Documentation diff --git a/development_docs/architecture.md b/development_docs/architecture.md index 8f87053..fbe6e81 100644 --- a/development_docs/architecture.md +++ b/development_docs/architecture.md @@ -67,7 +67,7 @@ other source → supplied BuildPlan │ │ │ ## Dependency direction -The public `agent_plugins` package re-exports build planning, wheel attachment, discovery, filesystem, schema-value, and diagnostic types. `agent_plugins.build` separately exports the low-level `BuildBackend`. +The public `agent_plugins` package re-exports build planning, wheel attachment, discovery, briefing rendering, filesystem, schema-value, and diagnostic types. `agent_plugins.build` separately exports the low-level `BuildBackend`. Artifact operations depend on plan validation and archive writers. Archive writers depend on plan values and the marker codec. Runtime discovery depends on the marker codec and file inventory. Project inspection composes build planning with the same inventory model. @@ -76,6 +76,13 @@ Versioned schema loaders produce normalized values. Stdio resolution consumes th The CLI parses input, calls domain functions, and writes their rendered output. Core modules do not depend on terminal state. +`read()` selects one skill, defaulting to the distribution argument exactly. +The CLI's `read` command selects every skill unless `--skill` is supplied. +Both use `_read.py` for the same metadata, interpreter identity, environment +guidance, documentation links, and complete skill source. Reading a briefing +uses distribution metadata and selected files without importing the target +package or activating a host integration. + ## Public object model `Plugin` and `Skill` are slotted filesystem handles. Plugin equality and hashing diff --git a/development_docs/testing-and-release.md b/development_docs/testing-and-release.md index cb4fc6a..8d5f0d6 100644 --- a/development_docs/testing-and-release.md +++ b/development_docs/testing-and-release.md @@ -52,6 +52,7 @@ The distribution verifier: | Build-plan selection and CLI JSON | `tests/test_plan.py` | | Public wheel attachment, preservation, validation, signatures, and `RECORD` | `tests/test_wheel.py` | | CLI read and attachment output, warnings, and exit statuses | `tests/test_cli.py` | +| Python briefings, skill selection, CLI parity, and module help | `tests/test_read.py` | | Backend parity, sdist rebuilds, editable markers, and sdist modes | `tests/test_build_backends.py` | | Installed distribution discovery and marker failures | `tests/test_discovery.py` | | Project-selected plugin inventory, paths, display, named skills | `tests/test_plugin.py` | diff --git a/docs/guide/inspect-installed.md b/docs/guide/inspect-installed.md index 0b2875a..7ee0f49 100644 --- a/docs/guide/inspect-installed.md +++ b/docs/guide/inspect-installed.md @@ -17,11 +17,31 @@ Read a package's complete primary agent instructions in one command: uvx --with my-project agent-plugins read my-project ``` -uv installs `my-project` and `agent-plugins` in one temporary environment. The -command prints the complete version-matched guidance carried by `my-project`. +uv installs `my-project` and `agent-plugins` in an isolated tool environment. +The command reads that installation. Its cached paths may remain readable +locally, but the project or notebook can have a different installation or +filesystem. -Use `locate()` when the Python library is already installed in the environment -where the agent will work: +## Read in the execution environment + +When the package is already installed, read its briefing in the Python +environment where the agent will work: + +```python +import agent_plugins as ap + +print(ap.read("agent-plugins")) +print(ap.read("agent-plugins", skill="package-agent-plugin")) +``` + +`read()` returns one skill with distribution identity, interpreter, resource +guidance, and the plugin inventory. It defaults to the skill whose directory +name matches the distribution argument exactly. Pass `skill=` for another +name. The CLI includes all skills unless `--skill` is supplied. + +Read again when switching environments or installations. Reuse loaded +instructions for the same installation and follow their runtime readiness +checks. To inspect files programmatically, use `locate()`. ## Locate one distribution diff --git a/docs/reference/cli.md b/docs/reference/cli.md index cab0eee..90d9082 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -33,14 +33,16 @@ agent-plugins read DISTRIBUTION [--skill NAME] `DISTRIBUTION` is an installed Python distribution name. Human output is a Markdown briefing containing: -- Python distribution and plugin manifest metadata +- Python distribution and plugin manifest metadata, interpreter path, and + declared `Documentation` and `Documentation Index` URLs - the bounded selected plugin file tree and installed root - client extension names and manifest validation issues - MCP server names and transport types, with configured commands, arguments, environment values, URLs, and headers excluded - the complete `SKILL.md` source for every packaged Agent Skill by default -The generated introduction explains relative resource paths. Skill source +The generated introduction explains environment ownership and relative resource +paths, including the disposable tool environment used by `uvx`. Skill source remains complete. The inventory uses the standard tree bounds of four levels and 100 files, and reports omitted depth or file counts. Inspect the printed installed root when a skill routes to a resource outside that view. @@ -60,6 +62,13 @@ separate inputs. uv installs the requirement into the temporary command environment. `read` selects that installed distribution through Python metadata. +To inspect an existing project or notebook installation, run +`python -m agent_plugins read my-package` with that environment's interpreter. +Its paths and package version may differ from the isolated tool environment. +Use [`ap.read()`](./python-api#read-a-briefing) inside a Python execution host. +The Python API defaults to the same-name skill, while the CLI defaults to all +skills. + ## `agent-plugins plan` ```text diff --git a/docs/reference/python-api.md b/docs/reference/python-api.md index 37c39ee..28c0be8 100644 --- a/docs/reference/python-api.md +++ b/docs/reference/python-api.md @@ -15,15 +15,15 @@ The installed `agent-plugins` distribution includes separate consumer and packaging skills: ```python -plugin = ap.locate("agent-plugins") -print(plugin.skill("agent-plugins").source) -print(plugin.skill("package-agent-plugin").source) +print(ap.read("agent-plugins")) +print(ap.read("agent-plugins", skill="package-agent-plugin")) ``` The distribution supports Python 3.10 through 3.14 and ships a `py.typed` marker. | Task | Entry point | | --- | --- | +| Read a skill with its installed package context | [`read()`](#read-a-briefing) | | Preview selected package files | [`build_plan()`](#build-planning) | | Add a plugin to an existing wheel | [`attach_wheel()`](#wheel-attachment) | | Find plugins in this environment | [`locate()` and `installed()`](#installed-discovery) | @@ -33,6 +33,37 @@ The distribution supports Python 3.10 through 3.14 and ships a `py.typed` marker | Inspect tool configuration and prepare a launch | [`MCPConfig`](#mcpconfig) | | Wrap a Python build backend | [`BuildBackend`](#buildbackend) | +## Read a briefing + +### `read(distribution_name, *, skill=None)` + +```python +def read(distribution_name: str, *, skill: str | None = None) -> str: ... +``` + +Returns a Markdown briefing from the current interpreter's installation. The +briefing includes distribution and plugin metadata, the interpreter and resource +root, environment guidance, a bounded inventory, MCP summaries, and the complete +selected `SKILL.md` source. It also includes `Documentation` and `Documentation +Index` URLs declared in the distribution's `Project-URL` metadata. + +- `distribution_name` is the installed Python distribution name. +- `skill` is the structural skill directory name. Omitted or `None` uses + `distribution_name` exactly, including its spelling. An unavailable name raises + an error listing the available skills. + +The function returns text without printing, importing the target package, or +activating its components. Raises `AgentPluginError` for an absent or unusable +installation or unavailable skill. Invalid plugin documents raise +`ValidationError`. + +The [CLI `read`](./cli#agent-plugins-read) uses the same renderer but includes all +skills by default. Use `--skill NAME` for the equivalent single-skill output. + +To expose a core skill through `help(my_package.agent)`, set the optional agent +module's `__doc__` to `ap.read("my-package")`. Read the bundled +`package-agent-plugin` skill for the complete authoring pattern. + ## Build planning ### `build_plan(project=".")` diff --git a/pyproject.toml b/pyproject.toml index 7a22459..b9da8a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ classifiers = [ [project.urls] Documentation = "https://peter-gy.github.io/agent-plugins/" +"Documentation Index" = "https://peter-gy.github.io/agent-plugins/llms.txt" Issues = "https://github.com/peter-gy/agent-plugins/issues" Source = "https://github.com/peter-gy/agent-plugins" diff --git a/scripts/verify-dist.sh b/scripts/verify-dist.sh index 9d207fb..7a6ba1f 100755 --- a/scripts/verify-dist.sh +++ b/scripts/verify-dist.sh @@ -59,6 +59,7 @@ expected_files = { "skills/agent-plugins/agents/openai.yaml", "skills/package-agent-plugin/SKILL.md", "skills/package-agent-plugin/agents/openai.yaml", + "skills/package-agent-plugin/references/briefings.md", "skills/package-agent-plugin/references/build-variants.md", "skills/package-agent-plugin/references/verify-artifacts.md", } @@ -122,6 +123,8 @@ selected = subprocess.run( assert skill.source in selected.stdout assert package_skill.source not in selected.stdout assert selected.stderr == "" +assert ap.read("agent-plugins") == selected.stdout +assert package_skill.source in ap.read("agent-plugins", skill="package-agent-plugin") PY } diff --git a/skills/agent-plugins/SKILL.md b/skills/agent-plugins/SKILL.md index 0612da2..cb41f87 100644 --- a/skills/agent-plugins/SKILL.md +++ b/skills/agent-plugins/SKILL.md @@ -1,103 +1,132 @@ --- name: agent-plugins -description: Read and inspect the version-matched Agent Plugin carried by an installed Python distribution. Use when a package README points to agent-plugins, when loading packaged Agent Skills and resources, listing or locating installed plugins, inspecting manifest or MCP summaries, or troubleshooting discovery. For adding Agent Plugin packaging to a repository, use the package-agent-plugin skill. +description: Read version-matched agent briefings and resources from installed Python distributions. Use when a package README points to agent-plugins, a Python host exposes packaged guidance, or a task needs plugin discovery, linked resources, or installation diagnostics. For adding Agent Plugin packaging to a project, use package-agent-plugin. --- # Read installed Agent Plugins -Use `agent-plugins` to load the instructions and resources shipped with the -same version of a Python package that the agent will use. +Read the instructions shipped with the Python installation that will execute +the task. A briefing combines distribution identity, environment guidance, +plugin inventory, and complete skill instructions. The host supplies execution +tools, dependency management, and runtime connections. -## Start from a package README +## Choose the entrypoint -Run the package and `agent-plugins` in one temporary environment: +If the package is already installed, read its briefing in that Python +environment: -```console -uvx --with my-package agent-plugins read my-package +```python +import agent_plugins as ap + +print(ap.read("my-package")) ``` -The requirement after `--with` tells uv what to install. The final argument is -the installed Python distribution to inspect. Use a version constraint when -the task requires an exact release: +`ap.read()` returns a string and selects the skill whose directory name matches +the distribution argument exactly. Pass `skill="use-my-package"` when the +workflow has another name. An unavailable skill raises `AgentPluginError` and +lists the available names. Use the host's captured `help(module)` output when +a package exposes the same briefing through an agent module. + +When starting from a README before setting up an execution environment, run: ```console -uvx --with 'my-package==1.2.3' agent-plugins read my-package +uvx --with my-package agent-plugins read my-package ``` -Follow each applicable skill in the output. Resolve its relative links from the -instruction file's directory. Use `--skill NAME` when one plugin carries -several skills and the task needs a single workflow. +The requirement after `--with` selects what uv installs. The final argument +identifies the installed distribution to inspect. Pin the requirement, such as +`'my-package==1.2.3'`, when the task requires a particular release. -`read` reports MCP metadata for discovery. It does not start servers or print -configured commands, arguments, environment values, URLs, or headers. The -agent client owns component activation, permissions, processes, and data. +The CLI includes every packaged skill by default. Select one with +`--skill NAME`. Follow the workflows applicable to the request. Running +`uvx agent-plugins` with no arguments reads this package's consumer and +packaging skills. -## Choose the CLI operation - -| Task | Command | -| --- | --- | -| Read one installed plugin and all primary instructions | `agent-plugins read DISTRIBUTION` | -| Read one named skill with the plugin context | `agent-plugins read DISTRIBUTION --skill NAME` | -| List every discoverable plugin and skill path | `agent-plugins list` | -| List discoverable plugins as JSON | `agent-plugins list --json` | -| Print one installed plugin root | `agent-plugins locate DISTRIBUTION` | -| Preview files selected from a source project | `agent-plugins plan [PROJECT]` | -| Attach a configured plugin to a prebuilt wheel | `agent-plugins attach-wheel WHEEL` | +## Use the execution environment -`plan` and `attach-wheel` are repository packaging operations. Load the -`package-agent-plugin` skill before changing a project or wheel. +Check the briefing's distribution version and interpreter before using its +examples. `uvx` uses an isolated, disposable tool environment. Its resource +paths can remain readable while cached, but the project or notebook may have +a different installation or filesystem. -Running `uvx agent-plugins` with no arguments reads the Agent Plugin carried by -`agent-plugins` itself. It is the shortcut for: +Install dependencies through the target project's or host's package manager, +preserving its version policy. Then read that installation's briefing. From a +terminal, bind the CLI to the intended interpreter: ```console -uvx agent-plugins read agent-plugins +/path/to/environment/bin/python -m agent_plugins read my-package ``` -Use `agent-plugins --help` or `agent-plugins COMMAND --help` for command syntax. -Successful commands write data to stdout. Expected discovery, validation, -configuration, and filesystem failures return status `1` with an -`agent-plugins: error:` diagnostic on stderr. Argument errors return status -`2`. +In a remote kernel or service, execute `ap.read()` there. Reuse the briefing +while that environment and installation remain unchanged. If a package's +module help already supplied the applicable skill, continue with its workflow. +Rediscover after switching environments or installations. Restart a process +that still holds imports from a replaced package. -## Read the current Python environment +Package availability and runtime readiness are separate. Follow the selected +workflow's connection, activation, and verification steps before treating a +browser, service, device, or other host as ready. -When the target package is already installed in the active environment, run: +## Read linked resources -```console -agent-plugins read my-package -``` - -Use the Python API when the task needs one skill or a linked resource: +Relative links are resolved from the skill directory. When the agent cannot +read that filesystem directly, retrieve the text through Python in its owning +environment: ```python import agent_plugins as ap plugin = ap.locate("my-package") -skill = plugin.skill("use-my-package") - -print(skill.source) -reference = skill.file("references/api.md") -print(reference.read_text(encoding="utf-8")) +skill = plugin.skill("my-package") +print(skill.tree()) ``` -`plugin.skills` contains every immediate `skills//SKILL.md` selected by -the installed package. `skill.file()` accepts an exact selected path below that -skill and rechecks containment. `plugin.tree()` and `skill.tree()` provide a -bounded inventory before reading more files. +Choose a resource named by the skill. For a packaged `references/api.md`: -Use `plugin.manifest` for validated plugin metadata. `plugin.mcp` is an -`MCPConfig` when the package selected `mcp.json`. Accessing parsed manifest or -MCP fields validates and caches the document. Handle `AgentPluginError` for -missing or unusable installed plugins and `ValidationError` for invalid plugin, -MCP, or skill documents. +```python +print(skill.file("references/api.md").read_text(encoding="utf-8")) +``` -## Keep the environment explicit +`skill.file()` checks selected-file membership and containment. Reacquire the +handle in each execution if the host discards local bindings between calls. +Read additional resources at the decision points specified by the workflow. +`skill.source` returns the full instruction file, and `skill.body` omits its +frontmatter when raw text is needed instead of a briefing. -`uvx` creates a temporary environment for the command. Install the package in -the notebook, service, or project environment where its Python API will run. -The instructions printed by `read` describe the exact distribution version -resolved for that command. +Use published documentation and its `llms.txt` index for broader examples and +reference. Check that an online API matches the active installation. A source +checkout describes that checkout and may differ from an installed release. -Python distribution names and manifest plugin names are independent. Pass the -name used by pip or uv to `read`, `locate`, and `ap.locate()`. +## Inspect or recover + +| Task | Operation | +| --- | --- | +| Find plugins in this interpreter | `ap.installed()` | +| Inspect one plugin's selected files | `ap.locate("my-package").tree()` | +| Discover its skill names | `[skill.name for skill in plugin.skills]` | +| Inspect plugin metadata | `plugin.manifest` | +| Inspect MCP configuration | `plugin.mcp` | +| List installed plugins from a terminal | `agent-plugins list --json` | +| Print one installed plugin root | `agent-plugins locate my-package` | + +Pass the Python distribution name to `read()` and `locate()`. The manifest +plugin name and Python import name can differ from it. + +For an absent distribution or unusable plugin, correct the target installation +before retrying. For an unavailable skill, select a listed structural name. +`ValidationError` identifies an invalid plugin document. Preserve that +diagnostic when the packaged files need repair. + +Briefings summarize MCP names and transports while leaving configured commands, +arguments, environment values, URLs, and headers out of the output. The host +owns component activation and execution permissions. + +Use `agent-plugins --help` or `agent-plugins COMMAND --help` for CLI syntax. +Success writes to stdout. Expected operational failures return status `1` +with a diagnostic on stderr. Argument errors return status `2`. + +For project packaging, build planning, or wheel attachment, read the +`package-agent-plugin` skill. + +Use the [documentation index](https://peter-gy.github.io/agent-plugins/llms.txt) +for additional API and integration guidance. diff --git a/skills/package-agent-plugin/SKILL.md b/skills/package-agent-plugin/SKILL.md index b6d89d9..9b6b276 100644 --- a/skills/package-agent-plugin/SKILL.md +++ b/skills/package-agent-plugin/SKILL.md @@ -1,12 +1,29 @@ --- name: package-agent-plugin -description: Add Agent Plugin packaging to a Python project. Use when creating plugin.json and skills, configuring uv_build or Hatchling, attaching a plugin to a prebuilt wheel, exposing runtime plugin access, or verifying wheel, source distribution, and editable artifacts. For consuming instructions from an installed package, use the agent-plugins skill. +description: Package version-matched agent briefings with a Python project. Use when authoring core skills and references, exposing guidance through Python module help, configuring a build adapter, attaching a plugin to a wheel, or verifying installed handoffs. For consuming an installed package's instructions, use agent-plugins. --- # Package an Agent Plugin Package instructions beside the Python code they describe so the library and -its Agent Plugin share one release. +its Agent Plugin share one release. Author one compact core skill that works +from a CLI briefing, Python module help, or a directly loaded skill file. + +## Design the briefing + +Start from a fresh agent's task: what the package enables, which host or extras +it requires, the first complete action, how to verify success, and where to +read more. Include every import and binding needed by the first example. + +Keep package concepts, workflow choices, essential invariants, and verification +in the core skill. Put substantial setup variants and specialized workflows in +linked references, with a condition explaining when each is needed. Let the +generated briefing supply installation identity and resource-access guidance. +Rereading the current skill should not be a prerequisite for following it. + +Read [briefing design](references/briefings.md) when authoring the skill or +adding a Python help entrypoint. It covers ownership, a reusable skill shape, +documentation links, and fresh-agent acceptance scenarios. ## Build the smallest complete integration @@ -17,7 +34,7 @@ my-package/ |-- plugin.json |-- pyproject.toml |-- skills/ -| `-- use-my-package/ +| `-- my-package/ | `-- SKILL.md `-- src/ `-- my_package/ @@ -33,19 +50,13 @@ Create `plugin.json`: } ``` -Create `skills/use-my-package/SKILL.md` with a discriminating description and -the shortest complete workflow an agent needs: +Name the core skill `my-package` to match `[project].name`. This lets +`ap.read("my-package")` select it by default. A task-specific skill can use +another name and be selected explicitly. -```md ---- -name: use-my-package -description: Use My Package to read and transform project records from Python. ---- - -# Use My Package - -Import `my_package`, open the project input, and call `transform()`. -``` +Create `skills/my-package/SKILL.md` with `name` and a discriminating +`description` in YAML frontmatter, followed by the package's shortest complete +workflow. Use real public APIs and expected results from the project. Configure an existing uv_build project in `pyproject.toml`: @@ -90,7 +101,35 @@ uvx --with my-package agent-plugins read my-package Keep `agent-plugins` in `[build-system].requires` for packaging. Add it to `[project].dependencies` when installed Python code calls `agent_plugins` -directly at runtime. +directly at runtime, with a lower bound that includes the APIs used. + +## Expose the same briefing in Python + +Use `ap.read()` in the package's agent module to deliver its core skill through +standard Python help: + +```python +# src/my_package/agent.py +import agent_plugins as _ap + +__doc__ = _ap.read("my-package") +``` + +The caller runs `import my_package.agent` followed by `help(my_package.agent)`. +`help()` prints the briefing and returns `None`. Use `print(ap.read(...))` when +the host needs explicit text output. Pass `skill="task-name"` for a differently +named core skill. Host capability registration remains the host's contract. + +Keep ordinary package imports independent of this optional help module. Give +the agent module a small introspection surface and inspect its actual +`help()` output, since exported classes can add extensive API documentation. +Keep detailed signatures on the corresponding API objects. See +[briefing design](references/briefings.md#python-module-help) for runtime access +and documentation ownership. + +Verify both the CLI and Python briefing from the installed wheel. A successful +read establishes access to instructions. Exercise the first workflow in its +required host to establish runtime readiness and a verified result. ## Choose a different build path @@ -106,3 +145,6 @@ directly at runtime. Use the `agent-plugins` skill when the repository work is complete and the task becomes consuming an installed package's instructions or resources. + +Use the [documentation index](https://peter-gy.github.io/agent-plugins/llms.txt) +for additional packaging and integration guidance. diff --git a/skills/package-agent-plugin/references/briefings.md b/skills/package-agent-plugin/references/briefings.md new file mode 100644 index 0000000..c678aef --- /dev/null +++ b/skills/package-agent-plugin/references/briefings.md @@ -0,0 +1,143 @@ +# Design package briefings + +An independently loaded skill must give a fresh agent enough information to +choose a workflow, execute its first action, verify the result, and find the +next relevant resource. CLI and Python help should present the same authored +core, with environment context supplied by `agent-plugins`. + +## Assign each surface an owner + +| Surface | Content | +| --- | --- | +| Package README | Bootstrap command and the already-installed Python route | +| Generated briefing | Distribution version, interpreter, resource location, documentation links, and selected skill source | +| Core skill | Applicability, package concepts, prerequisites, workflow, and verification | +| Skill references | Conditional setup, specialized workflows, and detailed recovery | +| API docstrings | Signatures, inputs, returns, side effects, and operation semantics | +| Project instructions | Repository or authored-project conventions | +| Published docs | Broader guides, examples, and reference pages | + +Package-specific host requirements and optional extras belong with the +workflow that needs them. The execution host or project manager owns dependency +installation and version policy. An importable agent module already has its +base package installed, so its help should route to additional setup only when +the selected operation requires it. + +Distinguish instructions being available, a package being installed in the +execution environment, and the required runtime being connected and ready. +Describe the check that establishes each relevant transition. + +## Shape the core skill + +Use this outline when the package has several workflows. Collapse sections +when a smaller skill can convey the same contract: + +```text +Frontmatter: name and concrete activation conditions +Purpose: outcome, essential objects, and their ownership +Choose: task-to-workflow routes and their prerequisites +Start: smallest complete example, including imports and bindings +Verify: observable success and the main misleading success signal +Continue: conditional references, recovery, and published documentation +``` + +Prefer a compact core, often around 100–200 lines. Length follows the complete +workflow rather than a quota. Move substantial conditional material into +focused `references/` files and link it at the decision point where it is +needed. Keep essential correctness constraints beside the example they govern. + +Write the core for direct loading as well as generated briefings. A first +example must not depend on a variable created by module help, a prior task, or +another execution call. Repeat short imports where they make an example +independent. For hosts that discard scratch bindings, explain how to reacquire +handles and which identities or results must survive between calls. + +Do not require the agent to read the skill it is already following. Reuse +loaded instructions for the same environment and installation. Route back to +discovery when the environment or installation changes or an API mismatch +requires checking the source of the instructions. + +Use relative links beneath the skill root and resolve packaged resources with +`Skill.file()`. Absolute paths in a briefing describe its owning installation. +They can be inaccessible from another host or disappear with a disposable +environment. Provide a Python resource-access route for remote execution. + +Keep additional task skills independently selectable when their activation +conditions differ. A general package capability should route to a specialized +workflow only when the user's request calls for it. + +## Python module help + +The packaging skill's minimal `agent.py` example sets `__doc__` from +`ap.read("my-package")`. It reads the installed core when that module is +imported. Keep it out of the package's ordinary import path. Restart the host +after replacing an already imported installation. + +`ap.read()` returns one Markdown string. Omitted `skill` uses the distribution +argument exactly. An explicit `skill` selects a structural directory name. +The CLI uses the same renderer but includes all skills unless `--skill` is +provided. Both entrypoints preserve the selected instruction source. + +If callers need resource handles, expose small accessors over the public API: + +```python +import agent_plugins as _ap + + +def agent_plugin() -> _ap.Plugin: + return _ap.locate("my-package") + + +def agent_skill() -> _ap.Skill: + return agent_plugin().skill("my-package") +``` + +Callers can then read a linked file inside the execution host with +`agent_skill().file("references/setup.md").read_text(encoding="utf-8")`, when +that resource is packaged. Add accessors when consumers need them, and retain +host adapters that implement package-specific operations. + +Inspect actual `help(module)` output. Python can append documentation for +exported functions and classes, even when the module docstring is short. +Keep initial help focused and route detailed introspection to the relevant +objects or submodules. Check that help reads instructions without connecting +services, mounting components, or performing the example's operations. + +## Publish documentation links + +Declare documentation URLs in Python package metadata: + +```toml +[project.urls] +Documentation = "https://example.org/my-package/" +"Documentation Index" = "https://example.org/my-package/llms.txt" +``` + +`agent-plugins` includes these two labels in generated briefings. The +`Documentation Index` label is a package-authoring convention. Use the actual +index URL when one is published, and verify its linked destinations as part of +the documentation build. A new manifest property is unnecessary. + +Keep a short documentation link in the core skill too, so it works when loaded +independently of Python metadata. Load specific linked pages as needed. Check +online APIs against the installed version. A development checkout describes +that checkout and may differ from a released installation. + +## Verify the handoff + +Exercise the entrypoints an agent will actually use: + +1. From a README, run the isolated CLI bootstrap and follow one linked resource. + Confirm the output distinguishes that installation from the execution host. +2. In a project with an existing installation, read through its interpreter and + verify the reported version and resource origin. +3. Through the intended Python host, read module help and retrieve a reference + using Python when direct filesystem access is unavailable. +4. Load `SKILL.md` directly and follow its first example using fresh bindings. +5. Continue a related task in the same environment and verify that the routing + advances to relevant work instead of requiring repeated discovery. + +Test required runtime readiness and the example's expected result separately +from successful imports and resource reads. Keep failures actionable: an absent +package requires installation, an unknown skill requires selecting an available +name, and an unavailable runtime requires its host's connection workflow. diff --git a/skills/package-agent-plugin/references/verify-artifacts.md b/skills/package-agent-plugin/references/verify-artifacts.md index e339783..4b68e90 100644 --- a/skills/package-agent-plugin/references/verify-artifacts.md +++ b/skills/package-agent-plugin/references/verify-artifacts.md @@ -21,7 +21,9 @@ Verify every release boundary: attach the plugin to the externally built wheel. 3. Rebuild a wheel from the source distribution. 4. Install the direct and rebuilt wheels in clean environments. -5. Run `agent-plugins read my-package` against both installed wheels. +5. Run `agent-plugins read my-package` and `ap.read("my-package")` against both + installed wheels. Pass an explicit skill name when it differs from the + distribution name. Compare the Python briefing with CLI `--skill` output. 6. Install the project as editable and confirm `locate()` resolves the authored plugin root. 7. Compare source and installed file inventories and bytes. @@ -34,3 +36,7 @@ Let `AgentPluginError` fail missing configuration, unusable paths, or discovery. Let `ValidationError` fail invalid manifest, MCP, or skill documents. Artifact verification should exercise the CLI through the installed console script as well as the Python API. + +When the package exposes module help, inspect its captured output and follow +the [briefing handoff scenarios](briefings.md#verify-the-handoff). Confirm that +each referenced setup or workflow file is available in the installed inventory. diff --git a/src/agent_plugins/__init__.py b/src/agent_plugins/__init__.py index eabca70..86d5038 100644 --- a/src/agent_plugins/__init__.py +++ b/src/agent_plugins/__init__.py @@ -5,6 +5,7 @@ from ._discovery import installed, locate from ._errors import AgentPluginError from ._plugin import Plugin +from ._read import read from ._schema import ( Author, Manifest, @@ -40,4 +41,5 @@ "build_plan", "installed", "locate", + "read", ] diff --git a/src/agent_plugins/_read.py b/src/agent_plugins/_read.py index 2ed620a..530039f 100644 --- a/src/agent_plugins/_read.py +++ b/src/agent_plugins/_read.py @@ -3,6 +3,7 @@ from __future__ import annotations import re +import sys from importlib.metadata import Distribution from ._discovery import _locate @@ -12,6 +13,30 @@ from ._skill import Skill +def read(distribution_name: str, *, skill: str | None = None) -> str: + """Return a Markdown briefing for one installed Agent Skill. + + Args: + distribution_name: Python distribution to inspect in this interpreter. + skill: Structural skill name. Omitted or None uses distribution_name + exactly, including its spelling. + + The briefing includes package metadata, environment and resource guidance, + a bounded plugin inventory, and the complete selected skill source. It + returns text without printing, importing the target package, or activating + its components. + + Raises: + AgentPluginError: The distribution, plugin, or selected skill is + unavailable or unusable. + ValidationError: A selected plugin document is invalid. + """ + return render_read( + distribution_name, + skill_name=distribution_name if skill is None else skill, + ) + + def render_read(distribution_name: str, *, skill_name: str | None = None) -> str: """Return a getting-started briefing for one installed Agent Plugin.""" distribution, plugin = _locate(distribution_name) @@ -30,6 +55,7 @@ def _markdown( f"# Agent Plugin: {_code(manifest.name)}", "", f"Python distribution: {_code(f'{distribution_name}=={distribution.version}')}", + f"Python interpreter: {_code(sys.executable)}", f"Installed root: {_code(str(plugin.path))}", ] summary = distribution.metadata["Summary"] @@ -47,6 +73,18 @@ def _markdown( lines.append(f"Homepage: {_one_line(manifest.homepage)}") if manifest.repository: lines.append(f"Repository: {_one_line(manifest.repository)}") + for project_url in distribution.metadata.get_all("Project-URL") or (): + label, separator, url = project_url.partition(",") + if ( + separator + and url.strip() + and label.strip().casefold() + in { + "documentation", + "documentation index", + } + ): + lines.append(f"{_one_line(label)}: {_one_line(url)}") if manifest.license: lines.append(f"License: {_one_line(manifest.license)}") if manifest.keywords: @@ -56,6 +94,23 @@ def _markdown( lines.extend( ( + "", + ( + "This briefing describes the installation in the Python environment " + "shown here. Resource paths belong to that environment. If invoked " + "through uvx, the package is in an isolated, disposable tool " + "environment, not installed into your project or notebook. Cached " + "paths may remain readable locally but may be inaccessible from " + "another execution host." + ), + "", + ( + "Before running package code in another environment, read the " + "briefing from that installation. Reuse loaded instructions while " + "the environment and installation remain unchanged. The host owns " + "dependency installation and runtime connections. Reading these " + "instructions does not establish runtime readiness." + ), "", ( "Complete installed Agent Skill instructions follow. Resolve relative " @@ -67,8 +122,9 @@ def _markdown( _fenced_block(plugin.tree(), language="text"), "", ( - "The inventory is bounded. Inspect the installed root shown above when " - "a skill routes to a deeper resource." + "The inventory is bounded. Resolve linked resources through " + "agent_plugins.locate() in the owning Python environment, or read " + "them beneath the installed root when its filesystem is accessible." ), ) ) diff --git a/tests/test_plan.py b/tests/test_plan.py index 83ea713..6021e88 100644 --- a/tests/test_plan.py +++ b/tests/test_plan.py @@ -22,6 +22,7 @@ def test_project_build_plan_includes_its_agent_plugin() -> None: "skills/agent-plugins/agents/openai.yaml", "skills/package-agent-plugin/SKILL.md", "skills/package-agent-plugin/agents/openai.yaml", + "skills/package-agent-plugin/references/briefings.md", "skills/package-agent-plugin/references/build-variants.md", "skills/package-agent-plugin/references/verify-artifacts.md", ] diff --git a/tests/test_read.py b/tests/test_read.py new file mode 100644 index 0000000..7b8bd26 --- /dev/null +++ b/tests/test_read.py @@ -0,0 +1,122 @@ +from __future__ import annotations + +import json +import sys +from pathlib import Path +from types import ModuleType + +import pytest + +import agent_plugins as ap +from agent_plugins._cli import main + + +@pytest.mark.parametrize("skill", [None, "example-package", "publish"]) +def test_read_returns_selected_briefing_shared_with_cli_and_module_help( + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + skill: str | None, +) -> None: + _install_plugin(tmp_path, monkeypatch, "example-package", "publish") + selected = "example-package" if skill is None else skill + plugin = ap.locate("example-package") + + text = ap.read("example-package", skill=skill) + + assert capsys.readouterr().out == "" + assert plugin.skill(selected).source in text + other = "publish" if selected == "example-package" else "example-package" + assert plugin.skill(other).source not in text + assert "Python distribution: `example-package==1.2.3`" in text + assert f"Python interpreter: `{sys.executable}`" in text + assert f"Installed root: `{plugin.path}`" in text + assert "Documentation: https://example.org/docs/" in text + assert "Documentation Index: https://example.org/docs/llms.txt" in text + assert "uvx" in text + assert "example_package" not in sys.modules + + assert main(["read", "example-package", "--skill", selected]) == 0 + assert capsys.readouterr().out == text + + module = ModuleType("example_agent") + module.__doc__ = text + help(module) + help_text = capsys.readouterr().out + assert "Python distribution: `example-package==1.2.3`" in help_text + assert f"Use the {selected} workflow." in help_text + + +def test_read_requires_the_default_skill_even_with_one_other_skill( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + _install_plugin(tmp_path, monkeypatch, "publish") + + with pytest.raises(ap.AgentPluginError, match="Available skills: publish"): + ap.read("example-package") + + +@pytest.mark.parametrize( + ("skill", "message"), + [ + ("", "Invalid Agent Skill directory name"), + ("missing", "Available skills: example-package"), + ], +) +def test_read_reports_an_unavailable_explicit_skill( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, skill: str, message: str +) -> None: + _install_plugin(tmp_path, monkeypatch, "example-package") + + with pytest.raises(ap.AgentPluginError, match=message): + ap.read("example-package", skill=skill) + + +def test_read_reports_an_uninstalled_distribution() -> None: + with pytest.raises(ap.AgentPluginError, match="is not installed"): + ap.read("distribution-that-does-not-exist") + + +def _install_plugin( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch, *skills: str +) -> None: + dist_info = tmp_path / "example_package-1.2.3.dist-info" + dist_info.mkdir() + (dist_info / "METADATA").write_text( + "Metadata-Version: 2.4\n" + "Name: example-package\n" + "Version: 1.2.3\n" + "Project-URL: Documentation, https://example.org/docs/\n" + "Project-URL: Documentation Index, https://example.org/docs/llms.txt\n", + encoding="utf-8", + ) + root = tmp_path / "example_package-1.2.3.agent-plugin" + root.mkdir() + (root / "plugin.json").write_text( + json.dumps( + { + "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", + "name": "independent-plugin-name", + } + ), + encoding="utf-8", + ) + files = ["plugin.json"] + for name in skills: + relative = f"skills/{name}/SKILL.md" + path = root / relative + path.parent.mkdir(parents=True) + path.write_text( + f"---\nname: {name}\ndescription: Use {name}.\n---\n\n" + f"Use the {name} workflow.\n", + encoding="utf-8", + ) + files.append(relative) + (dist_info / "agent_plugins.json").write_text( + json.dumps({"root": root.name, "files": files}), encoding="utf-8" + ) + (tmp_path / "example_package.py").write_text( + 'raise AssertionError("Reading instructions must not import the package")\n', + encoding="utf-8", + ) + monkeypatch.syspath_prepend(str(tmp_path))