Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
- [ ] Tests added or updated
- [ ] `make check` passes locally (`ruff` + `mypy`)
- [ ] `CHANGELOG.md` updated under `[Unreleased]`
- [ ] Documentation updated (if the public API changed)
- [ ] Documentation updated (if the public API changed), `docs/agents.md` included

## Related issues

Expand Down
1 change: 1 addition & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ jobs:
- run: uv sync --no-dev --group docs
- run: cp CHANGELOG.md docs/changelog.md
- run: uv run zensical build --clean
- run: uv run python scripts/emit_markdown.py
- uses: actions/upload-pages-artifact@v5
with:
path: site
Expand Down
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,20 @@ Breaking changes: add `!` after the type (`feat!:`) or include a `BREAKING CHANG
4. Run `make check && make test-unit` locally
5. Open a PR against `master`

## The agents page

`docs/agents.md` is the whole library on one page, written for a coding assistant: the
public API, the rules that break code when they are broken, the mistakes models make, and a
map of which page to fetch for the rest. People hand it to an assistant instead of the site,
which is what makes a stale one worse than none — it teaches a model an API that no longer
exists.

It is part of the public API, so it changes in the same pull request the API does: a name
added, renamed or removed, a changed default or signature, a new rule a caller has to obey.
A new docs page means a new row in the documentation map. The review check is mechanical —
if the diff changes the public surface and `docs/agents.md` is untouched, the pull request
is not finished.

## Releasing (maintainers only)

Releases are fully automated via [Release Please](https://github.com/googleapis/release-please).
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ metrics.
The core depends only on `grpcio`. Every integration is an opt-in extra, so you
install exactly what you use.

> [!TIP]
> **Building this with an AI assistant?** Hand it
> **[one page](https://bedrock-python.github.io/grpc-client-kit/agents/)** instead of the
> whole site: the whole public API, who owns a channel and who closes it, what a timeout
> and a retry actually cover, which batteries are opt-in — plus the mistakes models make
> with this API and a map of which page to fetch for the rest. Every docs page is also
> served as raw Markdown at its own URL, and a **Copy page** button at the top of each one
> hands it straight to a chat window.

## Why grpc-client-kit

- **Channels are pooled by identity, not by address.** Target, security,
Expand Down Expand Up @@ -313,6 +322,7 @@ Full documentation at [bedrock-python.github.io/grpc-client-kit](https://bedrock
| [Observability](https://bedrock-python.github.io/grpc-client-kit/guide/observability/) | log records, CLIENT spans and the metrics an RPC emits |
| [Advanced](https://bedrock-python.github.io/grpc-client-kit/guide/advanced/) | interceptors that re-issue calls, target validation, ownership and DI wiring |
| [API reference](https://bedrock-python.github.io/grpc-client-kit/reference/) | generated from the source |
| [For AI agents](https://bedrock-python.github.io/grpc-client-kit/agents/) | the whole API surface, the rules that break code when broken and a map of the rest, on one page to hand to a coding assistant |

## License

Expand Down
556 changes: 556 additions & 0 deletions docs/agents.md

Large diffs are not rendered by default.

158 changes: 158 additions & 0 deletions docs/assets/javascripts/copy-page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* Behaviour for the "Copy page" control.
*
* Every handler is delegated from `document`, because the theme swaps the
* content in place when instant navigation is on: a listener bound to an
* element of one page would not survive the move to the next.
*/
(function () {
"use strict";

var RESET_AFTER_MS = 2000;

/* Where the Markdown of a page is written, as an absolute URL. Two data
attributes say how far the site root is from here and where this page sits
below it; scripts/emit_markdown.py writes the file to match. The site's own
name is a third, so this file is the same in every project that carries it. */
function markdownUrl(widget) {
var base = (widget.dataset.copyBase || ".").replace(/\/$/, "");
var page = widget.dataset.copyPage || "";
var relative = page === "" ? "index.md" : page.replace(/\/$/, "") + ".md";
return new URL(base + "/" + relative, window.location.href).href;
}

function prompt(widget) {
var title = widget.dataset.copyTitle || document.title;
var site = widget.dataset.copySite || "project";
return (
"Read " +
markdownUrl(widget) +
' -- the "' +
title +
'" page of the ' +
site +
" documentation -- so I can ask questions about it."
);
}

function destination(widget, name) {
var question = encodeURIComponent(prompt(widget));
switch (name) {
case "markdown":
return markdownUrl(widget);
case "chatgpt":
return "https://chatgpt.com/?hints=search&q=" + question;
case "claude":
return "https://claude.ai/new?q=" + question;
case "perplexity":
return "https://www.perplexity.ai/search?q=" + question;
default:
return markdownUrl(widget);
}
}

function write(text) {
if (navigator.clipboard && window.isSecureContext) {
return navigator.clipboard.writeText(text);
}
/* Insecure origins have no clipboard API; the old selection dance still
works there, which keeps a local preview usable. */
return new Promise(function (resolve, reject) {
var area = document.createElement("textarea");
area.value = text;
area.style.position = "fixed";
area.style.opacity = "0";
document.body.appendChild(area);
area.select();
var ok = document.execCommand("copy");
document.body.removeChild(area);
ok ? resolve() : reject(new Error("copy refused"));
});
}

function announce(widget, label, state) {
var slot = widget.querySelector("[data-copy-label]");
if (slot) slot.textContent = label;
if (state) {
widget.dataset.copyState = state;
} else {
delete widget.dataset.copyState;
}
}

function copy(widget) {
var reset = function () {
window.setTimeout(function () {
announce(widget, "Copy page", null);
}, RESET_AFTER_MS);
};
fetch(markdownUrl(widget))
.then(function (response) {
if (!response.ok) throw new Error(String(response.status));
return response.text();
})
.then(write)
.then(function () {
announce(widget, "Copied", "copied");
reset();
})
.catch(function () {
announce(widget, "Copy failed", null);
reset();
});
}

function close(widget) {
var menu = widget.querySelector("[data-copy-menu]");
var toggle = widget.querySelector("[data-copy-toggle]");
if (menu) menu.hidden = true;
if (toggle) toggle.setAttribute("aria-expanded", "false");
}

function closeAll(except) {
var widgets = document.querySelectorAll(".md-copy-page");
for (var i = 0; i < widgets.length; i++) {
if (widgets[i] !== except) close(widgets[i]);
}
}

document.addEventListener("click", function (event) {
var target = event.target;
if (!(target instanceof Element)) return;

var widget = target.closest(".md-copy-page");
if (!widget) {
closeAll(null);
return;
}
closeAll(widget);

if (target.closest("[data-copy-action]")) {
event.preventDefault();
close(widget);
copy(widget);
return;
}

var toggle = target.closest("[data-copy-toggle]");
if (toggle) {
event.preventDefault();
var menu = widget.querySelector("[data-copy-menu]");
if (!menu) return;
/* The destinations are filled in on the way out rather than at load:
the page under the widget may have changed since. */
var links = menu.querySelectorAll("[data-copy-open]");
for (var i = 0; i < links.length; i++) {
links[i].href = destination(widget, links[i].dataset.copyOpen);
}
menu.hidden = !menu.hidden;
toggle.setAttribute("aria-expanded", menu.hidden ? "false" : "true");
return;
}

if (target.closest("[data-copy-open]")) close(widget);
});

document.addEventListener("keydown", function (event) {
if (event.key === "Escape") closeAll(null);
});
})();
Loading