Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
85cd136
docs: scaffold Agent Playbooks guides and review handoff
tylerhutcherson Oct 1, 2026
114112c
docs(iris): refine Playbooks developer guides and examples
tylerhutcherson Oct 12, 2026
5ddff46
MOD-19391 Add Python, TypeScript, and cURL pseudo-clients for Iris ex…
andy-stark-redis Oct 14, 2026
eb745e9
docs: align Playbooks navigation with the Iris docs space
tylerhutcherson Oct 14, 2026
e2f9e31
docs: give the FAQ authoring guide a task-specific URL
tylerhutcherson Oct 14, 2026
6c6a2b9
docs: simplify cross-language naming explanations
tylerhutcherson Oct 14, 2026
c442956
docs: use standard TCE sources for Playbooks examples
tylerhutcherson Oct 14, 2026
67f9016
docs: separate Playbooks MCP and agent integration guides
tylerhutcherson Oct 14, 2026
03da1f1
docs: simplify Playbooks MCP examples and guide navigation
tylerhutcherson Oct 14, 2026
f5a1e09
docs: improve Playbooks navigation and Python examples
tylerhutcherson Oct 15, 2026
b07e7ee
docs: add Playbooks workflow diagrams
tylerhutcherson Oct 15, 2026
772939a
docs: clarify Redis Agent Playbooks terminology
tylerhutcherson Oct 15, 2026
9996b99
docs: align Playbooks guides with alpha.3 SDKs
tylerhutcherson Oct 18, 2026
05a9897
docs: add unified Python and CLI Playbooks examples
tylerhutcherson Oct 21, 2026
c51fa40
docs: focus Proposals guide on reusable agent knowledge
tylerhutcherson Oct 21, 2026
12bd469
docs: show Iris CLI commands directly
tylerhutcherson Oct 21, 2026
428b113
docs: simplify Playbooks examples and fix retention diagram
tylerhutcherson Oct 21, 2026
7520513
docs: fix Playbooks quickstart navigation
tylerhutcherson Oct 22, 2026
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
41 changes: 41 additions & 0 deletions .github/workflows/check-playbook-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
name: Check Playbooks documentation

on:
pull_request:
paths:
- 'content/develop/ai/context-engine/agent-playbook/**'
- 'build/agent_playbook_docs/**'
- 'local_examples/iris/playbooks/**'
- 'build/components/example.py'
- 'build/local_examples.py'
- 'build/example-test-harness/clients.tsv'
- 'config.toml'
- 'layouts/partials/process-markdown-content.html'
- 'layouts/_default/apireference.html'
- '.github/workflows/check-playbook-docs.yml'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI ignores Playbooks diagram asset changes

Medium Severity

The Playbooks docs workflow path filters omit static/images/ai/context-engine/. test_proposal_diagram_qualifies_default_retention_windows reads agent-playbook-proposals.svg, so a diagram-only edit that drops the default 30-day labels would not run that check.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 7520513. Configure here.

workflow_dispatch:

permissions:
contents: read

jobs:
examples:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: pip
cache-dependency-path: build/agent_playbook_docs/requirements.txt
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: npm
cache-dependency-path: build/agent_playbook_docs/package-lock.json
- run: python -m pip install -r build/agent_playbook_docs/requirements.txt
- run: npm ci --prefix build/agent_playbook_docs
- run: python build/agent_playbook_docs/check_sdk_source.py
- run: python build/agent_playbook_docs/examples.py build/agent_playbook_docs/.generated
- run: build/agent_playbook_docs/node_modules/.bin/tsc -p build/agent_playbook_docs
- run: python -m pytest build/agent_playbook_docs --rootdir . -q
2 changes: 2 additions & 0 deletions build/agent_playbook_docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.generated/
!package-lock.json
71 changes: 71 additions & 0 deletions build/agent_playbook_docs/check_sdk_source.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Check the docs API against the locked iris-sdks public contract.

Without arguments, verify the vendored contract without network access.
With --sdk-repo, also verify the local SDK contract, example overlay, and version.
An upstream change requires reviewing the source lock and SDK pin together.
"""

import argparse
from copy import deepcopy
import hashlib
import json
from pathlib import Path

import yaml


ROOT = Path(__file__).resolve().parents[2]
LOCK = Path(__file__).with_name("source-lock.json")
SPEC = ROOT / "content/develop/ai/context-engine/agent-playbook/api-reference/openapi.json"


def wire_contract(document):
result = deepcopy(document)
result["info"].pop("title", None)
result.pop("tags", None)
for methods in result["paths"].values():
for operation in methods.values():
if not isinstance(operation, dict) or "operationId" not in operation:
continue
operation.pop("tags", None)
operation.pop("summary", None)
return result


def digest(document):
content = json.dumps(wire_contract(document), sort_keys=True, separators=(",", ":"))
return hashlib.sha256(content.encode()).hexdigest()


def check(sdk_repo=None):
lock = json.loads(LOCK.read_text())
expected = lock["data_plane_contract_sha256"]
if digest(json.loads(SPEC.read_text())) != expected:
raise ValueError("Docs contract differs from the locked SDK input. Regenerate and review it.")
if sdk_repo is not None:
upstream = json.loads((sdk_repo / lock["sdk_contract_path"]).read_text())
if digest(upstream) != expected:
raise ValueError("SDK contract changed. Review the API, examples, version pin, and source lock together.")
overlay = sdk_repo / lock["sdk_example_overlay_path"]
if hashlib.sha256(overlay.read_bytes()).hexdigest() != lock["sdk_example_overlay_sha256"]:
raise ValueError("SDK usage examples changed. Review the docs walkthroughs before updating the source lock.")
config = yaml.safe_load((sdk_repo / "playbook-python-sdk/.speakeasy/gen.yaml").read_text())
if config["python"]["version"] != lock["sdk_generator_version"]:
raise ValueError("SDK version changed. Review the docs dependency pin.")
typescript = json.loads((sdk_repo / "playbook-ts-sdk/package.json").read_text())
if f"{typescript['name']}@{typescript['version']}" != lock["typescript_package"]:
raise ValueError("TypeScript SDK version changed. Review the docs dependency pin.")
return lock


if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sdk-repo", type=Path)
args = parser.parse_args()
try:
lock = check(args.sdk_repo)
except ValueError as error:
parser.exit(1, str(error) + "\n")
print(f"Docs match the locked SDK contract ({lock['python_package']}).")
if args.sdk_repo:
print("Local SDK contract, usage overlay, and generator version also match.")
160 changes: 160 additions & 0 deletions build/agent_playbook_docs/examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"""Read the standard TCE sources for Playbooks validation.

Site builds use build/local_examples.py and clients-example. This adapter exposes
those same processed source files and named steps to the API example checks.
"""

from functools import lru_cache
from pathlib import Path
import re
import shutil
import sys
from tempfile import TemporaryDirectory

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / "build"))
from components.example import Example
from local_examples import get_example_id_from_file, get_language_from_extension

DOCS = ROOT / "content/develop/ai/context-engine/agent-playbook"
SOURCES = ROOT / "local_examples/iris/playbooks"
GUIDES = ("quickstart", "faqs", "skills", "proposals", "retrieval", "mcp", "limits")
AGENT_GUIDES = ("openai_agent", "langgraph_agent")
PAGE_NAMES = {"faqs": "manage-faqs", "skills": "manage-skills"}
CLIENTS = {}
for row in (ROOT / "build/example-test-harness/clients.tsv").read_text().splitlines():
if row.startswith("iris-") and not row.startswith("iris-cli\t"):
fields = row.split("\t")
CLIENTS[fields[6].rsplit(".", 1)[1]] = fields[1]
assert set(CLIENTS) == {"py", "mts", "sh"}
ALL_CLIENTS = {**CLIENTS, "cli.sh": "CLI (Iris)"}
CALLS = re.compile(r'\{\{< clients-example\s+([^>]*?)/>\}\}', re.S)
ATTRS = re.compile(r'(\w+)="([^"]*)"')
FENCE = re.compile(r"```(?:python|typescript|bash)\n(.*?)\n```", re.S)


def source_path(page, suffix, action=None):
name = f"{page}_{action}" if action else "mcp_client" if page == "mcp" else page
return SOURCES / page / f"{name}.{suffix}"


@lru_cache(maxsize=128)
def _processed(path, mtime):
with TemporaryDirectory(prefix="playbook-tce-") as directory:
target = Path(directory) / path.name
shutil.copy2(path, target)
example = Example(get_language_from_extension(path.name), str(target))
assert example.named_steps, f"No named steps in {path}"
return target.read_text(), example.named_steps


def processed(path):
return _processed(path, path.stat().st_mtime_ns)


def bundle(set_id):
result = {}
for path in SOURCES.rglob("*"):
suffix = "cli.sh" if path.name.endswith(".cli.sh") else path.suffix.lstrip(".")
if suffix in ALL_CLIENTS and get_example_id_from_file(str(path)) == set_id:
assert suffix not in result, f"Duplicate {suffix} source in {set_id}"
result[suffix] = path
assert result, f"Unknown Playbook example set: {set_id}"
return result


def step(path, name):
source, steps = processed(path)
assert name in steps, f"Missing {name} in {path}"
start, end = map(int, steps[name].split("-"))
return "\n".join(source.splitlines()[start - 1:end])


def calls(page):
source = (DOCS / f"{PAGE_NAMES.get(page, page)}.md").read_text()
return [dict(ATTRS.findall(body)) for body in CALLS.findall(source)]


def blocks(page):
"""Collect published shared steps and their retained TypeScript test fixtures."""
result = []
seen = set()
for attrs in calls(page):
if not {CLIENTS["py"], CLIENTS["sh"]}.issubset(attrs.get("lang_filter", "").split(",")):
continue
name = attrs["step"]
assert name not in seen, f"Duplicate step in {page}: {name}"
seen.add(name)
files = bundle(attrs["set"])
result.append((name, [step(files[suffix], name) for suffix in CLIENTS]))
return result


def skill_package():
return step(source_path("skills", "sh"), "playbook-skill-package")


def scripts(page, omit=()):
selected = [block for block in blocks(page) if block[0] not in omit]
if page not in ("quickstart", "mcp"):
setup = next(block for block in blocks("quickstart") if block[0] == "playbook-connect")
selected.insert(0, setup)
result = ["\n\n".join(block[1][i] for block in selected) + "\n" for i in range(3)]
if page == "skills":
result[2] = skill_package() + "\n\n" + result[2]
result[2] = "set -euo pipefail\n\n" + result[2]
return dict(zip(CLIENTS, result))


def full_examples():
return {source_path(page, suffix): processed(source_path(page, suffix))[0]
for page in ("quickstart", "skills") for suffix in ALL_CLIENTS}


def cli_script(page):
selected = [attrs for attrs in calls(page)
if "CLI (Iris)" in attrs.get("lang_filter", "").split(",")
and not attrs["set"].endswith(("_delete", "_withdraw"))]
if not selected:
return None
pieces = []
if page != "quickstart":
pieces.append(step(source_path("quickstart", "cli.sh"), "playbook-connect"))
for attrs in selected:
pieces.append(step(bundle(attrs["set"])["cli.sh"], attrs["step"]))
if page == "skills":
pieces.insert(0, skill_package())
return "set -euo pipefail\n\n" + "\n\n".join(pieces) + "\n"


def agent_scripts():
return {(page, suffix): processed(source_path(page, suffix))[0]
for page in AGENT_GUIDES for suffix in ("py", "mts")}


def standalone_python(page, heading):
assert (page, heading) == ("quickstart", "Use Python asynchronously")
return processed(SOURCES / "async/async_example.py")[0]


if __name__ == "__main__":
import argparse

parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("output", type=Path)
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=True)
for path in args.output.glob("*_download.*"):
path.unlink()
for page in GUIDES:
for suffix, source in scripts(page).items():
(args.output / f"{page}_example.{suffix}").write_text(source)
if source := cli_script(page):
(args.output / f"{page}_example.cli.sh").write_text(source)
for path, source in full_examples().items():
suffix = "cli.sh" if path.name.endswith(".cli.sh") else path.suffix[1:]
name = path.name.removesuffix("." + suffix)
(args.output / f"{name}_copy.{suffix}").write_text(source)
for (page, suffix), source in agent_scripts().items():
(args.output / f"{page}_example.{suffix}").write_text(source)
(args.output / "async_search_example.py").write_text(standalone_python("quickstart", "Use Python asynchronously"))
Loading
Loading