agent-readable 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,7 @@
1
+ from importlib.metadata import version
2
+
3
+ from ._protocol import AgentReadable, AgentReadableMixin, agent_help
4
+
5
+ __version__ = version("agent-readable")
6
+
7
+ __all__ = ["AgentReadable", "AgentReadableMixin", "agent_help", "__version__"]
@@ -0,0 +1,55 @@
1
+ """CLI: ``python -m agent_readable package.module:ClassName``,
2
+ ``python -m agent_readable package.module.ClassName``,
3
+ or ``python -m agent_readable package.module``."""
4
+
5
+ from __future__ import annotations
6
+
7
+ import importlib
8
+ import sys
9
+ import types
10
+
11
+ from . import agent_help
12
+
13
+
14
+ def _resolve(dotted_path: str) -> type | types.ModuleType:
15
+ """Import and resolve a target path to a class or module.
16
+
17
+ Accepts ``package.module:ClassName``, ``package.module.ClassName``, or
18
+ ``package.module``.
19
+ """
20
+ if ":" in dotted_path:
21
+ module_path, _, attr = dotted_path.partition(":")
22
+ elif "." in dotted_path:
23
+ module_path, _, attr = dotted_path.rpartition(".")
24
+ else:
25
+ return importlib.import_module(dotted_path)
26
+
27
+ module = importlib.import_module(module_path)
28
+
29
+ for part in attr.split(".") if attr else []:
30
+ module = getattr(module, part)
31
+
32
+ if not isinstance(module, type) and not isinstance(module, types.ModuleType):
33
+ raise TypeError(
34
+ f"{dotted_path!r} resolved to {type(module).__name__}, "
35
+ "expected a class or module"
36
+ )
37
+
38
+ return module
39
+
40
+
41
+ def main() -> None:
42
+ if len(sys.argv) < 2:
43
+ print(
44
+ "Usage: python -m agent_readable "
45
+ "(package.module:ClassName | package.module.ClassName | package.module)",
46
+ file=sys.stderr,
47
+ )
48
+ sys.exit(1)
49
+
50
+ target = _resolve(sys.argv[1])
51
+ print(agent_help(target))
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()
@@ -0,0 +1,363 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import types
5
+ from typing import Any, Protocol, runtime_checkable
6
+
7
+
8
+ @runtime_checkable
9
+ class AgentReadable(Protocol):
10
+ """Protocol for classes that expose agent-oriented documentation.
11
+
12
+ Implementing ``__agent_help__()`` opts the class into **full replacement**:
13
+ the returned string IS what ``agent_help(cls)`` returns, verbatim. Notes from
14
+ ``__agent_notes__()`` are NOT auto-appended — implementers control the entire
15
+ output. To get auto-doc + appended notes instead, use ``AgentReadableMixin``
16
+ (or just define ``__agent_notes__()`` and let the auto-doc path run).
17
+ """
18
+
19
+ @classmethod
20
+ def __agent_help__(cls) -> str:
21
+ """Return agent-oriented help for this class (full output, verbatim)."""
22
+ ...
23
+
24
+
25
+ class AgentReadableMixin:
26
+ """
27
+ Optional mixin for classes that want agent-readable documentation.
28
+
29
+ Deriving from this mixin gives your class a default ``__agent_help__()`` that
30
+ auto-generates structured docs from introspection.
31
+
32
+ Optionally, to append more help instructions to coding agents, override
33
+ ``__agent_notes__()`` to add usage examples, do/don't rules, and anti-patterns, etc.
34
+ """
35
+
36
+ @classmethod
37
+ def __agent_help__(cls) -> str:
38
+ """Return agent-oriented help for this class.
39
+
40
+ Do NOT override this method directly. To add custom notes, override
41
+ ``__agent_notes__()`` instead.
42
+
43
+ Coding agents:
44
+ Call ``agent_help(TheClass)`` to learn how to use the class before
45
+ generating code that uses it. Treat the output as the canonical usage guide.
46
+ """
47
+ return _base_agent_doc(cls)
48
+
49
+ @classmethod
50
+ def __agent_notes__(cls) -> str:
51
+ """
52
+ Additive custom guidance, collected from every class in the MRO.
53
+
54
+ Unlike ``__agent_help__`` (which replaces the entire output),
55
+ ``__agent_notes__`` is **appended** to the auto-generated docs and
56
+ **accumulates across the MRO** — every class that defines its own
57
+ ``__agent_notes__`` contributes a section. The leaf class is tagged as
58
+ taking precedence over inherited notes when they conflict.
59
+
60
+ Do not prepend ``super().__agent_notes__()`` — collection is automatic.
61
+ Defining this method on any class is enough; the ``AgentReadableMixin``
62
+ is not required.
63
+ """
64
+ return ""
65
+
66
+
67
+ def agent_help(obj: Any) -> str:
68
+ """
69
+ Return agent-oriented help for a class, instance, or module.
70
+
71
+ Dispatch for classes/instances:
72
+
73
+ 1. **``__agent_help__()`` is defined** — call it and return its result
74
+ verbatim. The ``AgentReadableMixin`` default returns
75
+ ``_base_agent_doc(cls)`` (auto-doc with ``__agent_notes__`` appended);
76
+ duck-typed implementations return whatever the user formatted, so notes
77
+ are NOT auto-included on that path — the implementer owns the full
78
+ output.
79
+ 2. **``__agent_help__`` is missing** — fall through to
80
+ ``_base_agent_doc(cls)``, which appends ``__agent_notes__()`` from every
81
+ class in the MRO automatically.
82
+ 3. **``__agent_help__()`` raises** — same fallback as path 2 (auto-doc with
83
+ notes).
84
+
85
+ Notes accumulation lives in ``_base_agent_doc()``, which is why duck-typed
86
+ ``__agent_help__()`` skips it: that path never reaches ``_base_agent_doc``.
87
+
88
+ For modules: if the module defines a ``__agent_help__`` attribute (callable
89
+ or string), it is used directly. Otherwise auto-generated docs are produced
90
+ via ``_module_doc()``. Module ``__agent_notes__`` is not part of the
91
+ protocol — modules don't have an MRO to accumulate over.
92
+ """
93
+ if inspect.ismodule(obj):
94
+ fn = getattr(obj, "__agent_help__", None)
95
+ if callable(fn):
96
+ try:
97
+ result = fn()
98
+ if isinstance(result, str):
99
+ return result
100
+ return str(result)
101
+ except Exception:
102
+ pass
103
+ if isinstance(fn, str):
104
+ return fn
105
+ return _module_doc(obj)
106
+
107
+ target = obj if inspect.isclass(obj) else obj.__class__
108
+
109
+ fn = getattr(target, "__agent_help__", None)
110
+ if callable(fn):
111
+ try:
112
+ result = fn()
113
+ if isinstance(result, str):
114
+ return result
115
+ return str(result)
116
+ except Exception:
117
+ pass
118
+
119
+ return _base_agent_doc(target)
120
+
121
+
122
+ def _module_doc(module: types.ModuleType) -> str:
123
+ """Generate compact Markdown documentation for a module."""
124
+ parts: list[str] = []
125
+
126
+ parts.append(f"# {module.__name__}")
127
+ parts.append("")
128
+
129
+ doc = inspect.getdoc(module)
130
+ if doc:
131
+ parts.append("## Purpose")
132
+ parts.append("")
133
+ parts.append(doc)
134
+ parts.append("")
135
+
136
+ public_api = _collect_module_api(module)
137
+ if public_api:
138
+ parts.append("## Public API")
139
+ parts.append("")
140
+ parts.extend(public_api)
141
+ parts.append("")
142
+
143
+ parts.append("## Agent usage rules")
144
+ parts.append("")
145
+ parts.append("- Prefer the public API listed above.")
146
+ parts.append("- Do not use private names starting with `_`.")
147
+ parts.append("- Do not invent unsupported behavior.")
148
+ parts.append(
149
+ "- If usage is ambiguous, prefer the simplest documented usage pattern."
150
+ )
151
+
152
+ return "\n".join(parts)
153
+
154
+
155
+ def _collect_module_api(module: types.ModuleType) -> list[str]:
156
+ lines: list[str] = []
157
+ mod_name = module.__name__
158
+ for name, obj in inspect.getmembers(module):
159
+ if name.startswith("_"):
160
+ continue
161
+ obj_module = getattr(obj, "__module__", None)
162
+ if (
163
+ obj_module is not None
164
+ and obj_module != mod_name
165
+ and not obj_module.startswith(mod_name + ".")
166
+ ):
167
+ continue
168
+ if inspect.isclass(obj):
169
+ summary = _first_doc_line(obj)
170
+ line = f"- `{name}` class"
171
+ if summary:
172
+ line += f": {summary}"
173
+ lines.append(line)
174
+ elif inspect.isfunction(obj):
175
+ sig = _safe_signature(obj)
176
+ summary = _first_doc_line(obj)
177
+ line = f"- `{name}{sig}` function"
178
+ if summary:
179
+ line += f": {summary}"
180
+ lines.append(line)
181
+ elif isinstance(obj, types.ModuleType):
182
+ continue
183
+ return lines
184
+
185
+
186
+ def _base_agent_doc(cls: type) -> str:
187
+ """
188
+ Generate compact Markdown documentation for AI coding agents.
189
+
190
+ Appends ``__agent_notes__()`` from every class in ``cls.__mro__``,
191
+ whether or not the class uses ``AgentReadableMixin``.
192
+ """
193
+ parts: list[str] = []
194
+
195
+ parts.append(f"# {cls.__name__}")
196
+ parts.append("")
197
+
198
+ constructor = _format_constructor(cls)
199
+ if constructor:
200
+ parts.append("## Constructor")
201
+ parts.append("")
202
+ parts.append("```python")
203
+ parts.append(constructor)
204
+ parts.append("```")
205
+ parts.append("")
206
+
207
+ doc = inspect.getdoc(cls)
208
+ if doc:
209
+ parts.append("## Purpose")
210
+ parts.append("")
211
+ parts.append(doc)
212
+ parts.append("")
213
+
214
+ public_api = _collect_public_api(cls)
215
+ if public_api:
216
+ parts.append("## Public API")
217
+ parts.append("")
218
+ parts.extend(public_api)
219
+ parts.append("")
220
+
221
+ parts.append("## Agent usage rules")
222
+ parts.append("")
223
+ parts.append("- Prefer the public API listed above.")
224
+ parts.append("- Do not use private methods or attributes starting with `_`.")
225
+ parts.append("- Do not invent unsupported behavior.")
226
+ parts.append(
227
+ "- If usage is ambiguous, prefer the simplest documented usage pattern."
228
+ )
229
+
230
+ base = "\n".join(parts)
231
+ notes = _collect_agent_notes(cls)
232
+ if notes:
233
+ return base + "\n\n" + "\n\n".join(notes)
234
+ return base
235
+
236
+
237
+ def _collect_agent_notes(cls: type) -> list[str]:
238
+ """Collect ``__agent_notes__()`` sections from every class in ``cls.__mro__``.
239
+
240
+ Each class that defines its own ``__agent_notes__`` becomes a Markdown section.
241
+ The leaf class's notes are tagged as taking precedence over inherited ones when
242
+ conflicts arise.
243
+ """
244
+ notes: list[str] = []
245
+ parent_names: list[str] = []
246
+ for klass in reversed(cls.__mro__):
247
+ raw = klass.__dict__.get("__agent_notes__")
248
+ if raw is None:
249
+ continue
250
+ fn = raw.__func__ if isinstance(raw, classmethod) else raw
251
+ result = fn(cls)
252
+ if result:
253
+ header = f"## Notes from class {klass.__name__}"
254
+ if klass is cls and parent_names:
255
+ header += (
256
+ f" (inherits {', '.join(parent_names)}; "
257
+ "if notes conflict, these take precedence)"
258
+ )
259
+ notes.append(header + "\n\n" + result.strip())
260
+ if klass is not cls:
261
+ parent_names.append(klass.__name__)
262
+ return notes
263
+
264
+
265
+ def _format_constructor(cls: type) -> str | None:
266
+ try:
267
+ signature = inspect.signature(cls)
268
+ except (TypeError, ValueError):
269
+ return None
270
+
271
+ return f"{cls.__name__}{signature}"
272
+
273
+
274
+ def _collect_public_api(cls: type) -> list[str]:
275
+ lines: list[str] = []
276
+
277
+ for name, _ in inspect.getmembers(cls):
278
+ if name.startswith("_"):
279
+ continue
280
+
281
+ try:
282
+ raw = inspect.getattr_static(cls, name)
283
+ except AttributeError:
284
+ continue
285
+
286
+ if isinstance(raw, property):
287
+ summary = _first_doc_line(raw)
288
+ line = f"- `{name}` property"
289
+ if summary:
290
+ line += f": {summary}"
291
+ lines.append(line)
292
+ continue
293
+
294
+ if isinstance(raw, staticmethod):
295
+ fn = raw.__func__
296
+ kind = "staticmethod"
297
+ elif isinstance(raw, classmethod):
298
+ fn = raw.__func__
299
+ kind = "classmethod"
300
+ elif inspect.isfunction(raw) or (callable(raw) and not isinstance(raw, type)):
301
+ fn = raw
302
+ kind = "method"
303
+ else:
304
+ continue
305
+
306
+ sig = _safe_signature(fn)
307
+ if (kind == "method" or kind == "classmethod") and sig != "(...)":
308
+ sig = _strip_first_param(sig)
309
+ summary = _first_doc_line(fn)
310
+
311
+ line = f"- `{name}{sig}` {kind}"
312
+ if summary:
313
+ line += f": {summary}"
314
+
315
+ lines.append(line)
316
+
317
+ return lines
318
+
319
+
320
+ def _safe_signature(obj: Any) -> str:
321
+ try:
322
+ return str(inspect.signature(obj))
323
+ except (TypeError, ValueError):
324
+ return "(...)"
325
+
326
+
327
+ def _strip_first_param(sig: str) -> str:
328
+ """Strip the first parameter (``self`` / ``cls``) from a signature string.
329
+
330
+ Input is always produced by ``inspect.signature(...)`` and therefore starts
331
+ with ``(`` and contains a balanced closing ``)``.
332
+ """
333
+ rest = sig[1:] # drop leading "("
334
+ depth = 0
335
+ for i, ch in enumerate(rest):
336
+ if ch in "([":
337
+ depth += 1
338
+ elif ch in ")]":
339
+ if depth > 0:
340
+ depth -= 1
341
+ else:
342
+ return "()" + rest[i + 1 :]
343
+ elif ch == "," and depth == 0:
344
+ return "(" + rest[i + 1 :].lstrip()
345
+ return (
346
+ "()" # pragma: no cover -- unreachable: inspect.signature always closes parens
347
+ )
348
+
349
+
350
+ def _first_doc_line(obj: Any) -> str:
351
+ doc = inspect.getdoc(obj)
352
+ if not doc:
353
+ return ""
354
+
355
+ paragraph: list[str] = []
356
+ for line in doc.splitlines():
357
+ stripped = line.strip()
358
+ if stripped:
359
+ paragraph.append(stripped)
360
+ elif paragraph:
361
+ break
362
+
363
+ return " ".join(paragraph)
@@ -0,0 +1,622 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-readable
3
+ Version: 0.1.0
4
+ Summary: A lightweight protocol for exposing agent-oriented documentation from Python classes and modules
5
+ Project-URL: Repository, https://github.com/zydo/agent-readable
6
+ Author: zydo and agent-readable contributors
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: agent,agent-help,ai,ai-agent,coding-agent,context-engineering,docstring,documentation,llm,mixin,prompt-engineering,protocol,vibe-coding
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
20
+ Classifier: Topic :: Software Development :: Code Generators
21
+ Classifier: Topic :: Software Development :: Documentation
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+
27
+ # agent-readable
28
+
29
+ A lightweight Python protocol for exposing agent-oriented documentation from classes and modules.
30
+
31
+ ## Problem
32
+
33
+ AI coding agents recognize established libraries from their training data, but they hallucinate when APIs change, when libraries are new, or when correct usage depends on rules that aren't visible from the API surface — pre-conditions, lifecycle order, anti-patterns, *"use `call()` for non-streaming, `stream()` for streaming."*
34
+
35
+ Today there's nowhere to put those rules where an agent will reliably find them. Docstrings document the API surface, not behavioral rules. `AGENTS.md` and `llms.txt` work at project granularity, drift fast, and don't travel with refactors. `help()` is verbose and only describes interfaces.
36
+
37
+ `agent-readable` adds two dunders — `__agent_help__` (full custom output) and `__agent_notes__` (additive guidance that accumulates across inheritance) — that live next to the code. Library authors annotate once; any agent that calls `agent_help(cls)` gets the rules.
38
+
39
+ ```text
40
+ help(logging.Logger) 217 lines — every inherited method, dunder, and MRO detail.
41
+ agent_help(logging.Logger) 56 lines — structured sections + any author notes.
42
+ ```
43
+
44
+ The compactness is a side effect; the structure is the point.
45
+
46
+ ## Installation
47
+
48
+ ```bash
49
+ pip install agent-readable
50
+ ```
51
+
52
+ Requires Python 3.10+. No runtime dependencies.
53
+
54
+ ## Quickstart
55
+
56
+ ```python
57
+ from agent_readable import AgentReadableMixin, agent_help
58
+
59
+
60
+ class Sensor(AgentReadableMixin):
61
+ """Reads a value from a hardware sensor."""
62
+
63
+ def __init__(self, pin: int, *, unit: str = "C"): ...
64
+
65
+ def read(self) -> float:
66
+ """Read the current sensor value."""
67
+
68
+ def calibrate(self, offset: float):
69
+ """Apply a calibration offset."""
70
+
71
+ @classmethod
72
+ def __agent_notes__(cls) -> str:
73
+ return """
74
+ ## Do
75
+
76
+ - Call `calibrate()` once during setup, before `read()`.
77
+
78
+ ## Do not
79
+
80
+ - Do not call `read()` before `calibrate()` on first use.
81
+ """
82
+
83
+
84
+ print(agent_help(Sensor))
85
+ ```
86
+
87
+ Output:
88
+
89
+ ````
90
+ # Sensor
91
+
92
+ ## Constructor
93
+
94
+ ```python
95
+ Sensor(pin: int, *, unit: str = 'C')
96
+ ```
97
+
98
+ ## Purpose
99
+
100
+ Reads a value from a hardware sensor.
101
+
102
+ ## Public API
103
+
104
+ - `calibrate(offset: float)` method: Apply a calibration offset.
105
+ - `read() -> float` method: Read the current sensor value.
106
+
107
+ ## Agent usage rules
108
+
109
+ - Prefer the public API listed above.
110
+ - Do not use private methods or attributes starting with `_`.
111
+ - Do not invent unsupported behavior.
112
+ - If usage is ambiguous, prefer the simplest documented usage pattern.
113
+
114
+ ## Notes from class Sensor
115
+
116
+ ## Do
117
+
118
+ - Call `calibrate()` once during setup, before `read()`.
119
+
120
+ ## Do not
121
+
122
+ - Do not call `read()` before `calibrate()` on first use.
123
+ ````
124
+
125
+ ## Why it matters
126
+
127
+ `help()` documents the **API surface** — what each method does. But agents fail less often on *what methods exist* than on *how to use them*: lifecycle order, pre-conditions, anti-patterns, "this method is for X, that one for Y." Those rules don't fit in docstrings (which describe single methods) and don't belong in a project-level `AGENTS.md` (which describes whole repos). They're class-level, and they need to travel with refactors.
128
+
129
+ `agent_help()` gives them a home next to the code:
130
+
131
+ ```
132
+ # help(sqlite3.Connection) — 200+ lines of terminal output:
133
+ Help on class Connection in module sqlite3:
134
+
135
+ class Connection(builtins.object)
136
+ | SQLite database connection object.
137
+ |
138
+ | Methods defined here:
139
+ |
140
+ | __call__(self, /, *args, **kwargs)
141
+ | __del__(self, /, *args, **kwargs)
142
+ | __enter__(self, /, *args, **kwargs)
143
+ | ...
144
+ | backup(self, target, /, *, pages=-1, progress=None, ...)
145
+ | blobopen(self, table, column, rowid, /, *, readonly=False, ...)
146
+ | ... (continues for 200+ more lines)
147
+ ```
148
+
149
+ `agent_help(sqlite3.Connection)` produces a scannable summary instead — see Example 1 below. Notes from `__agent_notes__()` accumulate across inheritance. Class docs travel with the code in commits, reviews, and refactors. Drift gets caught in code review, not weeks later in a sidecar file.
150
+
151
+ The examples below demonstrate four ways to use `agent_help()`.
152
+
153
+ ## Example 1: Wrapping an existing class
154
+
155
+ Add agent-readable docs to any class, including ones you don't own. Full example: [`examples/sqlite_connection.py`](examples/sqlite_connection.py).
156
+
157
+ ```python
158
+ import sqlite3
159
+ from agent_readable import AgentReadableMixin, agent_help
160
+
161
+
162
+ class Connection(sqlite3.Connection, AgentReadableMixin):
163
+ """An agent-friendly wrapper around sqlite3.Connection."""
164
+
165
+ @classmethod
166
+ def __agent_notes__(cls) -> str:
167
+ return (
168
+ "Additional notes about using Connection here. "
169
+ "For example, common pitfalls, best practices, etc."
170
+ )
171
+ ```
172
+
173
+ Override `__agent_notes__()` to add extra guidance, or leave it out for auto-generated docs only.
174
+
175
+ `agent_help(Connection)` output:
176
+
177
+ ```
178
+ # Connection
179
+
180
+ ## Purpose
181
+
182
+ An agent-friendly wrapper around sqlite3.Connection.
183
+
184
+ ## Public API
185
+
186
+ - `backup(/, target, *, pages=-1, progress=None, name='main', sleep=0.25)` method: Makes a backup of the database.
187
+ - ...
188
+ - `close(/)` method: Close the database connection.
189
+ - `commit(/)` method: Commit any pending transaction to the database.
190
+ - ...
191
+ - `execute(...)` method: Executes an SQL statement.
192
+ - ...
193
+ - `rollback(/)` method: Roll back to the start of any pending transaction.
194
+ - ...
195
+
196
+ ## Agent usage rules
197
+
198
+ - Prefer the public API listed above.
199
+ - Do not use private methods or attributes starting with `_`.
200
+ - Do not invent unsupported behavior.
201
+ - If usage is ambiguous, prefer the simplest documented usage pattern.
202
+
203
+ ## Notes from class Connection
204
+
205
+ Additional notes about using Connection here. For example, common pitfalls, best practices, etc.
206
+ ```
207
+
208
+ ## Example 2: Inheritance with accumulated notes
209
+
210
+ Override `__agent_notes__()` to add usage guidance. Notes accumulate through inheritance automatically. Full example: [`examples/temperature.py`](examples/temperature.py).
211
+
212
+ ```python
213
+ from agent_readable import AgentReadableMixin, agent_help
214
+
215
+
216
+ class Sensor(AgentReadableMixin):
217
+ """Reads a value from a hardware sensor."""
218
+
219
+ def __init__(self, pin: int, *, unit: str = "C"): ...
220
+
221
+ def read(self) -> float:
222
+ """Read the current sensor value."""
223
+
224
+ def calibrate(self, offset: float):
225
+ """Apply a calibration offset."""
226
+
227
+ @classmethod
228
+ def __agent_notes__(cls) -> str:
229
+ return """
230
+ ## Do
231
+
232
+ - Call `calibrate()` once during setup, before `read()`.
233
+ - Handle negative values — sensors may report below zero.
234
+
235
+ ## Do not
236
+
237
+ - Do not call `read()` before `calibrate()` on first use.
238
+ """
239
+
240
+
241
+ class CalibratedSensor(Sensor):
242
+ """A sensor with factory calibration applied."""
243
+
244
+ def reset(self):
245
+ """Reset to factory calibration."""
246
+
247
+ @classmethod
248
+ def __agent_notes__(cls) -> str:
249
+ return """
250
+ ## Do
251
+
252
+ - Call `reset()` if readings drift unexpectedly.
253
+
254
+ ## Do not
255
+
256
+ - Do not call `calibrate()` — use `reset()` instead. Factory calibration
257
+ is pre-applied and `calibrate()` would double-adjust.
258
+ """
259
+ ```
260
+
261
+ `agent_help(CalibratedSensor)` output — includes inherited notes with conflict resolution:
262
+
263
+ ````
264
+ # CalibratedSensor
265
+
266
+ ## Constructor
267
+
268
+ ```python
269
+ CalibratedSensor(pin: int, *, unit: str = 'C')
270
+ ```
271
+
272
+ ## Purpose
273
+
274
+ A sensor with factory calibration applied.
275
+
276
+ ## Public API
277
+
278
+ - `calibrate(offset: float)` method: Apply a calibration offset.
279
+ - `read() -> float` method: Read the current sensor value.
280
+ - `reset()` method: Reset to factory calibration.
281
+
282
+ ## Agent usage rules
283
+
284
+ - Prefer the public API listed above.
285
+ - Do not use private methods or attributes starting with `_`.
286
+ - Do not invent unsupported behavior.
287
+ - If usage is ambiguous, prefer the simplest documented usage pattern.
288
+
289
+ ## Notes from class Sensor
290
+
291
+ ## Do
292
+
293
+ - Call `calibrate()` once during setup, before `read()`.
294
+ - Handle negative values — sensors may report below zero.
295
+
296
+ ## Do not
297
+
298
+ - Do not call `read()` before `calibrate()` on first use.
299
+
300
+ ## Notes from class CalibratedSensor (inherits Sensor; if notes conflict, these take precedence)
301
+
302
+ ## Do
303
+
304
+ - Call `reset()` if readings drift unexpectedly.
305
+
306
+ ## Do not
307
+
308
+ - Do not call `calibrate()` — use `reset()` instead. Factory calibration
309
+ is pre-applied and `calibrate()` would double-adjust.
310
+ ````
311
+
312
+ The child class's notes explicitly state they take precedence over the parent's — so the agent knows `reset()` replaces `calibrate()` for `CalibratedSensor`.
313
+
314
+ ## Example 3: Duck-typed (no mixin needed)
315
+
316
+ Any class that defines a `__agent_help__()` classmethod works — no inheritance required. Full example: [`examples/duck_type.py`](examples/duck_type.py).
317
+
318
+ ```python
319
+ from agent_readable import agent_help
320
+
321
+
322
+ class RateLimiter:
323
+ """Token bucket rate limiter."""
324
+
325
+ def __init__(self, max_tokens: int, refill_rate: float): ...
326
+
327
+ def acquire(self, tokens: int = 1) -> bool:
328
+ """Try to acquire tokens. Returns False if rate-limited."""
329
+
330
+ def wait(self, tokens: int = 1) -> None:
331
+ """Block until tokens are available."""
332
+
333
+ @classmethod
334
+ def __agent_help__(cls) -> str:
335
+ return (
336
+ "# RateLimiter\n"
337
+ "\n"
338
+ "## Constructor\n"
339
+ "\n"
340
+ "```python\n"
341
+ "RateLimiter(max_tokens: int, refill_rate: float)\n"
342
+ "```\n"
343
+ "\n"
344
+ "## Do\n"
345
+ "\n"
346
+ "- Use `acquire()` for non-blocking checks.\n"
347
+ "- Use `wait()` when you must proceed regardless of rate.\n"
348
+ "- Set `refill_rate` to tokens/second.\n"
349
+ "\n"
350
+ "## Do not\n"
351
+ "\n"
352
+ "- Do not call `acquire()` in a tight loop without sleeping.\n"
353
+ "- Do not assume `acquire()` always returns True.\n"
354
+ )
355
+
356
+
357
+ print(agent_help(RateLimiter))
358
+ ```
359
+
360
+ ## Example 4: Any class — no setup required
361
+
362
+ Even without the mixin or duck-typing, `agent_help()` still generates compact, structured Markdown from introspection. If the class (or any class in its MRO) defines `__agent_notes__()`, those notes are auto-appended too — no mixin required. The output is still more agent-friendly than `help()`.
363
+ Full example: [`examples/any_class.py`](examples/any_class.py).
364
+
365
+ ```python
366
+ import logging
367
+ from agent_readable import agent_help
368
+
369
+ print(agent_help(logging.Logger))
370
+ ```
371
+
372
+ `help(logging.Logger)` produces **217 lines** of output. `agent_help(logging.Logger)` produces a compact, scannable summary:
373
+
374
+ ````
375
+ # Logger
376
+
377
+ ## Constructor
378
+
379
+ ```python
380
+ Logger(name, level=0)
381
+ ```
382
+
383
+ ## Purpose
384
+
385
+ Instances of the Logger class represent a single logging channel. A
386
+ "logging channel" indicates an area of an application.
387
+
388
+ ...
389
+
390
+ ## Public API
391
+
392
+ - `addFilter(filter)` method: Add the specified filter to this handler.
393
+ - `addHandler(hdlr)` method: Add the specified handler to this logger.
394
+ - ...
395
+ - `debug(msg, *args, **kwargs)` method: Log 'msg % args' with severity 'DEBUG'.
396
+ - `error(msg, *args, **kwargs)` method: Log 'msg % args' with severity 'ERROR'.
397
+ - ...
398
+ - `info(msg, *args, **kwargs)` method: Log 'msg % args' with severity 'INFO'.
399
+ - ...
400
+ - `setLevel(level)` method: Set the logging level of this logger. level must be an int or a str.
401
+ - ...
402
+ - `warning(msg, *args, **kwargs)` method: Log 'msg % args' with severity 'WARNING'.
403
+ ````
404
+
405
+ No mixin, no duck-typing — just pass any class to `agent_help()`.
406
+
407
+ ## Example 5: Modules
408
+
409
+ `agent_help()` also works on modules — it generates a summary with the module docstring, public functions, and classes. Full example: [`examples/module_support.py`](examples/module_support.py).
410
+
411
+ ```python
412
+ import sys
413
+ from agent_readable import agent_help
414
+
415
+ print(agent_help(sys.modules[__name__]))
416
+ ```
417
+
418
+ Output:
419
+
420
+ ````python
421
+ # __main__
422
+
423
+ ## Purpose
424
+
425
+ Example: Using agent_help() on modules.
426
+
427
+ Demonstrates both shapes of module support:
428
+ 1. A custom module (this file itself).
429
+ 2. A stdlib module (pathlib).
430
+
431
+ Run this file to see both outputs:
432
+ python examples/module_support.py
433
+
434
+ ## Public API
435
+
436
+ - `Query` class: Build and execute a query.
437
+ - `connect(host: str, port: int = 5432) -> str` function: Connect to a database server.
438
+ - `disconnect()` function: Close the connection.
439
+
440
+ ## Agent usage rules
441
+
442
+ - Prefer the public API listed above.
443
+ - Do not use private names starting with `_`.
444
+ - Do not invent unsupported behavior.
445
+ - If usage is ambiguous, prefer the simplest documented usage pattern.
446
+ ````
447
+
448
+ You can also pass any stdlib or third-party module — same `module_support.py` shows it.
449
+
450
+ ```python
451
+ import pathlib
452
+ from agent_readable import agent_help
453
+
454
+ print(agent_help(pathlib))
455
+ ```
456
+
457
+ Output:
458
+
459
+ ````python
460
+ # pathlib
461
+
462
+ ## Purpose
463
+
464
+ Object-oriented filesystem paths.
465
+
466
+ This module provides classes to represent abstract paths and concrete
467
+ paths with operations that have semantics appropriate for different
468
+ operating systems.
469
+
470
+ ## Public API
471
+
472
+ - `DirEntryInfo` class: Implementation of pathlib.types.PathInfo that provides status
473
+ - `Path` class: PurePath subclass that can make system calls.
474
+ - `PathInfo` class: Implementation of pathlib.types.PathInfo that provides status
475
+ - `PosixPath` class: Path subclass for non-Windows systems.
476
+ - `PurePath` class: Base class for manipulating paths without I/O.
477
+ - `PurePosixPath` class: PurePath subclass for non-Windows systems.
478
+ - `PureWindowsPath` class: PurePath subclass for Windows systems.
479
+ - `UnsupportedOperation` class: An exception that is raised when an unsupported operation is attempted.
480
+ - `WindowsPath` class: Path subclass for Windows systems.
481
+ - `copy_info(info, target, follow_symlinks=True)` function: Copy metadata from the given PathInfo to the given local path.
482
+ - `copyfileobj(source_f, target_f)` function: Copy data from file-like object source_f to file-like object target_f.
483
+ - `ensure_different_files(source, target)` function: Raise OSError(EINVAL) if both paths refer to the same file.
484
+ - `ensure_distinct_paths(source, target)` function: Raise OSError(EINVAL) if the other path is within this path.
485
+ - `magic_open(path, mode='r', buffering=-1, encoding=None, errors=None, newline=None)` function: Open the file pointed to by this path and return a file object, as
486
+
487
+ ## Agent usage rules
488
+
489
+ - Prefer the public API listed above.
490
+ - Do not use private names starting with `_`.
491
+ - Do not invent unsupported behavior.
492
+ - If usage is ambiguous, prefer the simplest documented usage pattern.
493
+ ````
494
+
495
+ Modules support less customization than classes — there is no mixin inheritance or `__agent_notes__()`. You can override the auto-generated output entirely by setting a module-level `__agent_help__` attribute (callable or string), but this is discouraged since it replaces the auto-generated summary — signatures, purpose, and public API listing are all lost. Prefer clear docstrings on the module and its functions/classes instead.
496
+
497
+ ```python
498
+ import sys
499
+
500
+ # Discouraged — replaces everything, including auto-generated docs.
501
+ sys.modules[__name__].__agent_help__ = "Custom module help."
502
+ ```
503
+
504
+ ## CLI
505
+
506
+ ```bash
507
+ # Any stdlib class
508
+ python -m agent_readable sqlite3:Connection
509
+
510
+ # A class in your own package
511
+ python -m agent_readable my_package.temperature:CalibratedSensor
512
+
513
+ # The library itself — self-documenting
514
+ python -m agent_readable agent_readable:AgentReadableMixin
515
+
516
+ # Any module
517
+ python -m agent_readable pathlib
518
+ ```
519
+
520
+ Outputs agent-oriented documentation for the given class or module to stdout.
521
+
522
+ ## FAQ
523
+
524
+ ### How does my agent know to call `agent_help()` instead of `help()`?
525
+
526
+ Today, you tell it. Either:
527
+
528
+ - Paste [`AGENT-PROMPT.md`](AGENT-PROMPT.md) into your conversation, or
529
+ - Add it permanently to your repo's `AGENTS.md` / `CLAUDE.md` / `.cursor/rules` / equivalent instruction file.
530
+
531
+ "You tell it" is the hard part of any new agent protocol. An MCP server (so MCP-aware clients auto-discover the tool) is on the roadmap.
532
+
533
+ ### How is this different from `AGENTS.md` / `llms.txt` / Cursor rules?
534
+
535
+ Different granularity, different drift profile.
536
+
537
+ - `AGENTS.md` / `llms.txt` / `.cursor/rules` are **project-level**: one file per repo. Good for *"use pnpm,"* *"run lint before commit,"* *"this codebase prefers functional style."*
538
+ - `agent-readable` is **class- or module-level**: rules live next to the API they describe. Good for *"`ResourcePool.call()` is for non-streaming requests; for streaming, use `stream()` instead."*
539
+
540
+ Use both — they don't compete. The advantage of class-level: docs travel with the code. When someone refactors `ResourcePool` into two classes, the rules move with them in the same PR; they don't sit stale in a sidecar file.
541
+
542
+ ### Why not just write better docstrings?
543
+
544
+ Docstrings answer *what does this do?* They aren't designed for *when may I call this?* or *what's the wrong way to use this?* Mixing both into the docstring makes the API summary noisier without helping agents find the rules. `__agent_notes__()` is for the second category, and it accumulates across the MRO automatically — class docstrings don't compose like that.
545
+
546
+ ### Does this work for libraries I don't own?
547
+
548
+ Yes. Two ways:
549
+
550
+ - Subclass + `AgentReadableMixin` (Example 1).
551
+ - Monkey-patch: `ThirdPartyClass.__agent_notes__ = classmethod(lambda cls: "...")`. `agent_help()` collects notes from the entire MRO; the mixin is not required.
552
+
553
+ ### Does it work without my class doing anything?
554
+
555
+ Yes — `agent_help()` falls back to introspection (Example 4). You get a structured summary of every plain class, mixin or not. Notes are added on top *if* the class defines them; otherwise the auto-doc is what you see.
556
+
557
+ ## Keeping agent docs up to date
558
+
559
+ Agent docs can go stale when classes change — new methods, changed behavior, removed APIs. Copy [`AGENT-PROMPT.md`](AGENT-PROMPT.md) into your coding agent's conversation, or permanently add it to your repo's `AGENTS.md`, `CLAUDE.md`, `.cursor/rules`, `.trae/rules`, `.github/copilot-instructions.md`, or equivalent instruction file. It tells your coding agent to run `agent_help()` before modifying a class, prefer docstrings over `__agent_notes__()`, and verify that docs stay accurate after changes.
560
+
561
+ ## The `__agent_help__` protocol
562
+
563
+ `__agent_help__()` is a dunder protocol, similar in spirit to ecosystem protocols such as:
564
+
565
+ - `__str__` (str) — string representation
566
+ - `__rich_repr__` (Rich) — custom console representation
567
+ - `__html__` (Django, Jinja2) — HTML rendering
568
+ - `__array__` (NumPy) — array conversion
569
+ - `__fspath__` (os.fspath) — filesystem path conversion
570
+
571
+ Classes that define a `@classmethod` named `__agent_help__` returning a `str` are considered agent-readable. Modules can define a top-level `__agent_help__` attribute (callable or string). Call the top-level `agent_help(obj)` function to get the docs — just like `str()` calls `__str__()`. The `AgentReadable` `typing.Protocol` and `AgentReadableMixin` are provided for convenience and type-checking, but neither is required.
572
+
573
+ ### `__agent_help__` vs `__agent_notes__`
574
+
575
+ The two dunders intentionally encode different composition rules:
576
+
577
+ | Aspect | `__agent_help__()` | `__agent_notes__()` |
578
+ |-----------------|-------------------------------------------------|-----------------------------------------------------------------------------|
579
+ | Semantics | **Replacement** — returned string IS the output | **Additive** — appended to auto-generated docs |
580
+ | Composition | Single class wins (the one closest in MRO) | Accumulated across the MRO; leaf class wins on conflict (header marks this) |
581
+ | When to use | Total control over the rendered text | "Auto-doc + my extra do/don't rules" |
582
+ | Skipped when | (always called if defined) | Skipped when a duck-typed `__agent_help__` is present (it owns the output) |
583
+ | Mixin required? | No — duck-typed classmethod is enough | No — defining `__agent_notes__` on any class is enough |
584
+
585
+ ## Class docstring hints
586
+
587
+ For classes that inherit `AgentReadableMixin`, add a short hint in the class docstring:
588
+
589
+ ```python
590
+ class ResourcePool(AgentReadableMixin):
591
+ """
592
+ Rotates interchangeable resources such as API keys.
593
+
594
+ Agent usage:
595
+ Run ``agent_help(ResourcePool)`` before using this class in generated code.
596
+ """
597
+ ```
598
+
599
+ This way, even agents that only see the source or call `help()` are reminded to check `agent_help()`.
600
+
601
+ ## API reference
602
+
603
+ ### `AgentReadable`
604
+
605
+ A `typing.Protocol` (runtime-checkable) that requires `__agent_help__() -> str`.
606
+
607
+ ### `AgentReadableMixin`
608
+
609
+ A mixin class for *classes* providing a default `__agent_help__()` implementation that generates structured Markdown from introspection. The mixin is convenience — defining `__agent_notes__()` directly on any class (no inheritance) also works; notes are collected automatically regardless. The mixin does not apply to modules; modules are supported directly by `agent_help()`.
610
+
611
+ If a class inherits from `AgentReadableMixin`, coding agents should call `agent_help(TheClass)` before generating code that uses it.
612
+
613
+ ### `agent_help(obj)`
614
+
615
+ Returns a string of agent-oriented documentation for a class, instance, or module.
616
+
617
+ - For classes and instances: if `__agent_help__()` is defined (via mixin or duck-typing), it is called and its return value is used verbatim — duck-typed implementations are responsible for their own formatting and notes are NOT auto-appended. Otherwise, auto-generated docs are produced from introspection, with `__agent_notes__()` from every class in the MRO appended automatically. If `__agent_help__()` raises, falls back to the auto-generated path (which does include notes).
618
+ - For modules: if the module defines a `__agent_help__` attribute (callable or string), it is used. Otherwise, auto-generated docs are produced from the module docstring and its public functions and classes.
619
+
620
+ ## License
621
+
622
+ MIT
@@ -0,0 +1,7 @@
1
+ agent_readable/__init__.py,sha256=257u1njad3rF_T8sQ3jlzf7RCStS34koSH-3ChdnygY,230
2
+ agent_readable/__main__.py,sha256=8mf7bomOzOl7RgWcoFe2oESTbKa2OTBW8lc21NCBW3M,1487
3
+ agent_readable/_protocol.py,sha256=Em1YNBfKnNJI3xmhEbxO1BnxLA2i8J8gUnv9qJ4qXBI,11557
4
+ agent_readable-0.1.0.dist-info/METADATA,sha256=2lqK-OPe8KwnNrFiPPZCEgDr_e0aOVg4pKwmWOv-088,22995
5
+ agent_readable-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ agent_readable-0.1.0.dist-info/licenses/LICENSE,sha256=e9e5wQN-xeo0CxeZJIlbXjA7pF7vpCSa6hWYYQVUVN0,1093
7
+ agent_readable-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zydo and agent-readable contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.