diff --git a/.github/workflows/check-playbook-docs.yml b/.github/workflows/check-playbook-docs.yml new file mode 100644 index 0000000000..860f0b1032 --- /dev/null +++ b/.github/workflows/check-playbook-docs.yml @@ -0,0 +1,35 @@ +name: Check Playbooks documentation + +on: + pull_request: + paths: + - 'content/develop/ai/context-engine/agent-playbook/**' + - 'build/agent_playbook_docs/**' + - 'layouts/_default/apireference.html' + - '.github/workflows/check-playbook-docs.yml' + 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 diff --git a/build/agent_playbook_docs/.gitignore b/build/agent_playbook_docs/.gitignore new file mode 100644 index 0000000000..9300b25092 --- /dev/null +++ b/build/agent_playbook_docs/.gitignore @@ -0,0 +1,2 @@ +.generated/ +!package-lock.json diff --git a/build/agent_playbook_docs/check_sdk_source.py b/build/agent_playbook_docs/check_sdk_source.py new file mode 100644 index 0000000000..f3311afc1f --- /dev/null +++ b/build/agent_playbook_docs/check_sdk_source.py @@ -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.") diff --git a/build/agent_playbook_docs/examples.py b/build/agent_playbook_docs/examples.py new file mode 100644 index 0000000000..1c9c68c689 --- /dev/null +++ b/build/agent_playbook_docs/examples.py @@ -0,0 +1,102 @@ +"""Extract runnable examples from the documentation, their only editable source. + +The guides share the quickstart connection block. MCP has its own complete client. +Validation scripts stay in a temporary directory. Downloadable walkthroughs are +generated beside their pages and checked against these same snippets. +""" + +from pathlib import Path +import re + +ROOT = Path(__file__).resolve().parents[2] +DOCS = ROOT / "content/develop/ai/context-engine/agent-playbook" +GUIDES = ("quickstart", "faqs", "skills", "proposals", "retrieval", "mcp", "limits") +DOWNLOADS = { + "quickstart": {"playbook-quickstart-delete"}, + "skills": {"playbook-skill-withdraw", "playbook-skill-delete"}, +} +TABS = re.compile(r'\{\{< multitabs id="([^"]+)"[^>]*>\}\}(.*?)\{\{< /multitabs >\}\}', re.S) +FENCE = re.compile(r"```(?:python|typescript|bash)\n(.*?)\n```", re.S) + + +def blocks(page): + result = [] + seen = set() + for name, body in TABS.findall((DOCS / f"{page}.md").read_text()): + assert name not in seen, f"Duplicate tab group in {page}: {name}" + seen.add(name) + if name.endswith("-install") or name == "playbook-install": + continue + sections = body.split("-tab-sep-") + assert len(sections) == 3, name + snippets = [] + for section in sections: + matches = FENCE.findall(section) + assert len(matches) == 1, name + snippets.append(matches[0]) + result.append((name, snippets)) + return result + + +def skill_package(): + text = (DOCS / "skills.md").read_text() + return next(code for code in FENCE.findall(text) if code.startswith("mkdir -p refund-support-reply/")) + + +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(("py", "mts", "sh"), result)) + + +def downloads(): + result = {} + for page, omit in DOWNLOADS.items(): + assert omit <= {name for name, _ in blocks(page)}, (page, omit) + for suffix, source in scripts(page, omit=omit).items(): + marker = "//" if suffix == "mts" else "#" + header = f"{marker} Generated from {page}.md; edit the page, then run examples.py --refresh-downloads.\n" + result[DOCS / page / f"{page}.{suffix}"] = header + source + result[DOCS / "skills/prepare_skill.sh"] = ( + "# Generated from skills.md; edit the page, then run examples.py --refresh-downloads.\n" + "set -euo pipefail\n\n" + skill_package() + "\n" + ) + return result + + +def standalone_python(page, heading): + source = (DOCS / f"{page}.md").read_text().split(f"## {heading}\n", 1)[1].split("\n## ", 1)[0] + snippets = re.findall(r"```python\n(.*?)\n```", source, re.S) + assert len(snippets) == 1, (page, heading) + return snippets[0] + "\n" + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("output", type=Path, nargs="?") + parser.add_argument("--refresh-downloads", action="store_true", + help="Refresh the downloadable scripts from the displayed walkthroughs.") + args = parser.parse_args() + if args.refresh_downloads: + for path, source in downloads().items(): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(source) + if args.output is None: + if not args.refresh_downloads: + parser.error("Provide an output directory or --refresh-downloads.") + raise SystemExit(0) + args.output.mkdir(parents=True, exist_ok=True) + for page in GUIDES: + for suffix, source in scripts(page).items(): + (args.output / f"{page}_example.{suffix}").write_text(source) + for path, source in downloads().items(): + (args.output / f"{path.stem}_download{path.suffix}").write_text(source) + (args.output / "async_search_example.py").write_text(standalone_python("quickstart", "Use Python asynchronously")) diff --git a/build/agent_playbook_docs/live_examples.py b/build/agent_playbook_docs/live_examples.py new file mode 100644 index 0000000000..2ed748f187 --- /dev/null +++ b/build/agent_playbook_docs/live_examples.py @@ -0,0 +1,185 @@ +"""Run displayed examples and downloads against a disposable local Playbooks stack. + +Provision one playbook per case through the control plane and obtain scoped keys +from Identity Service. The stack must allow the selected embedding model. +MCP agent cases use a real model when OPENAI_API_KEY is set. Hosted MCP additionally +requires --hosted-url pointing to this stack through a public HTTPS endpoint. +""" + +import argparse +import base64 +import json +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import time +from urllib.error import HTTPError +from urllib.parse import urlsplit +from urllib.request import Request, urlopen +from uuid import uuid4 + +from redis_agent_playbook import Playbook + +from examples import GUIDES, downloads, scripts, skill_package, standalone_python + +HERE = Path(__file__).resolve().parent + + +def request(method, url, token, body=None): + data = None if body is None else json.dumps(body).encode() + req = Request(url, data=data, method=method, headers={ + "Authorization": f"Bearer {token}", "Content-Type": "application/json", + }) + with urlopen(req, timeout=60) as response: + data = response.read() + return json.loads(data) if data else None + + +def create_key(ids_url, control_token, playbook_id, actions): + for attempt in range(10): + try: + result = request("POST", ids_url + "/v1/api-keys", control_token, { + "name": "docs-validation-" + uuid4().hex[:12], + "grants": [{"product": "playbook", "resourceType": "playbook", + "resourceId": playbook_id, "actions": actions}], + }) + return result["token"] + except HTTPError as error: + if error.code not in (404, 409, 503) or attempt == 9: + raise + time.sleep(1) + + +def seed(client, directory, kind): + if kind == "faq": + draft = client.create_faq(content={ + "attributes": {"locale": "en-US", "topic": "refund"}, + "payload": {"faq": { + "question": "How do I check the status of a refund?", + "answer": "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number.", + }}, + }) + client.publish_faq(faq_id=draft.entry.entry_id, version=draft.version.version) + else: + package = base64.b64encode((directory / "refund-support-reply.zip").read_bytes()).decode() + draft = client.create_skill(package_base64=package, content={ + "attributes": {"locale": "en-US", "topic": "refund"}, + }) + client.publish_skill(skill_id=draft.entry.entry_id, version=draft.version.version) + return draft.entry.entry_id + + +def cases(): + result = {} + for guide in GUIDES: + for suffix, source in scripts(guide).items(): + result[f"{guide}.{suffix}"] = (guide, suffix, source) + for path, source in downloads().items(): + if path.name != "prepare_skill.sh": + result[f"{path.stem}_download{path.suffix}"] = (path.stem, path.suffix[1:], source) + result["async_search.py"] = ("async_search", "py", standalone_python("quickstart", "Use Python asynchronously")) + return result + + +def run_case(name, case, args, log_dir): + guide, suffix, source = case + created = request("POST", args.cp_url + "/v1/playbooks", args.admin_token, { + "name": "docs-validation-" + uuid4().hex[:12], + "embedding": {"provider": args.embedding_provider, "model": args.embedding_model, + "dimensions": args.embedding_dimensions}, + }) + playbook_id = created["playbookId"] + author_key = reader_key = "" + try: + author_key = create_key(args.ids_url, args.control_token, playbook_id, ["read", "write", "publish", "propose"]) + with tempfile.TemporaryDirectory(prefix="playbook-live-") as directory: + directory = Path(directory) + (directory / "node_modules").symlink_to(HERE / "node_modules", target_is_directory=True) + preparation = subprocess.run(["bash", "-euc", skill_package()], cwd=directory, + text=True, capture_output=True, timeout=30) + if preparation.returncode: + raise RuntimeError("Skill package preparation failed") + env = {**os.environ, "PLAYBOOK_URL": args.dp_url, "PLAYBOOK_ID": playbook_id, + "PLAYBOOK_API_KEY": author_key, "OPENAI_AGENTS_DISABLE_TRACING": "1"} + with Playbook(args.dp_url, playbook_id=playbook_id, api_key=author_key, retry_config=None) as client: + if guide in ("retrieval", "limits", "async_search"): + env["PLAYBOOK_FAQ_ID"] = seed(client, directory, "faq") + if guide in ("retrieval", "mcp"): + env["PLAYBOOK_SKILL_ID"] = seed(client, directory, "skill") + if guide in ("retrieval", "limits", "async_search", "mcp"): + reader_key = create_key(args.ids_url, args.control_token, playbook_id, ["read"]) + env["PLAYBOOK_API_KEY"] = reader_key + if guide == "mcp" and suffix == "sh": + env["PLAYBOOK_URL"] = args.hosted_url.rstrip("/") + # A file named mcp.py shadows the installed MCP package. + path = directory / ("mcp_example.py" if name == "mcp.py" else name) + path.write_text(source) + command = { + "py": [sys.executable, str(path)], + "mts": [str(HERE / "node_modules/.bin/tsx"), str(path)], + "sh": ["bash", str(path)], + }[suffix] + completed = subprocess.run(command, env=env, cwd=directory, text=True, + capture_output=True, timeout=180) + output = completed.stdout + completed.stderr + for secret in (author_key, reader_key, os.environ.get("OPENAI_API_KEY")): + if secret: + output = output.replace(secret, "") + (log_dir / (name + ".log")).write_text(output) + if completed.returncode: + raise RuntimeError(f"Exit {completed.returncode}; see {name}.log") + if name.startswith("quickstart_download"): + entries = client.list_faqs().faqs + if not any(entry.latest_version == 2 and entry.publication.version == 2 for entry in entries if entry.publication): + raise RuntimeError("Quickstart download did not retain its updated publication") + if name.startswith("skills_download"): + if not any(entry.publication for entry in client.list_skills().skills): + raise RuntimeError("Skill download did not retain a publication") + if guide == "mcp" and ("Processing" not in completed.stdout or "load_skill" not in completed.stdout or "fetch_resource" not in completed.stdout): + raise RuntimeError("Agent did not load the skill/template and preserve the refund stage") + finally: + request("DELETE", args.cp_url + "/v1/playbooks/" + playbook_id, args.admin_token) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--cp-url", default="http://127.0.0.1:9200") + parser.add_argument("--dp-url", default="http://127.0.0.1:9201") + parser.add_argument("--ids-url", default="http://127.0.0.1:9202") + parser.add_argument("--admin-token", default=os.environ.get("PLAYBOOK_ADMIN_TOKEN", "local-admin")) + parser.add_argument("--control-token", default=os.environ.get("PLAYBOOK_IDS_CONTROL_TOKEN", "local-control")) + parser.add_argument("--embedding-provider", default="noop") + parser.add_argument("--embedding-model", default="playbook-local") + parser.add_argument("--embedding-dimensions", type=int, default=384) + parser.add_argument("--hosted-url", help="Public HTTPS base URL forwarding to the local data plane") + parser.add_argument("--case", action="append", choices=cases()) + parser.add_argument("--log-dir", type=Path, required=True) + args = parser.parse_args() + if args.hosted_url and urlsplit(args.hosted_url).scheme != "https": + parser.error("--hosted-url must use HTTPS") + args.log_dir.mkdir(parents=True, exist_ok=True) + results = [] + for name, case in cases().items(): + if args.case and name not in args.case: + continue + if name == "mcp.sh" and not args.hosted_url: + results.append({"case": name, "status": "skipped", "reason": "--hosted-url is unset"}) + continue + if case[0] == "mcp" and not os.environ.get("OPENAI_API_KEY"): + results.append({"case": name, "status": "skipped", "reason": "OPENAI_API_KEY is unset"}) + continue + try: + run_case(name, case, args, args.log_dir) + result = {"case": name, "status": "passed"} + except Exception as error: + result = {"case": name, "status": "failed", "reason": str(error)} + results.append(result) + print(json.dumps(result), flush=True) + (args.log_dir / "results.json").write_text(json.dumps(results, indent=2) + "\n") + return int(any(result["status"] == "failed" for result in results)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/build/agent_playbook_docs/package-lock.json b/build/agent_playbook_docs/package-lock.json new file mode 100644 index 0000000000..164f7ba3dd --- /dev/null +++ b/build/agent_playbook_docs/package-lock.json @@ -0,0 +1,917 @@ +{ + "name": "agent-playbook-docs-checks", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "agent-playbook-docs-checks", + "dependencies": { + "@openai/agents": "0.19.0", + "@redis-iris/agent-playbook": "0.1.0-alpha.2" + }, + "devDependencies": { + "@types/node": "22.19.7", + "tsx": "4.21.0", + "typescript": "5.9.3" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.3.1.tgz", + "integrity": "sha512-mIGZXpHsjnZ6lD+gD/WCMpR5k8yVQQ8nNFH1N0Srf7AvnwTUMYD6pvTY8ng2+He5FfV7YMZLmrjL7cLa/cc3dQ==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@modelcontextprotocol/core": "2.3.1", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.8", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.3.1.tgz", + "integrity": "sha512-laVmIhPGpWi7rG9q+0UigblRN0Hws/Yjlb6qLFtVqryXVLkc4h+yN8z731POD2LnAGQlFe7/tXhUm7Bi0YB91g==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@openai/agents": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@openai/agents/-/agents-0.19.0.tgz", + "integrity": "sha512-3kPyu2cmWT5IgIhgDVmEYZxhZUY+vcS7jKPWIk6OGdezGuYtsbNtsa9cpv2uKvSTKc9xe1Xq6l1DF31mRbmTzQ==", + "license": "MIT", + "dependencies": { + "@openai/agents-core": "0.19.0", + "@openai/agents-openai": "0.19.0", + "@openai/agents-realtime": "0.19.0", + "openai": "^7.2.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@openai/agents-core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@openai/agents-core/-/agents-core-0.19.0.tgz", + "integrity": "sha512-TCK6yEsTlpgCcKfQvqNWhWcWv56750tkDwlxLOY5rAujTIHWBoJ0eVrFoL1jxqcpxR3vAPt80qWJ2nCzRMvESQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "debug": "^4.4.3", + "openai": "^7.2.0" + }, + "optionalDependencies": { + "@modelcontextprotocol/client": "^2.0.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@openai/agents-openai": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@openai/agents-openai/-/agents-openai-0.19.0.tgz", + "integrity": "sha512-+TSSYjDOIJl34lqw+9nM310Xc6xHbhqnl1mMrUeYEJcEX4ATkzlfbT4nTsHCdmE7yAHuM1TXnoCW0FAzxdHOHQ==", + "license": "MIT", + "dependencies": { + "@openai/agents-core": "0.19.0", + "openai": "^7.2.0" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@openai/agents-realtime": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@openai/agents-realtime/-/agents-realtime-0.19.0.tgz", + "integrity": "sha512-XZgJWGagYWCNkpWtLieOYKTgampSo968tbrGlOWI0hnU5RowqyvfNGmpLfeUHAjDOZ+J7hBoWKQk2/DLcz6rmw==", + "license": "MIT", + "dependencies": { + "@openai/agents-core": "0.19.0", + "@types/ws": "^8.18.1", + "ws": "^8.21.3" + }, + "peerDependencies": { + "zod": "^4.0.0" + } + }, + "node_modules/@redis-iris/agent-playbook": { + "version": "0.1.0-alpha.2", + "resolved": "https://registry.npmjs.org/@redis-iris/agent-playbook/-/agent-playbook-0.1.0-alpha.2.tgz", + "integrity": "sha512-BvoWx3C0qDEgeWZmRjk5cXKc+Rio24JfRZLO5Lih6N+YtIqKPGysJcLbLt+kqYTw9SHGwBC+PTxziFjdZDtNTw==", + "license": "MIT", + "dependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.7.tgz", + "integrity": "sha512-MciR4AKGHWl7xwxkBa6xUGxQJ4VBOmPTF7sL+iGzuahOFaO0jHCsuEfS80pan1ef4gWId1oWOweIhrDEYLuaOw==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "optional": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "optional": true, + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.3", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.3.tgz", + "integrity": "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "optional": true + }, + "node_modules/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/openai": { + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-7.30.0.tgz", + "integrity": "sha512-tvvcPdhS56B/79o0mJS5h4JndAov5Sz78T7CFl4gJ+C9S3GoSqHNLO6lG/8kdK+/c13zWEI97Srf66ar0V242g==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "undici": ">=5 <9", + "ws": "^8.21.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "undici": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "optional": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/ws": { + "version": "8.22.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.22.0.tgz", + "integrity": "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/build/agent_playbook_docs/package.json b/build/agent_playbook_docs/package.json new file mode 100644 index 0000000000..cd809d19a4 --- /dev/null +++ b/build/agent_playbook_docs/package.json @@ -0,0 +1,14 @@ +{ + "name": "agent-playbook-docs-checks", + "private": true, + "type": "module", + "dependencies": { + "@openai/agents": "0.19.0", + "@redis-iris/agent-playbook": "0.1.0-alpha.2" + }, + "devDependencies": { + "@types/node": "22.19.7", + "tsx": "4.21.0", + "typescript": "5.9.3" + } +} diff --git a/build/agent_playbook_docs/prepare_reference.py b/build/agent_playbook_docs/prepare_reference.py new file mode 100644 index 0000000000..39cd14c32e --- /dev/null +++ b/build/agent_playbook_docs/prepare_reference.py @@ -0,0 +1,85 @@ +"""Add documentation labels to the public Playbooks data-plane OpenAPI contract. + +Request/response schemas, routes, security, and operation IDs are preserved. +Input must be the public Smithy projection, not an internal projection. +""" + +import argparse +from copy import deepcopy +import json +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[2] +GROUPS = { + "Skills": [ + ("CreateSkill", "Create a skill"), ("ListSkills", "List skills"), + ("GetSkill", "Get a skill"), ("AppendSkillVersion", "Append a skill version"), + ("ListSkillVersions", "List skill versions"), ("GetSkillVersion", "Get a skill version"), + ("PublishSkill", "Publish a skill version"), ("WithdrawSkill", "Withdraw a skill"), + ("GetSkillResource", "Read a skill resource"), + ("DeleteSkill", "Delete a skill"), ("DeleteSkills", "Delete multiple skills"), + ], + "FAQs": [ + ("CreateFaq", "Create an FAQ"), ("ListFaqs", "List FAQs"), + ("GetFaq", "Get an FAQ"), ("AppendFaqVersion", "Append an FAQ version"), + ("ListFaqVersions", "List FAQ versions"), ("GetFaqVersion", "Get an FAQ version"), + ("PublishFaq", "Publish an FAQ version"), ("WithdrawFaq", "Withdraw an FAQ"), + ("DeleteFaq", "Delete an FAQ"), ("DeleteFaqs", "Delete multiple FAQs"), + ], + "Retrieval": [ + ("DiscoverEntries", "Discover published entries"), + ("SearchFaqs", "Search published FAQs"), ("SearchSkills", "Search published skills"), + ("LookupEntries", "Look up entries"), + ], + "Proposals": [ + ("CreateProposal", "Create a proposal"), ("SubmitProposalEvidence", "Submit proposal evidence"), + ("ListProposals", "List proposals"), ("SearchProposals", "Search proposals"), + ("GetProposal", "Get a proposal"), ("ReplaceProposal", "Replace a proposal candidate"), + ("AcceptProposal", "Accept a reviewed proposal"), + ("DismissProposal", "Dismiss a proposal"), ("RestoreProposal", "Restore a proposal"), + ("PurgeProposal", "Purge a proposal"), + ], + "Maintenance": [ + ("GetPlaybookHealth", "Check playbook health"), ("ClearPlaybook", "Clear playbook content"), + ], +} + +def prepare(source, title, groups): + document = deepcopy(source) + labels = {op: (group, label) for group, entries in groups.items() for op, label in entries} + operations = { + operation["operationId"]: (path, method, operation) + for path, methods in document["paths"].items() + for method, operation in methods.items() if "operationId" in operation + } + if operations.keys() != labels.keys(): + raise ValueError("Public operations changed; review the documentation groups before regenerating.") + document["info"]["title"] = title + document["tags"] = [{"name": name} for name in groups] + # Preserve shared path properties while ordering methods by the task sequence. + ordered = {} + for op_id, (group, label) in labels.items(): + path, method, operation = operations[op_id] + operation["tags"] = [group] + operation["summary"] = label + if path not in ordered: + ordered[path] = {k: v for k, v in document["paths"][path].items() + if not isinstance(v, dict) or "operationId" not in v} + ordered[path][method] = operation + document["paths"] = ordered + return document + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--data-plane", type=Path, required=True) + args = parser.parse_args() + result = prepare( + json.loads(args.data_plane.read_text()), + "Redis Agent Playbooks data-plane API", + GROUPS, + ) + target = ROOT / "content/develop/ai/context-engine/agent-playbook/api-reference/openapi.json" + target.write_text(json.dumps(result, indent=2) + "\n") + print(target.relative_to(ROOT)) diff --git a/build/agent_playbook_docs/requirements.txt b/build/agent_playbook_docs/requirements.txt new file mode 100644 index 0000000000..2339da7674 --- /dev/null +++ b/build/agent_playbook_docs/requirements.txt @@ -0,0 +1,6 @@ +pytest==9.1.1 +openapi-spec-validator==0.9.0 +PyYAML==6.0.3 +redis-agent-playbook==0.1.0a2 +openai-agents==0.23.1 +mcp==1.30.0 diff --git a/build/agent_playbook_docs/source-lock.json b/build/agent_playbook_docs/source-lock.json new file mode 100644 index 0000000000..dce9825dc6 --- /dev/null +++ b/build/agent_playbook_docs/source-lock.json @@ -0,0 +1,12 @@ +{ + "sdk_repository": "git@github.com:redis/iris-sdks", + "sdk_revision": "5a8b901f2167016f272eaf138b3c82de9b07504f", + "python_package": "redis-agent-playbook==0.1.0a2", + "typescript_package": "@redis-iris/agent-playbook@0.1.0-alpha.2", + "sdk_generator_version": "0.1.0-alpha.2", + "sdk_contract_path": "openapi-playbook.json", + "data_plane_contract_sha256": "a0ec8f45ce3e3a9ae5455f51b49709ba318d6f6c5c25b7f4eaec8c2a2447243f", + "sdk_example_overlay_path": ".speakeasy/playbook/overlay-code-samples.yaml", + "sdk_example_overlay_sha256": "8fc077efef1e3f71d2eff43ddc7a9771d451ca58d58ab73af58c7e961850d4d8", + "service_revision": "5d06fa953f7cc961730f40d9ee019808d65a19d2" +} diff --git a/build/agent_playbook_docs/test_examples.py b/build/agent_playbook_docs/test_examples.py new file mode 100644 index 0000000000..9eba8b21c6 --- /dev/null +++ b/build/agent_playbook_docs/test_examples.py @@ -0,0 +1,716 @@ +"""Execute the displayed clients against a local, schema-checked HTTP fixture. + +This verifies client signatures, serialized requests, response handling, and tab +parity. MCP agent runs use a scripted local model response; the HTTP tab checks +hosted MCP configuration. No live model or deployed service is called. +""" + +import ast +import base64 +from copy import deepcopy +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from hashlib import sha256 +import io +import json +import os +from pathlib import Path +import re +import subprocess +import sys +import threading +from urllib.parse import parse_qs, urlsplit +from zipfile import ZipFile + +from jsonschema import Draft4Validator +import pytest +import yaml + +from examples import DOCS, FENCE, GUIDES, blocks, downloads, scripts, skill_package, standalone_python + +HERE = Path(__file__).parent +SPEC = json.loads((DOCS / 'api-reference/openapi.json').read_text()) +PB = 'pb_' + 'a' * 32 +NOW = '2026-10-07T12:00:00Z' +DIGEST = 'sha256:' + 'd' * 64 +RESOURCE = 'res_' + 'e' * 24 +FILE_TEXT = ( + 'Refund listed: Your order history shows "". Support can help if you need more information about that stage.\n\n' + 'Refund missing: Contact support with your order number and explain that no refund appears in your order history.\n' +) +TASK = ( + "Draft a customer reply using the refund-support-reply skill and template. " + "The customer says their refund stage is Processing." +) +DISCOVERY_QUERY = 'Draft a reply for a customer whose refund shows Processing' +READY_REPLY = 'Your order history shows "Processing". Support can help if you need more information about that stage.' + +def validate(value, schema): + Draft4Validator({**schema, 'components': SPEC['components']}).validate(value) + + +class ContractFixture: + def __init__(self, tool_error=False): + self.entries = {} + self.versions = {} + self.proposals = {} + self.calls = [] + self.errors = [] + self.mcp_methods = [] + self.model_requests = [] + self.tool_error = tool_error + self.omit_totals = False + self.resource_contents = {DIGEST: FILE_TEXT.encode()} + self.skill_id = self.create('skill', self.skill_content())['entry']['entryId'] + self.publish(self.skill_id, 1) + self.faq_id = self.create('faq', {'payload': {'faq': {'question': 'Sample question', 'answer': 'Sample answer'}}})['entry']['entryId'] + self.publish(self.faq_id, 1) + + def skill_content(self): + return {'intents': [], 'attributes': {}, 'payload': {'skill': { + 'format': 'agent-skills', + 'manifest': {'name': 'refund-support-reply', 'description': 'Draft a customer reply from a reported refund stage or a missing refund.'}, + 'instructions': 'Read references/reply-template.md, select the matching template, and preserve the reported stage.', + 'resources': [{ + 'resourceId': RESOURCE, 'uri': 'playbook://returned-version/reply-template.md', + 'path': 'references/reply-template.md', 'digest': DIGEST, + 'sizeBytes': len(FILE_TEXT), 'mimeType': 'text/markdown', 'executable': False, + }], + }}} + + def create(self, kind, content): + eid = 'ent_' + f'{len(self.entries) + 1:032x}' + self.entries[eid] = {'entryId': eid, 'entryType': kind, 'latestVersion': 0, 'createdAt': NOW, 'updatedAt': NOW} + self.versions[eid] = [] + return self.append(eid, content) + + def append(self, eid, content): + entry = self.entries[eid] + entry['latestVersion'] += 1 + version = {'entryId': eid, 'entryType': entry['entryType'], 'version': entry['latestVersion'], + 'createdAt': NOW, 'contentDigest': DIGEST, 'content': deepcopy(content)} + self.versions[eid].append(version) + return {'entry': deepcopy(entry), 'version': deepcopy(version)} + + def preview_content(self, content): + content = deepcopy(content) + if 'skill' in content['payload']: + skill = content['payload']['skill'] + content['payload']['skill'] = {**skill['manifest'], 'uri': 'playbook://returned-version/SKILL.md'} + return content + + def publish(self, eid, version): + self.entries[eid]['publication'] = { + 'version': version, 'at': NOW, + 'content': self.preview_content(self.versions[eid][version - 1]['content']), + } + return deepcopy(self.entries[eid]) + + def preview(self, eid, scored=False): + entry = self.entries[eid] + pub = entry['publication'] + result = {'entryId': eid, 'entryType': entry['entryType'], 'version': pub['version'], + 'intents': pub['content'].get('intents', []), **pub['content']['payload']} + if scored: + result['score'] = 0.95 + return result + + def parse_package(self, body): + with ZipFile(io.BytesIO(base64.b64decode(body['packageBase64']))) as archive: + assert set(n for n in archive.namelist() if not n.endswith('/')) == { + 'refund-support-reply/SKILL.md', 'refund-support-reply/references/reply-template.md', + } + source = archive.read('refund-support-reply/SKILL.md').decode() + manifest = yaml.safe_load(source.split('---', 2)[1]) + assert manifest['name'] == 'refund-support-reply' + assert manifest['description'] + template = archive.read('refund-support-reply/references/reply-template.md') + content = self.skill_content() + content.update(body.get('content', {})) + content['payload']['skill']['manifest'] = manifest + content['payload']['skill']['instructions'] = source.split('---', 2)[2].strip() + digest = 'sha256:' + sha256(template).hexdigest() + self.resource_contents[digest] = template + content['payload']['skill']['resources'][0].update(digest=digest, sizeBytes=len(template)) + return content + + def page(self, items, member, query, include_total=True): + # Exercise empty and short pages without allowing clients to stop early. + cursor = query.get('cursor') + if cursor is None: + result = {member: [], 'nextCursor': 'fixture+page/1='} + if include_total and not self.omit_totals: + result['total'] = len(items) + return result + if cursor == 'fixture+page/1=': + result = {member: deepcopy(items[:1])} + if len(items) > 1: + result['nextCursor'] = 'fixture+page/2=' + return result + assert cursor == 'fixture+page/2=', query + return {member: deepcopy(items[1:])} + + def proposal_preview(self, proposal): + preview = deepcopy(proposal) + preview.pop('evidence') + content = preview['action']['add']['candidate']['content'] + preview['action']['add']['candidate']['content'] = { + 'payload': {'faq': {'question': content['payload']['faq']['question']}}, + } + return preview + + def mcp(self, body): + method = body['method'] + self.mcp_methods.append(method) + if 'id' not in body: + return 202, None + if method == 'server/discover': + return 200, {'jsonrpc': '2.0', 'id': body['id'], + 'error': {'code': -32601, 'message': 'Method not found'}} + if method == 'initialize': + result = {'protocolVersion': body['params']['protocolVersion'], 'capabilities': {'tools': {}}, + 'serverInfo': {'name': 'docs-contract-fixture', 'version': '1'}} + elif method == 'tools/list': + schemas = { + 'discover_entries': {'query': {'type': 'string'}}, + 'load_skill': {'entry_id': {'type': 'string'}, 'version': {'type': 'integer'}}, + 'fetch_resource': {'uri': {'type': 'string'}}, + } + result = {'tools': [{ + 'name': name, + 'inputSchema': {'type': 'object', 'properties': properties, + 'required': list(properties), 'additionalProperties': False}, + } for name, properties in schemas.items()]} + else: + assert method == 'tools/call' + name, args = body['params']['name'], body['params']['arguments'] + self.calls.append((name, args)) + if self.tool_error: + result = {'isError': True, 'content': [{'type': 'text', 'text': 'fixture tool failure'}]} + else: + if name == 'discover_entries': + assert args == {'query': DISCOVERY_QUERY} + data = {'pinnedEntries': [], 'discoveredEntries': [self.preview(self.skill_id, True)], 'truncated': False} + elif name == 'load_skill': + assert args == {'entry_id': self.skill_id, 'version': 1} + data = {'entryId': self.skill_id, 'version': 1, 'content': self.skill_content()['payload']['skill']} + else: + assert name == 'fetch_resource' + assert args == {'uri': self.skill_content()['payload']['skill']['resources'][0]['uri']} + data = {'uri': args['uri'], 'mimeType': 'text/markdown', 'digest': DIGEST, 'text': FILE_TEXT} + result = {'isError': False, 'content': [{'type': 'text', 'text': json.dumps(data)}], 'structuredContent': data} + return 200, {'jsonrpc': '2.0', 'id': body['id'], 'result': result} + + def model_response(self, body): + """Script tool choices while the real Agents SDK owns the MCP connection.""" + self.model_requests.append(deepcopy(body)) + assert body['model'] == 'gpt-5-mini' + assert 'Use Agent Playbooks' in body['instructions'] + if body['tools'][0]['type'] == 'mcp': + tool = body['tools'][0] + assert tool['server_label'] == 'playbook' + assert tool['headers'] == {'Authorization': 'Bearer reader-test-key'} + assert tool['allowed_tools'] == ['discover_entries', 'load_skill', 'fetch_resource'] + assert tool['require_approval'] == 'never' + assert body['input'] == TASK + output = [{ + 'type': 'mcp_call', 'id': f'mcp_fixture_{index}', 'server_label': 'playbook', + 'name': name, 'arguments': json.dumps(arguments), 'output': '{}', 'error': None, + } for index, (name, arguments) in enumerate(( + ('discover_entries', {'query': DISCOVERY_QUERY}), + ('load_skill', {'entry_id': self.skill_id, 'version': 1}), + ('fetch_resource', {'uri': self.skill_content()['payload']['skill']['resources'][0]['uri']}), + ))] + [self.model_message(READY_REPLY)] + else: + assert 'reader-test-key' not in json.dumps(body) + assert {tool['name'] for tool in body['tools']} == {'discover_entries', 'load_skill', 'fetch_resource'} + results = [item for item in body['input'] if item.get('type') == 'function_call_output'] + data = self.tool_output(results[-1]['output']) if results else None + if results and self.tool_error: + assert 'error' in json.dumps(data).lower() or 'failure' in json.dumps(data).lower() + output = self.model_message('Unable to retrieve playbook guidance: tool failed.') + elif not results: + output = self.model_call('discover_entries', {'query': DISCOVERY_QUERY}) + elif len(results) == 1: + preview = data['discoveredEntries'][0] + output = self.model_call('load_skill', {'entry_id': preview['entryId'], 'version': preview['version']}) + elif len(results) == 2: + resource = data['content']['resources'][0] + output = self.model_call('fetch_resource', {'uri': resource['uri']}) + else: + assert len(results) == 3 + assert '' in data['text'] + output = self.model_message(READY_REPLY) + return 200, { + 'id': f'resp_fixture_{len(self.model_requests)}', 'object': 'response', + 'created_at': 0, 'status': 'completed', 'model': body['model'], + 'output': output if isinstance(output, list) else [output], + 'usage': {'input_tokens': 10, 'output_tokens': 10, 'total_tokens': 20, + 'input_tokens_details': {'cached_tokens': 0}, 'output_tokens_details': {'reasoning_tokens': 0}}, + } + + def model_call(self, name, arguments): + return {'type': 'function_call', 'id': f'fc_{len(self.model_requests)}', + 'call_id': f'call_{len(self.model_requests)}', 'name': name, + 'arguments': json.dumps(arguments), 'status': 'completed'} + + def model_message(self, text): + return {'type': 'message', 'id': f'msg_{len(self.model_requests)}', 'role': 'assistant', + 'status': 'completed', 'content': [{'type': 'output_text', 'text': text, 'annotations': []}]} + + @staticmethod + def tool_output(value): + """Unwrap the text content blocks returned by either agent SDK.""" + while True: + if isinstance(value, str): + try: + value = json.loads(value) + except json.JSONDecodeError: + return value + elif isinstance(value, list): + assert len(value) == 1, value + value = value[0] + elif isinstance(value, dict) and value.get('type') in ('text', 'input_text'): + value = value['text'] + else: + return value + + def request(self, method, path, body): + if path == '/v1/responses': + assert method == 'POST' + return self.model_response(body) + if path.endswith('/mcp'): + return self.mcp(body) if method == 'POST' else (405, None) + url = urlsplit(path) + path = url.path + parsed_query = parse_qs(url.query, keep_blank_values=True) + assert all(len(values) == 1 for values in parsed_query.values()) + query = {key: values[0] for key, values in parsed_query.items()} + for route, methods in SPEC['paths'].items(): + match = re.fullmatch(re.sub(r'\{[^}]+\}', '([^/]+)', route), path) + if match and method.lower() in methods: + operation = methods[method.lower()] + params = dict(zip(re.findall(r'\{([^}]+)\}', route), match.groups())) + break + else: + raise AssertionError(f'Unknown operation: {method} {path}') + assert params['playbookId'] == PB + for parameter in operation.get('parameters', []): + values = params if parameter['in'] == 'path' else query + if parameter.get('required'): + assert parameter['name'] in values, parameter + if parameter['name'] not in values: + continue + value = values[parameter['name']] + schema = parameter['schema'] + if schema.get('type') == 'integer': + value = int(value) + elif schema.get('type') == 'boolean': + assert value in ('true', 'false') + value = value == 'true' + validate(value, schema) + if body is not None: + validate(body, operation['requestBody']['content']['application/json']['schema']) + name = operation['operationId'] + self.calls.append((name, {'query': query} if query else deepcopy(body))) + eid = params.get('faqId', params.get('skillId')) + pid = params.get('proposalId') + code = 200 + if name == 'GetPlaybookHealth': + result = {key: 'ready' for key in ('status', 'metadataRedis', 'contentRedis', 'inference', 'license')} + result['playbookId'] = PB + elif name in ('CreateFaq', 'CreateSkill'): + result = self.create('faq' if name == 'CreateFaq' else 'skill', body['content'] if name == 'CreateFaq' else self.parse_package(body)) + if 'entryKey' in body: + self.entries[result['entry']['entryId']]['entryKey'] = body['entryKey'] + result['entry']['entryKey'] = body['entryKey'] + code = 201 + elif name in ('AppendFaqVersion', 'AppendSkillVersion'): + result = self.append(eid, body['content'] if name == 'AppendFaqVersion' else self.parse_package(body)) + code = 201 + elif name in ('PublishFaq', 'PublishSkill'): + result = self.publish(eid, body['version']) + elif name in ('GetFaqVersion', 'GetSkillVersion'): + result = deepcopy(self.versions[eid][int(params['version']) - 1]) + elif name == 'GetSkillResource': + assert params['resourceId'] == RESOURCE + version = self.versions[eid][int(params['version']) - 1] + resource = version['content']['payload']['skill']['resources'][0] + return 200, self.resource_contents[resource['digest']] + elif name in ('GetFaq', 'GetSkill'): + result = self.entries[eid] + elif name in ('ListFaqs', 'ListSkills'): + kind = 'faq' if name == 'ListFaqs' else 'skill' + assert query.get('sortBy') == 'updatedAt' and query.get('sortOrder') == 'desc' + assert query.get('limit') == '50' + entries = [entry for entry in self.entries.values() if entry['entryType'] == kind] + result = self.page(entries, 'faqs' if kind == 'faq' else 'skills', query) + elif name in ('ListFaqVersions', 'ListSkillVersions'): + assert query.get('limit') == '50' + result = self.page(list(reversed(self.versions[eid])), 'versions', query, include_total=False) + elif name == 'LookupEntries': + results = [] + for reference in body['references']: + entry = self.entries.get(reference.get('entryId')) + if 'entryKey' in reference: + entry = next((e for e in self.entries.values() if e.get('entryKey') == reference['entryKey']), None) + if entry is None: + results.append({'missing': {'reference': reference}}) + continue + latest = self.versions[entry['entryId']][-1] + state = {'entryId': entry['entryId'], 'entryType': entry['entryType'], + 'latest': {'version': latest['version'], 'contentDigest': latest['contentDigest']}, + 'versionCount': len(self.versions[entry['entryId']])} + if 'entryKey' in entry: + state['entryKey'] = entry['entryKey'] + if 'publication' in entry: + version = self.versions[entry['entryId']][entry['publication']['version'] - 1] + state['publication'] = {'version': version['version'], 'contentDigest': version['contentDigest']} + results.append({'found': {'reference': reference, 'entry': state}}) + result = {'results': results, 'maxVersions': 100} + elif name in ('WithdrawFaq', 'WithdrawSkill'): + self.entries[eid].pop('publication', None) + result = self.entries[eid] + elif name in ('DeleteFaq', 'DeleteSkill'): + del self.entries[eid] + result, code = None, 204 + elif name in ('SearchFaqs', 'SearchSkills', 'DiscoverEntries'): + ids = [key for key, e in self.entries.items() if 'publication' in e] + if name != 'DiscoverEntries': + kind = 'faq' if name == 'SearchFaqs' else 'skill' + ids = [key for key in ids if self.entries[key]['entryType'] == kind] + result = {'matches': [self.preview(key, True) for key in ids]} + else: + pins = [p['entryId'] for p in body.get('pinnedEntries', [])] + result = {'pinnedEntries': [self.preview(key) for key in pins], + 'discoveredEntries': [self.preview(key, True) for key in ids if key not in pins], 'truncated': False} + elif name == 'CreateProposal': + pid = 'prp_' + f'{len(self.proposals) + 1:032x}' + result = {'proposalId': pid, 'status': 'open', 'contentDigest': DIGEST, 'proposalRevision': 1, + 'evidenceCount': 0, 'createdAt': NOW, 'updatedAt': NOW, 'inboxExpiresAt': NOW, + 'purgeAt': NOW, 'action': body['action'], 'evidence': []} + self.proposals[pid] = deepcopy(result) + code = 201 + elif name == 'GetProposal': + result = deepcopy(self.proposals[pid]) + elif name == 'ListProposals': + assert query.get('sortBy') == 'updatedAt' and query.get('sortOrder') == 'desc' + assert query.get('limit') == '50' + statuses = ('open', 'promoting') if query['view'] == 'inbox' else ('trashed', 'promoted') + items = [self.proposal_preview(p) for p in self.proposals.values() if p['status'] in statuses] + result = self.page(items, 'proposals', query) + elif name == 'SearchProposals': + result = {'matches': [{'proposal': self.proposal_preview(p), 'score': 0.9} + for p in self.proposals.values() if p['status'] == 'open']} + elif name == 'SubmitProposalEvidence': + evidence = {**body, 'evidenceId': params['evidenceId']} + self.proposals[pid]['evidence'].append(evidence) + self.proposals[pid]['evidenceCount'] += 1 + result = {'evidenceId': params['evidenceId'], 'disposition': 'added', 'evidenceCount': 1, 'updatedAt': NOW} + elif name in ('ReplaceProposal', 'DismissProposal', 'RestoreProposal'): + p = self.proposals[pid] + if name != 'RestoreProposal': + assert body['expectedProposalRevision'] == p['proposalRevision'] + if name == 'ReplaceProposal': + p['action'] = body['action'] + p['proposalRevision'] += 1 + p['status'] = 'trashed' if name == 'DismissProposal' else 'open' + result = deepcopy(p) + elif name == 'AcceptProposal': + p = self.proposals[pid] + assert p['status'] == 'open' + assert body['expectedProposalRevision'] == p['proposalRevision'] + result = self.create('faq', p['action']['add']['candidate']['content']) + p['status'] = 'promoted' + else: + raise AssertionError(name) + if result is not None: + code = next(int(status) for status in operation['responses'] if status.startswith('2')) + validate(result, operation['responses'][str(code)]['content']['application/json']['schema']) + return code, result + + +@pytest.fixture +def fixture_server(): + state = ContractFixture() + + class Handler(BaseHTTPRequestHandler): + def log_message(self, *args): + pass + + def handle_request(self): + try: + if self.path == '/v1/responses': + assert self.headers.get('Authorization') == 'Bearer model-test-key' + else: + assert self.headers.get('Authorization') in ('Bearer author-test-key', 'Bearer reader-test-key') + raw = self.rfile.read(int(self.headers.get('Content-Length', '0'))) + body = json.loads(raw) if raw else None + code, result = state.request(self.command, self.path, body) + except Exception as error: + state.errors.append(error) + code, result = 500, {'error': str(error)} + content = result if isinstance(result, bytes) else json.dumps(result).encode() if result is not None else b'' + self.send_response(code) + self.send_header('Content-Type', 'text/markdown' if isinstance(result, bytes) else 'application/json') + self.send_header('Content-Length', str(len(content))) + if isinstance(result, bytes) and code == 200: + self.send_header('Content-Digest', 'sha-256=:' + base64.b64encode(sha256(content).digest()).decode() + ':') + self.end_headers() + self.wfile.write(content) + + do_GET = do_POST = do_PUT = do_PATCH = do_DELETE = handle_request + + server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + env = {**os.environ, 'PLAYBOOK_URL': f'http://127.0.0.1:{server.server_port}', 'PLAYBOOK_ID': PB, + 'PLAYBOOK_API_KEY': 'author-test-key', + 'PLAYBOOK_SKILL_ID': state.skill_id, 'PLAYBOOK_FAQ_ID': state.faq_id, + 'OPENAI_API_KEY': 'model-test-key', 'OPENAI_BASE_URL': f'http://127.0.0.1:{server.server_port}/v1', + 'OPENAI_AGENTS_DISABLE_TRACING': '1'} + yield state, env + server.shutdown() + server.server_close() + thread.join() + + +def package_sample(directory): + source = skill_package() + subprocess.run(['bash', '-euc', source], cwd=directory, check=True, capture_output=True) + + +def execute(page, suffix, directory, env, source=None): + if source is None: + source = scripts(page)[suffix] + path = directory / f'{page}_example.{suffix}' + path.write_text(source) + if suffix == 'py': + ast.parse(source) + command = [sys.executable, str(path)] + elif suffix == 'mts': + (directory / 'node_modules').symlink_to((HERE / 'node_modules').resolve(), target_is_directory=True) + command = [str((HERE / 'node_modules/.bin/tsx').resolve()), str(path)] + else: + command = ['bash', str(path)] + package_sample(directory) + return subprocess.run(command, env=env, cwd=directory, text=True, capture_output=True, timeout=30) + + +def normalized(calls): + calls = deepcopy(calls) + for name, body in calls: + if isinstance(body, dict): + body.pop('observedAt', None) + if 'packageBase64' in body: + with ZipFile(io.BytesIO(base64.b64decode(body.pop('packageBase64')))) as archive: + body['files'] = {n: archive.read(n).decode() for n in sorted(archive.namelist()) if not n.endswith('/')} + return calls + + +@pytest.mark.parametrize('page,omit_totals', + [(guide, False) for guide in GUIDES if guide != 'mcp'] + + [(guide, True) for guide in ('faqs', 'skills', 'proposals')]) +def test_displayed_tabs_execute_and_send_equivalent_requests(page, omit_totals, fixture_server, tmp_path): + state, env = fixture_server + traces = [] + for suffix in ('py', 'mts', 'sh'): + fresh = ContractFixture() + fresh.omit_totals = omit_totals + if page == 'proposals': + for index, status in enumerate(('open', 'open', 'promoted')): + _, proposal = fresh.request('POST', f'/v1/playbooks/{PB}/proposals', {'action': {'add': { + 'target': {'newEntry': {}}, 'candidate': {'content': {'payload': {'faq': { + 'question': f'Sample review question {index}', 'answer': 'Sample review answer', + }}}}, + }}}) + fresh.proposals[proposal['proposalId']]['status'] = status + fresh.calls.clear() + state.__dict__.update(fresh.__dict__) + directory = tmp_path / suffix + directory.mkdir() + result = execute(page, suffix, directory, env) + assert not state.errors, state.errors + assert result.returncode == 0, result.stdout + result.stderr + if page == 'skills': + created = next(body for name, body in state.calls if name == 'CreateSkill') + appended = next(body for name, body in state.calls if name == 'AppendSkillVersion') + assert 'channel' not in created['content']['attributes'] + assert appended['content']['attributes'] == {**created['content']['attributes'], 'channel': 'chat'} + with ZipFile(io.BytesIO(base64.b64decode(created['packageBase64']))) as archive: + assert archive.read('refund-support-reply/references/reply-template.md').decode().strip() in result.stdout + if page in ('faqs', 'skills', 'proposals'): + list_calls = {} + for name, request in state.calls: + if name.startswith('List'): + query = request['query'] + list_calls.setdefault((name, query.get('view')), []).append(query.get('cursor')) + assert list_calls + for cursors in list_calls.values(): + assert cursors == [None, 'fixture+page/1=', 'fixture+page/2='], list_calls + if omit_totals: + assert 'unknown' in result.stdout + traces.append(normalized(state.calls)) + assert traces[0] == traces[1] == traces[2] + + +@pytest.mark.parametrize('page', ('quickstart', 'skills')) +@pytest.mark.parametrize('suffix', ('py', 'mts', 'sh')) +def test_downloaded_walkthrough_retains_published_sample(page, suffix, fixture_server, tmp_path): + state, env = fixture_server + path = DOCS / page / f'{page}.{suffix}' + assert path in downloads() + if page == 'skills' and suffix != 'sh': + prepared = subprocess.run(['bash', str(DOCS / 'skills/prepare_skill.sh')], + cwd=tmp_path, text=True, capture_output=True) + assert prepared.returncode == 0, prepared.stdout + prepared.stderr + result = execute(page, suffix, tmp_path, env, source=path.read_text()) + assert not state.errors, state.errors + assert result.returncode == 0, result.stdout + result.stderr + assert not any(name.startswith(('Delete', 'Withdraw')) for name, _ in state.calls) + kind = 'faq' if page == 'quickstart' else 'skill' + sample = next(entry for entry in state.entries.values() + if entry['entryType'] == kind and entry['entryId'] not in (state.faq_id, state.skill_id)) + assert sample['publication']['version'] == (2 if page == 'quickstart' else 1) + assert sample['entryId'] in result.stdout + if page == 'quickstart': + assert state.calls[-1][0] == 'SearchFaqs' + final_answer = result.stdout.rsplit('Published answer:', 1)[-1] if suffix != 'sh' else result.stdout.rsplit('"answer":', 1)[-1] + assert 'cannot guarantee a bank posting date' in final_answer + + +@pytest.mark.parametrize('suffix', ('py', 'mts', 'sh')) +def test_proposal_edit_preserves_the_candidate_just_reviewed(suffix, fixture_server, tmp_path, monkeypatch): + state, env = fixture_server + original_request = state.request + changed = False + + def another_reviewer(method, path, body): + nonlocal changed + if method == 'GET' and re.fullmatch(r'/v1/playbooks/[^/]+/proposals/prp_[0-9a-f]+', path) and not changed: + proposal = state.proposals[path.rsplit('/', 1)[1]] + content = proposal['action']['add']['candidate']['content'] + content['metadata'] = {'policyRevision': 'reviewed-update'} + content['payload']['faq']['answer'] += ' Include the support case number if you have one.' + proposal['proposalRevision'] += 1 + changed = True + return original_request(method, path, body) + + monkeypatch.setattr(state, 'request', another_reviewer) + result = execute('proposals', suffix, tmp_path, env) + assert not state.errors, state.errors + assert result.returncode == 0, result.stdout + result.stderr + replacement = next(body for name, body in state.calls if name == 'ReplaceProposal') + content = replacement['action']['add']['candidate']['content'] + assert replacement['expectedProposalRevision'] == 2 + assert content['metadata'] == {'policyRevision': 'reviewed-update'} + assert content['payload']['faq']['answer'].endswith( + ' Include the support case number if you have one. Do not send full card details.' + ) + + +@pytest.mark.parametrize('suffix', ('py', 'mts', 'sh')) +@pytest.mark.parametrize('json_error', (True, False)) +def test_error_example_reports_failure_and_exits(suffix, json_error, fixture_server, tmp_path, monkeypatch): + state, env = fixture_server + original_request = state.request + status = 404 if json_error else 502 + problem = {'status': 404, 'title': 'Not found', 'type': '/errors/resource-not-found'} + validate(problem, {'$ref': '#/components/schemas/NotFoundErrorResponseContent'}) + + def failed_read(method, path, body): + if path.endswith('/faqs/' + state.faq_id): + state.calls.append(('GetFaq', None)) + return status, problem if json_error else b'Upstream unavailable' + return original_request(method, path, body) + + monkeypatch.setattr(state, 'request', failed_read) + result = execute('limits', suffix, tmp_path, env) + assert not state.errors, state.errors + assert result.returncode != 0 + output = result.stdout + result.stderr + assert 'Request failed:' in output and str(status) in output + assert ('/errors/resource-not-found' if json_error else 'unknown') in output + assert [name for name, _ in state.calls] == ['GetPlaybookHealth', 'GetFaq'] + + +@pytest.mark.parametrize('suffix', ('py', 'mts', 'sh')) +@pytest.mark.parametrize('missing', (False, True)) +def test_lookup_example_handles_found_and_missing_references(suffix, missing, fixture_server, tmp_path): + state, env = fixture_server + if missing: + env['PLAYBOOK_FAQ_ID'] = 'ent_' + '0' * 32 + else: + state.entries[state.faq_id]['entryKey'] = 'refund-status' + index = ('py', 'mts', 'sh').index(suffix) + setup = next(code for name, code in blocks('quickstart') if name == 'playbook-connect')[index] + lookup = next(code for name, code in blocks('faqs') if name == 'playbook-faq-lookup')[index] + binding = ( + 'faq_id = os.environ["PLAYBOOK_FAQ_ID"]', + 'const faqId = requiredEnv("PLAYBOOK_FAQ_ID");', + 'FAQ_ID="$PLAYBOOK_FAQ_ID"', + )[index] + source = '\n\n'.join((setup, binding, lookup)) + result = execute('lookup', suffix, tmp_path, env, source=source) + assert not state.errors, state.errors + assert result.returncode == 0, result.stdout + result.stderr + assert ('missing' in result.stdout.lower()) == missing + assert [name for name, _ in state.calls] == ['GetPlaybookHealth', 'LookupEntries'] + assert state.calls[-1][1]['references'] == [ + {'entryId': env['PLAYBOOK_FAQ_ID']}, {'entryKey': 'refund-status'}, + ] + + +def test_displayed_async_python_example(fixture_server, tmp_path): + state, env = fixture_server + env['PLAYBOOK_API_KEY'] = 'reader-test-key' + source = standalone_python('quickstart', 'Use Python asynchronously') + result = execute('async_search', 'py', tmp_path, env, source=source) + assert not state.errors, state.errors + assert result.returncode == 0, result.stdout + result.stderr + assert 'Sample answer' in result.stdout + assert state.calls == [('SearchFaqs', { + 'query': 'Where can I see how my refund is progressing?', 'limit': 5, + })] + + +@pytest.mark.parametrize('suffix', ('py', 'mts')) +def test_mcp_agent_uses_server_tools(suffix, fixture_server, tmp_path): + state, env = fixture_server + env['PLAYBOOK_API_KEY'] = 'reader-test-key' + result = execute('mcp', suffix, tmp_path, env) + assert not state.errors, state.errors + assert result.returncode == 0, result.stdout + result.stderr + assert READY_REPLY in result.stdout + assert '' not in result.stdout + for name in ('discover_entries', 'load_skill', 'fetch_resource'): + assert 'Tool: ' + name in result.stdout + assert [name for name, _ in state.calls] == ['discover_entries', 'load_skill', 'fetch_resource'] + assert 'initialize' in state.mcp_methods and 'tools/list' in state.mcp_methods + assert len(state.model_requests) == 4 + + +@pytest.mark.parametrize('suffix', ('py', 'mts')) +def test_mcp_agent_reports_tool_error(suffix, fixture_server, tmp_path): + state, env = fixture_server + env['PLAYBOOK_API_KEY'] = 'reader-test-key' + state.tool_error = True + result = execute('mcp', suffix, tmp_path, env) + assert not state.errors, state.errors + assert result.returncode == 0, result.stdout + result.stderr + assert 'tool failed' in result.stdout + assert READY_REPLY not in result.stdout + assert [name for name, _ in state.calls] == ['discover_entries'] + + +def test_hosted_mcp_request_configures_service_connection(fixture_server, tmp_path): + state, env = fixture_server + env['PLAYBOOK_API_KEY'] = 'reader-test-key' + result = execute('mcp', 'sh', tmp_path, env) + assert not state.errors, state.errors + assert result.returncode == 0, result.stdout + result.stderr + assert READY_REPLY in result.stdout + for name in ('discover_entries', 'load_skill', 'fetch_resource'): + assert 'Tool: ' + name in result.stdout + assert len(state.model_requests) == 1 + tool = state.model_requests[0]['tools'][0] + assert tool['server_url'] == f'{env["PLAYBOOK_URL"]}/v1/playbooks/{PB}/mcp' diff --git a/build/agent_playbook_docs/test_reference.py b/build/agent_playbook_docs/test_reference.py new file mode 100644 index 0000000000..a382a21bbb --- /dev/null +++ b/build/agent_playbook_docs/test_reference.py @@ -0,0 +1,37 @@ +"""Check the public contract and documentation links without a running service.""" + +import json +from pathlib import Path +import re + +from openapi_spec_validator import validate + +from check_sdk_source import check +from examples import downloads + + +ROOT = Path(__file__).resolve().parents[2] +DOCS = ROOT / "content/develop/ai/context-engine/agent-playbook" + + +def test_public_openapi_is_valid(): + validate(json.loads((DOCS / "api-reference/openapi.json").read_text())) + + +def test_relrefs_resolve(): + for path in DOCS.rglob("*.md"): + source = path.read_text() + for target in re.findall(r'\{\{< relref "([^"]+)" >\}\}', source): + page = ROOT / "content" / target.split("#", 1)[0].lstrip("/") + assert any(candidate.exists() for candidate in [ + page.with_suffix(".md"), page / "_index.md", page / "index.md", + ]), (path, target) + + +def test_source_lock(): + check() + + +def test_downloads_match_displayed_examples(): + for path, source in downloads().items(): + assert path.read_text() == source, f"Refresh {path} with examples.py --refresh-downloads" diff --git a/build/agent_playbook_docs/tsconfig.json b/build/agent_playbook_docs/tsconfig.json new file mode 100644 index 0000000000..872b28cc88 --- /dev/null +++ b/build/agent_playbook_docs/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "types": ["node"] + }, + "include": [".generated/*_example.mts", ".generated/*_download.mts"] +} diff --git a/content/develop/ai/context-engine/_index.md b/content/develop/ai/context-engine/_index.md index f00075d8a4..f585aecaf8 100644 --- a/content/develop/ai/context-engine/_index.md +++ b/content/develop/ai/context-engine/_index.md @@ -23,6 +23,8 @@ Redis Iris eliminates the infrastructure burden of building context-aware AI age {{< tile-card color="bg-rose-300" title="Context Retriever" description="Governed, schema-first data access tools for agents" url="/develop/ai/context-engine/context-retriever" >}} +Explore [Agent Playbooks]({{< relref "/develop/ai/context-engine/agent-playbook" >}}) to publish approved answers and procedures for agents. + ## What is Redis Iris? Redis Iris is a production-ready context engine for AI agents that: diff --git a/content/develop/ai/context-engine/agent-playbook/_index.md b/content/develop/ai/context-engine/agent-playbook/_index.md new file mode 100644 index 0000000000..27fbaf8403 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/_index.md @@ -0,0 +1,47 @@ +--- +Title: Redis Agent Playbooks +alwaysopen: false +categories: +- docs +- develop +- ai +description: Publish reusable skills and answers that agents can discover and load for a task. +linkTitle: Agent Playbooks +hideListLinks: true +weight: 25 +bannerText: Documentation review draft. +bannerChildren: true +--- + +Redis Agent Playbooks lets agents share curated answers and procedures without bundling them into each application. Publish a version, retrieve it for a task, and prepare revisions while agents use the current publication. + +A FAQ gives an agent a validated answer, such as where a customer can find their refund status. A skill ([Agent Skill](https://agentskills.io/home)) guides the agent through drafting a reply from that status using a template. + +## Choose what to publish + +| Entry | Use it for | What the agent receives | +| ----- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------- | +| Skill | Procedures for completing a task, with optional scripts, references, or templates. | A name and description first; full instructions and files on demand. | +| FAQ | A validated answer to a recurring question. | The question and answer in the retrieval result. | + +A **playbook** is a registry of entries with its own access grants and retrieval settings. A publisher selects the version of each **entry** agents receive. Your application controls how retrieved content enters the model's context and which actions the agent can take. + +See [Agent Playbooks concepts]({{< relref "/develop/ai/context-engine/agent-playbook/concepts" >}}) for versions, publication, and review. + +## Get started + +1. Set up a playbook instance. Operator guides are coming soon. +2. [Publish and retrieve your first FAQ]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart" >}}). + +## Manage playbook entries + +- [Manage FAQs]({{< relref "/develop/ai/context-engine/agent-playbook/faqs" >}}): write, publish, and update FAQs. +- [Manage skills]({{< relref "/develop/ai/context-engine/agent-playbook/skills" >}}): package Agent Skills and load their instructions and files on demand. +- [Review proposals]({{< relref "/develop/ai/context-engine/agent-playbook/proposals" >}}): check proposed entries and accumulated evidence before accepting them. + +## Serve playbook entries at runtime + +- [Retrieve relevant FAQs or Agent Skills]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval" >}}) on demand. +- [Connect with MCP]({{< relref "/develop/ai/context-engine/agent-playbook/mcp" >}}). + +Use the [API reference]({{< relref "/develop/ai/context-engine/agent-playbook/api-reference" >}}) for operations and schemas, and [service limits and errors]({{< relref "/develop/ai/context-engine/agent-playbook/limits" >}}) for constraints and recovery guidance. diff --git a/content/develop/ai/context-engine/agent-playbook/api-reference.md b/content/develop/ai/context-engine/agent-playbook/api-reference.md new file mode 100644 index 0000000000..41d057ae6d --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/api-reference.md @@ -0,0 +1,18 @@ +--- +Title: Redis Agent Playbooks data-plane API +linkTitle: API reference +layout: apireference +type: page +weight: 80 +params: + sourcefile: ./openapi.json + sortOperationsAlphabetically: false + schemaDefinitionsTagName: Data models + showPageContent: true +--- + +Use the Agent Playbooks data-plane API to author, publish, and retrieve FAQs and skills, and review proposals. FAQs and skills have separate authoring operations. + +[Download the OpenAPI contract]({{< relref "/develop/ai/context-engine/agent-playbook/api-reference/" >}}openapi.json) to generate clients or inspect schemas. + +See [Agent Playbooks concepts]({{< relref "/develop/ai/context-engine/agent-playbook/concepts" >}}) for entries, versions, and permissions, [Retrieve knowledge]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval" >}}) for search and discovery, and [service limits and errors]({{< relref "/develop/ai/context-engine/agent-playbook/limits" >}}) for constraints and recovery. diff --git a/content/develop/ai/context-engine/agent-playbook/api-reference/openapi.json b/content/develop/ai/context-engine/agent-playbook/api-reference/openapi.json new file mode 100644 index 0000000000..2f471a8ed5 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/api-reference/openapi.json @@ -0,0 +1,9585 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Redis Agent Playbooks data-plane API", + "version": "1.0.0", + "description": "Public type-specific FAQ and Skill authoring, publication, and exact-version API." + }, + "paths": { + "/v1/playbooks/{playbookId}/skills": { + "post": { + "operationId": "CreateSkill", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSkillRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "CreateSkill 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSkillResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "Create a skill" + }, + "get": { + "operationId": "ListSkills", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "sortBy", + "in": "query", + "description": "Default updatedAt descending, then entry ID ascending.", + "schema": { + "$ref": "#/components/schemas/EntrySort" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Defaults to desc. ID ties are always asc.", + "schema": { + "$ref": "#/components/schemas/SortOrder" + } + }, + { + "name": "createdAfter", + "in": "query", + "description": "Exclusive bound at microsecond precision.", + "schema": { + "type": "string", + "description": "Exclusive bound at microsecond precision.", + "format": "date-time" + } + }, + { + "name": "createdBefore", + "in": "query", + "description": "Exclusive bound at microsecond precision.", + "schema": { + "type": "string", + "description": "Exclusive bound at microsecond precision.", + "format": "date-time" + } + }, + { + "name": "published", + "in": "query", + "description": "Selects publication presence.", + "schema": { + "type": "boolean", + "description": "Selects publication presence." + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ListSkills 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSkillsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "List skills" + }, + "delete": { + "description": "Deletes the requested Skill families sequentially. Each family deletion is atomic, but the batch is not: deletions completed before an error remain applied. Retrying the same request is safe because already absent entries are ignored.", + "operationId": "DeleteSkills", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteSkillsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteSkills 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteSkillsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "Delete multiple skills" + } + }, + "/v1/playbooks/{playbookId}/skills/{skillId}": { + "get": { + "operationId": "GetSkill", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "skillId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetSkill 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSkillResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "Get a skill" + }, + "delete": { + "operationId": "DeleteSkill", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "skillId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "204": { + "description": "DeleteSkill 204 response" + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "Delete a skill" + } + }, + "/v1/playbooks/{playbookId}/skills/{skillId}/versions": { + "post": { + "operationId": "AppendSkillVersion", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppendSkillVersionRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "skillId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "AppendSkillVersion 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppendSkillVersionResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "Append a skill version" + }, + "get": { + "operationId": "ListSkillVersions", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "skillId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + }, + { + "name": "createdAfter", + "in": "query", + "description": "UTC timestamp resolved to microsecond precision.", + "schema": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + } + }, + { + "name": "createdBefore", + "in": "query", + "description": "UTC timestamp resolved to microsecond precision.", + "schema": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ListSkillVersions 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListSkillVersionsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "List skill versions" + } + }, + "/v1/playbooks/{playbookId}/skills/{skillId}/versions/{version}": { + "get": { + "operationId": "GetSkillVersion", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "skillId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + }, + { + "name": "version", + "in": "path", + "schema": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetSkillVersion 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetSkillVersionResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "Get a skill version" + } + }, + "/v1/playbooks/{playbookId}/skills/{skillId}/publication": { + "put": { + "description": "Publishes the selected immutable Skill version. Repeating the currently published version is a no-op. Concurrent publication changes are last-write-wins in Playbook v1.", + "operationId": "PublishSkill", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishSkillRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "skillId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "PublishSkill 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishSkillResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "Publish a skill version" + }, + "delete": { + "description": "Withdraws the Skill publication without changing version content. An already-unpublished entry is a no-op. Concurrent publication changes are last-write-wins in Playbook v1.", + "operationId": "WithdrawSkill", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "skillId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "WithdrawSkill 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithdrawSkillResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "Withdraw a skill" + } + }, + "/v1/playbooks/{playbookId}/skills/{skillId}/versions/{version}/resources/{resourceId}": { + "get": { + "description": "Returns the exact immutable Skill resource bytes. The Content-Digest response header carries their SHA-256 digest using RFC 9530 syntax.", + "operationId": "GetSkillResource", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "skillId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + }, + { + "name": "version", + "in": "path", + "schema": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "required": true + }, + { + "name": "resourceId", + "in": "path", + "description": "Opaque immutable Skill resource identifier.", + "schema": { + "type": "string", + "pattern": "^res_[0-9a-f]{24}$", + "description": "Opaque immutable Skill resource identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetSkillResource 200 response", + "headers": { + "Content-Digest": { + "description": "SHA-256 content digest using the RFC 9530 Content-Digest representation.", + "schema": { + "type": "string", + "pattern": "^sha-256=:[A-Za-z0-9+/]{43}=:$", + "description": "SHA-256 content digest using the RFC 9530 Content-Digest representation." + }, + "required": true + }, + "Content-Type": { + "schema": { + "type": "string" + }, + "required": true + } + }, + "content": { + "*/*": { + "schema": { + "$ref": "#/components/schemas/GetSkillResourceOutputPayload" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Skills" + ], + "summary": "Read a skill resource" + } + }, + "/v1/playbooks/{playbookId}/faqs": { + "post": { + "operationId": "CreateFaq", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFaqRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "CreateFaq 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateFaqResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "Create an FAQ" + }, + "get": { + "operationId": "ListFaqs", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "sortBy", + "in": "query", + "description": "Default updatedAt descending, then entry ID ascending.", + "schema": { + "$ref": "#/components/schemas/EntrySort" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Defaults to desc. ID ties are always asc.", + "schema": { + "$ref": "#/components/schemas/SortOrder" + } + }, + { + "name": "createdAfter", + "in": "query", + "description": "Exclusive bound at microsecond precision.", + "schema": { + "type": "string", + "description": "Exclusive bound at microsecond precision.", + "format": "date-time" + } + }, + { + "name": "createdBefore", + "in": "query", + "description": "Exclusive bound at microsecond precision.", + "schema": { + "type": "string", + "description": "Exclusive bound at microsecond precision.", + "format": "date-time" + } + }, + { + "name": "published", + "in": "query", + "description": "Selects publication presence.", + "schema": { + "type": "boolean", + "description": "Selects publication presence." + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ListFaqs 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFaqsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "List FAQs" + }, + "delete": { + "description": "Deletes the requested FAQ families sequentially. Each family deletion is atomic, but the batch is not: deletions completed before an error remain applied. Retrying the same request is safe because already absent entries are ignored.", + "operationId": "DeleteFaqs", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteFaqsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteFaqs 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteFaqsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "Delete multiple FAQs" + } + }, + "/v1/playbooks/{playbookId}/faqs/{faqId}": { + "get": { + "operationId": "GetFaq", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "faqId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetFaq 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetFaqResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "Get an FAQ" + }, + "delete": { + "operationId": "DeleteFaq", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "faqId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "204": { + "description": "DeleteFaq 204 response" + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "Delete an FAQ" + } + }, + "/v1/playbooks/{playbookId}/faqs/{faqId}/versions": { + "post": { + "operationId": "AppendFaqVersion", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppendFaqVersionRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "faqId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "AppendFaqVersion 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AppendFaqVersionResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "Append an FAQ version" + }, + "get": { + "operationId": "ListFaqVersions", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "faqId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + }, + { + "name": "createdAfter", + "in": "query", + "description": "UTC timestamp resolved to microsecond precision.", + "schema": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + } + }, + { + "name": "createdBefore", + "in": "query", + "description": "UTC timestamp resolved to microsecond precision.", + "schema": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ListFaqVersions 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListFaqVersionsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "List FAQ versions" + } + }, + "/v1/playbooks/{playbookId}/faqs/{faqId}/versions/{version}": { + "get": { + "operationId": "GetFaqVersion", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "faqId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + }, + { + "name": "version", + "in": "path", + "schema": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetFaqVersion 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetFaqVersionResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "Get an FAQ version" + } + }, + "/v1/playbooks/{playbookId}/faqs/{faqId}/publication": { + "put": { + "description": "Publishes the selected immutable FAQ version. Repeating the currently published version is a no-op. Concurrent publication changes are last-write-wins in Playbook v1.", + "operationId": "PublishFaq", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishFaqRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "faqId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "PublishFaq 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishFaqResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "Publish an FAQ version" + }, + "delete": { + "description": "Withdraws the FAQ publication without changing version content. An already-unpublished entry is a no-op. Concurrent publication changes are last-write-wins in Playbook v1.", + "operationId": "WithdrawFaq", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "faqId", + "in": "path", + "description": "Opaque stable entry identifier.", + "schema": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "WithdrawFaq 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WithdrawFaqResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "FAQs" + ], + "summary": "Withdraw an FAQ" + } + }, + "/v1/playbooks/{playbookId}/discover": { + "post": { + "operationId": "DiscoverEntries", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiscoverEntriesRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DiscoverEntries 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DiscoverEntriesResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Retrieval" + ], + "summary": "Discover published entries" + } + }, + "/v1/playbooks/{playbookId}/faqs/search": { + "post": { + "operationId": "SearchFaqs", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchFaqsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "SearchFaqs 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchFaqsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Retrieval" + ], + "summary": "Search published FAQs" + } + }, + "/v1/playbooks/{playbookId}/skills/search": { + "post": { + "operationId": "SearchSkills", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchSkillsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "SearchSkills 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchSkillsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Retrieval" + ], + "summary": "Search published skills" + } + }, + "/v1/playbooks/{playbookId}/entries/lookup": { + "post": { + "description": "Resolves bounded entry references to compact management state. Results preserve input order and report misses explicitly. Each item is authoritative when read, but the batch is not a global snapshot.", + "operationId": "LookupEntries", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LookupEntriesRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "LookupEntries 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LookupEntriesResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Retrieval" + ], + "summary": "Look up entries" + } + }, + "/v1/playbooks/{playbookId}/proposals": { + "post": { + "operationId": "CreateProposal", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProposalRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "201": { + "description": "CreateProposal 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateProposalResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "Create a proposal" + }, + "get": { + "operationId": "ListProposals", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "view", + "in": "query", + "schema": { + "$ref": "#/components/schemas/ProposalView" + }, + "required": true + }, + { + "name": "sortBy", + "in": "query", + "description": "Default updatedAt descending, then proposal ID ascending.", + "schema": { + "$ref": "#/components/schemas/ProposalSort" + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Defaults to asc for deadlines, desc otherwise. ID ties are always asc.", + "schema": { + "$ref": "#/components/schemas/SortOrder" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "maximum": 200, + "minimum": 1, + "format": "int32" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ListProposals 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListProposalsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "List proposals" + } + }, + "/v1/playbooks/{playbookId}/proposals/{proposalId}/evidence/{evidenceId}": { + "put": { + "operationId": "SubmitProposalEvidence", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitProposalEvidenceRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "proposalId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "required": true + }, + { + "name": "evidenceId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^evd_[0-9a-f]{32}$" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "SubmitProposalEvidence 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SubmitProposalEvidenceResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "Submit proposal evidence" + } + }, + "/v1/playbooks/{playbookId}/proposals/search": { + "post": { + "operationId": "SearchProposals", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchProposalsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "SearchProposals 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchProposalsResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "Search proposals" + } + }, + "/v1/playbooks/{playbookId}/proposals/{proposalId}": { + "get": { + "operationId": "GetProposal", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "proposalId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetProposal 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetProposalResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "Get a proposal" + }, + "patch": { + "operationId": "ReplaceProposal", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceProposalRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "proposalId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "ReplaceProposal 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ReplaceProposalResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "Replace a proposal candidate" + }, + "delete": { + "operationId": "PurgeProposal", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "proposalId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "required": true + } + ], + "responses": { + "204": { + "description": "PurgeProposal 204 response" + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "Purge a proposal" + } + }, + "/v1/playbooks/{playbookId}/proposals/{proposalId}/acceptance": { + "post": { + "description": "Promotes the reviewed revision to one canonical unpublished FAQ.\nConcurrent calls and retries resume or return the proposal's recorded target.\nA complete target may be visible to management before the proposal reaches\npromoted, but remains absent from serving until separately published.", + "operationId": "AcceptProposal", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AcceptProposalRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "proposalId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "required": true + } + ], + "responses": { + "201": { + "description": "AcceptProposal 201 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AcceptProposalResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "Accept a reviewed proposal" + } + }, + "/v1/playbooks/{playbookId}/proposals/{proposalId}/dismissal": { + "put": { + "operationId": "DismissProposal", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DismissProposalRequestContent" + } + } + } + }, + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "proposalId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DismissProposal 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DismissProposalResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "Dismiss a proposal" + }, + "delete": { + "operationId": "RestoreProposal", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "proposalId", + "in": "path", + "schema": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "RestoreProposal 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RestoreProposalResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Proposals" + ], + "summary": "Restore a proposal" + } + }, + "/v1/playbooks/{playbookId}/health": { + "get": { + "operationId": "GetPlaybookHealth", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetPlaybookHealth 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetPlaybookHealthResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Maintenance" + ], + "summary": "Check playbook health" + } + }, + "/v1/playbooks/{playbookId}/clear": { + "post": { + "operationId": "ClearPlaybook", + "parameters": [ + { + "name": "playbookId", + "in": "path", + "description": "Opaque Playbook identifier.", + "schema": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "required": true + }, + { + "name": "scope", + "in": "query", + "description": "Content to clear. Omission clears all entries and proposals while preserving the Playbook.", + "schema": { + "$ref": "#/components/schemas/ClearScope" + } + } + ], + "responses": { + "200": { + "description": "ClearPlaybook 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ClearPlaybookResponseContent" + } + } + } + }, + "400": { + "description": "RequestValidationError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestValidationErrorResponseContent" + } + } + } + }, + "401": { + "description": "AuthenticationError 401 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AuthenticationErrorResponseContent" + } + } + } + }, + "403": { + "description": "ForbiddenError 403 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ForbiddenErrorResponseContent" + } + } + } + }, + "404": { + "description": "NotFoundError 404 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/NotFoundErrorResponseContent" + } + } + } + }, + "408": { + "description": "TimeoutError 408 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TimeoutErrorResponseContent" + } + } + } + }, + "409": { + "description": "PlaybookConflictError 409 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookConflictErrorResponseContent" + } + } + } + }, + "413": { + "description": "PayloadTooLargeError 413 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PayloadTooLargeErrorResponseContent" + } + } + } + }, + "423": { + "description": "ResourceSuspendedError 423 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ResourceSuspendedErrorResponseContent" + } + } + } + }, + "424": { + "description": "FailedDependencyError 424 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FailedDependencyErrorResponseContent" + } + } + } + }, + "429": { + "description": "TooManyRequestsError 429 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TooManyRequestsErrorResponseContent" + } + } + } + }, + "500": { + "description": "PlaybookInternalError 500 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookInternalErrorResponseContent" + } + } + } + }, + "503": { + "description": "PlaybookServiceUnavailableError 503 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorResponseContent" + } + } + } + } + }, + "tags": [ + "Maintenance" + ], + "summary": "Clear playbook content" + } + } + }, + "components": { + "schemas": { + "AcceptProposalRequestContent": { + "type": "object", + "properties": { + "expectedProposalRevision": { + "type": "integer", + "minimum": 1, + "format": "int32" + } + }, + "required": [ + "expectedProposalRevision" + ] + }, + "AcceptProposalResponseContent": { + "type": "object", + "properties": { + "entry": { + "$ref": "#/components/schemas/StableEntry" + }, + "version": { + "$ref": "#/components/schemas/Version" + } + }, + "required": [ + "entry", + "version" + ] + }, + "AddProposalAction": { + "type": "object", + "properties": { + "target": { + "$ref": "#/components/schemas/ProposalTarget" + }, + "candidate": { + "$ref": "#/components/schemas/ProposalCandidate" + } + }, + "required": [ + "candidate", + "target" + ] + }, + "AddProposalActionInput": { + "type": "object", + "properties": { + "target": { + "$ref": "#/components/schemas/ProposalTargetInput" + }, + "candidate": { + "$ref": "#/components/schemas/ProposalCandidateInput" + } + }, + "required": [ + "candidate", + "target" + ] + }, + "AddProposalActionPreview": { + "type": "object", + "properties": { + "target": { + "$ref": "#/components/schemas/ProposalTarget" + }, + "candidate": { + "$ref": "#/components/schemas/ProposalCandidatePreview" + } + }, + "required": [ + "candidate", + "target" + ] + }, + "AppendFaqVersionRequestContent": { + "type": "object", + "properties": { + "content": { + "$ref": "#/components/schemas/FaqAuthoredContent" + } + }, + "required": [ + "content" + ] + }, + "AppendFaqVersionResponseContent": { + "type": "object", + "properties": { + "entry": { + "$ref": "#/components/schemas/StableEntry" + }, + "version": { + "$ref": "#/components/schemas/Version" + } + }, + "required": [ + "entry", + "version" + ] + }, + "AppendSkillVersionRequestContent": { + "type": "object", + "properties": { + "packageBase64": { + "type": "string", + "maxLength": 6990508, + "minLength": 1 + }, + "content": { + "$ref": "#/components/schemas/ContentMetadata" + } + }, + "required": [ + "packageBase64" + ] + }, + "AppendSkillVersionResponseContent": { + "type": "object", + "properties": { + "entry": { + "$ref": "#/components/schemas/StableEntry" + }, + "version": { + "$ref": "#/components/schemas/Version" + } + }, + "required": [ + "entry", + "version" + ] + }, + "AttributeMap": { + "type": "object", + "additionalProperties": {}, + "maxProperties": 32, + "description": "Exact-match attributes. Values are typed scalars or nonempty lists of typed scalars." + }, + "AuthenticationErrorResponseContent": { + "type": "object", + "description": "Authentication credentials are missing, malformed, or invalid.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 401, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/AuthenticationErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "AuthenticationErrorType": { + "type": "string", + "description": "Problem type URI for authentication errors.", + "enum": [ + "/errors/authentication-failed" + ] + }, + "AuthoredContent": { + "type": "object", + "description": "Complete normalized authored content. Responses emit empty intents/attributes collections.", + "properties": { + "intents": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "maxItems": 5, + "uniqueItems": true + }, + "attributes": { + "$ref": "#/components/schemas/AttributeMap" + }, + "metadata": { + "description": "Author-provided JSON metadata. Its normalized JSON encoding must not exceed 65,536 bytes or nesting depth 16; deployments may enforce lower limits. Exceeding the byte limit returns payload-too-large, while excessive nesting returns invalid-data." + }, + "payload": { + "$ref": "#/components/schemas/EntryPayload" + } + }, + "required": [ + "payload" + ] + }, + "CandidatePreviewContent": { + "type": "object", + "properties": { + "payload": { + "$ref": "#/components/schemas/CandidatePreviewPayload" + }, + "metadata": { + "description": "Author-provided context, not verified attribution. Returned verbatim." + } + }, + "required": [ + "payload" + ] + }, + "CandidatePreviewPayload": { + "oneOf": [ + { + "type": "object", + "title": "faq", + "properties": { + "faq": { + "$ref": "#/components/schemas/FaqCandidatePreview" + } + }, + "required": [ + "faq" + ] + } + ] + }, + "ClearPlaybookResponseContent": { + "type": "object", + "properties": { + "deletedEntriesCount": { + "type": "integer", + "description": "Number of entries removed by this successful attempt. Entries already absent at\nthe start of the attempt are ignored and do not contribute to the count.", + "format": "int32" + } + }, + "required": [ + "deletedEntriesCount" + ] + }, + "ClearScope": { + "type": "string", + "enum": [ + "all", + "entries", + "proposals" + ], + "default": "all" + }, + "ContentMetadata": { + "type": "object", + "description": "Author-provided context shared by FAQ, Skill and proposal authoring.", + "properties": { + "intents": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "maxItems": 5, + "uniqueItems": true + }, + "attributes": { + "$ref": "#/components/schemas/AttributeMap" + }, + "metadata": { + "description": "Author-provided JSON metadata. Its normalized JSON encoding must not exceed 65,536 bytes or nesting depth 16; deployments may enforce lower limits. Exceeding the byte limit returns payload-too-large, while excessive nesting returns invalid-data." + } + } + }, + "ContentPreview": { + "type": "object", + "description": "Compact published content shared by management entries and serving previews.\nFAQ answers are included; Skill instructions and resource descriptors require a Version read.", + "properties": { + "intents": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "maxItems": 5, + "uniqueItems": true + }, + "attributes": { + "$ref": "#/components/schemas/AttributeMap" + }, + "metadata": { + "description": "Author-provided JSON metadata. Its normalized JSON encoding must not exceed 65,536 bytes or nesting depth 16; deployments may enforce lower limits. Exceeding the byte limit returns payload-too-large, while excessive nesting returns invalid-data." + }, + "payload": { + "$ref": "#/components/schemas/PreviewPayload" + } + }, + "required": [ + "payload" + ] + }, + "CreateFaqRequestContent": { + "type": "object", + "properties": { + "content": { + "$ref": "#/components/schemas/FaqAuthoredContent" + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + } + }, + "required": [ + "content" + ] + }, + "CreateFaqResponseContent": { + "type": "object", + "properties": { + "entry": { + "$ref": "#/components/schemas/StableEntry" + }, + "version": { + "$ref": "#/components/schemas/Version" + } + }, + "required": [ + "entry", + "version" + ] + }, + "CreateProposalRequestContent": { + "type": "object", + "properties": { + "action": { + "$ref": "#/components/schemas/ProposalActionInput" + } + }, + "required": [ + "action" + ] + }, + "CreateProposalResponseContent": { + "type": "object", + "properties": { + "proposalId": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "status": { + "$ref": "#/components/schemas/ProposalStatus" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + }, + "proposalRevision": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "evidenceCount": { + "type": "integer", + "maximum": 100, + "minimum": 0, + "format": "int32" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last successful candidate replacement, new evidence, dismissal, or restoration\n(creation initially). Duplicate evidence, repeated dismissal/restoration, and\ntime-based expiry do not change it. New evidence, replacement, and restoration\nalso refresh inbox/purge deadlines.", + "format": "date-time" + }, + "inboxExpiresAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "purgeAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "action": { + "$ref": "#/components/schemas/ProposalAction" + }, + "evidence": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProposalEvidence" + }, + "maxItems": 100 + } + }, + "required": [ + "action", + "contentDigest", + "createdAt", + "evidence", + "evidenceCount", + "inboxExpiresAt", + "proposalId", + "proposalRevision", + "purgeAt", + "status", + "updatedAt" + ] + }, + "CreateSkillRequestContent": { + "type": "object", + "properties": { + "packageBase64": { + "type": "string", + "maxLength": 6990508, + "minLength": 1 + }, + "content": { + "$ref": "#/components/schemas/ContentMetadata" + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + } + }, + "required": [ + "packageBase64" + ] + }, + "CreateSkillResponseContent": { + "type": "object", + "properties": { + "entry": { + "$ref": "#/components/schemas/StableEntry" + }, + "version": { + "$ref": "#/components/schemas/Version" + } + }, + "required": [ + "entry", + "version" + ] + }, + "DeleteFaqsRequestContent": { + "type": "object", + "properties": { + "entryIds": { + "type": "array", + "items": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "maxItems": 100, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "entryIds" + ] + }, + "DeleteFaqsResponseContent": { + "type": "object", + "properties": { + "deletedEntriesCount": { + "type": "integer", + "description": "Number of entries removed by this successful attempt. Entries already absent at\nthe start of the attempt are ignored and do not contribute to the count.", + "format": "int32" + } + }, + "required": [ + "deletedEntriesCount" + ] + }, + "DeleteSkillsRequestContent": { + "type": "object", + "properties": { + "entryIds": { + "type": "array", + "items": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "maxItems": 100, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "entryIds" + ] + }, + "DeleteSkillsResponseContent": { + "type": "object", + "properties": { + "deletedEntriesCount": { + "type": "integer", + "description": "Number of entries removed by this successful attempt. Entries already absent at\nthe start of the attempt are ignored and do not contribute to the count.", + "format": "int32" + } + }, + "required": [ + "deletedEntriesCount" + ] + }, + "DiscoverEntriesRequestContent": { + "type": "object", + "properties": { + "query": { + "type": "string", + "maxLength": 4096, + "minLength": 1 + }, + "filter": { + "$ref": "#/components/schemas/Filter" + }, + "limit": { + "type": "integer", + "maximum": 50, + "minimum": 1, + "description": "Optional request cap, still bounded by the Playbook discovery maxResults policy.", + "format": "int32" + }, + "pinnedEntries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntryReference" + }, + "maxItems": 50 + } + }, + "required": [ + "query" + ] + }, + "DiscoverEntriesResponseContent": { + "type": "object", + "properties": { + "pinnedEntries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PinnedPlaybookPreviewEntry" + } + }, + "discoveredEntries": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DiscoveredPlaybookPreviewEntry" + } + }, + "truncated": { + "type": "boolean", + "description": "True when eligible fetched candidates exceed the result slots left by pins, independently of token admission. This count signal is bounded to the fetched candidates, not the entire index." + }, + "budgetTruncated": { + "type": "boolean", + "description": "True when the token budget skips an eligible fetched entry while filling result slots. Later fetched candidates may fill those slots if they fit; admission stops at the result count limit or the end of the fetched candidates. Empty results with this flag do not mean no knowledge matched. Omitted means false; independent of count-based truncated." + } + }, + "required": [ + "discoveredEntries", + "pinnedEntries", + "truncated" + ] + }, + "DiscoveredPlaybookPreviewEntry": { + "type": "object", + "description": "Scored published preview shared by type-specific search and unified discovery.\nFAQs include their answer; Skills expose only their name, description and exact-version URI.", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "version": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "intents": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "maxItems": 5, + "uniqueItems": true + }, + "faq": { + "$ref": "#/components/schemas/FaqContent" + }, + "skill": { + "$ref": "#/components/schemas/SkillPreviewContent" + }, + "score": { + "type": "number", + "maximum": 1, + "minimum": 0, + "description": "Normalized cosine similarity: (2 - Redis cosine distance) / 2.", + "format": "float" + } + }, + "required": [ + "entryId", + "entryType", + "intents", + "score", + "version" + ] + }, + "DismissProposalRequestContent": { + "type": "object", + "properties": { + "expectedProposalRevision": { + "type": "integer", + "minimum": 1, + "description": "Optional revision check for an open proposal; omission dismisses the current\ncandidate regardless of revision. A mismatch returns ProposalRevisionConflict.\nDismissal preserves the candidate for restoration; an already trashed proposal\nis a successful no-op regardless of the supplied revision.", + "format": "int32" + } + } + }, + "DismissProposalResponseContent": { + "type": "object", + "properties": { + "proposalId": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "status": { + "$ref": "#/components/schemas/ProposalStatus" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + }, + "proposalRevision": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "evidenceCount": { + "type": "integer", + "maximum": 100, + "minimum": 0, + "format": "int32" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last successful candidate replacement, new evidence, dismissal, or restoration\n(creation initially). Duplicate evidence, repeated dismissal/restoration, and\ntime-based expiry do not change it. New evidence, replacement, and restoration\nalso refresh inbox/purge deadlines.", + "format": "date-time" + }, + "inboxExpiresAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "purgeAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "action": { + "$ref": "#/components/schemas/ProposalAction" + }, + "evidence": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProposalEvidence" + }, + "maxItems": 100 + } + }, + "required": [ + "action", + "contentDigest", + "createdAt", + "evidence", + "evidenceCount", + "inboxExpiresAt", + "proposalId", + "proposalRevision", + "purgeAt", + "status", + "updatedAt" + ] + }, + "EntryLookupResult": { + "oneOf": [ + { + "type": "object", + "title": "found", + "properties": { + "found": { + "$ref": "#/components/schemas/FoundEntryLookupResult" + } + }, + "required": [ + "found" + ] + }, + { + "type": "object", + "title": "missing", + "properties": { + "missing": { + "$ref": "#/components/schemas/MissingEntryLookupResult" + } + }, + "required": [ + "missing" + ] + } + ] + }, + "EntryLookupState": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "latest": { + "$ref": "#/components/schemas/EntryVersionDigest" + }, + "versionCount": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "publication": { + "$ref": "#/components/schemas/EntryVersionDigest" + } + }, + "required": [ + "entryId", + "entryType", + "latest", + "versionCount" + ] + }, + "EntryPayload": { + "oneOf": [ + { + "type": "object", + "title": "faq", + "properties": { + "faq": { + "$ref": "#/components/schemas/FaqContent" + } + }, + "required": [ + "faq" + ] + }, + { + "type": "object", + "title": "skill", + "properties": { + "skill": { + "$ref": "#/components/schemas/SkillContent" + } + }, + "required": [ + "skill" + ] + } + ] + }, + "EntryReference": { + "description": "One discovery pin. Smithy unions enforce exactly one identifier.", + "oneOf": [ + { + "type": "object", + "title": "entryId", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + } + }, + "required": [ + "entryId" + ] + }, + { + "type": "object", + "title": "entryKey", + "properties": { + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + } + }, + "required": [ + "entryKey" + ] + } + ] + }, + "EntrySort": { + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ], + "default": "updatedAt" + }, + "EntryType": { + "type": "string", + "enum": [ + "faq", + "skill" + ] + }, + "EntryVersionDigest": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + } + }, + "required": [ + "contentDigest", + "version" + ] + }, + "EqualsPredicate": { + "type": "object", + "properties": { + "name": { + "type": "string", + "pattern": "^[A-Za-z][A-Za-z0-9_.-]{0,63}$" + }, + "value": {} + }, + "required": [ + "name", + "value" + ] + }, + "EvidenceProducer": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9][a-z0-9.-]*$" + }, + "version": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._+-]*$" + } + }, + "required": [ + "name", + "version" + ] + }, + "FailedDependencyErrorResponseContent": { + "type": "object", + "description": "A dependent resource required to process the request is unavailable or unhealthy.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 424, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/FailedDependencyErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "FailedDependencyErrorType": { + "type": "string", + "description": "Problem type URI for failed-dependency errors.", + "enum": [ + "/errors/resource-unavailable", + "/errors/database-out-of-memory" + ] + }, + "FaqAuthoredContent": { + "type": "object", + "description": "Author-provided context shared by FAQ, Skill and proposal authoring.", + "properties": { + "intents": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "maxItems": 5, + "uniqueItems": true + }, + "attributes": { + "$ref": "#/components/schemas/AttributeMap" + }, + "metadata": { + "description": "Author-provided JSON metadata. Its normalized JSON encoding must not exceed 65,536 bytes or nesting depth 16; deployments may enforce lower limits. Exceeding the byte limit returns payload-too-large, while excessive nesting returns invalid-data." + }, + "payload": { + "$ref": "#/components/schemas/FaqPayload" + } + }, + "required": [ + "payload" + ] + }, + "FaqCandidatePreview": { + "type": "object", + "properties": { + "question": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + } + }, + "required": [ + "question" + ] + }, + "FaqContent": { + "type": "object", + "properties": { + "question": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "answer": { + "type": "string", + "maxLength": 32768, + "minLength": 1 + } + }, + "required": [ + "answer", + "question" + ] + }, + "FaqPayload": { + "description": "FAQ writes accept only a FAQ payload; complete Skill payloads are archive-derived.", + "oneOf": [ + { + "type": "object", + "title": "faq", + "properties": { + "faq": { + "$ref": "#/components/schemas/FaqContent" + } + }, + "required": [ + "faq" + ] + } + ] + }, + "FieldError": { + "type": "object", + "description": "Validation error details for a single request field.", + "properties": { + "field": { + "type": "string", + "description": "Name of the invalid request field." + }, + "rule": { + "type": "string", + "description": "Validation rule that was violated." + }, + "message": { + "type": "string", + "description": "Human-readable validation error message." + } + }, + "required": [ + "field", + "message", + "rule" + ] + }, + "Filter": { + "description": "Recursive exact-match filter. Servers enforce depth five and at most 64 leaves.", + "oneOf": [ + { + "type": "object", + "title": "and", + "properties": { + "and": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Filter" + }, + "minItems": 1 + } + }, + "required": [ + "and" + ] + }, + { + "type": "object", + "title": "or", + "properties": { + "or": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Filter" + }, + "minItems": 1 + } + }, + "required": [ + "or" + ] + }, + { + "type": "object", + "title": "eq", + "properties": { + "eq": { + "$ref": "#/components/schemas/EqualsPredicate" + } + }, + "required": [ + "eq" + ] + } + ] + }, + "ForbiddenErrorResponseContent": { + "type": "object", + "description": "The caller is authenticated but not allowed to access the requested resource.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 403, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ForbiddenErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "ForbiddenErrorType": { + "type": "string", + "description": "Problem type URI for authorization errors.", + "enum": [ + "/errors/insufficient-permissions" + ] + }, + "FoundEntryLookupResult": { + "type": "object", + "properties": { + "reference": { + "$ref": "#/components/schemas/EntryReference" + }, + "entry": { + "$ref": "#/components/schemas/EntryLookupState" + } + }, + "required": [ + "entry", + "reference" + ] + }, + "GetFaqResponseContent": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "latestVersion": { + "type": "integer", + "minimum": 1, + "description": "Newest immutable version number. Versions are append-only, so this also equals the version count.", + "format": "int32" + }, + "publication": { + "$ref": "#/components/schemas/Publication" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last create, append, publication change, or withdrawal of an existing publication. Repeating the current publication, already-unpublished withdrawal, and reads leave it unchanged. Publication/append updates never move this timestamp backwards.", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "entryId", + "entryType", + "latestVersion", + "updatedAt" + ] + }, + "GetFaqVersionResponseContent": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "version": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + }, + "content": { + "$ref": "#/components/schemas/AuthoredContent" + } + }, + "required": [ + "content", + "contentDigest", + "createdAt", + "entryId", + "entryType", + "version" + ] + }, + "GetPlaybookHealthResponseContent": { + "type": "object", + "properties": { + "playbookId": { + "type": "string", + "pattern": "^pb_[0-9a-f]{32}$", + "description": "Opaque Playbook identifier." + }, + "status": { + "$ref": "#/components/schemas/ReadyStatus" + }, + "metadataRedis": { + "$ref": "#/components/schemas/ReadyStatus" + }, + "contentRedis": { + "$ref": "#/components/schemas/ReadyStatus" + }, + "inference": { + "$ref": "#/components/schemas/ReadyStatus" + }, + "license": { + "$ref": "#/components/schemas/ReadyStatus" + } + }, + "required": [ + "contentRedis", + "inference", + "license", + "metadataRedis", + "playbookId", + "status" + ] + }, + "GetProposalResponseContent": { + "type": "object", + "properties": { + "proposalId": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "status": { + "$ref": "#/components/schemas/ProposalStatus" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + }, + "proposalRevision": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "evidenceCount": { + "type": "integer", + "maximum": 100, + "minimum": 0, + "format": "int32" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last successful candidate replacement, new evidence, dismissal, or restoration\n(creation initially). Duplicate evidence, repeated dismissal/restoration, and\ntime-based expiry do not change it. New evidence, replacement, and restoration\nalso refresh inbox/purge deadlines.", + "format": "date-time" + }, + "inboxExpiresAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "purgeAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "action": { + "$ref": "#/components/schemas/ProposalAction" + }, + "evidence": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProposalEvidence" + }, + "maxItems": 100 + } + }, + "required": [ + "action", + "contentDigest", + "createdAt", + "evidence", + "evidenceCount", + "inboxExpiresAt", + "proposalId", + "proposalRevision", + "purgeAt", + "status", + "updatedAt" + ] + }, + "GetSkillResourceOutputPayload": { + "type": "string", + "format": "binary" + }, + "GetSkillResponseContent": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "latestVersion": { + "type": "integer", + "minimum": 1, + "description": "Newest immutable version number. Versions are append-only, so this also equals the version count.", + "format": "int32" + }, + "publication": { + "$ref": "#/components/schemas/Publication" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last create, append, publication change, or withdrawal of an existing publication. Repeating the current publication, already-unpublished withdrawal, and reads leave it unchanged. Publication/append updates never move this timestamp backwards.", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "entryId", + "entryType", + "latestVersion", + "updatedAt" + ] + }, + "GetSkillVersionResponseContent": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "version": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + }, + "content": { + "$ref": "#/components/schemas/AuthoredContent" + } + }, + "required": [ + "content", + "contentDigest", + "createdAt", + "entryId", + "entryType", + "version" + ] + }, + "ListFaqVersionsResponseContent": { + "type": "object", + "properties": { + "versions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Version" + } + }, + "nextCursor": { + "type": "string" + } + }, + "required": [ + "versions" + ] + }, + "ListFaqsResponseContent": { + "type": "object", + "properties": { + "nextCursor": { + "type": "string", + "description": "Continue until nextCursor is absent, including after short or empty pages.\nLive updates/indexing delay can repeat or skip moved items; this is not a snapshot." + }, + "total": { + "type": "integer", + "minimum": 0, + "description": "First-page count matching the original filters at count-query time. Omitted on\ncursored requests or when counting is unavailable; absence means unknown, not zero.", + "format": "int32" + }, + "faqs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StableEntry" + } + } + }, + "required": [ + "faqs" + ] + }, + "ListProposalsResponseContent": { + "type": "object", + "properties": { + "nextCursor": { + "type": "string", + "description": "Continue until nextCursor is absent, including after short or empty pages.\nLive updates/indexing delay can repeat or skip moved items; this is not a snapshot." + }, + "total": { + "type": "integer", + "minimum": 0, + "description": "First-page count matching the original filters at count-query time. Omitted on\ncursored requests or when counting is unavailable; absence means unknown, not zero.", + "format": "int32" + }, + "proposals": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProposalPreview" + } + } + }, + "required": [ + "proposals" + ] + }, + "ListSkillVersionsResponseContent": { + "type": "object", + "properties": { + "versions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Version" + } + }, + "nextCursor": { + "type": "string" + } + }, + "required": [ + "versions" + ] + }, + "ListSkillsResponseContent": { + "type": "object", + "properties": { + "nextCursor": { + "type": "string", + "description": "Continue until nextCursor is absent, including after short or empty pages.\nLive updates/indexing delay can repeat or skip moved items; this is not a snapshot." + }, + "total": { + "type": "integer", + "minimum": 0, + "description": "First-page count matching the original filters at count-query time. Omitted on\ncursored requests or when counting is unavailable; absence means unknown, not zero.", + "format": "int32" + }, + "skills": { + "type": "array", + "items": { + "$ref": "#/components/schemas/StableEntry" + } + } + }, + "required": [ + "skills" + ] + }, + "LookupEntriesRequestContent": { + "type": "object", + "properties": { + "references": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntryReference" + }, + "maxItems": 100, + "minItems": 1, + "uniqueItems": true + } + }, + "required": [ + "references" + ] + }, + "LookupEntriesResponseContent": { + "type": "object", + "properties": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntryLookupResult" + } + }, + "maxVersions": { + "type": "integer", + "minimum": 1, + "format": "int32" + } + }, + "required": [ + "maxVersions", + "results" + ] + }, + "MissingEntryLookupResult": { + "type": "object", + "properties": { + "reference": { + "$ref": "#/components/schemas/EntryReference" + } + }, + "required": [ + "reference" + ] + }, + "NewEntryTarget": { + "type": "object", + "properties": { + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + } + } + }, + "NotFoundErrorResponseContent": { + "type": "object", + "description": "The requested resource does not exist.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 404, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/NotFoundErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "NotFoundErrorType": { + "type": "string", + "description": "Problem type URI for not-found errors.", + "enum": [ + "/errors/resource-not-found" + ] + }, + "PayloadTooLargeErrorResponseContent": { + "type": "object", + "description": "The request payload exceeds the maximum supported size.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 413, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/PayloadTooLargeErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "PayloadTooLargeErrorType": { + "type": "string", + "description": "Problem type URI for payload-too-large errors.", + "enum": [ + "/errors/payload-too-large" + ] + }, + "PinnedPlaybookPreviewEntry": { + "type": "object", + "description": "Flat preview wire shape. Exactly one of faq or skill is present and matches entryType.", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "version": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "intents": { + "type": "array", + "items": { + "type": "string", + "maxLength": 2048, + "minLength": 1 + }, + "maxItems": 5, + "uniqueItems": true + }, + "faq": { + "$ref": "#/components/schemas/FaqContent" + }, + "skill": { + "$ref": "#/components/schemas/SkillPreviewContent" + } + }, + "required": [ + "entryId", + "entryType", + "intents", + "version" + ] + }, + "PlaybookConflictErrorResponseContent": { + "type": "object", + "description": "One schema per HTTP status preserves the complete problem catalog in generated\nclients, following the Iris/Agent Memory dedicated error-type enum pattern.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 409, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/PlaybookConflictErrorType" + }, + "existingProposalId": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$", + "description": "Present for proposal-already-exists. Identifies the retained duplicate." + }, + "currentProposalRevision": { + "type": "integer", + "minimum": 1, + "description": "Present when proposal-revision-conflict reports the current candidate revision.", + "format": "int32" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "PlaybookConflictErrorType": { + "type": "string", + "description": "Distinguish resource state conflicts by type, never by human-readable title.", + "enum": [ + "/errors/entry-not-published", + "/errors/entry-key-conflict", + "/errors/content-limit-exceeded", + "/errors/pinned-entry-unavailable", + "/errors/proposal-already-exists", + "/errors/proposal-evidence-conflict", + "/errors/proposal-evidence-capacity", + "/errors/proposal-revision-conflict", + "/errors/proposal-trashed", + "/errors/proposal-open", + "/errors/proposal-promotion-locked" + ] + }, + "PlaybookInternalErrorResponseContent": { + "type": "object", + "description": "Stored integrity failures require operator investigation; editing or blindly\nretrying the request does not repair the stored record.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 500, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/PlaybookInternalErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "PlaybookInternalErrorType": { + "type": "string", + "description": "Server faults are distinct from correctable resource-state conflicts.", + "enum": [ + "/errors/unexpected-error", + "/errors/content-integrity-failed" + ] + }, + "PlaybookServiceUnavailableErrorResponseContent": { + "type": "object", + "description": "Retry reads and proposal acceptance with bounded backoff. Acceptance resumes\nits recorded canonical target. An uncertain create or append response may have\nwritten another logical record or version and should not be blindly retried\nwhen the caller requires exactly one result.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 503, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/PlaybookServiceUnavailableErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "PlaybookServiceUnavailableErrorType": { + "type": "string", + "enum": [ + "/errors/service-unavailable", + "/errors/playbook-not-ready" + ] + }, + "PreviewPayload": { + "oneOf": [ + { + "type": "object", + "title": "faq", + "properties": { + "faq": { + "$ref": "#/components/schemas/FaqContent" + } + }, + "required": [ + "faq" + ] + }, + { + "type": "object", + "title": "skill", + "properties": { + "skill": { + "$ref": "#/components/schemas/SkillPreviewContent" + } + }, + "required": [ + "skill" + ] + } + ] + }, + "ProposalAction": { + "description": "Exactly one action. Response clients must preserve unrecognized members.", + "oneOf": [ + { + "type": "object", + "title": "add", + "properties": { + "add": { + "$ref": "#/components/schemas/AddProposalAction" + } + }, + "required": [ + "add" + ] + } + ] + }, + "ProposalActionInput": { + "oneOf": [ + { + "type": "object", + "title": "add", + "properties": { + "add": { + "$ref": "#/components/schemas/AddProposalActionInput" + } + }, + "required": [ + "add" + ], + "additionalProperties": false + } + ] + }, + "ProposalActionPreview": { + "oneOf": [ + { + "type": "object", + "title": "add", + "properties": { + "add": { + "$ref": "#/components/schemas/AddProposalActionPreview" + } + }, + "required": [ + "add" + ] + } + ] + }, + "ProposalCandidate": { + "type": "object", + "description": "Complete suggested authored content. Resource URIs are derived on output.", + "properties": { + "content": { + "$ref": "#/components/schemas/AuthoredContent" + } + }, + "required": [ + "content" + ] + }, + "ProposalCandidateInput": { + "type": "object", + "description": "Launch proposal authoring accepts FAQ content only.", + "properties": { + "content": { + "$ref": "#/components/schemas/FaqAuthoredContent" + } + }, + "required": [ + "content" + ] + }, + "ProposalCandidatePreview": { + "type": "object", + "properties": { + "content": { + "$ref": "#/components/schemas/CandidatePreviewContent" + } + }, + "required": [ + "content" + ] + }, + "ProposalEvidence": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "integer", + "maximum": 1, + "minimum": 1, + "format": "int32" + }, + "observedAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "producer": { + "$ref": "#/components/schemas/EvidenceProducer" + }, + "source": { + "$ref": "#/components/schemas/ProposalSource" + }, + "evidenceId": { + "type": "string", + "pattern": "^evd_[0-9a-f]{32}$" + } + }, + "required": [ + "evidenceId", + "observedAt", + "producer", + "schemaVersion", + "source" + ] + }, + "ProposalEvidenceDisposition": { + "type": "string", + "enum": [ + "added", + "alreadyPresent" + ] + }, + "ProposalPreview": { + "type": "object", + "description": "Compact management projection; GET the proposal for the complete candidate.", + "properties": { + "proposalId": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "status": { + "$ref": "#/components/schemas/ProposalStatus" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + }, + "proposalRevision": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "evidenceCount": { + "type": "integer", + "maximum": 100, + "minimum": 0, + "format": "int32" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last successful candidate replacement, new evidence, dismissal, or restoration\n(creation initially). Duplicate evidence, repeated dismissal/restoration, and\ntime-based expiry do not change it. New evidence, replacement, and restoration\nalso refresh inbox/purge deadlines.", + "format": "date-time" + }, + "inboxExpiresAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "purgeAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "action": { + "$ref": "#/components/schemas/ProposalActionPreview" + } + }, + "required": [ + "action", + "contentDigest", + "createdAt", + "evidenceCount", + "inboxExpiresAt", + "proposalId", + "proposalRevision", + "purgeAt", + "status", + "updatedAt" + ] + }, + "ProposalSort": { + "type": "string", + "enum": [ + "createdAt", + "inboxExpiresAt", + "purgeAt", + "updatedAt", + "evidenceCount" + ], + "default": "updatedAt" + }, + "ProposalSource": { + "type": "object", + "description": "Caller-supplied context for a proposal or supporting evidence, not verified\nattribution. Reference and content, when present, are source-specific JSON\nobjects. Playbook does not interpret their fields.", + "properties": { + "type": { + "type": "string", + "maxLength": 128, + "minLength": 1 + }, + "reference": {}, + "content": {} + }, + "required": [ + "type" + ] + }, + "ProposalStatus": { + "type": "string", + "enum": [ + "open", + "trashed", + "promoting", + "promoted" + ] + }, + "ProposalTarget": { + "description": "Exactly one target. Launch supports creating a new entry.", + "oneOf": [ + { + "type": "object", + "title": "newEntry", + "properties": { + "newEntry": { + "$ref": "#/components/schemas/NewEntryTarget" + } + }, + "required": [ + "newEntry" + ] + } + ] + }, + "ProposalTargetInput": { + "description": "Exactly one target. Request clients must send only a supported member.", + "oneOf": [ + { + "type": "object", + "title": "newEntry", + "properties": { + "newEntry": { + "$ref": "#/components/schemas/NewEntryTarget" + } + }, + "required": [ + "newEntry" + ], + "additionalProperties": false + } + ] + }, + "ProposalView": { + "type": "string", + "enum": [ + "inbox", + "trash" + ] + }, + "Publication": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "at": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "content": { + "$ref": "#/components/schemas/ContentPreview" + } + }, + "required": [ + "at", + "content", + "version" + ] + }, + "PublishFaqRequestContent": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "minimum": 1, + "format": "int32" + } + }, + "required": [ + "version" + ] + }, + "PublishFaqResponseContent": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "latestVersion": { + "type": "integer", + "minimum": 1, + "description": "Newest immutable version number. Versions are append-only, so this also equals the version count.", + "format": "int32" + }, + "publication": { + "$ref": "#/components/schemas/Publication" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last create, append, publication change, or withdrawal of an existing publication. Repeating the current publication, already-unpublished withdrawal, and reads leave it unchanged. Publication/append updates never move this timestamp backwards.", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "entryId", + "entryType", + "latestVersion", + "updatedAt" + ] + }, + "PublishSkillRequestContent": { + "type": "object", + "properties": { + "version": { + "type": "integer", + "minimum": 1, + "format": "int32" + } + }, + "required": [ + "version" + ] + }, + "PublishSkillResponseContent": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "latestVersion": { + "type": "integer", + "minimum": 1, + "description": "Newest immutable version number. Versions are append-only, so this also equals the version count.", + "format": "int32" + }, + "publication": { + "$ref": "#/components/schemas/Publication" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last create, append, publication change, or withdrawal of an existing publication. Repeating the current publication, already-unpublished withdrawal, and reads leave it unchanged. Publication/append updates never move this timestamp backwards.", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "entryId", + "entryType", + "latestVersion", + "updatedAt" + ] + }, + "ReadyStatus": { + "type": "string", + "enum": [ + "ready" + ] + }, + "ReplaceProposalRequestContent": { + "type": "object", + "properties": { + "action": { + "$ref": "#/components/schemas/ProposalActionInput" + }, + "expectedProposalRevision": { + "type": "integer", + "minimum": 1, + "format": "int32" + } + }, + "required": [ + "action", + "expectedProposalRevision" + ] + }, + "ReplaceProposalResponseContent": { + "type": "object", + "properties": { + "proposalId": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "status": { + "$ref": "#/components/schemas/ProposalStatus" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + }, + "proposalRevision": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "evidenceCount": { + "type": "integer", + "maximum": 100, + "minimum": 0, + "format": "int32" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last successful candidate replacement, new evidence, dismissal, or restoration\n(creation initially). Duplicate evidence, repeated dismissal/restoration, and\ntime-based expiry do not change it. New evidence, replacement, and restoration\nalso refresh inbox/purge deadlines.", + "format": "date-time" + }, + "inboxExpiresAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "purgeAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "action": { + "$ref": "#/components/schemas/ProposalAction" + }, + "evidence": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProposalEvidence" + }, + "maxItems": 100 + } + }, + "required": [ + "action", + "contentDigest", + "createdAt", + "evidence", + "evidenceCount", + "inboxExpiresAt", + "proposalId", + "proposalRevision", + "purgeAt", + "status", + "updatedAt" + ] + }, + "RequestValidationErrorResponseContent": { + "type": "object", + "description": "One HTTP 400 schema preserves every validation problem type in generated clients.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 400, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/RequestValidationErrorType" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldError" + }, + "description": "Optional field-level validation details, as in the shared Iris BadRequestError." + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "RequestValidationErrorType": { + "type": "string", + "description": "Machine-readable validation failure; SDKs branch on type, never title text.", + "enum": [ + "/errors/invalid-data", + "/errors/invalid-cursor", + "/errors/unknown-request-member" + ] + }, + "ResourceDescriptor": { + "type": "object", + "properties": { + "resourceId": { + "type": "string", + "pattern": "^res_[0-9a-f]{24}$", + "description": "REST and storage locator used by the exact resource-content route; not Skill identity." + }, + "uri": { + "type": "string", + "description": "Canonical URI for this exact Skill file version on the Playbook MCP server." + }, + "path": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of exact resource bytes in sha256: form." + }, + "sizeBytes": { + "type": "integer", + "format": "int64" + }, + "mimeType": { + "type": "string" + }, + "executable": { + "type": "boolean" + } + }, + "required": [ + "digest", + "executable", + "mimeType", + "path", + "resourceId", + "sizeBytes", + "uri" + ] + }, + "ResourceSuspendedErrorResponseContent": { + "type": "object", + "description": "The requested resource exists but is suspended by an administrator.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 423, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/ResourceSuspendedErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "ResourceSuspendedErrorType": { + "type": "string", + "description": "Problem type URI for suspended resources.", + "enum": [ + "/errors/resource-suspended" + ] + }, + "RestoreProposalResponseContent": { + "type": "object", + "properties": { + "proposalId": { + "type": "string", + "pattern": "^prp_[0-9a-f]{32}$" + }, + "status": { + "$ref": "#/components/schemas/ProposalStatus" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + }, + "proposalRevision": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "evidenceCount": { + "type": "integer", + "maximum": 100, + "minimum": 0, + "format": "int32" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last successful candidate replacement, new evidence, dismissal, or restoration\n(creation initially). Duplicate evidence, repeated dismissal/restoration, and\ntime-based expiry do not change it. New evidence, replacement, and restoration\nalso refresh inbox/purge deadlines.", + "format": "date-time" + }, + "inboxExpiresAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "purgeAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "action": { + "$ref": "#/components/schemas/ProposalAction" + }, + "evidence": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProposalEvidence" + }, + "maxItems": 100 + } + }, + "required": [ + "action", + "contentDigest", + "createdAt", + "evidence", + "evidenceCount", + "inboxExpiresAt", + "proposalId", + "proposalRevision", + "purgeAt", + "status", + "updatedAt" + ] + }, + "ScoredProposalPreview": { + "type": "object", + "properties": { + "proposal": { + "$ref": "#/components/schemas/ProposalPreview" + }, + "score": { + "type": "number", + "maximum": 1, + "minimum": 0, + "format": "float" + } + }, + "required": [ + "proposal", + "score" + ] + }, + "SearchFaqsRequestContent": { + "type": "object", + "properties": { + "query": { + "type": "string", + "maxLength": 4096, + "minLength": 1 + }, + "filter": { + "$ref": "#/components/schemas/Filter" + }, + "minimumScore": { + "type": "number", + "maximum": 1, + "minimum": 0, + "description": "Normalized cosine similarity floor applied to this request. Omission uses the corresponding FAQ or Skill discovery threshold.", + "format": "float" + }, + "limit": { + "type": "integer", + "maximum": 50, + "minimum": 1, + "description": "Caller-controlled top K. Omission uses discovery.maxResults; an explicit value is not capped by discovery policy.", + "format": "int32" + } + }, + "required": [ + "query" + ] + }, + "SearchFaqsResponseContent": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DiscoveredPlaybookPreviewEntry" + } + } + }, + "required": [ + "matches" + ] + }, + "SearchProposalsRequestContent": { + "type": "object", + "properties": { + "query": { + "type": "string", + "maxLength": 4096, + "minLength": 1 + }, + "minimumScore": { + "type": "number", + "maximum": 1, + "minimum": 0, + "format": "float" + }, + "limit": { + "type": "integer", + "maximum": 50, + "minimum": 1, + "description": "Maximum result count. Omission uses 10.", + "format": "int32", + "default": 10 + } + }, + "required": [ + "query" + ] + }, + "SearchProposalsResponseContent": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ScoredProposalPreview" + } + } + }, + "required": [ + "matches" + ] + }, + "SearchSkillsRequestContent": { + "type": "object", + "properties": { + "query": { + "type": "string", + "maxLength": 4096, + "minLength": 1 + }, + "filter": { + "$ref": "#/components/schemas/Filter" + }, + "minimumScore": { + "type": "number", + "maximum": 1, + "minimum": 0, + "description": "Normalized cosine similarity floor applied to this request. Omission uses the corresponding FAQ or Skill discovery threshold.", + "format": "float" + }, + "limit": { + "type": "integer", + "maximum": 50, + "minimum": 1, + "description": "Caller-controlled top K. Omission uses discovery.maxResults; an explicit value is not capped by discovery policy.", + "format": "int32" + } + }, + "required": [ + "query" + ] + }, + "SearchSkillsResponseContent": { + "type": "object", + "properties": { + "matches": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DiscoveredPlaybookPreviewEntry" + } + } + }, + "required": [ + "matches" + ] + }, + "SkillContent": { + "type": "object", + "description": "Complete parsed Skill content, including the instructions extracted from SKILL.md.\nResources contain descriptors and URIs, never file bytes. Search and discovery use the\nsmaller SkillPreviewContent, also used by StableEntry publications. Version responses use this complete shape.", + "properties": { + "format": { + "$ref": "#/components/schemas/SkillFormat" + }, + "manifest": { + "$ref": "#/components/schemas/SkillManifest" + }, + "instructions": { + "type": "string" + }, + "resources": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ResourceDescriptor" + } + } + }, + "required": [ + "format", + "instructions", + "manifest", + "resources" + ] + }, + "SkillFormat": { + "type": "string", + "enum": [ + "agent-skills" + ] + }, + "SkillManifest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$" + }, + "description": { + "type": "string", + "maxLength": 1024, + "minLength": 1 + }, + "license": { + "type": "string" + }, + "compatibility": { + "type": "string", + "maxLength": 500, + "minLength": 1 + }, + "metadata": { + "$ref": "#/components/schemas/SkillMetadata" + }, + "allowedTools": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "description", + "name" + ] + }, + "SkillMetadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "SkillPreviewContent": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "description": { + "type": "string" + }, + "uri": { + "type": "string", + "description": "Canonical SKILL.md URI for the exact published Skill version." + } + }, + "required": [ + "description", + "name", + "uri" + ] + }, + "SortOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "StableEntry": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "latestVersion": { + "type": "integer", + "minimum": 1, + "description": "Newest immutable version number. Versions are append-only, so this also equals the version count.", + "format": "int32" + }, + "publication": { + "$ref": "#/components/schemas/Publication" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last create, append, publication change, or withdrawal of an existing publication. Repeating the current publication, already-unpublished withdrawal, and reads leave it unchanged. Publication/append updates never move this timestamp backwards.", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "entryId", + "entryType", + "latestVersion", + "updatedAt" + ] + }, + "SubmitProposalEvidenceRequestContent": { + "type": "object", + "properties": { + "schemaVersion": { + "type": "integer", + "maximum": 1, + "minimum": 1, + "format": "int32" + }, + "observedAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "producer": { + "$ref": "#/components/schemas/EvidenceProducer" + }, + "source": { + "$ref": "#/components/schemas/ProposalSource" + } + }, + "required": [ + "observedAt", + "producer", + "schemaVersion", + "source" + ] + }, + "SubmitProposalEvidenceResponseContent": { + "type": "object", + "properties": { + "evidenceId": { + "type": "string", + "pattern": "^evd_[0-9a-f]{32}$" + }, + "disposition": { + "$ref": "#/components/schemas/ProposalEvidenceDisposition" + }, + "evidenceCount": { + "type": "integer", + "maximum": 100, + "minimum": 1, + "format": "int32" + }, + "updatedAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + } + }, + "required": [ + "disposition", + "evidenceCount", + "evidenceId", + "updatedAt" + ] + }, + "TimeoutErrorResponseContent": { + "type": "object", + "description": "The request timed out before the service could complete it.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 408, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/TimeoutErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "TimeoutErrorType": { + "type": "string", + "description": "Problem type URI for timeout errors.", + "enum": [ + "/errors/timeout" + ] + }, + "TooManyRequestsErrorResponseContent": { + "type": "object", + "description": "The service rejected the request because rate limits were exceeded.", + "properties": { + "title": { + "type": "string", + "description": "A short, human-readable summary of the problem\n type. It SHOULD NOT change from occurrence to occurrence of the\n problem, except for purposes of localization (e.g., using\n proactive content negotiation; see [RFC7231], Section 3.4)." + }, + "status": { + "type": "integer", + "default": 429, + "description": "The HTTP status code ([RFC7231], Section 6) generated by the origin server for this occurrence of the problem.", + "format": "int32" + }, + "detail": { + "type": "string", + "description": "A human-readable explanation specific to this occurrence of the problem." + }, + "instance": { + "type": "string", + "description": "A URI reference that identifies the specific occurrence of the problem. It may or may not yield further information if dereferenced." + }, + "type": { + "$ref": "#/components/schemas/TooManyRequestsErrorType" + } + }, + "required": [ + "status", + "title", + "type" + ] + }, + "TooManyRequestsErrorType": { + "type": "string", + "description": "Problem type URI for rate-limit errors.", + "enum": [ + "/errors/too-many-requests" + ] + }, + "Version": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "version": { + "type": "integer", + "minimum": 1, + "format": "int32" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "contentDigest": { + "type": "string", + "pattern": "^sha256:[0-9a-f]{64}$", + "description": "SHA-256 digest of normalized authored Playbook content in sha256: form. The canonicalization contract is documented as playbook-entry-v1." + }, + "content": { + "$ref": "#/components/schemas/AuthoredContent" + } + }, + "required": [ + "content", + "contentDigest", + "createdAt", + "entryId", + "entryType", + "version" + ] + }, + "WithdrawFaqResponseContent": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "latestVersion": { + "type": "integer", + "minimum": 1, + "description": "Newest immutable version number. Versions are append-only, so this also equals the version count.", + "format": "int32" + }, + "publication": { + "$ref": "#/components/schemas/Publication" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last create, append, publication change, or withdrawal of an existing publication. Repeating the current publication, already-unpublished withdrawal, and reads leave it unchanged. Publication/append updates never move this timestamp backwards.", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "entryId", + "entryType", + "latestVersion", + "updatedAt" + ] + }, + "WithdrawSkillResponseContent": { + "type": "object", + "properties": { + "entryId": { + "type": "string", + "pattern": "^ent_[0-9a-f]{32}$", + "description": "Opaque stable entry identifier." + }, + "entryKey": { + "type": "string", + "maxLength": 64, + "minLength": 1, + "pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$", + "description": "Optional Playbook-unique immutable human-readable entry key." + }, + "entryType": { + "$ref": "#/components/schemas/EntryType" + }, + "latestVersion": { + "type": "integer", + "minimum": 1, + "description": "Newest immutable version number. Versions are append-only, so this also equals the version count.", + "format": "int32" + }, + "publication": { + "$ref": "#/components/schemas/Publication" + }, + "createdAt": { + "type": "string", + "description": "UTC timestamp resolved to microsecond precision.", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last create, append, publication change, or withdrawal of an existing publication. Repeating the current publication, already-unpublished withdrawal, and reads leave it unchanged. Publication/append updates never move this timestamp backwards.", + "format": "date-time" + } + }, + "required": [ + "createdAt", + "entryId", + "entryType", + "latestVersion", + "updatedAt" + ] + }, + "PageLimit": { + "default": 50 + } + }, + "securitySchemes": { + "smithy.api.httpBearerAuth": { + "type": "http", + "description": "HTTP Bearer authentication", + "scheme": "Bearer" + } + } + }, + "security": [ + { + "smithy.api.httpBearerAuth": [] + } + ], + "tags": [ + { + "name": "Skills" + }, + { + "name": "FAQs" + }, + { + "name": "Retrieval" + }, + { + "name": "Proposals" + }, + { + "name": "Maintenance" + } + ] +} diff --git a/content/develop/ai/context-engine/agent-playbook/concepts.md b/content/develop/ai/context-engine/agent-playbook/concepts.md new file mode 100644 index 0000000000..1e7e839594 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/concepts.md @@ -0,0 +1,90 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Understand playbooks, entries, versions, publication, discovery, proposals, and evidence. +hideListLinks: true +linktitle: Concepts +title: Agent Playbooks concepts +weight: 5 +--- + +## Playbooks + +Agent Playbooks stores reusable answers and procedures as **entries**. Publish a **version** for retrieval, then revise new content while agents use that publication. + +Embeddings and semantic search match agent tasks with relevant entries. Discovery settings control similarity thresholds and result limits. Your application supplies the task text and retrieval options. + +A **proposal** inbox holds suggested answers and evidence from agent interactions for review. + +Access **grants** apply across a playbook. For example, you can give a support agent **read** access to a support playbook and give a content reviewer permission to **write** or **publish**. Use separate playbooks when workloads must not share visibility. Attribute filters help select content within a playbook, but do not replace access grants. + +Playbook API keys can grant these actions: + +| Action | What it allows | +| --------- | ------------------------------------------------------------------------------------------------------- | +| `read` | Retrieve published knowledge, list and inspect entry summaries, and read proposals. | +| `write` | Create entries, append versions, inspect drafts, delete entries, and review proposals. Includes `read`. | +| `publish` | Publish or withdraw entries and inspect versions. Includes `read`. | +| `propose` | Create proposals and attach evidence. Includes `read`. | + +`write` does not include `publish` or `propose`. You can assign authoring and publication to different people or applications. + +## Entries + +An entry is a reusable answer or procedure. Agent Playbooks supports two types: + +- A **FAQ** pairs a question with an answer. Retrieval returns the full answer directly. +- A **skill (Agent Skill)** packages instructions and optional supporting files for a procedure. Retrieval returns a preview so an agent can decide whether to load the instructions and resources. + +An entry keeps the same identifier across versions. You can also assign an immutable, human-readable entry key when you create it. Use the entry identifier to maintain existing content; creating another entry produces a separate history. + +Both types can include optional intents, attributes, and metadata: + +- **Intents** are optional task paraphrases returned on discovery to help an agent or application interpret the entry. +- **Attributes** support exact-match retrieval filters, such as locale or audience. +- **Metadata** holds additional agent or application context; retrieval filters do not search it. + +For fields and types, see the [entry model]({{< relref "/develop/ai/context-engine/agent-playbook/api-reference#schema/StableEntry" >}}). + +### Versions + +A version is a complete, immutable snapshot of an entry's authored content. Updating an entry appends a new version rather than editing the existing snapshot. + +The latest version and the published version can differ. Version 2 of a refund procedure can remain under review while agents retrieve version 1. Publishing version 2 switches retrieval to the update and retains version 1 for rollback. + + + +Management operations let you inspect drafts and historical versions with the required permissions. See the [version model]({{< relref "/develop/ai/context-engine/agent-playbook/api-reference#schema/Version" >}}) for fields and types. + +### Publication + +Publication selects the version of an entry that search and discovery can return. + +Publish a reviewed version to make it available to agents. Publish an earlier version to roll back. Withdraw the publication to stop serving the entry while retaining its history. Delete the entry to remove it and its versions permanently. + +## Discovery + +Discovery returns relevant, published FAQ answers and skill previews for an agent task. + +Similarity scores help rank relevant content. Filter attributes narrow the audience or topic. Pins let an application select known published skills or FAQs regardless of similarity. + +For skills, the agent selects a preview, loads its instructions, and fetches supporting files as needed. Your application controls how it uses that content. + +## Proposals + +A proposal is a suggested playbook entry awaiting expert review. See the [proposal model]({{< relref "/develop/ai/context-engine/agent-playbook/api-reference#schema/GetProposalResponseContent" >}}) for fields and types. + +A human or agent can suggest a question and answer, edit the candidate, or attach evidence. + +Accepting a reviewed proposal *promotes* its candidate into an *unpublished* entry. Proposal workflows currently support FAQs; skills are planned for a future release. + +### Evidence + +Evidence gives reviewers source context for a proposal. It can come from background extraction or an authorized client acting for a human or agent. See the [evidence model]({{< relref "/develop/ai/context-engine/agent-playbook/api-reference#schema/ProposalEvidence" >}}) for fields and types. + +More evidence means more submitted observations, not greater factual accuracy. Check the candidate against its sources before accepting and publishing it. + +See [Review proposals]({{< relref "/develop/ai/context-engine/agent-playbook/proposals" >}}) for the procedure. diff --git a/content/develop/ai/context-engine/agent-playbook/faqs.md b/content/develop/ai/context-engine/agent-playbook/faqs.md new file mode 100644 index 0000000000..75ddb50602 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/faqs.md @@ -0,0 +1,513 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Write answers, promote reviewed proposals, and publish FAQ versions for retrieval. +hideListLinks: true +linktitle: Manage FAQs +title: Manage FAQs +weight: 20 +--- + +Publish a FAQ to give agents an approved answer to a recurring question. Write it directly or promote a reviewed proposal. + +## Before you begin + +Use an existing playbook and an API key with `write` permission to create and maintain entries. Publishing and withdrawing require `publish`. See [access permissions]({{< relref "/develop/ai/context-engine/agent-playbook/concepts#playbooks" >}}). + +Start with the quickstart's [client setup]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart#connect-to-the-playbook" >}}), then append this page's snippets in order. Run each write once. + +## Write an answer + +Write a question in the language a user would use. Include conditions that distinguish it from similar questions, such as the product or region. Make the answer self-contained: the agent receives it without the source conversation or document. + +The sample FAQ explains where to find refund status. Use a [skill]({{< relref "/develop/ai/context-engine/agent-playbook/skills" >}}) when the agent must interpret the customer's reported stage, choose a template, and draft a reply. + +Use these fields to describe the answer and its audience: + +| Field | Use it for | +| ------------------------------ | ---------------------------------------------------------- | +| `content.payload.faq.question` | The recurring question. | +| `content.payload.faq.answer` | The approved answer. | +| `content.intents` | Optional task phrases returned with discovery previews. | +| `content.attributes` | Exact-match filters, such as audience, locale, or product. | +| `content.metadata` | Application context, such as a source-document reference. | + +Semantic matching uses the question, not the answer or intents. Use attributes to filter the audience. Metadata does not control publication. + +For example, prefer “How do I check the status of an existing refund?” to “Refund help.” Keep “How do I request a refund?” separate: it needs a different answer. + +## Create a draft FAQ + +Submit the complete content with `create_faq`. The sample assigns `refund-status` as its entry key. Keys are immutable and unique within a playbook; choose another key if that one is already in use. Save the returned entry identifier and version number. + +The new FAQ is unpublished. Review it before publishing; search and discovery do not return it yet. + +{{< multitabs id="playbook-faq-create" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +faq_content = { + "intents": ["Check a refund", "Find an existing refund"], + "attributes": {"locale": "en-US", "topic": "refund"}, + "payload": { + "faq": { + "question": "How do I check the status of a refund?", + "answer": "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number.", + } + }, +} + +draft = playbook.create_faq(content=faq_content, entry_key="refund-status") +faq_id = draft.entry.entry_id +version = draft.version.version +print(faq_id, version, draft.entry.publication) +``` + +-tab-sep- + +```typescript +const faqContent = { + intents: ["Check a refund", "Find an existing refund"], + attributes: { locale: "en-US", topic: "refund" }, + payload: { faq: { + question: "How do I check the status of a refund?", + answer: "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number.", + } }, +}; +const draft = await playbook.createFaq({ content: faqContent, entryKey: "refund-status" }); +const faqId = draft.entry.entryId; +const version = draft.version.version; +console.log(faqId, version, draft.entry.publication); +``` + +-tab-sep- + +```bash +cat > faq.json <<'JSON' +{ + "entryKey": "refund-status", + "content": { + "intents": ["Check a refund", "Find an existing refund"], + "attributes": {"locale": "en-US", "topic": "refund"}, + "payload": {"faq": { + "question": "How do I check the status of a refund?", + "answer": "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number." + }} + } +} +JSON +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @faq.json > draft.json +FAQ_ID=$(jq -er '.entry.entryId' draft.json) +VERSION=$(jq -er '.version.version' draft.json) +jq '{entryId: .entry.entryId, version: .version.version, publication: .entry.publication}' draft.json +``` + +{{< /multitabs >}} + +## Promote a reviewed proposal + +If an answer comes from the proposal inbox, [review the candidate and its evidence]({{< relref "/develop/ai/context-engine/agent-playbook/proposals#review-and-edit-a-candidate" >}}) first. Accept the proposal with the revision you reviewed. + +Acceptance returns an entry and version for one unpublished FAQ. Save those values and continue with publication. Promotion does not make the answer available to agents, and it does not update an existing FAQ. + +## Publish a version + +1. Retrieve the exact version you intend to publish. +2. Review its answer, intents, and audience attributes. +3. Publish that version with `publish_faq`. +4. Test retrieval with a reader key and a representative question. + +Test a paraphrase such as “Where can I see my refund?” and a nearby question the answer does not cover, such as “How do I request a refund?” Check the returned entry and audience, then use the [retrieval guide]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval" >}}) to tune matching. + +{{< multitabs id="playbook-faq-publish" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +stored = playbook.get_faq_version(faq_id=faq_id, version=version) +print(stored.content) +``` + +-tab-sep- + +```typescript +const stored = await playbook.getFaqVersion(faqId, version); +console.log(stored.content); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/faqs/$FAQ_ID/versions/$VERSION" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq .content +``` + +{{< /multitabs >}} + +Publish the version you reviewed. + +{{< multitabs id="playbook-faq-publication" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.publish_faq(faq_id=faq_id, version=version) +``` + +-tab-sep- + +```typescript +await playbook.publishFaq(faqId, { version }); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/faqs/$FAQ_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq -n --argjson version "$VERSION" '{version: $version}')" | jq .publication +``` + +{{< /multitabs >}} + +{{< multitabs id="playbook-faq-search" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +results = playbook.search_faqs( + query="Where can I see how my refund is progressing?", + filter_={"eq": {"name": "locale", "value": "en-US"}}, + limit=5, +) +for match in results.matches: + if match.faq is not None: + print(match.entry_id, match.version, match.score, match.faq.answer) +``` + +-tab-sep- + +```typescript +const results = await playbook.searchFaqs({ + query: "Where can I see how my refund is progressing?", + filter: { eq: { name: "locale", value: "en-US" } }, + limit: 5, +}); +for (const match of results.matches) { + if (match.faq) { + console.log(match.entryId, match.version, match.score, match.faq.answer); + } +} +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Where can I see how my refund is progressing?","filter":{"eq":{"name":"locale","value":"en-US"}},"limit":5}' \ + | jq '.matches[] | {entryId, version, score, answer: .faq.answer}' +``` + +{{< /multitabs >}} + +## Update an answer + +Append a complete replacement version to the same entry with `append_faq_version`. Include the intents, attributes, and metadata you want that version to contain. + +Coordinate publishers for the same entry. Concurrent publication changes are last-write-wins. Publishing an already published version is a no-op. + +{{< multitabs id="playbook-faq-update" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +from copy import deepcopy + +updated_content = deepcopy(faq_content) +updated_content["payload"]["faq"]["answer"] += " Support can check the current stage but cannot guarantee a bank posting date." +updated = playbook.append_faq_version(faq_id=faq_id, content=updated_content) +print("Review this version:", updated.version.version, updated.version.content) +``` + +-tab-sep- + +```typescript +const updatedContent = structuredClone(faqContent); +updatedContent.payload.faq.answer += " Support can check the current stage but cannot guarantee a bank posting date."; +const updated = await playbook.appendFaqVersion(faqId, { content: updatedContent }); +console.log("Review this version:", updated.version.version, updated.version.content); +``` + +-tab-sep- + +```bash +jq 'del(.entryKey) | .content.payload.faq.answer += " Support can check the current stage but cannot guarantee a bank posting date."' \ + faq.json > faq-update.json +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/$FAQ_ID/versions" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @faq-update.json > updated.json +jq .version updated.json +``` + +{{< /multitabs >}} + +The identifier stays the same, and search still serves the first publication. Review and publish the new version, then search again. + +{{< multitabs id="playbook-faq-republish" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.publish_faq(faq_id=faq_id, version=updated.version.version) +``` + +-tab-sep- + +```typescript +await playbook.publishFaq(faqId, { version: updated.version.version }); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/faqs/$FAQ_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq '{version: .version.version}' updated.json)" | jq .publication +``` + +{{< /multitabs >}} + +To roll back, publish the first version retained in `version` (Python and TypeScript) or `VERSION` (HTTP). + +{{< multitabs id="playbook-faq-rollback" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.publish_faq(faq_id=faq_id, version=version) +``` + +-tab-sep- + +```typescript +await playbook.publishFaq(faqId, { version }); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/faqs/$FAQ_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq -n --argjson version "$VERSION" '{version: $version}')" | jq .publication +``` + +{{< /multitabs >}} + +## List entries and inspect history + +Use `list_faqs` to list entries, including unpublished ones, and `get_faq` to read a summary. Both require `read`. Summaries show the latest version number and any publication, without draft content. + +Follow `nextCursor` (`next_cursor` in Python) until it is absent, even after an empty page. Keep filters and sort settings unchanged. The optional first-page `total` is a separately measured count; absence means unknown, not zero. Lists can change while you page through them. + +{{< multitabs id="playbook-faq-list" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +cursor = None +while True: + page = playbook.list_faqs(sort_by="updatedAt", sort_order="desc", limit=50, cursor=cursor) + if cursor is None: + print("Total FAQs:", page.total if page.total is not None else "unknown") + for entry in page.faqs: + print(entry.entry_id, entry.latest_version, entry.publication) + cursor = page.next_cursor + if cursor is None: + break +``` + +-tab-sep- + +```typescript +let faqCursor: string | undefined; +while (true) { + const page = await playbook.listFaqs( + "updatedAt", "desc", undefined, undefined, undefined, 50, faqCursor, + ); + if (faqCursor === undefined) console.log("Total FAQs:", page.total ?? "unknown"); + for (const entry of page.faqs) console.log(entry.entryId, entry.latestVersion, entry.publication); + faqCursor = page.nextCursor; + if (faqCursor === undefined) break; +} +``` + +-tab-sep- + +```bash +CURSOR='' +while true; do + PAGE_ARGS=(--data-urlencode sortBy=updatedAt --data-urlencode sortOrder=desc --data-urlencode limit=50) + if [[ -n "$CURSOR" ]]; then PAGE_ARGS+=(--data-urlencode "cursor=$CURSOR"); fi + curl --fail-with-body --silent --show-error --get "$PLAYBOOK_API/faqs" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" "${PAGE_ARGS[@]}" > faq-page.json + if [[ -z "$CURSOR" ]]; then jq '.total // "unknown"' faq-page.json; fi + jq '.faqs[] | {entryId, latestVersion, publication}' faq-page.json + CURSOR=$(jq -r '.nextCursor // empty' faq-page.json) + [[ -n "$CURSOR" ]] || break +done +``` + +{{< /multitabs >}} + +To select only published or unpublished entries, set `published` to `true` or `false`. Omit it to include both. The TypeScript client takes this as the fifth argument to `listFaqs`. + +Listing full version history requires `write` or `publish`. Versions arrive newest first. This example uses the FAQ created in this guide: + +{{< multitabs id="playbook-faq-history" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +cursor = None +while True: + page = playbook.list_faq_versions(faq_id=faq_id, limit=50, cursor=cursor) + for item in page.versions: + print(item.version, item.content_digest, item.content) + cursor = page.next_cursor + if cursor is None: + break +``` + +-tab-sep- + +```typescript +let faqVersionCursor: string | undefined; +while (true) { + const page = await playbook.listFaqVersions(faqId, undefined, undefined, 50, faqVersionCursor); + for (const item of page.versions) console.log(item.version, item.contentDigest, item.content); + faqVersionCursor = page.nextCursor; + if (faqVersionCursor === undefined) break; +} +``` + +-tab-sep- + +```bash +CURSOR='' +while true; do + PAGE_ARGS=(--data-urlencode limit=50) + if [[ -n "$CURSOR" ]]; then PAGE_ARGS+=(--data-urlencode "cursor=$CURSOR"); fi + curl --fail-with-body --silent --show-error --get "$PLAYBOOK_API/faqs/$FAQ_ID/versions" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" "${PAGE_ARGS[@]}" > faq-versions.json + jq '.versions[] | {version, contentDigest, content}' faq-versions.json + CURSOR=$(jq -r '.nextCursor // empty' faq-versions.json) + [[ -n "$CURSOR" ]] || break +done +``` + +{{< /multitabs >}} + +## Look up entry state + +Use `lookup_entries` with `write` or `publish` permission to inspect known FAQs or skills. Supply 1–100 references, each containing either an entry identifier or an entry key. Results preserve request order and report missing entries explicitly. The batch is not a fixed snapshot. + +Each found entry includes `latest`, optional `publication`, and `versionCount`. The response also reports the playbook's `maxVersions` limit. The two version summaries include `contentDigest`: a server-generated hash of normalized authored content. Compare their digests to check whether the latest and published content match. It is not a hash of the uploaded ZIP or the raw request, and it is not an idempotency key. + +{{< multitabs id="playbook-faq-lookup" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +from redis_agent_playbook.models import Found + +lookup = playbook.lookup_entries(references=[ + {"entry_id": faq_id}, {"entry_key": "refund-status"}, +]) +print("Version limit:", lookup.max_versions) +for result in lookup.results: + if isinstance(result, Found): + state = result.found.entry + print(state.entry_id, state.version_count, state.latest, state.publication) + else: + print("Missing:", result.missing.reference) +``` + +-tab-sep- + +```typescript +const lookup = await playbook.lookupEntries({ + references: [{ entryId: faqId }, { entryKey: "refund-status" }], +}); +console.log("Version limit:", lookup.maxVersions); +for (const result of lookup.results) { + if ("found" in result) { + const state = result.found.entry; + console.log(state.entryId, state.versionCount, state.latest, state.publication); + } else { + console.log("Missing:", result.missing.reference); + } +} +``` + +-tab-sep- + +```bash +jq -n --arg id "$FAQ_ID" '{references: [{entryId: $id}, {entryKey: "refund-status"}]}' \ + | curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/entries/lookup" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @- \ + | jq '{maxVersions, results}' +``` + +{{< /multitabs >}} + +The example finds the same FAQ by identifier and by key. Either identifies the entry across versions, not a particular publication. Before retrying an uncertain write, inspect its state and relevant version. Appending identical content can still create another version. + +## Withdraw or delete an FAQ + +Use `withdraw_faq` to stop serving the answer while retaining the entry and its versions. Use `delete_faq` to remove the entry and its history permanently. + +{{< multitabs id="playbook-faq-withdraw" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.withdraw_faq(faq_id=faq_id) +``` + +-tab-sep- + +```typescript +await playbook.withdrawFaq(faqId); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X DELETE "$PLAYBOOK_API/faqs/$FAQ_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq . +``` + +{{< /multitabs >}} + +When you no longer need the sample history, delete the entry. + +{{< multitabs id="playbook-faq-delete" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.delete_faq(faq_id=faq_id) +``` + +-tab-sep- + +```typescript +await playbook.deleteFaq(faqId); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X DELETE "$PLAYBOOK_API/faqs/$FAQ_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" +``` + +{{< /multitabs >}} diff --git a/content/develop/ai/context-engine/agent-playbook/limits.md b/content/develop/ai/context-engine/agent-playbook/limits.md new file mode 100644 index 0000000000..ca2bdcc7b2 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/limits.md @@ -0,0 +1,200 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Find content and retrieval limits, proposal retention, error types, and retry guidance. +hideListLinks: true +linktitle: Limits and errors +title: Service limits and errors +weight: 90 +--- + +Check these limits when preparing content or handling request failures. Confirm deployment overrides with your administrator. + +The [API reference]({{< relref "/develop/ai/context-engine/agent-playbook/api-reference" >}}) defines field formats and operation-specific schemas. + +## Content limits + +These defaults can be lower in your deployment. Byte limits apply to decoded content, not base64 length. A kibibyte (KiB) is 1,024 bytes; a mebibyte (MiB) is 1,024 KiB. + +| Content | Default ceiling | +|:--------|:----------------| +| Entries per playbook | 10,000 | +| Versions per entry | 100 | +| Retained content across an entry's versions | 256 MiB | +| Files per skill version, including `SKILL.md` | 64 | +| ZIP archive entries, including directories | 256 | +| Compressed skill archive | 5 MiB | +| Expanded skill archive | 20 MiB | +| Individual skill file | 5 MiB | +| Skill instructions file (`SKILL.md`) | 1 MiB | +| Version metadata | 64 KiB, nesting depth 16 | + +Retained data includes version content and resources. Withdrawal does not free capacity; deletion removes the entry's history. You cannot delete an individual version. + +These field and structure limits also apply: + +| Field or structure | Limit | +|:-------------------|:------| +| FAQ question | 2,048 characters | +| FAQ answer | 32,768 characters | +| Intents | Five per entry version; 2,048 characters each | +| Attributes | 32 names; up to 20 scalar items in each list value | +| Entry key or skill name | 64 characters | +| Skill description | 1,024 characters | +| Skill compatibility text | 500 characters | + +## Retrieval limits + +| Request or setting | Limit or default | +|:-------------------|:-----------------| +| Search or discovery query | 1–4,096 characters | +| Type-specific search results | 1–50; omission uses the playbook's result default | +| Discovery results | 1–50, bounded by `discovery.maxResults`; default `10` | +| Similarity thresholds | `0`–`1`; default `0.7` for each entry type | +| Discovery preview ceiling | Disabled by default; minimum configured value `64` estimated tokens | +| Filter depth | Five | +| Equality predicates in a filter | 64 | +| Management list page | 1–200 results; default `50` | +| Entry lookup or bulk deletion | 1–100 entry references or identifiers | + +Pins consume discovery slots and preview budget. The preview ceiling applies to discovery rather than type-specific search, loaded instructions, or fetched files. See [Retrieve knowledge]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval" >}}) for the behavior of each setting. + +Follow a list response's next cursor until it is absent, even if a page is empty. Keep the collection, filters, and ordering consistent between pages. Lists reflect concurrent changes and do not represent a fixed snapshot. + +Entry and proposal lists can include a separately measured first-page `total`. Absence means unknown, not zero. Cursored pages and version history omit totals. Use the cursor, not the count or page length, to decide when to stop. See [List entries and inspect history]({{< relref "/develop/ai/context-engine/agent-playbook/faqs#list-entries-and-inspect-history" >}}) for examples. + +## Proposal and evidence limits + +| Limit or policy | Value | +|:----------------|:------| +| Open proposals per playbook | Default 5,000; configurable from 1 to 10,000 | +| Proposal review window | Default 30 days; configurable from 1 to 365 days | +| Evidence records per proposal | 100 | +| Total evidence payload per proposal | 256 KiB | +| Individual evidence payload | 12 KiB | +| Evidence schema version | `1` | +| Proposal search results | 1–50; default `10` | + +Unreviewed proposals leave the inbox at `inboxExpiresAt` and become unavailable after `purgeAt`. With the default policy, creation, new evidence, candidate replacement, and restoration set deadlines of 30 days for inbox review and 60 days for retention. Dismissing a proposal moves it to trash immediately and starts a 30-day retention window. An exact evidence retry does not refresh the deadlines. + +Proposal retention does not affect an accepted FAQ's history. Store required review records outside the inbox. + +## Handle request errors + +HTTP errors include a status and machine-readable `type`. Use the type to choose a recovery action. For MCP, inspect `isError` before reading tool content. See [MCP troubleshooting]({{< relref "/develop/ai/context-engine/agent-playbook/mcp#troubleshoot-the-connection" >}}). + +| Status | Meaning | Action | +|:-------|:--------|:-------| +| `400` | Invalid input, cursor, or unsupported request member. | Correct fields or restore the cursor's original request parameters. | +| `401` | Authentication failed. | Check the endpoint and key, including expiry or revocation. | +| `403` | The caller lacks access. | Check tenant, playbook, and action grants. `write` does not include `publish`. | +| `404` | The requested resource is unavailable. | Check identifiers and retention. Rediscover a previously selected skill once. | +| `409` | Content or review state conflicts with the request. | Inspect the error type and current state before changing the request. | +| `413` | Content exceeds a payload limit. | Reduce the package, metadata, answer, or evidence payload. | +| `423` | The playbook is suspended. | Ask the administrator to inspect its state. | +| `424` | A required dependency failed. | Check playbook health and ask the administrator to inspect the dependency. | +| `429` | Requests are throttled. | Use bounded backoff and honor `Retry-After` when supplied. | +| `500` | An internal or content-integrity failure occurred. | Preserve safe diagnostic details and report persistent failures. Stored integrity failures require administrator investigation. | +| `503` | The service or playbook is unavailable. | Check readiness and use bounded retries for safe operations. | + +For example, inspect a failed entry read without retrying it automatically. Use the [client setup]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart#connect-to-the-playbook" >}}) and export the FAQ identifier you want to inspect: + +```bash +export PLAYBOOK_FAQ_ID='' +``` + +{{< multitabs id="playbook-error-type" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +import json +from redis_agent_playbook.errors import PlaybookError + +try: + entry = playbook.get_faq(faq_id=os.environ["PLAYBOOK_FAQ_ID"]) + print(entry) +except PlaybookError as error: + try: + problem = json.loads(error.body) + except ValueError: + problem = {} + print("Request failed:", error.status_code, problem.get("type", "unknown")) + raise +``` + +-tab-sep- + +```typescript +const { PlaybookError } = await import("@redis-iris/agent-playbook/models/errors"); +try { + console.log(await playbook.getFaq(requiredEnv("PLAYBOOK_FAQ_ID"))); +} catch (error) { + if (error instanceof PlaybookError) { + let problem: { type?: string } = {}; + try { problem = JSON.parse(error.body); } catch { /* The body may not be JSON. */ } + console.error("Request failed:", error.statusCode, problem.type ?? "unknown"); + } + throw error; +} +``` + +-tab-sep- + +```bash +STATUS=$(curl --silent --show-error -o response.json -w '%{http_code}' \ + "$PLAYBOOK_API/faqs/$PLAYBOOK_FAQ_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY") +if [[ "$STATUS" == 2* ]]; then + jq . response.json +else + printf 'Request failed: HTTP %s\n' "$STATUS" >&2 + jq -r '.type // "unknown"' response.json >&2 2>/dev/null || printf 'unknown\n' >&2 + exit 1 +fi +``` + +{{< /multitabs >}} + +## Resolve conflicts + +The `409` error type identifies the recovery step: + +| Error type | Action | +|:-----------|:-------| +| `/errors/entry-not-published` | Refresh retrieval or publish the intended version. | +| `/errors/entry-key-conflict` | Use the existing entry or choose another key. | +| `/errors/content-limit-exceeded` | Free retained capacity or confirm a higher supported deployment limit. | +| `/errors/pinned-entry-unavailable` | Correct or remove the pin. | +| `/errors/proposal-already-exists` | Inspect `existingProposalId` or attach evidence to that proposal. | +| `/errors/proposal-evidence-conflict` | Retry the original evidence payload or use a new evidence identifier for a new observation. | +| `/errors/proposal-evidence-capacity` | Review the proposal with its retained evidence; additional evidence does not fit. | +| `/errors/proposal-revision-conflict` | Read and review the current candidate before resubmitting. | +| `/errors/proposal-trashed` | Restore the proposal before modifying or accepting it. | +| `/errors/proposal-open` | Dismiss the proposal before purging it. | +| `/errors/proposal-promotion-locked` | Resume acceptance or use its recorded FAQ. Candidate edits and evidence changes are locked. | + +## Retry uncertain operations + +Use bounded retries for reads and transient failures. Proposal acceptance can resume its recorded target. For an exact evidence retry, keep the same identifier and payload. + +A timeout, including `408`, does not prove a create or append failed. Read state before retrying to avoid another entry or version. Check the Python SDK retry configuration when controlling write retries. + +Bulk deletion is not atomic across the batch. Earlier deletions can remain applied after a later failure. Retrying the same batch is safe because absent entries are ignored. + +## Remove content in bulk + + + +Use a key with `write` permission for bulk deletion or clearing. Choose the scope explicitly before sending a request: + +| Task | Python method | TypeScript method | HTTP request | +|:-----|:--------------|:------------------|:-------------| +| Delete selected FAQ entries and all their versions | `delete_faqs(entry_ids=...)` | `deleteFaqs({ entryIds: ... })` | `DELETE /v1/playbooks/{playbookId}/faqs` with an `entryIds` array | +| Delete selected skills, versions, and files | `delete_skills(entry_ids=...)` | `deleteSkills({ entryIds: ... })` | `DELETE /v1/playbooks/{playbookId}/skills` with an `entryIds` array | +| Clear a playbook's entries, proposals, or both | `clear_playbook(scope=...)` | `clearPlaybook(scope)` | `POST /v1/playbooks/{playbookId}/clear?scope=...` | + +Bulk deletion accepts 1–100 entry identifiers of the selected type. Clearing accepts `entries`, `proposals`, or `all`; omitting the scope means `all`. Clearing preserves the playbook and its configuration, but permanently removes the selected content. It can make partial progress before a failure. The returned `deletedEntriesCount` counts entries removed by that request, not proposals. + +For ordinary sample cleanup, use the single-entry deletion steps in [Manage FAQs]({{< relref "/develop/ai/context-engine/agent-playbook/faqs#withdraw-or-delete-an-faq" >}}) or [Manage skills]({{< relref "/develop/ai/context-engine/agent-playbook/skills#withdraw-or-delete-a-skill" >}}). diff --git a/content/develop/ai/context-engine/agent-playbook/mcp.md b/content/develop/ai/context-engine/agent-playbook/mcp.md new file mode 100644 index 0000000000..9d8409a0c7 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/mcp.md @@ -0,0 +1,244 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Connect Agent Playbooks to an agent through MCP and let the agent retrieve published knowledge as needed. +hideListLinks: true +linktitle: MCP +title: Connect with MCP +weight: 60 +--- + +Use MCP to let an agent discover knowledge, load a skill, and fetch its resources. This example produces a customer reply from a reported refund stage using a published skill and template. + +## Before you begin + +Get the data-plane endpoint, playbook identifier, and an API key with `read` permission. Export `PLAYBOOK_URL`, `PLAYBOOK_ID`, and `PLAYBOOK_API_KEY` as shown in [Save connection values]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart#save-connection-values" >}}). + +Publish `refund-support-reply` with [Manage skills]({{< relref "/develop/ai/context-engine/agent-playbook/skills" >}}) and keep it published while testing. The [quickstart FAQ]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart" >}}) is optional; this task uses the skill to draft a reply. + +The Python and TypeScript examples use the [OpenAI Agents SDK](https://developers.openai.com/api/docs/guides/agents/integrations-observability). The HTTP example uses the [OpenAI Responses API with remote MCP](https://developers.openai.com/api/docs/guides/tools-connectors-mcp). Set an OpenAI key for model calls: + +```bash +export OPENAI_API_KEY='' +``` + +Install the dependencies for your chosen tab: + +{{< multitabs id="playbook-mcp-install" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```bash +python -m pip install openai-agents==0.23.1 mcp==1.30.0 +``` + +-tab-sep- + +```bash +npm install @openai/agents@0.19.0 +npm install --save-dev tsx typescript @types/node +``` + +-tab-sep- + +```bash +curl --version +jq --version +``` + +{{< /multitabs >}} + +## Configure the connection + +Use these settings with an agent framework that supports Streamable HTTP and custom request headers: + +| Setting | Value | +| -------------- | ------------------------------------------------------ | +| Transport | Streamable HTTP | +| Endpoint | `/v1/playbooks//mcp` | +| Request header | `Authorization: Bearer ` | + +Python and TypeScript connect from your application; the endpoint must be reachable there. The HTTP example connects from OpenAI and needs an `https://` endpoint reachable from OpenAI. A local address works only for an MCP client on the same host. + +Keep the playbook key in connection settings and `OPENAI_API_KEY` in the environment. They authenticate knowledge access and model requests, respectively. Keep both out of agent instructions. + +## Connect an agent + +Attach the MCP connection to your agent. The SDK or Responses API executes the model's tool calls. Agent Playbooks supplies the skill; the model drafts the reply. + +Save the Python example as `mcp_example.py` or the TypeScript example as `mcp-example.mts`. Run `python mcp_example.py` or `npx tsx mcp-example.mts`. Run the HTTP example in Bash. + +{{< multitabs id="playbook-mcp-example" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +import asyncio +import os +from agents import Agent, Runner +from agents.mcp import MCPServerStreamableHttp + +async def main(): + url = f"{os.environ['PLAYBOOK_URL'].rstrip('/')}/v1/playbooks/{os.environ['PLAYBOOK_ID']}/mcp" + async with MCPServerStreamableHttp( + name="Agent Playbooks", + params={ + "url": url, + "headers": {"Authorization": f"Bearer {os.environ['PLAYBOOK_API_KEY']}"}, + }, + cache_tools_list=True, + client_session_timeout_seconds=30, + ) as playbook: + agent = Agent( + name="Support assistant", + model="gpt-5-mini", + instructions=( + "Use Agent Playbooks to find published guidance before answering. " + "For reply-drafting tasks, call load_skill with the entry identifier and version from discovery. " + "Fetch the reply template using its uri from load_skill; never construct a file URI. " + "Follow the skill instructions and return the completed customer reply. " + "If no relevant knowledge is available or a tool fails, say so." + ), + mcp_servers=[playbook], + ) + + result = await Runner.run( + agent, + "Draft a customer reply using the refund-support-reply skill and template. " + "The customer says their refund stage is Processing.", + ) + for item in result.new_items: + if item.type == "tool_call_item" and getattr(item.raw_item, "type", None) == "function_call": + print("Tool:", item.raw_item.name, item.raw_item.arguments) + print(result.final_output) + +asyncio.run(main()) +``` + +-tab-sep- + +```typescript +import { Agent, MCPServerStreamableHttp, run } from "@openai/agents"; + +function requiredEnv(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`Set ${name}`); + return value; +} +const url = `${requiredEnv("PLAYBOOK_URL").replace(/\/$/, "")}/v1/playbooks/${requiredEnv("PLAYBOOK_ID")}/mcp`; +const playbook = new MCPServerStreamableHttp({ + name: "Agent Playbooks", + url, + requestInit: { + headers: { Authorization: `Bearer ${requiredEnv("PLAYBOOK_API_KEY")}` }, + }, + cacheToolsList: true, + clientSessionTimeoutSeconds: 30, +}); + +try { + await playbook.connect(); + const agent = new Agent({ + name: "Support assistant", + model: "gpt-5-mini", + instructions: + "Use Agent Playbooks to find published guidance before answering. " + + "For reply-drafting tasks, call load_skill with the entry identifier and version from discovery. " + + "Fetch the reply template using its uri from load_skill; never construct a file URI. " + + "Follow the skill instructions and return the completed customer reply. " + + "If no relevant knowledge is available or a tool fails, say so.", + mcpServers: [playbook], + }); + const result = await run( + agent, + "Draft a customer reply using the refund-support-reply skill and template. " + + "The customer says their refund stage is Processing.", + ); + for (const item of result.newItems) { + if (item.type === "tool_call_item" && item.rawItem.type === "function_call") { + console.log("Tool:", item.rawItem.name, item.rawItem.arguments); + } + } + console.log(result.finalOutput); +} finally { + await playbook.close(); +} +``` + +-tab-sep- + +```bash +set -euo pipefail +MCP_URL="${PLAYBOOK_URL%/}/v1/playbooks/$PLAYBOOK_ID/mcp" + +jq -n --arg url "$MCP_URL" --arg key "$PLAYBOOK_API_KEY" '{ + model: "gpt-5-mini", + instructions: ( + "Use Agent Playbooks to find published guidance before answering. " + + "For reply-drafting tasks, call load_skill with the entry identifier and version from discovery. " + + "Fetch the reply template using its uri from load_skill; never construct a file URI. " + + "Follow the skill instructions and return the completed customer reply. " + + "If no relevant knowledge is available or a tool fails, say so." + ), + tools: [{ + type: "mcp", + server_label: "playbook", + server_url: $url, + headers: {Authorization: ("Bearer " + $key)}, + allowed_tools: ["discover_entries", "load_skill", "fetch_resource"], + require_approval: "never" + }], + input: ( + "Draft a customer reply using the refund-support-reply skill and template. " + + "The customer says their refund stage is Processing." + ) +}' | curl --fail-with-body --silent --show-error \ + "${OPENAI_BASE_URL:-https://api.openai.com/v1}/responses" \ + -H "Authorization: Bearer $OPENAI_API_KEY" \ + -H 'Content-Type: application/json' \ + --data-binary @- > agent-response.json +jq -r '.output[] | select(.type == "mcp_call") | "Tool: \(.name) \(.arguments)"' agent-response.json +jq -r '.output[] | select(.type == "message") | .content[] | select(.type == "output_text") | .text' agent-response.json +``` + +{{< /multitabs >}} + +Check the printed calls for `discover_entries`, `load_skill`, and `fetch_resource`: they select the skill, load its instructions, and read the template. The model chooses the calls, so order and count can vary. The HTTP example allows them without per-call approval. + +With the sample template, a completed reply is: + +```text +Your order history shows "Processing". Support can help if you need more information about that stage. +``` + +Check that the reply preserves “Processing,” contains no placeholders, and promises no payment date. Also try these tasks: + +| Task | Check | +| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- | +| “Draft a reply for a customer who says no refund is listed.” | Uses the missing refund template and asks the customer to contact support with their order number. | +| “Draft a refund reply.” | Asks what order history shows instead of inventing a stage. | +| “How do I request a new refund?” | Avoids using the status-reply skill for a different procedure. | + +For an answer lookup, ask “Where can I find refund status?” with the sample FAQ published. Its answer can be used directly without loading a skill. If retrieval returns no relevant knowledge or a tool fails, the agent should report that rather than invent guidance. + +For selection controls, see [Discovery]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#discovery" >}}). For skill loading behavior, see [Load skills with progressive disclosure]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#load-skills-with-progressive-disclosure" >}}). + +## Read skill files as MCP resources + +If your client supports standard MCP resources, it can read the same published skill files without calling `fetch_resource`. Use the URI returned by discovery or `load_skill`. + +| Method | Request | Result | +| -------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------- | +| `resources/templates/list` | List resource templates on the initialized connection. | The template for published skill files. This is not an inventory of skills. | +| `resources/read` | Set `uri` to a returned skill file URI. | A `contents` array with the URI, media type, and file content. | + +Read `text` when present. Otherwise, base64-decode `blob` to get the file bytes. An empty file can use an empty blob, so check which field is present rather than whether its value is nonempty. The `fetch_resource` tool uses the same text/blob distinction and also returns a file digest. Standard resource results do not include that digest field. + +Both methods use the same connection and reader key as the tools. File reads enforce the current publication even with a management key. If the publication changes between discovery and reading, rediscover and reassess the selected skill. Standard resource methods are client operations; an agent framework that exposes only tools can continue to use `fetch_resource`. + +## Troubleshoot the connection + +- If the connection fails, check the full MCP endpoint, network reachability, and reader key. A hosted client cannot reach your application's local address. +- If the agent has no playbook tools, check that the connection is attached through `mcp_servers`, `mcpServers`, or the HTTP request's `tools` field. +- If retrieval fails or returns no matches, inspect the agent run's tool results. Follow the Retrieval guide's guidance on [pins]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#pin-required-knowledge" >}}), [preview limits]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#limit-discovery-previews" >}}), and [publication changes]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#load-skills-with-progressive-disclosure" >}}). + +Preserve error details for diagnosis without logging keys or private content. See [service limits and errors]({{< relref "/develop/ai/context-engine/agent-playbook/limits" >}}) for request constraints and recovery. diff --git a/content/develop/ai/context-engine/agent-playbook/proposals.md b/content/develop/ai/context-engine/agent-playbook/proposals.md new file mode 100644 index 0000000000..342bae8e08 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/proposals.md @@ -0,0 +1,567 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Create proposals, attach evidence, review candidates, and promote approved answers into unpublished FAQs. +hideListLinks: true +linktitle: Review proposals +title: Review proposals +weight: 40 +--- + +Review suggested answers as proposals. Check each candidate against its evidence, accept it into an unpublished FAQ, and publish the reviewed version. + +The current proposal workflow adds new FAQs. It does not create skills or revise existing entries. + +## Before you begin + +Use an existing playbook and API keys with these permissions: + +- `propose` to create candidates and attach evidence. +- `write` to edit, accept, dismiss, restore, or purge proposals. +- `publish` to publish the FAQ returned by acceptance. + +Start with the quickstart's [client setup]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart#connect-to-the-playbook" >}}), then append this page's snippets in order. Run each write once. + +This walkthrough needs all three permissions on `PLAYBOOK_API_KEY`. Applications can use separate keys for suggestion, review, and publication. + +## Find proposals to review + +A key with `read` permission can list, search, and inspect proposals. The inbox contains open candidates and acceptances in `promoting` state. Only open candidates can be edited. Read the complete proposal and its evidence before deciding what to accept. + +Follow the cursor until it is absent, even after an empty page. Keep the view and sort settings unchanged. An absent first-page `total` means unknown. + +{{< multitabs id="playbook-proposal-inbox" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +cursor = None +while True: + page = playbook.list_proposals(view="inbox", sort_by="updatedAt", sort_order="desc", limit=50, cursor=cursor) + if cursor is None: + print("Total inbox proposals:", page.total if page.total is not None else "unknown") + for item in page.proposals: + print(item.proposal_id, item.status, item.evidence_count, item.action) + cursor = page.next_cursor + if cursor is None: + break +``` + +-tab-sep- + +```typescript +let inboxCursor: string | undefined; +while (true) { + const page = await playbook.listProposals("inbox", "updatedAt", "desc", 50, inboxCursor); + if (inboxCursor === undefined) console.log("Total inbox proposals:", page.total ?? "unknown"); + for (const item of page.proposals) console.log(item.proposalId, item.status, item.evidenceCount, item.action); + inboxCursor = page.nextCursor; + if (inboxCursor === undefined) break; +} +``` + +-tab-sep- + +```bash +CURSOR='' +while true; do + PAGE_ARGS=(--data-urlencode view=inbox --data-urlencode sortBy=updatedAt --data-urlencode sortOrder=desc --data-urlencode limit=50) + if [[ -n "$CURSOR" ]]; then PAGE_ARGS+=(--data-urlencode "cursor=$CURSOR"); fi + curl --fail-with-body --silent --show-error --get "$PLAYBOOK_API/proposals" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" "${PAGE_ARGS[@]}" > inbox.json + if [[ -z "$CURSOR" ]]; then jq '.total // "unknown"' inbox.json; fi + jq '.proposals[] | {proposalId, status, evidenceCount, action}' inbox.json + CURSOR=$(jq -r '.nextCursor // empty' inbox.json) + [[ -n "$CURSOR" ]] || break +done +``` + +{{< /multitabs >}} + +Search open proposals before submitting another candidate. Search excludes trash, promotion history, and published FAQs. Review similar questions to check whether they need the same answer. + +{{< multitabs id="playbook-proposal-search" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +related = playbook.search_proposals(query="Contact support about a missing refund", minimum_score=0.7, limit=5) +for match in related.matches: + print(match.proposal.proposal_id, match.score, match.proposal.action) +``` + +-tab-sep- + +```typescript +const related = await playbook.searchProposals({ + query: "Contact support about a missing refund", minimumScore: 0.7, limit: 5, +}); +for (const match of related.matches) console.log(match.proposal.proposalId, match.score, match.proposal.action); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/proposals/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Contact support about a missing refund","minimumScore":0.7,"limit":5}' \ + | jq '.matches[] | {proposalId: .proposal.proposalId, score, action: .proposal.action}' +``` + +{{< /multitabs >}} + +## Create a proposal + +Prepare a question and answer, with intents describing the task and attributes for audience filters. Submit them with `create_proposal` as an `add` action targeting `newEntry`. You can supply an entry key for the FAQ that acceptance will create. + +The candidate enters the inbox without changing what agents retrieve. + +Save the returned proposal identifier and `proposalRevision`. The revision identifies the candidate a reviewer edits or accepts. + +{{< multitabs id="playbook-proposal-create" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +action = {"add": { + "target": {"new_entry": {}}, + "candidate": {"content": { + "intents": ["Contact support about a missing refund"], + "attributes": {"locale": "en-US", "topic": "refund"}, + "payload": {"faq": { + "question": "What should I send support about a missing refund?", + "answer": "Send your order number and the date you requested the refund.", + }}, + }}, +}} +proposal = playbook.create_proposal(action=action) +proposal_id = proposal.proposal_id +print(proposal_id, proposal.proposal_revision) +``` + +-tab-sep- + +```typescript +const action = { add: { + target: { newEntry: {} }, + candidate: { content: { + intents: ["Contact support about a missing refund"], + attributes: { locale: "en-US", topic: "refund" }, + payload: { faq: { + question: "What should I send support about a missing refund?", + answer: "Send your order number and the date you requested the refund.", + } }, + } }, +} }; +const proposal = await playbook.createProposal({ action }); +const proposalId = proposal.proposalId; +console.log(proposalId, proposal.proposalRevision); +``` + +-tab-sep- + +```bash +cat > proposal.json <<'JSON' +{"action":{"add":{ + "target":{"newEntry":{}}, + "candidate":{"content":{ + "intents":["Contact support about a missing refund"], + "attributes":{"locale":"en-US","topic":"refund"}, + "payload":{"faq":{ + "question":"What should I send support about a missing refund?", + "answer":"Send your order number and the date you requested the refund." + }} + }} +}}} +JSON +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/proposals" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @proposal.json > proposal-created.json +PROPOSAL_ID=$(jq -er '.proposalId' proposal-created.json) +jq '{proposalId, proposalRevision}' proposal-created.json +``` + +{{< /multitabs >}} + +### Handle an existing candidate + +Creation or candidate replacement can return `409` with type `/errors/proposal-already-exists`. Read the proposal identified by `existingProposalId` before submitting more content. In Python, catch `PlaybookConflictErrorResponseContent` and read `error.data.existing_proposal_id`; TypeScript exposes `error.existingProposalId` on the corresponding error class. HTTP callers read the field from the response body. + +If the existing open candidate addresses the same need, use its identifier to [attach evidence]({{< relref "/develop/ai/context-engine/agent-playbook/proposals#attach-evidence" >}}). Review its current revision before editing or accepting it. Restore a trashed candidate before adding evidence; promoted candidates remain locked against review changes. Duplicate detection is advisory, so concurrent submissions can still produce candidates that need consolidation during review. + +## Attach evidence + +Submit source context to the open proposal with `submit_proposal_evidence`. Each observation includes: + +| Field | Purpose | +|:------|:--------| +| `evidenceId` | Identifier for this observation. Reuse it for an exact retry. | +| `schemaVersion` | Evidence format version; currently `1`. | +| `observedAt` | Time of the source observation. | +| `producer` | Name and version of the application or agent submitting it. | +| `source` | Source type, with optional reference and content objects. | + +Attach a supporting passage and a reference the reviewer can check. The service does not independently verify caller-supplied attribution. + +Evidence is immutable. Retrying the same identifier and payload returns `alreadyPresent`. Reusing that identifier for changed content returns a conflict. Use a new identifier for a new observation. + +Evidence stays with the proposal. Acceptance does not copy it into the FAQ. See [evidence limits]({{< relref "/develop/ai/context-engine/agent-playbook/limits#proposal-and-evidence-limits" >}}) before attaching large source content. + +This example attaches a policy excerpt. The HTTP example uses `openssl` to generate an identifier. Keep that identifier and payload for exact retries. + +{{< multitabs id="playbook-proposal-evidence" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +from datetime import datetime, timezone +from uuid import uuid4 + +evidence_id = "evd_" + uuid4().hex +observed_at = datetime.now(timezone.utc) +evidence = playbook.submit_proposal_evidence( + proposal_id=proposal_id, + evidence_id=evidence_id, + schema_version=1, + observed_at=observed_at, + producer={"name": "docs-example", "version": "1"}, + source={ + "type": "document", + "reference": {"documentId": "sample-refund-policy"}, + "content": {"excerpt": "For refund inquiries, collect the order number and request date. Do not request full card details."}, + }, +) +print(evidence.disposition) +``` + +-tab-sep- + +```typescript +const { randomUUID } = await import("node:crypto"); +const evidenceId = "evd_" + randomUUID().replaceAll("-", ""); +const observedAt = new Date(); +const evidence = await playbook.submitProposalEvidence(proposalId, evidenceId, { + schemaVersion: 1, + observedAt, + producer: { name: "docs-example", version: "1" }, + source: { + type: "document", + reference: { documentId: "sample-refund-policy" }, + content: { excerpt: "For refund inquiries, collect the order number and request date. Do not request full card details." }, + }, +}); +console.log(evidence.disposition); +``` + +-tab-sep- + +```bash +EVIDENCE_ID="evd_$(openssl rand -hex 16)" +jq -n --arg observedAt "$(date -u +%Y-%m-%dT%H:%M:%SZ)" '{ + schemaVersion: 1, observedAt: $observedAt, + producer: {name: "docs-example", version: "1"}, + source: { + type: "document", + reference: {documentId: "sample-refund-policy"}, + content: {excerpt: "For refund inquiries, collect the order number and request date. Do not request full card details."} + } +}' > evidence.json +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/proposals/$PROPOSAL_ID/evidence/$EVIDENCE_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @evidence.json | jq .disposition +``` + +{{< /multitabs >}} + +## Review and edit a candidate + +1. List the inbox or search proposals for the topic you want to review. +1. Retrieve the complete proposal, including its candidate and evidence. +1. Check that the sources support the answer, its conditions, and its intended audience. +1. Replace the candidate with `replace_proposal` if it needs changes, supplying `expectedProposalRevision` for the revision you read. +1. Review the returned candidate and record its new revision. + +Editing replaces the complete proposed action and preserves existing evidence. + +If another reviewer changes the candidate, the revision check fails. Read and review the current candidate before trying again. An evidence count can guide review priority, but it is not a correctness score. + +{{< multitabs id="playbook-proposal-read" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +reviewed = playbook.get_proposal(proposal_id=proposal_id) +print(reviewed.model_dump_json(by_alias=True, indent=2)) +``` + +-tab-sep- + +```typescript +const reviewed = await playbook.getProposal(proposalId); +console.log(JSON.stringify(reviewed, null, 2)); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/proposals/$PROPOSAL_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" > reviewed.json +jq . reviewed.json +``` + +{{< /multitabs >}} + +After checking the source excerpt, add its guidance about card details. Start from the action you just read, preserve its other fields, and supply that revision. + +{{< multitabs id="playbook-proposal-replace" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +revised_action = reviewed.action.model_dump(exclude_unset=True) +revised_action["add"]["candidate"]["content"]["payload"]["faq"]["answer"] += " Do not send full card details." +revised = playbook.replace_proposal( + proposal_id=proposal_id, + action=revised_action, + expected_proposal_revision=reviewed.proposal_revision, +) +print(revised.proposal_revision, revised.action) +``` + +-tab-sep- + +```typescript +const reviewedContent = reviewed.action.add.candidate.content; +if (!("faq" in reviewedContent.payload)) throw new Error("Expected an FAQ proposal"); +const revisedAction = { add: { + target: reviewed.action.add.target, + candidate: { content: { + ...reviewedContent, + payload: { faq: { + ...reviewedContent.payload.faq, + answer: reviewedContent.payload.faq.answer + " Do not send full card details.", + } }, + } }, +} }; +const revised = await playbook.replaceProposal(proposalId, { + action: revisedAction, expectedProposalRevision: reviewed.proposalRevision, +}); +console.log(revised.proposalRevision, revised.action); +``` + +-tab-sep- + +```bash +jq --argjson revision "$(jq -er '.proposalRevision' reviewed.json)" ' + {action} | + .action.add.candidate.content.payload.faq.answer += " Do not send full card details." | + .expectedProposalRevision = $revision +' reviewed.json > proposal-update.json +curl --fail-with-body --silent --show-error \ + -X PATCH "$PLAYBOOK_API/proposals/$PROPOSAL_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @proposal-update.json > revised.json +jq '{proposalRevision, action}' revised.json +``` + +{{< /multitabs >}} + +## Dismiss, restore, or purge + +Dismiss a candidate you do not want to accept. It moves to trash, where you can inspect or restore it before its retention deadline. Restore a trashed proposal before editing it, attaching evidence, or accepting it. + +Open proposals also leave the inbox when their review window expires. Check `inboxExpiresAt` and `purgeAt` rather than treating the inbox as permanent storage. Candidate edits, new evidence, and restoration refresh the review deadlines. + +Purge a trashed proposal when you want to remove it permanently. Save any review records your application needs before the proposal is purged. + +Test dismissal and restoration before accepting the sample. Purging instead ends the walkthrough for that proposal. + +{{< multitabs id="playbook-proposal-trash" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.dismiss_proposal( + proposal_id=proposal_id, expected_proposal_revision=revised.proposal_revision, +) +playbook.restore_proposal(proposal_id=proposal_id) +# To purge a trashed proposal instead of restoring it: +# playbook.purge_proposal(proposal_id=proposal_id) +``` + +-tab-sep- + +```typescript +await playbook.dismissProposal(proposalId, { + expectedProposalRevision: revised.proposalRevision, +}); +await playbook.restoreProposal(proposalId); +// To purge a trashed proposal instead of restoring it: +// await playbook.purgeProposal(proposalId); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/proposals/$PROPOSAL_ID/dismissal" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq '{expectedProposalRevision: .proposalRevision}' revised.json)" | jq .status +curl --fail-with-body --silent --show-error \ + -X DELETE "$PLAYBOOK_API/proposals/$PROPOSAL_ID/dismissal" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq .status +# To purge instead of restoring, DELETE /proposals/$PROPOSAL_ID. +``` + +{{< /multitabs >}} + +## Accept and publish + +Use `accept_proposal` with the reviewed revision. It creates one unpublished FAQ and returns its entry and version. + +The `promoting` and `promoted` states prevent further review changes. If acceptance is interrupted, retry it to resume or retrieve the recorded FAQ. Use that FAQ rather than creating a replacement. + +Inspect the returned FAQ, then [publish its version]({{< relref "/develop/ai/context-engine/agent-playbook/faqs#publish-a-version" >}}). Acceptance does not grant publication permission. + +{{< multitabs id="playbook-proposal-final-review" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +final_review = playbook.get_proposal(proposal_id=proposal_id) +print(final_review.action, final_review.evidence) +``` + +-tab-sep- + +```typescript +const finalReview = await playbook.getProposal(proposalId); +console.log(finalReview.action, finalReview.evidence); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/proposals/$PROPOSAL_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" > final-review.json +jq '{action, evidence, proposalRevision}' final-review.json +``` + +{{< /multitabs >}} + +Accept the reviewed revision. + +{{< multitabs id="playbook-proposal-accept" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +accepted = playbook.accept_proposal( + proposal_id=proposal_id, + expected_proposal_revision=final_review.proposal_revision, +) +print(accepted.entry.entry_id, accepted.version.version, accepted.version.content) +``` + +-tab-sep- + +```typescript +const accepted = await playbook.acceptProposal(proposalId, { + expectedProposalRevision: finalReview.proposalRevision, +}); +console.log(accepted.entry.entryId, accepted.version.version, accepted.version.content); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/proposals/$PROPOSAL_ID/acceptance" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq '{expectedProposalRevision: .proposalRevision}' final-review.json)" > accepted.json +jq '{entryId: .entry.entryId, version: .version.version, content: .version.content}' accepted.json +``` + +{{< /multitabs >}} + +Publish the returned FAQ version after review. + +{{< multitabs id="playbook-proposal-publish" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.publish_faq(faq_id=accepted.entry.entry_id, version=accepted.version.version) +``` + +-tab-sep- + +```typescript +await playbook.publishFaq(accepted.entry.entryId, { version: accepted.version.version }); +``` + +-tab-sep- + +```bash +ACCEPTED_FAQ_ID=$(jq -er '.entry.entryId' accepted.json) +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/faqs/$ACCEPTED_FAQ_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq '{version: .version.version}' accepted.json)" | jq .publication +``` + +{{< /multitabs >}} + +The accepted FAQ remains after this example. Delete it with the [FAQ cleanup procedure]({{< relref "/develop/ai/context-engine/agent-playbook/faqs#withdraw-or-delete-an-faq" >}}) using the returned entry identifier when you finish. + +## Inspect promotion history + +The trash view includes dismissed or expired proposals and retained `promoted` history. A promoted proposal cannot be restored or edited. Read its detail with `get_proposal` while it is retained, or purge it when its review record is no longer needed. Purging the proposal does not delete the accepted FAQ. A `promoting` proposal remains in the inbox for acceptance recovery and cannot be purged. + +{{< multitabs id="playbook-proposal-history" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +cursor = None +while True: + page = playbook.list_proposals(view="trash", sort_by="updatedAt", sort_order="desc", limit=50, cursor=cursor) + if cursor is None: + print("Total trash proposals:", page.total if page.total is not None else "unknown") + for item in page.proposals: + print(item.proposal_id, item.status, item.evidence_count, item.action) + cursor = page.next_cursor + if cursor is None: + break +``` + +-tab-sep- + +```typescript +let trashCursor: string | undefined; +while (true) { + const page = await playbook.listProposals("trash", "updatedAt", "desc", 50, trashCursor); + if (trashCursor === undefined) console.log("Total trash proposals:", page.total ?? "unknown"); + for (const item of page.proposals) console.log(item.proposalId, item.status, item.evidenceCount, item.action); + trashCursor = page.nextCursor; + if (trashCursor === undefined) break; +} +``` + +-tab-sep- + +```bash +CURSOR='' +while true; do + PAGE_ARGS=(--data-urlencode view=trash --data-urlencode sortBy=updatedAt --data-urlencode sortOrder=desc --data-urlencode limit=50) + if [[ -n "$CURSOR" ]]; then PAGE_ARGS+=(--data-urlencode "cursor=$CURSOR"); fi + curl --fail-with-body --silent --show-error --get "$PLAYBOOK_API/proposals" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" "${PAGE_ARGS[@]}" > trash.json + if [[ -z "$CURSOR" ]]; then jq '.total // "unknown"' trash.json; fi + jq '.proposals[] | {proposalId, status, evidenceCount, action}' trash.json + CURSOR=$(jq -r '.nextCursor // empty' trash.json) + [[ -n "$CURSOR" ]] || break +done +``` + +{{< /multitabs >}} + +## Review background extraction results + +With background extraction enabled, extraction agents read eligible conversations from a connected Redis Agent Memory store. They can create FAQ proposals or attach evidence to existing open proposals for review. + +Extraction is opt-in. An administrator must configure the source connection and enable extraction on a deployment that supports it. Proposal permissions alone do not configure or start extraction. + + + +Review extracted candidates in the same inbox. Check their sources, answers, and audiences before accepting and publishing. Extraction does not publish answers automatically or extract skills. diff --git a/content/develop/ai/context-engine/agent-playbook/quickstart.md b/content/develop/ai/context-engine/agent-playbook/quickstart.md new file mode 100644 index 0000000000..65f8139ddd --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/quickstart.md @@ -0,0 +1,473 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Publish an FAQ, retrieve it for an agent, and update the answer with the Python SDK, TypeScript SDK, or HTTP API. +hideListLinks: true +linktitle: Quickstart +title: Agent Playbooks quickstart +weight: 10 +--- + +Publish a FAQ, retrieve its answer to “Where can I see how my refund is progressing?”, and update the answer agents receive. + +## Before you begin + +Create a playbook API key with `write` and `publish` permissions. Both include `read`. + +This walkthrough needs no agent framework or model-provider key. + +Follow one tab throughout the page. The SDKs are alpha prereleases pinned to the versions used here. Python requires Python 3.10 or later; TypeScript requires Node.js 20 or later. The HTTP examples use Bash, curl, and jq. + +{{< multitabs id="playbook-install" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```bash +python -m venv .venv +source .venv/bin/activate +python -m pip install redis-agent-playbook==0.1.0a2 +``` + +-tab-sep- + +```bash +npm init -y +npm install @redis-iris/agent-playbook@0.1.0-alpha.2 +npm install --save-dev tsx typescript @types/node +``` + +-tab-sep- + +```bash +curl --version +jq --version +``` + +{{< /multitabs >}} + +## Save connection values + +Export these values in the shell where you run the examples. Use the endpoint's base address, including `https://`, without the `/v1/playbooks/...` path. + +```bash +export PLAYBOOK_URL='' +export PLAYBOOK_ID='' +export PLAYBOOK_API_KEY='' +``` + +Keep keys out of skill files, source control, and model prompts. + +## Connect to the playbook + +Download the complete example: [Python]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart/" >}}quickstart.py), [TypeScript]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart/" >}}quickstart.mts), or [HTTP]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart/" >}}quickstart.sh). Run `python quickstart.py`, `npx tsx quickstart.mts`, or `bash quickstart.sh` once. The file publishes and retrieves the FAQ, publishes an update, and retains the entry for agent testing. + +{{< multitabs id="playbook-connect" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +import os +from redis_agent_playbook import Playbook + +playbook = Playbook( + os.environ["PLAYBOOK_URL"], + playbook_id=os.environ["PLAYBOOK_ID"], + api_key=os.environ["PLAYBOOK_API_KEY"], + retry_config=None, +) +print(playbook.get_playbook_health()) +``` + +-tab-sep- + +```typescript +import { Playbook } from "@redis-iris/agent-playbook"; + +function requiredEnv(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`Set ${name}`); + return value; +} + +const playbook = new Playbook({ + serverURL: requiredEnv("PLAYBOOK_URL"), + playbookId: requiredEnv("PLAYBOOK_ID"), + apiKey: requiredEnv("PLAYBOOK_API_KEY"), + retryConfig: { strategy: "none" }, +}); +console.log(await playbook.getPlaybookHealth()); +``` + +-tab-sep- + +```bash +set -euo pipefail +PLAYBOOK_API="${PLAYBOOK_URL%/}/v1/playbooks/$PLAYBOOK_ID" +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/health" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq . +``` + +{{< /multitabs >}} + +Retries are disabled to avoid repeating an uncertain write. Rerunning the file creates another entry; repeat only reads or searches when testing again. Keep the printed identifier for cleanup. + +## 1. Create an FAQ + +Create one answer with a locale and topic for retrieval filters. Save the returned entry identifier and version. + +{{< multitabs id="playbook-quickstart-create" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +faq_content = { + "intents": ["Check a refund", "Find an existing refund"], + "attributes": {"locale": "en-US", "topic": "refund"}, + "payload": { + "faq": { + "question": "How do I check the status of a refund?", + "answer": "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number.", + } + }, +} +draft = playbook.create_faq(content=faq_content) +faq_id = draft.entry.entry_id +version = draft.version.version +print(faq_id, version, draft.entry.publication) +``` + +-tab-sep- + +```typescript +const faqContent = { + intents: ["Check a refund", "Find an existing refund"], + attributes: { locale: "en-US", topic: "refund" }, + payload: { faq: { + question: "How do I check the status of a refund?", + answer: "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number.", + } }, +}; +const draft = await playbook.createFaq({ content: faqContent }); +const faqId = draft.entry.entryId; +const version = draft.version.version; +console.log(faqId, version, draft.entry.publication); +``` + +-tab-sep- + +```bash +cat > faq.json <<'JSON' +{ + "content": { + "intents": ["Check a refund", "Find an existing refund"], + "attributes": {"locale": "en-US", "topic": "refund"}, + "payload": {"faq": { + "question": "How do I check the status of a refund?", + "answer": "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number." + }} + } +} +JSON +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @faq.json > draft.json +FAQ_ID=$(jq -er '.entry.entryId' draft.json) +VERSION=$(jq -er '.version.version' draft.json) +jq '{entryId: .entry.entryId, version: .version.version, publication: .entry.publication}' draft.json +``` + +{{< /multitabs >}} + +The first version is `1`, and `publication` is absent or empty. You can review the entry, but search and discovery cannot return it yet. + +## 2. Review and publish the answer + +Read the exact version you created. + +{{< multitabs id="playbook-quickstart-publish" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +stored = playbook.get_faq_version(faq_id=faq_id, version=version) +print(stored.content) +``` + +-tab-sep- + +```typescript +const stored = await playbook.getFaqVersion(faqId, version); +console.log(stored.content); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/faqs/$FAQ_ID/versions/$VERSION" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq .content +``` + +{{< /multitabs >}} + +Review the question and answer. Then publish the version you read. + +{{< multitabs id="playbook-quickstart-publication" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.publish_faq(faq_id=faq_id, version=version) +``` + +-tab-sep- + +```typescript +await playbook.publishFaq(faqId, { version }); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/faqs/$FAQ_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq -n --argjson version "$VERSION" '{version: $version}')" | jq .publication +``` + +{{< /multitabs >}} + +The publication response identifies the version that agents can retrieve. + +## 3. Search with a different question + +Search with a paraphrase of the stored question. + +{{< multitabs id="playbook-quickstart-search" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +results = playbook.search_faqs( + query="Where can I see how my refund is progressing?", + filter_={"eq": {"name": "locale", "value": "en-US"}}, + limit=5, +) +for match in results.matches: + if match.faq is not None: + print(match.entry_id, match.version, match.score, match.faq.answer) +``` + +-tab-sep- + +```typescript +const results = await playbook.searchFaqs({ + query: "Where can I see how my refund is progressing?", + filter: { eq: { name: "locale", value: "en-US" } }, + limit: 5, +}); +for (const match of results.matches) { + if (match.faq) { + console.log(match.entryId, match.version, match.score, match.faq.answer); + } +} +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Where can I see how my refund is progressing?","filter":{"eq":{"name":"locale","value":"en-US"}},"limit":5}' \ + | jq '.matches[] | {entryId, version, score, answer: .faq.answer}' +``` + +{{< /multitabs >}} + +Check for your entry identifier, version `1`, and the answer about order history. Scores and rankings vary with the embedding model and other entries. If no match appears, repeat only the search after a short wait, then check the locale filter and [similarity threshold]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#control-relevance-and-result-count" >}}). + +## 4. Discover FAQs for an agent + +Discovery returns FAQ answers and skill previews in one request. This example filters for refund guidance to add to the agent's context. + +{{< multitabs id="playbook-quickstart-discover" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +discovery = playbook.discover_entries( + query="Help a customer find their refund status", + filter_={"eq": {"name": "topic", "value": "refund"}}, +) +print(discovery.model_dump_json(by_alias=True, indent=2)) +``` + +-tab-sep- + +```typescript +const discovery = await playbook.discoverEntries({ + query: "Help a customer find their refund status", + filter: { eq: { name: "topic", value: "refund" } }, +}); +console.log(JSON.stringify(discovery, null, 2)); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/discover" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Help a customer find their refund status","filter":{"eq":{"name":"topic","value":"refund"}}}' | jq . +``` + +{{< /multitabs >}} + +With no pins, `pinnedEntries` is empty. Matching published entries appear in `discoveredEntries`. When you add skills later, discovery returns their previews without loading all their instructions. + +## 5. Publish an updated answer + +Append a complete replacement version to the same entry. Include the attributes and intents you want to retain. + +{{< multitabs id="playbook-quickstart-update" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +from copy import deepcopy + +updated_content = deepcopy(faq_content) +updated_content["payload"]["faq"]["answer"] += " Support can check the current stage but cannot guarantee a bank posting date." +updated = playbook.append_faq_version(faq_id=faq_id, content=updated_content) +print("Review this version:", updated.version.version, updated.version.content) +``` + +-tab-sep- + +```typescript +const updatedContent = structuredClone(faqContent); +updatedContent.payload.faq.answer += " Support can check the current stage but cannot guarantee a bank posting date."; +const updated = await playbook.appendFaqVersion(faqId, { content: updatedContent }); +console.log("Review this version:", updated.version.version, updated.version.content); +``` + +-tab-sep- + +```bash +jq '.content.payload.faq.answer += " Support can check the current stage but cannot guarantee a bank posting date."' \ + faq.json > faq-update.json +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/$FAQ_ID/versions" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @faq-update.json > updated.json +jq .version updated.json +``` + +{{< /multitabs >}} + +The identifier stays the same, and search still serves the first publication. Review and publish the new version, then search again. + +{{< multitabs id="playbook-quickstart-republish" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.publish_faq(faq_id=faq_id, version=updated.version.version) +updated_results = playbook.search_faqs( + query="Where can I see how my refund is progressing?", + filter_={"eq": {"name": "locale", "value": "en-US"}}, + limit=5, +) +for match in updated_results.matches: + if match.entry_id == faq_id and match.faq is not None: + print("Published answer:", match.version, match.faq.answer) +``` + +-tab-sep- + +```typescript +await playbook.publishFaq(faqId, { version: updated.version.version }); +const updatedResults = await playbook.searchFaqs({ + query: "Where can I see how my refund is progressing?", + filter: { eq: { name: "locale", value: "en-US" } }, + limit: 5, +}); +for (const match of updatedResults.matches) { + if (match.entryId === faqId && match.faq) { + console.log("Published answer:", match.version, match.faq.answer); + } +} +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/faqs/$FAQ_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq '{version: .version.version}' updated.json)" | jq .publication +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Where can I see how my refund is progressing?","filter":{"eq":{"name":"locale","value":"en-US"}},"limit":5}' \ + | jq --arg id "$FAQ_ID" '.matches[] | select(.entryId == $id) | {version, answer: .faq.answer}' +``` + +{{< /multitabs >}} + +The result for your FAQ should now identify version `2` and include the added sentence about bank posting dates. If it still shows version `1` or is absent, repeat only the search. + +## Clean up + +Keep the sample published while testing [an agent]({{< relref "/develop/ai/context-engine/agent-playbook/mcp" >}}). The downloads omit cleanup. When you finish, delete the FAQ and its versions. If you used a download, open a new script with the client setup and set `faq_id` (Python), `faqId` (TypeScript), or `FAQ_ID` (HTTP) to the printed entry identifier before running this deletion. [Withdraw it]({{< relref "/develop/ai/context-engine/agent-playbook/faqs#withdraw-or-delete-an-faq" >}}) instead to retain history. + +{{< multitabs id="playbook-quickstart-delete" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.delete_faq(faq_id=faq_id) +``` + +-tab-sep- + +```typescript +await playbook.deleteFaq(faqId); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X DELETE "$PLAYBOOK_API/faqs/$FAQ_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" +``` + +{{< /multitabs >}} + +## Use Python asynchronously + +Every Python SDK operation has an `_async` counterpart. This standalone example uses `async with` to close connections after the search. Save it as `async_search.py` and run `python async_search.py` with the same environment variables and a reader key. Keep an FAQ published to test matching. + +```python +import asyncio +import os +from redis_agent_playbook import Playbook + +async def main(): + async with Playbook( + os.environ["PLAYBOOK_URL"], + playbook_id=os.environ["PLAYBOOK_ID"], + api_key=os.environ["PLAYBOOK_API_KEY"], + retry_config=None, + timeout_ms=30_000, + ) as playbook: + results = await playbook.search_faqs_async( + query="Where can I see how my refund is progressing?", limit=5, + ) + for match in results.matches: + if match.faq is not None: + print(match.faq.answer) + +asyncio.run(main()) +``` + +`timeout_ms` sets a request timeout in milliseconds on the client or an individual operation. For custom headers, proxies, or transport settings, pass an `httpx.Client` as `client` or an `httpx.AsyncClient` as `async_client`. You own and close a client you supply. See the [Python SDK's custom HTTP client documentation](https://pypi.org/project/redis-agent-playbook/0.1.0a2/#custom-http-client) for configuration examples. + +## Next steps + +- [Manage FAQs]({{< relref "/develop/ai/context-engine/agent-playbook/faqs" >}}) to roll back or withdraw an answer. +- [Manage skills]({{< relref "/develop/ai/context-engine/agent-playbook/skills" >}}) to publish a procedure and load supporting files. +- [Review proposals]({{< relref "/develop/ai/context-engine/agent-playbook/proposals" >}}) to accept suggested answers before publishing them. +- [Connect with MCP]({{< relref "/develop/ai/context-engine/agent-playbook/mcp" >}}) to retrieve this knowledge through agent tools. diff --git a/content/develop/ai/context-engine/agent-playbook/quickstart/quickstart.mts b/content/develop/ai/context-engine/agent-playbook/quickstart/quickstart.mts new file mode 100644 index 0000000000..7fd76de91d --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/quickstart/quickstart.mts @@ -0,0 +1,68 @@ +// Generated from quickstart.md; edit the page, then run examples.py --refresh-downloads. +import { Playbook } from "@redis-iris/agent-playbook"; + +function requiredEnv(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`Set ${name}`); + return value; +} + +const playbook = new Playbook({ + serverURL: requiredEnv("PLAYBOOK_URL"), + playbookId: requiredEnv("PLAYBOOK_ID"), + apiKey: requiredEnv("PLAYBOOK_API_KEY"), + retryConfig: { strategy: "none" }, +}); +console.log(await playbook.getPlaybookHealth()); + +const faqContent = { + intents: ["Check a refund", "Find an existing refund"], + attributes: { locale: "en-US", topic: "refund" }, + payload: { faq: { + question: "How do I check the status of a refund?", + answer: "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number.", + } }, +}; +const draft = await playbook.createFaq({ content: faqContent }); +const faqId = draft.entry.entryId; +const version = draft.version.version; +console.log(faqId, version, draft.entry.publication); + +const stored = await playbook.getFaqVersion(faqId, version); +console.log(stored.content); + +await playbook.publishFaq(faqId, { version }); + +const results = await playbook.searchFaqs({ + query: "Where can I see how my refund is progressing?", + filter: { eq: { name: "locale", value: "en-US" } }, + limit: 5, +}); +for (const match of results.matches) { + if (match.faq) { + console.log(match.entryId, match.version, match.score, match.faq.answer); + } +} + +const discovery = await playbook.discoverEntries({ + query: "Help a customer find their refund status", + filter: { eq: { name: "topic", value: "refund" } }, +}); +console.log(JSON.stringify(discovery, null, 2)); + +const updatedContent = structuredClone(faqContent); +updatedContent.payload.faq.answer += " Support can check the current stage but cannot guarantee a bank posting date."; +const updated = await playbook.appendFaqVersion(faqId, { content: updatedContent }); +console.log("Review this version:", updated.version.version, updated.version.content); + +await playbook.publishFaq(faqId, { version: updated.version.version }); +const updatedResults = await playbook.searchFaqs({ + query: "Where can I see how my refund is progressing?", + filter: { eq: { name: "locale", value: "en-US" } }, + limit: 5, +}); +for (const match of updatedResults.matches) { + if (match.entryId === faqId && match.faq) { + console.log("Published answer:", match.version, match.faq.answer); + } +} diff --git a/content/develop/ai/context-engine/agent-playbook/quickstart/quickstart.py b/content/develop/ai/context-engine/agent-playbook/quickstart/quickstart.py new file mode 100644 index 0000000000..30ee9d4250 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/quickstart/quickstart.py @@ -0,0 +1,63 @@ +# Generated from quickstart.md; edit the page, then run examples.py --refresh-downloads. +import os +from redis_agent_playbook import Playbook + +playbook = Playbook( + os.environ["PLAYBOOK_URL"], + playbook_id=os.environ["PLAYBOOK_ID"], + api_key=os.environ["PLAYBOOK_API_KEY"], + retry_config=None, +) +print(playbook.get_playbook_health()) + +faq_content = { + "intents": ["Check a refund", "Find an existing refund"], + "attributes": {"locale": "en-US", "topic": "refund"}, + "payload": { + "faq": { + "question": "How do I check the status of a refund?", + "answer": "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number.", + } + }, +} +draft = playbook.create_faq(content=faq_content) +faq_id = draft.entry.entry_id +version = draft.version.version +print(faq_id, version, draft.entry.publication) + +stored = playbook.get_faq_version(faq_id=faq_id, version=version) +print(stored.content) + +playbook.publish_faq(faq_id=faq_id, version=version) + +results = playbook.search_faqs( + query="Where can I see how my refund is progressing?", + filter_={"eq": {"name": "locale", "value": "en-US"}}, + limit=5, +) +for match in results.matches: + if match.faq is not None: + print(match.entry_id, match.version, match.score, match.faq.answer) + +discovery = playbook.discover_entries( + query="Help a customer find their refund status", + filter_={"eq": {"name": "topic", "value": "refund"}}, +) +print(discovery.model_dump_json(by_alias=True, indent=2)) + +from copy import deepcopy + +updated_content = deepcopy(faq_content) +updated_content["payload"]["faq"]["answer"] += " Support can check the current stage but cannot guarantee a bank posting date." +updated = playbook.append_faq_version(faq_id=faq_id, content=updated_content) +print("Review this version:", updated.version.version, updated.version.content) + +playbook.publish_faq(faq_id=faq_id, version=updated.version.version) +updated_results = playbook.search_faqs( + query="Where can I see how my refund is progressing?", + filter_={"eq": {"name": "locale", "value": "en-US"}}, + limit=5, +) +for match in updated_results.matches: + if match.entry_id == faq_id and match.faq is not None: + print("Published answer:", match.version, match.faq.answer) diff --git a/content/develop/ai/context-engine/agent-playbook/quickstart/quickstart.sh b/content/develop/ai/context-engine/agent-playbook/quickstart/quickstart.sh new file mode 100644 index 0000000000..685d8098ff --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/quickstart/quickstart.sh @@ -0,0 +1,71 @@ +# Generated from quickstart.md; edit the page, then run examples.py --refresh-downloads. +set -euo pipefail + +set -euo pipefail +PLAYBOOK_API="${PLAYBOOK_URL%/}/v1/playbooks/$PLAYBOOK_ID" +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/health" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq . + +cat > faq.json <<'JSON' +{ + "content": { + "intents": ["Check a refund", "Find an existing refund"], + "attributes": {"locale": "en-US", "topic": "refund"}, + "payload": {"faq": { + "question": "How do I check the status of a refund?", + "answer": "Open the order in your order history and select Refund status. If no refund is listed, contact support with your order number." + }} + } +} +JSON +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @faq.json > draft.json +FAQ_ID=$(jq -er '.entry.entryId' draft.json) +VERSION=$(jq -er '.version.version' draft.json) +jq '{entryId: .entry.entryId, version: .version.version, publication: .entry.publication}' draft.json + +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/faqs/$FAQ_ID/versions/$VERSION" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq .content + +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/faqs/$FAQ_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq -n --argjson version "$VERSION" '{version: $version}')" | jq .publication + +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Where can I see how my refund is progressing?","filter":{"eq":{"name":"locale","value":"en-US"}},"limit":5}' \ + | jq '.matches[] | {entryId, version, score, answer: .faq.answer}' + +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/discover" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Help a customer find their refund status","filter":{"eq":{"name":"topic","value":"refund"}}}' | jq . + +jq '.content.payload.faq.answer += " Support can check the current stage but cannot guarantee a bank posting date."' \ + faq.json > faq-update.json +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/$FAQ_ID/versions" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @faq-update.json > updated.json +jq .version updated.json + +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/faqs/$FAQ_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq '{version: .version.version}' updated.json)" | jq .publication +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Where can I see how my refund is progressing?","filter":{"eq":{"name":"locale","value":"en-US"}},"limit":5}' \ + | jq --arg id "$FAQ_ID" '.matches[] | select(.entryId == $id) | {version, answer: .faq.answer}' diff --git a/content/develop/ai/context-engine/agent-playbook/retrieval.md b/content/develop/ai/context-engine/agent-playbook/retrieval.md new file mode 100644 index 0000000000..59ba427a2a --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/retrieval.md @@ -0,0 +1,387 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Search published knowledge, discover skills and answers together, and control relevance and result size. +hideListLinks: true +linktitle: Retrieval +title: Retrieve knowledge +weight: 50 +--- + +Retrieve published knowledge for the user's task. Choose a retrieval method: + +- [**Discovery**]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#discovery" >}}) combines FAQ answers and skill previews so the agent can choose what to use. +- [**Search**]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#search" >}}) returns one entry type when your application needs answers or skill previews, with a relevance threshold and result count for each request. + +Both methods serve current publications. They exclude drafts, historical versions, and proposals. + +## Before you begin + +Use a data-plane endpoint, playbook identifier, and API key with `read` permission. Publish the entries you want to retrieve before testing them. + +Start with the quickstart's [client setup]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart#connect-to-the-playbook" >}}) with a reader key, then append this page's snippets in order. The examples use the published FAQ and skill from the task guides; keep them published until testing is complete. + +## Discovery + +Discovery returns answers and skill previews within a shared result limit. Pins include required entries; a preview budget limits returned content. + +Call `discover_entries` with a Python or TypeScript SDK, over HTTP, or through [MCP]({{< relref "/develop/ai/context-engine/agent-playbook/mcp" >}}). Load selected skill instructions and files with [progressive disclosure]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#load-skills-with-progressive-disclosure" >}}). + +Request up to five entries for the agent's task: + +{{< multitabs id="playbook-retrieval-discover" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +discovery = playbook.discover_entries(query="Draft a reply for a customer whose refund shows Processing", limit=5) +print(discovery.model_dump_json(by_alias=True, indent=2)) +``` + +-tab-sep- + +```typescript +const discovery = await playbook.discoverEntries({ query: "Draft a reply for a customer whose refund shows Processing", limit: 5 }); +console.log(JSON.stringify(discovery, null, 2)); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/discover" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Draft a reply for a customer whose refund shows Processing","limit":5}' | jq . +``` + +{{< /multitabs >}} + +### How discovery selects entries + +Discovery applies these steps: + +1. Resolve each pin to its current published version, preserving request order. +1. Reserve result slots and preview budget for the pins. +1. Rank matching FAQs and skills together using the query, attribute filter, and each type's similarity threshold. Exclude pinned entries from these matches. +1. Fill the remaining slots in rank order. When a preview does not fit the remaining token budget, skip it and consider later candidates that may fit. +1. Return pins in `pinnedEntries` and ranked matches in `discoveredEntries`, with count and budget truncation signals. + +If pins fill every result slot, discovery returns only the pins. An FAQ preview includes its question and answer. A skill preview includes its name, description, entry identifier, version, and file address. + +### Configure discovery + +The request controls are: + +| Control | Use it to | +|:--------|:----------| +| `query` | Describe the agent's task. Required even when supplying pins. | +| `filter` | Restrict semantic matches by attributes. Uses the same [filter expressions as search]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#select-an-audience-with-filters" >}}); pins bypass it. | +| `limit` | Reduce the combined result count. Omission uses `discovery.maxResults`, which also caps an explicit limit. | +| `pinnedEntries` | Require known published entries, in the order supplied. Python and the MCP tool use `pinned_entries`. | + +Your administrator sets the playbook controls: + +| Setting | Default | Effect | +|:--------|:--------|:-------| +| `discovery.faqMinimumScore` | `0.7` | Minimum similarity for discovered FAQ matches. | +| `discovery.skillMinimumScore` | `0.7` | Minimum similarity for discovered skill matches. | +| `discovery.maxResults` | `10` | Combined result ceiling for pins and discovered matches. | +| `discovery.maxPreviewTokens` | Disabled | Estimated token ceiling for the discovery response. | + +Discovery has no per-request similarity override. An administrator can adjust the playbook thresholds. + + + +### Pin required knowledge + +Use discovery pins when a task must include a known published entry. Supply exactly one entry identifier or entry key for each pin. Discovery returns pins in request order, separately from semantic matches. + +Pins bypass similarity thresholds and the request filter. Choose pins for the intended audience yourself. They still consume result slots and preview budget. + +A missing, unpublished, duplicate, or oversized pin fails the request. Use pins to test connectivity or include required content; evaluate semantic relevance without them. + +Export the identifier of a published skill, such as the sample in [Manage skills]({{< relref "/develop/ai/context-engine/agent-playbook/skills" >}}), before cleanup: + +```bash +export PLAYBOOK_SKILL_ID='' +``` + +{{< multitabs id="playbook-retrieval-pin" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +skill_id = os.environ["PLAYBOOK_SKILL_ID"] +pinned = playbook.discover_entries( + query="Draft a reply for a customer whose refund shows Processing", + pinned_entries=[{"entry_id": skill_id}], +) +selected_preview = pinned.pinned_entries[0] +print(selected_preview.entry_id, selected_preview.version) +``` + +-tab-sep- + +```typescript +const skillId = requiredEnv("PLAYBOOK_SKILL_ID"); +const pinned = await playbook.discoverEntries({ + query: "Draft a reply for a customer whose refund shows Processing", + pinnedEntries: [{ entryId: skillId }], +}); +const selectedPreview = pinned.pinnedEntries[0]; +if (!selectedPreview) throw new Error("Pinned skill is missing"); +console.log(selectedPreview.entryId, selectedPreview.version); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/discover" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq -n --arg id "$PLAYBOOK_SKILL_ID" '{query: "Draft a reply for a customer whose refund shows Processing", pinnedEntries: [{entryId: $id}]}')" > discovery.json +jq '.pinnedEntries[0] | {entryId, version}' discovery.json +``` + +{{< /multitabs >}} + +### Limit discovery previews + +Use the preview budget to limit the knowledge returned in one discovery response. It excludes model framing, conversation history, loaded instructions, and fetched files. Your application still controls the model's total context. + +| Signal | Meaning | +|:-------|:--------| +| `truncated: true` | Eligible fetched candidates exceed the slots left after pins. This does not count every match in the playbook. | +| `budgetTruncated: true` | Matching previews were skipped to fit the token ceiling. | + +Empty results with `budgetTruncated: true` can mean matches did not fit. Shorten FAQ answers or skill descriptions, or raise the ceiling through your administrator. Increasing the result count does not increase the token budget. Type-specific search does not use this budget. + +{{< multitabs id="playbook-retrieval-budget" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +print("Result limit reached:", pinned.truncated) +print("Preview budget reached:", pinned.budget_truncated or False) +``` + +-tab-sep- + +```typescript +console.log("Result limit reached:", pinned.truncated); +console.log("Preview budget reached:", pinned.budgetTruncated ?? false); +``` + +-tab-sep- + +```bash +jq '{truncated, budgetTruncated: (.budgetTruncated // false)}' discovery.json +``` + +{{< /multitabs >}} + +### Load skills with progressive disclosure + +Progressive disclosure lets an agent select a skill from its description before loading instructions and files. Discovery and skill search support the same loading steps. + +1. **Discover previews.** Retrieve names and descriptions so the agent can assess which skills match the task. FAQ answers are already included in the result. +1. **Select a skill.** Keep the entry identifier and version from the same preview together. +1. **Load instructions.** Retrieve that version's instructions and resource descriptors. With MCP, call `load_skill` using the selected `entry_id` and `version`. +1. **Fetch files as needed.** Retrieve only supporting files needed for the task. Use returned resource identifiers or URIs. With MCP, pass the returned `uri` to `fetch_resource`. +1. **Add content to context.** Your application chooses the instructions and files to give the model and controls any actions it takes. + +The preview budget applies only to the first step, not loaded instructions, files, or total model context. + +Reader keys can load only the current publication. MCP tools apply this restriction even to management keys. For example, discovery can return version `1`, then loading fails because version `2` was published in the meantime. Rediscover once, select again, and load instructions and files from the newly returned version. Keeping an identifier/version pair does not reserve access to an older publication. + +The example loads the skill selected in [Pin required knowledge]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval#pin-required-knowledge" >}}), then fetches its reply template. For a complete MCP client example, see [Connect an agent]({{< relref "/develop/ai/context-engine/agent-playbook/mcp#connect-an-agent" >}}). + +{{< multitabs id="playbook-retrieval-load" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +selected = playbook.get_skill_version(skill_id=selected_preview.entry_id, version=selected_preview.version) +skill = selected.content.payload.skill +if skill is None: + raise ValueError("Expected a skill") +print(skill.instructions) +resource = next((r for r in skill.resources if r.path == "references/reply-template.md"), None) +if resource is None: + raise ValueError("Reply template is missing") +response = playbook.get_skill_resource( + skill_id=selected.entry_id, version=selected.version, resource_id=resource.resource_id, +) +try: + print(response.result.read().decode("utf-8")) +finally: + response.result.close() +``` + +-tab-sep- + +```typescript +const selected = await playbook.getSkillVersion(selectedPreview.entryId, selectedPreview.version); +if (!("skill" in selected.content.payload)) throw new Error("Expected a skill"); +const skill = selected.content.payload.skill; +console.log(skill.instructions); +const resource = skill.resources.find(r => r.path === "references/reply-template.md"); +if (!resource) throw new Error("Reply template is missing"); +const response = await playbook.getSkillResource(selected.entryId, selected.version, resource.resourceId); +console.log(await new Response(response.result).text()); +``` + +-tab-sep- + +```bash +SKILL_ID=$(jq -er '.pinnedEntries[0].entryId' discovery.json) +SKILL_VERSION=$(jq -er '.pinnedEntries[0].version' discovery.json) +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/skills/$SKILL_ID/versions/$SKILL_VERSION" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" > selected-skill.json +jq -r '.content.payload.skill.instructions' selected-skill.json +RESOURCE_ID=$(jq -er '.content.payload.skill.resources[] | select(.path == "references/reply-template.md") | .resourceId' selected-skill.json) +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/skills/$SKILL_ID/versions/$SKILL_VERSION/resources/$RESOURCE_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" +``` + +{{< /multitabs >}} + +## Search + +Search returns ranked `matches` of one entry type. Use it to find an answer, select a skill, or tune retrieval per request. + +| Operation | Use it when | Result | +|:----------|:------------|:-------| +| `search_faqs` | Your application needs an answer. | Ranked FAQ matches with questions and answers. | +| `search_skills` | Your application needs a procedure. | Ranked skill previews with names, descriptions, and file addresses. | + +Send a query that describes the task and relevant conditions. A support question such as “How can I check an existing refund?” is more useful than a query containing only “refund.” + +Semantic matching compares your query with the FAQ question or the skill's name and description. Include the task and distinguishing conditions in those fields. FAQ answers, intents, metadata, skill instructions, and supporting files do not contribute to similarity scores. Attributes narrow the eligible matches through filters. These rules apply to both search and discovery. + +Retain returned entry identifiers and versions with the content you select. + +{{< multitabs id="playbook-retrieval-search" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +faq_results = playbook.search_faqs(query="Where is my refund?", limit=5) +skill_results = playbook.search_skills(query="Draft a reply about a reported refund stage", limit=5) +print(faq_results.model_dump_json(by_alias=True, indent=2)) +print(skill_results.model_dump_json(by_alias=True, indent=2)) +``` + +-tab-sep- + +```typescript +const faqResults = await playbook.searchFaqs({ query: "Where is my refund?", limit: 5 }); +const skillResults = await playbook.searchSkills({ query: "Draft a reply about a reported refund stage", limit: 5 }); +console.log(JSON.stringify(faqResults, null, 2)); +console.log(JSON.stringify(skillResults, null, 2)); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Where is my refund?","limit":5}' | jq . +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/skills/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Draft a reply about a reported refund stage","limit":5}' | jq . +``` + +{{< /multitabs >}} + +### Control relevance and result count + +Search accepts these request controls. The table uses HTTP field names; Python uses `minimum_score` and `filter_`. + +| Control | Default | Use it to | +|:--------|:--------|:----------| +| `query` | Required | Describe the task to match semantically. | +| `filter` | None | Restrict matches by entry attributes, such as locale or topic. | +| `minimumScore` | The playbook's threshold for that entry type; initially `0.7` | Set a similarity floor from `0` to `1` for this request. | +| `limit` | `discovery.maxResults`; initially `10` | Request 1–50 results. An explicit search limit is not capped by the playbook's discovery limit. | + +The threshold defaults come from `discovery.faqMinimumScore` and `discovery.skillMinimumScore`. Your administrator can change them. Search does not use discovery pins or the discovery preview budget. + +Similarity ranks matches; it does not measure answer correctness. Test “Where is my refund?” against “How do I request a refund?” The first needs the sample status answer; the second needs different guidance. Compare results across paraphrases before choosing a threshold. Higher thresholds can remove useful matches too. If no relevant match remains, ask for clarification or route to support. + +For example, override the FAQ search threshold and result count for one request: + +{{< multitabs id="playbook-retrieval-threshold" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +tuned = playbook.search_faqs(query="Where is my refund?", minimum_score=0.75, limit=3) +print(tuned.matches) +``` + +-tab-sep- + +```typescript +const tuned = await playbook.searchFaqs({ query: "Where is my refund?", minimumScore: 0.75, limit: 3 }); +console.log(tuned.matches); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Where is my refund?","minimumScore":0.75,"limit":3}' | jq .matches +``` + +{{< /multitabs >}} + +### Select an audience with filters + +Set attributes when [authoring FAQs]({{< relref "/develop/ai/context-engine/agent-playbook/faqs" >}}) or [uploading skills]({{< relref "/develop/ai/context-engine/agent-playbook/skills" >}}). Filter retrieval by those attributes to select a locale, product, or audience. + +Filters use exact, typed comparisons. Each filter node has one operator: `eq`, `and`, or `or`. In Python, the client uses `filter_`, `and_`, and `or_` to avoid reserved words. Direct requests use `filter`, `and`, and `or`. + +An equality filter matches a scalar attribute or a member of a list-valued attribute. Boolean `true`, integer `1`, and string `"true"` are different values. Filters do not search metadata and do not replace playbook access grants. + +{{< multitabs id="playbook-retrieval-filter" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +filtered = playbook.search_faqs( + query="Where is my refund?", + filter_={"and_": [ + {"eq": {"name": "locale", "value": "en-US"}}, + {"eq": {"name": "topic", "value": "refund"}}, + ]}, +) +print(filtered.model_dump_json(by_alias=True, indent=2)) +``` + +-tab-sep- + +```typescript +const filtered = await playbook.searchFaqs({ + query: "Where is my refund?", + filter: { and: [ + { eq: { name: "locale", value: "en-US" } }, + { eq: { name: "topic", value: "refund" } }, + ] }, +}); +console.log(JSON.stringify(filtered, null, 2)); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/faqs/search" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data '{"query":"Where is my refund?","filter":{"and":[{"eq":{"name":"locale","value":"en-US"}},{"eq":{"name":"topic","value":"refund"}}]}}' | jq . +``` + +{{< /multitabs >}} + +Use entry lookup or list operations for management state. Lists use pagination cursors; search and discovery use result limits. See [service limits and errors]({{< relref "/develop/ai/context-engine/agent-playbook/limits" >}}) for request constraints and recovery. diff --git a/content/develop/ai/context-engine/agent-playbook/skills.md b/content/develop/ai/context-engine/agent-playbook/skills.md new file mode 100644 index 0000000000..53f984ac58 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/skills.md @@ -0,0 +1,479 @@ +--- +alwaysopen: false +categories: +- docs +- develop +- ai +description: Package, validate, upload, version, and publish reusable agent skills. +hideListLinks: true +linktitle: Manage skills +title: Manage skills +weight: 30 +--- + +A skill tells an agent how to complete a task: what inputs or tools to use, which decisions to make, and what to return. This example drafts a customer reply by choosing and filling a refund template. + +## Before you begin + +Use an existing playbook and an API key with `write` permission to upload and maintain skills. Publishing and withdrawing require `publish`. + +Follow the quickstart to [install a client and configure it]({{< relref "/develop/ai/context-engine/agent-playbook/quickstart#before-you-begin" >}}). You also need Bash and the `zip` utility for the sample package. + +Download the complete example: [Python]({{< relref "/develop/ai/context-engine/agent-playbook/skills/" >}}skills.py), [TypeScript]({{< relref "/develop/ai/context-engine/agent-playbook/skills/" >}}skills.mts), or [HTTP]({{< relref "/develop/ai/context-engine/agent-playbook/skills/" >}}skills.sh). The HTTP script creates the package. For Python or TypeScript, run the package commands in [Prepare a skill package]({{< relref "/develop/ai/context-engine/agent-playbook/skills#prepare-a-skill-package" >}}) first. + +## Prepare a skill package + +Package skills using the [Agent Skills specification](https://agentskills.io/specification). Each directory contains `SKILL.md` with frontmatter and Markdown instructions. The required `name` matches the directory name; the required `description` explains the task and when to use the skill. + +Make the task and its conditions explicit in the description: semantic matching uses the skill's name and description. In the instructions, specify inputs, decisions, and expected output. Put tool requirements in `compatibility`. Reference supporting files by paths relative to the skill root, with a reason to read each one. + +“Help with refunds” is too broad: it overlaps refund requests and status questions. “Draft a customer reply from a reported refund stage or a missing refund” identifies the task and its inputs. + +To prepare an upload: + +1. Validate the directory against the Agent Skills specification. +2. Check that every referenced file is included and each path is relative to the skill root. +3. Create a ZIP archive containing exactly one skill directory. +4. Encode the archive as base64 for the upload request. + +Agent Playbooks also enforces [package and content limits]({{< relref "/develop/ai/context-engine/agent-playbook/limits#content-limits" >}}). + +Run these commands in a new directory. The sample uses customer-provided information without accessing an order system. Adapt its guidance to your policy. + +```bash +mkdir -p refund-support-reply/references +cat > refund-support-reply/SKILL.md <<'SKILL' +--- +name: refund-support-reply +description: Draft a customer reply for an existing refund using a reported refund stage or a report that no refund is listed. Use the matching reply template. Do not use to request a new refund. +--- + +# Draft a refund support reply + +1. Check whether the customer supplied a displayed refund stage or said no refund is listed. If neither is known, ask what their order history shows before drafting a reply. +2. Read references/reply-template.md for the listed and missing refund templates. +3. If a stage is supplied, choose the listed refund template and replace with that exact stage. +4. If no refund is listed, choose the missing refund template. Do not invent a stage. +5. Return only the completed customer reply. Do not leave placeholders in it. + +Do not claim to have accessed the order system, claim a refund was issued, or promise a bank posting date. +SKILL +cat > refund-support-reply/references/reply-template.md <<'TEMPLATE' +Refund listed: Your order history shows "". Support can help if you need more information about that stage. + +Refund missing: Contact support with your order number and explain that no refund appears in your order history. +TEMPLATE +zip -r refund-support-reply.zip refund-support-reply +``` + +## Upload and validate a skill + +Create an entry with `create_skill`. Supply the archive as `package_base64` in the Python client or `packageBase64` in a direct request. Include intents, attributes, and metadata alongside the package. + +Upload validation rejects invalid frontmatter, a mismatched root name, unsafe or duplicate paths, symbolic links, special files such as device files, and content exceeding its limits. Regular text and binary files are supported. Include only the skill directory in the archive. + +Save the returned entry identifier so later uploads update the same skill. + +Validation checks structure and limits, not whether the procedure suits its audience. Review its accuracy before publishing. Your application controls script execution and tool permissions. + +{{< multitabs id="playbook-skill-create" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +import base64 +from pathlib import Path + +package_base64 = base64.b64encode(Path("refund-support-reply.zip").read_bytes()).decode("ascii") +skill_context = { + "intents": ["Draft a refund support reply", "Choose a refund reply template"], + "attributes": {"locale": "en-US", "topic": "refund"}, +} +skill_draft = playbook.create_skill(package_base64=package_base64, content=skill_context) +skill_id = skill_draft.entry.entry_id +skill_version = skill_draft.version.version +print(skill_id, skill_version) +``` + +-tab-sep- + +```typescript +const { readFile } = await import("node:fs/promises"); +const packageBase64 = (await readFile("refund-support-reply.zip")).toString("base64"); +const skillContext = { + intents: ["Draft a refund support reply", "Choose a refund reply template"], + attributes: { locale: "en-US", topic: "refund" }, +}; +const skillDraft = await playbook.createSkill({ packageBase64, content: skillContext }); +const skillId = skillDraft.entry.entryId; +const skillVersion = skillDraft.version.version; +console.log(skillId, skillVersion); +``` + +-tab-sep- + +```bash +base64 < refund-support-reply.zip | tr -d '\r\n' > package.b64 +jq -n --rawfile package package.b64 '{ + packageBase64: $package, + content: { + intents: ["Draft a refund support reply", "Choose a refund reply template"], + attributes: {locale: "en-US", topic: "refund"} + } +}' > skill.json +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/skills" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @skill.json > skill-draft.json +SKILL_ID=$(jq -er '.entry.entryId' skill-draft.json) +SKILL_VERSION=$(jq -er '.version.version' skill-draft.json) +jq .version skill-draft.json +``` + +{{< /multitabs >}} + +## Review and publish a version + +1. Retrieve the exact version returned by the upload. +2. Review its name, description, instructions, and resource descriptors. +3. Read the supporting files needed to assess the procedure. +4. Publish the reviewed version with `publish_skill`. +5. Test retrieval with a reader key, then test the agent's use of the skill. + +Uploading a valid package does not publish it automatically. The current [proposal workflow]({{< relref "/develop/ai/context-engine/agent-playbook/proposals" >}}) creates FAQs, not skills. + +{{< multitabs id="playbook-skill-review" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +stored_skill = playbook.get_skill_version(skill_id=skill_id, version=skill_version) +print(stored_skill.content) +``` + +-tab-sep- + +```typescript +const storedSkill = await playbook.getSkillVersion(skillId, skillVersion); +console.log(storedSkill.content); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/skills/$SKILL_ID/versions/$SKILL_VERSION" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq .content +``` + +{{< /multitabs >}} + +Publish after review. The [MCP example]({{< relref "/develop/ai/context-engine/agent-playbook/mcp#connect-an-agent" >}}) asks an agent to draft a reply for the stage “Processing.” Check that it loads this skill and template, preserves the stage, and leaves no placeholders or invented payment date. Also test a missing refund and missing input. + +{{< multitabs id="playbook-skill-publish" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.publish_skill(skill_id=skill_id, version=skill_version) +``` + +-tab-sep- + +```typescript +await playbook.publishSkill(skillId, { version: skillVersion }); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/skills/$SKILL_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq -n --argjson version "$SKILL_VERSION" '{version: $version}')" | jq .publication +``` + +{{< /multitabs >}} + +## Update a skill + +Use `append_skill_version` to update the existing entry. Supply the complete package and the intents, attributes, and metadata you want to retain. + +The identifier stays the same. Agents use the previous publication until you review and publish the new version. + +To roll back, publish an earlier version. Concurrent publication changes are last-write-wins, so coordinate publishers. + +This example adds a `channel` attribute for chat retrieval and keeps the same instructions. To change the instructions or files, edit the directory and create a fresh ZIP archive before uploading. + +{{< multitabs id="playbook-skill-append" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +from copy import deepcopy + +updated_context = deepcopy(skill_context) +updated_context["attributes"]["channel"] = "chat" +replacement = base64.b64encode(Path("refund-support-reply.zip").read_bytes()).decode("ascii") +skill_update = playbook.append_skill_version( + skill_id=skill_id, package_base64=replacement, content=updated_context, +) +print("Review before publishing:", skill_update.version) +``` + +-tab-sep- + +```typescript +const updatedContext = { + ...skillContext, + attributes: { ...skillContext.attributes, channel: "chat" }, +}; +const replacement = (await readFile("refund-support-reply.zip")).toString("base64"); +const skillUpdate = await playbook.appendSkillVersion(skillId, { + packageBase64: replacement, content: updatedContext, +}); +console.log("Review before publishing:", skillUpdate.version); +``` + +-tab-sep- + +```bash +base64 < refund-support-reply.zip | tr -d '\r\n' > package.b64 +jq --rawfile package package.b64 \ + '.packageBase64 = $package | .content.attributes.channel = "chat"' \ + skill.json > skill-update.json +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/skills/$SKILL_ID/versions" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @skill-update.json | jq .version +``` + +{{< /multitabs >}} + +Leave the update unpublished here so the loading example uses the first version. Publishing the update makes its new attributes available to filters. + +## Load instructions and files + +Search and discovery return previews: a name, description, entry identifier, version, and file address. Select a relevant skill before loading its instructions. Fetch supporting files when the instructions or task call for them. + +Keep the returned identifier and version together, and use the returned resource addresses. If the skill is republished or withdrawn between discovery and loading, rediscover and reassess it. See [Retrieve knowledge]({{< relref "/develop/ai/context-engine/agent-playbook/retrieval" >}}) and [Connect with MCP]({{< relref "/develop/ai/context-engine/agent-playbook/mcp" >}}). + +Resource descriptors identify the file's media type, size, and digest. Decode known text files as UTF-8 (Unicode Transformation Format, 8-bit); keep binary files as bytes. Close the Python response stream after reading it. + +This example loads the first published version and reads its reply template. + +{{< multitabs id="playbook-skill-load" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +selected = playbook.get_skill_version(skill_id=skill_id, version=skill_version) +skill = selected.content.payload.skill +if skill is None: + raise ValueError("Expected a skill") +print(skill.instructions) +resource = next((r for r in skill.resources if r.path == "references/reply-template.md"), None) +if resource is None: + raise ValueError("Reply template is missing") +response = playbook.get_skill_resource( + skill_id=selected.entry_id, version=selected.version, resource_id=resource.resource_id, +) +try: + print(response.result.read().decode("utf-8")) +finally: + response.result.close() +``` + +-tab-sep- + +```typescript +const selected = await playbook.getSkillVersion(skillId, skillVersion); +if (!("skill" in selected.content.payload)) throw new Error("Expected a skill"); +const skill = selected.content.payload.skill; +console.log(skill.instructions); +const resource = skill.resources.find(r => r.path === "references/reply-template.md"); +if (!resource) throw new Error("Reply template is missing"); +const response = await playbook.getSkillResource(selected.entryId, selected.version, resource.resourceId); +console.log(await new Response(response.result).text()); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/skills/$SKILL_ID/versions/$SKILL_VERSION" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" > selected-skill.json +jq -r '.content.payload.skill.instructions' selected-skill.json +RESOURCE_ID=$(jq -er '.content.payload.skill.resources[] | select(.path == "references/reply-template.md") | .resourceId' selected-skill.json) +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/skills/$SKILL_ID/versions/$SKILL_VERSION/resources/$RESOURCE_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" +``` + +{{< /multitabs >}} + +To verify file content, compare the descriptor's SHA-256 (Secure Hash Algorithm 256-bit) digest with the downloaded bytes. It uses `sha256:`; the HTTP `Content-Digest` header represents the same hash as `sha-256=::`. This differs from the normalized entry digest in [entry lookup]({{< relref "/develop/ai/context-engine/agent-playbook/faqs#look-up-entry-state" >}}). + +## List skills and inspect history + +Use `list_skills` to inventory entries and `get_skill` to inspect one entry's summary. These operations require `read`; draft content and full version history require `write` or `publish`. To resolve a skill by its entry key or compare latest and published content digests, use the shared [entry lookup operation]({{< relref "/develop/ai/context-engine/agent-playbook/faqs#look-up-entry-state" >}}). + +Follow cursors even after empty pages, and keep filters and ordering unchanged. An absent first-page `total` means the count is unknown. See [pagination behavior]({{< relref "/develop/ai/context-engine/agent-playbook/limits#retrieval-limits" >}}). + +{{< multitabs id="playbook-skill-list" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +cursor = None +while True: + page = playbook.list_skills(sort_by="updatedAt", sort_order="desc", limit=50, cursor=cursor) + if cursor is None: + print("Total skills:", page.total if page.total is not None else "unknown") + for entry in page.skills: + print(entry.entry_id, entry.latest_version, entry.publication) + cursor = page.next_cursor + if cursor is None: + break +``` + +-tab-sep- + +```typescript +let skillCursor: string | undefined; +while (true) { + const page = await playbook.listSkills( + "updatedAt", "desc", undefined, undefined, undefined, 50, skillCursor, + ); + if (skillCursor === undefined) console.log("Total skills:", page.total ?? "unknown"); + for (const entry of page.skills) console.log(entry.entryId, entry.latestVersion, entry.publication); + skillCursor = page.nextCursor; + if (skillCursor === undefined) break; +} +``` + +-tab-sep- + +```bash +CURSOR='' +while true; do + PAGE_ARGS=(--data-urlencode sortBy=updatedAt --data-urlencode sortOrder=desc --data-urlencode limit=50) + if [[ -n "$CURSOR" ]]; then PAGE_ARGS+=(--data-urlencode "cursor=$CURSOR"); fi + curl --fail-with-body --silent --show-error --get "$PLAYBOOK_API/skills" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" "${PAGE_ARGS[@]}" > skill-page.json + if [[ -z "$CURSOR" ]]; then jq '.total // "unknown"' skill-page.json; fi + jq '.skills[] | {entryId, latestVersion, publication}' skill-page.json + CURSOR=$(jq -r '.nextCursor // empty' skill-page.json) + [[ -n "$CURSOR" ]] || break +done +``` + +{{< /multitabs >}} + +{{< multitabs id="playbook-skill-state" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +entry = playbook.get_skill(skill_id=skill_id) +print(entry.latest_version, entry.publication) +``` + +-tab-sep- + +```typescript +const entry = await playbook.getSkill(skillId); +console.log(entry.latestVersion, entry.publication); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error "$PLAYBOOK_API/skills/$SKILL_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq '{latestVersion, publication}' +``` + +{{< /multitabs >}} + +Read the complete versions newest first. Use a returned version number when reviewing or rolling back a publication: + +{{< multitabs id="playbook-skill-history" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +cursor = None +while True: + page = playbook.list_skill_versions(skill_id=skill_id, limit=50, cursor=cursor) + for item in page.versions: + print(item.version, item.content_digest, item.content) + cursor = page.next_cursor + if cursor is None: + break +``` + +-tab-sep- + +```typescript +let skillVersionCursor: string | undefined; +while (true) { + const page = await playbook.listSkillVersions(skillId, undefined, undefined, 50, skillVersionCursor); + for (const item of page.versions) console.log(item.version, item.contentDigest, item.content); + skillVersionCursor = page.nextCursor; + if (skillVersionCursor === undefined) break; +} +``` + +-tab-sep- + +```bash +CURSOR='' +while true; do + PAGE_ARGS=(--data-urlencode limit=50) + if [[ -n "$CURSOR" ]]; then PAGE_ARGS+=(--data-urlencode "cursor=$CURSOR"); fi + curl --fail-with-body --silent --show-error --get "$PLAYBOOK_API/skills/$SKILL_ID/versions" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" "${PAGE_ARGS[@]}" > skill-versions.json + jq '.versions[] | {version, contentDigest, content}' skill-versions.json + CURSOR=$(jq -r '.nextCursor // empty' skill-versions.json) + [[ -n "$CURSOR" ]] || break +done +``` + +{{< /multitabs >}} + +## Withdraw or delete a skill + +Withdraw the publication with `withdraw_skill` to stop serving the procedure while keeping its history. Delete the entry with `delete_skill` when you want to remove all its versions and files permanently. + +The downloads omit these operations. For cleanup after a download, use a new script with the client setup and set `skill_id` (Python), `skillId` (TypeScript), or `SKILL_ID` (HTTP) to the printed identifier. + +{{< multitabs id="playbook-skill-withdraw" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.withdraw_skill(skill_id=skill_id) +``` + +-tab-sep- + +```typescript +await playbook.withdrawSkill(skillId); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X DELETE "$PLAYBOOK_API/skills/$SKILL_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq . +``` + +{{< /multitabs >}} + +To remove the sample and its files permanently: + +{{< multitabs id="playbook-skill-delete" tab1="Python" tab2="TypeScript" tab3="HTTP" >}} + +```python +playbook.delete_skill(skill_id=skill_id) +``` + +-tab-sep- + +```typescript +await playbook.deleteSkill(skillId); +``` + +-tab-sep- + +```bash +curl --fail-with-body --silent --show-error \ + -X DELETE "$PLAYBOOK_API/skills/$SKILL_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" +``` + +{{< /multitabs >}} diff --git a/content/develop/ai/context-engine/agent-playbook/skills/prepare_skill.sh b/content/develop/ai/context-engine/agent-playbook/skills/prepare_skill.sh new file mode 100644 index 0000000000..25582c06d8 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/skills/prepare_skill.sh @@ -0,0 +1,26 @@ +# Generated from skills.md; edit the page, then run examples.py --refresh-downloads. +set -euo pipefail + +mkdir -p refund-support-reply/references +cat > refund-support-reply/SKILL.md <<'SKILL' +--- +name: refund-support-reply +description: Draft a customer reply for an existing refund using a reported refund stage or a report that no refund is listed. Use the matching reply template. Do not use to request a new refund. +--- + +# Draft a refund support reply + +1. Check whether the customer supplied a displayed refund stage or said no refund is listed. If neither is known, ask what their order history shows before drafting a reply. +2. Read references/reply-template.md for the listed and missing refund templates. +3. If a stage is supplied, choose the listed refund template and replace with that exact stage. +4. If no refund is listed, choose the missing refund template. Do not invent a stage. +5. Return only the completed customer reply. Do not leave placeholders in it. + +Do not claim to have accessed the order system, claim a refund was issued, or promise a bank posting date. +SKILL +cat > refund-support-reply/references/reply-template.md <<'TEMPLATE' +Refund listed: Your order history shows "". Support can help if you need more information about that stage. + +Refund missing: Contact support with your order number and explain that no refund appears in your order history. +TEMPLATE +zip -r refund-support-reply.zip refund-support-reply diff --git a/content/develop/ai/context-engine/agent-playbook/skills/skills.mts b/content/develop/ai/context-engine/agent-playbook/skills/skills.mts new file mode 100644 index 0000000000..9d142a76d9 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/skills/skills.mts @@ -0,0 +1,73 @@ +// Generated from skills.md; edit the page, then run examples.py --refresh-downloads. +import { Playbook } from "@redis-iris/agent-playbook"; + +function requiredEnv(name: string): string { + const value = process.env[name]; + if (!value) throw new Error(`Set ${name}`); + return value; +} + +const playbook = new Playbook({ + serverURL: requiredEnv("PLAYBOOK_URL"), + playbookId: requiredEnv("PLAYBOOK_ID"), + apiKey: requiredEnv("PLAYBOOK_API_KEY"), + retryConfig: { strategy: "none" }, +}); +console.log(await playbook.getPlaybookHealth()); + +const { readFile } = await import("node:fs/promises"); +const packageBase64 = (await readFile("refund-support-reply.zip")).toString("base64"); +const skillContext = { + intents: ["Draft a refund support reply", "Choose a refund reply template"], + attributes: { locale: "en-US", topic: "refund" }, +}; +const skillDraft = await playbook.createSkill({ packageBase64, content: skillContext }); +const skillId = skillDraft.entry.entryId; +const skillVersion = skillDraft.version.version; +console.log(skillId, skillVersion); + +const storedSkill = await playbook.getSkillVersion(skillId, skillVersion); +console.log(storedSkill.content); + +await playbook.publishSkill(skillId, { version: skillVersion }); + +const updatedContext = { + ...skillContext, + attributes: { ...skillContext.attributes, channel: "chat" }, +}; +const replacement = (await readFile("refund-support-reply.zip")).toString("base64"); +const skillUpdate = await playbook.appendSkillVersion(skillId, { + packageBase64: replacement, content: updatedContext, +}); +console.log("Review before publishing:", skillUpdate.version); + +const selected = await playbook.getSkillVersion(skillId, skillVersion); +if (!("skill" in selected.content.payload)) throw new Error("Expected a skill"); +const skill = selected.content.payload.skill; +console.log(skill.instructions); +const resource = skill.resources.find(r => r.path === "references/reply-template.md"); +if (!resource) throw new Error("Reply template is missing"); +const response = await playbook.getSkillResource(selected.entryId, selected.version, resource.resourceId); +console.log(await new Response(response.result).text()); + +let skillCursor: string | undefined; +while (true) { + const page = await playbook.listSkills( + "updatedAt", "desc", undefined, undefined, undefined, 50, skillCursor, + ); + if (skillCursor === undefined) console.log("Total skills:", page.total ?? "unknown"); + for (const entry of page.skills) console.log(entry.entryId, entry.latestVersion, entry.publication); + skillCursor = page.nextCursor; + if (skillCursor === undefined) break; +} + +const entry = await playbook.getSkill(skillId); +console.log(entry.latestVersion, entry.publication); + +let skillVersionCursor: string | undefined; +while (true) { + const page = await playbook.listSkillVersions(skillId, undefined, undefined, 50, skillVersionCursor); + for (const item of page.versions) console.log(item.version, item.contentDigest, item.content); + skillVersionCursor = page.nextCursor; + if (skillVersionCursor === undefined) break; +} diff --git a/content/develop/ai/context-engine/agent-playbook/skills/skills.py b/content/develop/ai/context-engine/agent-playbook/skills/skills.py new file mode 100644 index 0000000000..8e2c8e3690 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/skills/skills.py @@ -0,0 +1,78 @@ +# Generated from skills.md; edit the page, then run examples.py --refresh-downloads. +import os +from redis_agent_playbook import Playbook + +playbook = Playbook( + os.environ["PLAYBOOK_URL"], + playbook_id=os.environ["PLAYBOOK_ID"], + api_key=os.environ["PLAYBOOK_API_KEY"], + retry_config=None, +) +print(playbook.get_playbook_health()) + +import base64 +from pathlib import Path + +package_base64 = base64.b64encode(Path("refund-support-reply.zip").read_bytes()).decode("ascii") +skill_context = { + "intents": ["Draft a refund support reply", "Choose a refund reply template"], + "attributes": {"locale": "en-US", "topic": "refund"}, +} +skill_draft = playbook.create_skill(package_base64=package_base64, content=skill_context) +skill_id = skill_draft.entry.entry_id +skill_version = skill_draft.version.version +print(skill_id, skill_version) + +stored_skill = playbook.get_skill_version(skill_id=skill_id, version=skill_version) +print(stored_skill.content) + +playbook.publish_skill(skill_id=skill_id, version=skill_version) + +from copy import deepcopy + +updated_context = deepcopy(skill_context) +updated_context["attributes"]["channel"] = "chat" +replacement = base64.b64encode(Path("refund-support-reply.zip").read_bytes()).decode("ascii") +skill_update = playbook.append_skill_version( + skill_id=skill_id, package_base64=replacement, content=updated_context, +) +print("Review before publishing:", skill_update.version) + +selected = playbook.get_skill_version(skill_id=skill_id, version=skill_version) +skill = selected.content.payload.skill +if skill is None: + raise ValueError("Expected a skill") +print(skill.instructions) +resource = next((r for r in skill.resources if r.path == "references/reply-template.md"), None) +if resource is None: + raise ValueError("Reply template is missing") +response = playbook.get_skill_resource( + skill_id=selected.entry_id, version=selected.version, resource_id=resource.resource_id, +) +try: + print(response.result.read().decode("utf-8")) +finally: + response.result.close() + +cursor = None +while True: + page = playbook.list_skills(sort_by="updatedAt", sort_order="desc", limit=50, cursor=cursor) + if cursor is None: + print("Total skills:", page.total if page.total is not None else "unknown") + for entry in page.skills: + print(entry.entry_id, entry.latest_version, entry.publication) + cursor = page.next_cursor + if cursor is None: + break + +entry = playbook.get_skill(skill_id=skill_id) +print(entry.latest_version, entry.publication) + +cursor = None +while True: + page = playbook.list_skill_versions(skill_id=skill_id, limit=50, cursor=cursor) + for item in page.versions: + print(item.version, item.content_digest, item.content) + cursor = page.next_cursor + if cursor is None: + break diff --git a/content/develop/ai/context-engine/agent-playbook/skills/skills.sh b/content/develop/ai/context-engine/agent-playbook/skills/skills.sh new file mode 100644 index 0000000000..4b52d39273 --- /dev/null +++ b/content/develop/ai/context-engine/agent-playbook/skills/skills.sh @@ -0,0 +1,102 @@ +# Generated from skills.md; edit the page, then run examples.py --refresh-downloads. +set -euo pipefail + +mkdir -p refund-support-reply/references +cat > refund-support-reply/SKILL.md <<'SKILL' +--- +name: refund-support-reply +description: Draft a customer reply for an existing refund using a reported refund stage or a report that no refund is listed. Use the matching reply template. Do not use to request a new refund. +--- + +# Draft a refund support reply + +1. Check whether the customer supplied a displayed refund stage or said no refund is listed. If neither is known, ask what their order history shows before drafting a reply. +2. Read references/reply-template.md for the listed and missing refund templates. +3. If a stage is supplied, choose the listed refund template and replace with that exact stage. +4. If no refund is listed, choose the missing refund template. Do not invent a stage. +5. Return only the completed customer reply. Do not leave placeholders in it. + +Do not claim to have accessed the order system, claim a refund was issued, or promise a bank posting date. +SKILL +cat > refund-support-reply/references/reply-template.md <<'TEMPLATE' +Refund listed: Your order history shows "". Support can help if you need more information about that stage. + +Refund missing: Contact support with your order number and explain that no refund appears in your order history. +TEMPLATE +zip -r refund-support-reply.zip refund-support-reply + +set -euo pipefail +PLAYBOOK_API="${PLAYBOOK_URL%/}/v1/playbooks/$PLAYBOOK_ID" +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/health" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq . + +base64 < refund-support-reply.zip | tr -d '\r\n' > package.b64 +jq -n --rawfile package package.b64 '{ + packageBase64: $package, + content: { + intents: ["Draft a refund support reply", "Choose a refund reply template"], + attributes: {locale: "en-US", topic: "refund"} + } +}' > skill.json +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/skills" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @skill.json > skill-draft.json +SKILL_ID=$(jq -er '.entry.entryId' skill-draft.json) +SKILL_VERSION=$(jq -er '.version.version' skill-draft.json) +jq .version skill-draft.json + +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/skills/$SKILL_ID/versions/$SKILL_VERSION" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq .content + +curl --fail-with-body --silent --show-error \ + -X PUT "$PLAYBOOK_API/skills/$SKILL_ID/publication" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' \ + --data "$(jq -n --argjson version "$SKILL_VERSION" '{version: $version}')" | jq .publication + +base64 < refund-support-reply.zip | tr -d '\r\n' > package.b64 +jq --rawfile package package.b64 \ + '.packageBase64 = $package | .content.attributes.channel = "chat"' \ + skill.json > skill-update.json +curl --fail-with-body --silent --show-error \ + -X POST "$PLAYBOOK_API/skills/$SKILL_ID/versions" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" \ + -H 'Content-Type: application/json' --data-binary @skill-update.json | jq .version + +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/skills/$SKILL_ID/versions/$SKILL_VERSION" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" > selected-skill.json +jq -r '.content.payload.skill.instructions' selected-skill.json +RESOURCE_ID=$(jq -er '.content.payload.skill.resources[] | select(.path == "references/reply-template.md") | .resourceId' selected-skill.json) +curl --fail-with-body --silent --show-error \ + "$PLAYBOOK_API/skills/$SKILL_ID/versions/$SKILL_VERSION/resources/$RESOURCE_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" + +CURSOR='' +while true; do + PAGE_ARGS=(--data-urlencode sortBy=updatedAt --data-urlencode sortOrder=desc --data-urlencode limit=50) + if [[ -n "$CURSOR" ]]; then PAGE_ARGS+=(--data-urlencode "cursor=$CURSOR"); fi + curl --fail-with-body --silent --show-error --get "$PLAYBOOK_API/skills" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" "${PAGE_ARGS[@]}" > skill-page.json + if [[ -z "$CURSOR" ]]; then jq '.total // "unknown"' skill-page.json; fi + jq '.skills[] | {entryId, latestVersion, publication}' skill-page.json + CURSOR=$(jq -r '.nextCursor // empty' skill-page.json) + [[ -n "$CURSOR" ]] || break +done + +curl --fail-with-body --silent --show-error "$PLAYBOOK_API/skills/$SKILL_ID" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" | jq '{latestVersion, publication}' + +CURSOR='' +while true; do + PAGE_ARGS=(--data-urlencode limit=50) + if [[ -n "$CURSOR" ]]; then PAGE_ARGS+=(--data-urlencode "cursor=$CURSOR"); fi + curl --fail-with-body --silent --show-error --get "$PLAYBOOK_API/skills/$SKILL_ID/versions" \ + -H "Authorization: Bearer $PLAYBOOK_API_KEY" "${PAGE_ARGS[@]}" > skill-versions.json + jq '.versions[] | {version, contentDigest, content}' skill-versions.json + CURSOR=$(jq -r '.nextCursor // empty' skill-versions.json) + [[ -n "$CURSOR" ]] || break +done diff --git a/for-ais-only/agent-playbooks-docs-review.md b/for-ais-only/agent-playbooks-docs-review.md new file mode 100644 index 0000000000..2e022ca209 --- /dev/null +++ b/for-ais-only/agent-playbooks-docs-review.md @@ -0,0 +1,171 @@ +# Agent Playbooks developer documentation review + +Updated **2026-10-12** after the repository standards check, API reference presentation fix, and live verification against the current local service. **Ready for local human expert review of the core developer experience.** The extraction pipeline remains outside this work. The changes are prepared for a draft, WIP PR. No publication gate is included; merging to `main` publishes the pages. + +## First-time developer improvements + +The FAQ explains where to find refund status. The separate `refund-support-reply` skill drafts a customer reply from a reported stage or a missing refund, chooses a supporting template, preserves the customer's exact stage, and asks for missing input. The MCP walkthrough requests a completed reply for “Processing,” prints tool names and arguments, and shows the expected reply and additional cases. Its result no longer depends on a question that the FAQ can answer alone. Tool selection remains model-dependent; the documented checks do not promise a fixed call sequence. + +Quickstart and Skills offer complete Python, TypeScript, and HTTP downloads next to the walkthrough, using the same page-resource pattern as the downloadable OpenAPI contract. Their editable source remains the displayed Markdown blocks. `examples.py --refresh-downloads` refreshes the files, and a CI check rejects drift. Downloads retain published samples; the walkthrough's optional cleanup is excluded. Python and TypeScript skill downloads use the package commands in the skill guide; the HTTP download includes them. The generated package-preparation script remains available as a page resource. MCP remains a complete in-page program. No additional examples page or new site component was introduced. + +Repeated setup prose was shortened. Authoring now contrasts broad questions/descriptions with task-specific wording. Intents are identified as optional context returned in discovery rather than similarity inputs. Retrieval includes a concrete republishing race and guidance for comparing relevant and nearby out-of-scope questions. The extra prose is concentrated in runnable-example instructions, expected outcomes, and evaluation cases. The draft banner and alpha package versions remain unchanged; no beta release claim was inferred from editorial feedback. + +## Operator documentation handoff + +**Planning only: do not create operator pages in this change.** Follow the existing Iris layout, especially `content/operate/iris/agent-memory/` and its `self-managed/` section. Agent Memory's developer quickstarts link service creation and connection details to operator pages; Context Retriever links its Cloud setup and self-managed bootstrap separately. Keep Playbooks developer guides focused on content authoring, retrieval, and runtime integration. + +The intended section is `content/operate/iris/agent-playbook/`, matching the singular developer URL. These are proposed destinations, not published routes or claims that every deployment option is available: + +| Future operator material | Pattern and developer handoff | +|:-------------------------|:-----------------------------| +| `_index.md` | Deployment-options landing page, following Agent Memory. Link from the developer overview once it exists. Describe only supported deployment options. | +| `create-service.md` | Service/playbook provisioning prerequisites and readiness. Link from Quickstart's “Before you begin” for the supported managed path. | +| `view-service.md` | Where to obtain the runtime endpoint and playbook identifier, inspect readiness, and configure retrieval. Link from Quickstart's connection values and Retrieval's administrator settings. Use the final page's real anchors. | +| Key administration and authentication | Follow the access-management pattern in Context Retriever and the self-managed authentication guide in Agent Memory. Explain provisioning, expiry/revocation, and playbook action grants; link from Quickstart and Concepts. Choose final filenames when the operator scope is agreed. | +| `self-managed/` | Index, prerequisites, deployment planning, data-plane configuration, deployment procedures, authentication, operations, and control-plane reference. Link its setup route from Quickstart and its configuration guidance from Retrieval. Keep administrative credentials separate from runtime keys. | +| Source connection and extraction settings | Link from Proposals only after the separate extraction work is implemented and reviewed. Configuration is operator work; reviewing candidates remains developer work. | + +Unresolved TODO comments mark the overview, quickstart, retrieval settings, and extraction handoffs. Until the pages exist, the public docs state the administrator prerequisite and the absence of setup guidance. There are no relrefs to proposed paths, placeholder operator pages, or invented console instructions. When operator pages land, replace the temporary handoff wording, resolve the TODOs, and check page and anchor links together. + +## Completeness assessment + +The API reference covers all **37 public SDK operations**, and every operation is now described in the guides. The displayed examples execute **33 synchronous Python operations**, with equivalent TypeScript and HTTP examples, plus a standalone asynchronous search. Proposal purging remains a commented alternative; bulk FAQ deletion, bulk skill deletion, and clearing are documented in a maintenance table rather than run during the walkthroughs. These counts measure documentation coverage, not live-service correctness. + +| Area | Assessment | +|:-----|:-----------| +| First useful result | Covered: connection, health request, FAQ creation, exact-version review, publication, search, discovery, update, and cleanup. | +| FAQ and skill lifecycle | Covered: complete immutable versions, explicit publication, rollback, withdrawal, deletion, package preparation, resource reads, entry inventory, and version-history pagination. | +| Retrieval | Covered: search versus discovery, the authoring fields used for semantic matching, exact filters, pins, thresholds, count limits, preview budgets, and publication changes during skill loading. | +| Proposals and evidence | Covered: inbox pagination and search, manual creation, duplicate review, evidence submission, candidate replacement with revision checks, acceptance, publication, dismissal, restoration, retention, and promotion history. | +| MCP | Covered: working agent integration, three retrieval tools, authentication, progressive loading, tool failures, standard resource reads, and text/binary results. | +| SDK maintenance tasks | Covered: pagination through empty pages, optional totals, lookup by identifier or immutable key, found/missing results, content digests, bulk cleanup, asynchronous Python, timeouts, and custom-client ownership. | +| Core features arriving through Iris | The reviewed data-plane contracts align with the SDK. Browser administration, operator setup, and extraction configuration are separate from this developer path. Future MCP skill extensions are not implemented and should not be presented as available. | + +## Coverage added + +The user authorized these additions after the initial review. They extend existing pages without adding another reader path. + +1. **Semantic matching inputs.** FAQs and Retrieval now identify the FAQ question or skill name and description as the embedded fields. Intents, FAQ answers, metadata, skill instructions, and supporting files do not affect similarity scores. Attribute filters are explained separately. Source: `../iris/playbook/dataplane/internal/usecases/candidates/processor.go`, `embeddingInput`. +2. **Entry inventory, history, and lookup.** FAQs and Skills now include cursor loops for entries and full version history. The shared lookup example resolves the same FAQ by identifier and immutable key, handles found/missing results, and exposes latest/publication digests, version count, and the version limit. Concepts and Limits explain permissions, live lists, empty pages, and unknown totals. Sources: `entries/lookup.go`, `entries/service.go`, and the list/lookup structures in `playbook/dataplane/api/redis.ai.playbook.dataplane.smithy` under `../iris`. +3. **Proposal inbox, duplicate review, and history.** Proposals now includes inbox and trash pagination and semantic search. It explains the typed duplicate conflict and `existingProposalId`, reviewing before attaching evidence, and the distinct `promoting` and `promoted` states. Sources: `../iris/playbook/dataplane/internal/usecases/proposals/service.go`, `internal/adapters/repositories/proposals.go`, and `internal/domain/proposal.go`. +4. **Asynchronous Python and client configuration.** Quickstart now contains a standalone `async with Playbook(...)` search example, request timeout guidance, and a pointer for custom HTTP clients and their ownership. Source: `../iris-sdks/playbook-python-sdk/src/redis_agent_playbook/sdk.py` and the [published SDK documentation](https://pypi.org/project/redis-agent-playbook/0.1.0a2/). +5. **MCP resources and binary files.** MCP describes `resources/templates/list`, `resources/read`, text/blob decoding, empty blobs, and current-publication access. Skills distinguishes descriptor digests, HTTP content digest headers, and normalized entry digests. Sources: `../iris/playbook/dataplane/internal/adapters/handlers/mcp/skill_resources.go`, `tools.go`, and the resource handler. Proposed `skills/list`, `skills/get`, and directory-resource extensions remain deferred. +6. **Precise restrictions and maintenance scope.** Skills names the archive restrictions on symbolic links and special files while explaining support for regular binary files. Concepts replaces “embedding identity” with the fixed provider, model, and dimensions. Limits documents bulk deletion and clear scopes, permissions, defaults, partial progress, and count semantics. Sources: `../iris/playbook/dataplane/internal/usecases/candidates/skill_archive.go`, the playbook configuration contract, and the public bulk/clear operations. + +## Tone and terminology + +The prose leads with developer tasks and uses concrete descriptions of instructions, scripts, references, and templates. It preserves the product name **Redis Agent Playbooks**, the singular URL/package name `agent-playbook`, lowercase **skill** and **playbook** in ordinary prose, and the distinction between **entry versions** and **proposal revisions**. Acceptance/promotion creates an unpublished FAQ; publication remains a separate action. Frontmatter and API schemas are unchanged. + +The earlier teaching pass reviewed the pages as a new developer, a skill author, and a product reviewer. The overview now explains what Playbooks supplies at runtime and how applications control agent context and actions. The discovery guide shows a first request before configuration details. Authoring guidance distinguishes descriptions used for matching from instructions used to perform a task. Evaluation guidance names concrete checks: paraphrases, similar questions outside an answer's scope, correct template selection, and preservation of customer-provided information. Repeated package/reference introductions were removed. That pass shortened the section by 148 words without changing the examples. The subsequent first-time developer improvements described in this report intentionally revise the skill package and MCP task, add final FAQ retrieval, and generate downloadable programs. Frontmatter and the API contract remain unchanged. + +All four SVG diagrams, their embeds, sizing attributes, and diagram-specific references were removed. The existing explanations and numbered retrieval steps stand on their own. + +The second example audit corrected three practical issues: + +- Proposal replacement now edits the action just retrieved, preserving its fields and using the corresponding revision. It no longer pairs a fresh revision with the original creation payload, which could overwrite another reviewer's changes. +- Skill versioning now adds a chat-channel attribute while preserving the package and existing attributes. The sequential walkthrough makes a real content change without requiring an unshown file edit mid-script. The sample skill has distinct steps for listed and missing refunds and a response template with a clearly marked placeholder. +- The HTTP error example now exits unsuccessfully after a failed request and handles a non-JSON error body. Python resource reads report missing skills or templates clearly and still close streams. + +The test fixture now returns the actual uploaded template bytes and a matching file digest. Regression checks exercise earlier reviewer changes and JSON/non-JSON errors in Python, TypeScript, and HTTP. + +## Scope and reader path + +There are ten public pages under `content/develop/ai/context-engine/agent-playbook/`. The plural product name remains Redis Agent Playbooks. The URL path is singular. The old developer path and the complete operator draft have been removed. No old-path aliases are needed for this unpublished branch. + +A developer receives an existing ready playbook, its data-plane endpoint, and scoped keys. The reader path is **publish and search an FAQ → discover knowledge → update a published version → add skills or review proposals → connect an agent through MCP**. No console screens, installation route, CLI commands, or unreleased SDK interfaces are invented. + +| Page | Type | Outline | +|:-----|:-----|:--------| +| [Overview](../content/develop/ai/context-engine/agent-playbook/_index.md) | Overview | Product purpose; choose skills or FAQs; get started; find each guide and reference. | +| [Concepts](../content/develop/ai/context-engine/agent-playbook/concepts.md) | Concepts | Playbooks and access; entries; immutable versions; publication; discovery; proposals; evidence. | +| [Quickstart](../content/develop/ai/context-engine/agent-playbook/quickstart.md) | How-to | Connection prerequisites; create a refund-status FAQ; review and publish; search; discover; update; clean up; asynchronous Python. | +| [FAQs](../content/develop/ai/context-engine/agent-playbook/faqs.md) | How-to | Write answers and retrieval context; create drafts; promote reviewed proposals; publish; update; list entries and history; look up state; withdraw or delete. | +| [Skills](../content/develop/ai/context-engine/agent-playbook/skills.md) | How-to | Agent Skills format; package and validation; upload; review and publish; version; load text/binary files; list entries and history; withdraw or delete. | +| [Proposals](../content/develop/ai/context-engine/agent-playbook/proposals.md) | How-to | Browse and search the inbox; create; review duplicates; attach evidence; edit; accept and publish; dismiss/restore/purge; inspect history; review opt-in extraction output. | +| [Retrieval](../content/develop/ai/context-engine/agent-playbook/retrieval.md) | How-to | Discovery selection and controls; pins; preview budgets; progressive skill disclosure; search and its request controls. | +| [MCP](../content/develop/ai/context-engine/agent-playbook/mcp.md) | How-to | Connection prerequisites; transport and headers; attach the connection to an agent; submit a task; read skill files as standard resources; diagnose connection failures. | +| [API reference](../content/develop/ai/context-engine/agent-playbook/api-reference.md) | Reference | Interactive public data-plane contract and downloadable OpenAPI. | +| [Limits](../content/develop/ai/context-engine/agent-playbook/limits.md) | Reference | Content ceilings; retrieval and pagination constraints; proposal/evidence limits; retention; status codes; conflicts; safe retries; bulk cleanup. | + +FAQ means frequently asked question; API means application programming interface; SDK means software development kit; MCP means Model Context Protocol; HTTP means Hypertext Transfer Protocol. + +## Product boundaries to preserve + +- Creating and appending produce unpublished immutable versions. Publishing selects one exact version. Withdrawal retains history; deletion removes the entry family. +- Proposal authoring currently supports ADD/new-entry FAQs, not skill proposals or updates to existing entries. Acceptance promotes one reviewed candidate to an unpublished FAQ; publication is separate. +- Proposal evidence is immutable source context supplied by the caller. It is not a correctness vote or independently verified attribution, and it is not copied to an accepted FAQ. +- Search and discovery serve current publications. Type-specific search can override threshold and count. Combined discovery uses playbook thresholds, pins, and a preview budget. Pins bypass semantic ranking and filters but consume budget. +- MCP and direct management reads have different serving boundaries. MCP tools serve only current publications. Copy the returned entry/version pair and resource addresses. +- The source connection and extraction activation belong to management. The proposal guide explains reviewing its output without introducing operator setup or internal worker design. + +## Sources and release qualification + +The public reference remains matched to **`redis-agent-playbook==0.1.0a2`**, **`@redis-iris/agent-playbook@0.1.0-alpha.2`**, and the locked SDK contract. Both published packages were used for example validation. The [source lock](../build/agent_playbook_docs/source-lock.json) records the contract and usage overlay. The final comparison used local Iris at **`fac2560f19b8af03f7f2794f54fddd108755fe01`** and `iris-sdks` at **`31dcb7046443777a451f0e5e64199383f2151b34`**. The SDK source check passes, and all vendored request/response schemas match the current local SDK contract. + +Extraction implementation, binding setup, source provenance extensions, and SDK alignment for that pipeline belong to the separate PR requested by the user. The existing conditional overview of extraction is retained and is not counted as completed pipeline documentation. The main checkout's binding contract still states that its slice does not run extraction. No extraction worktree or live worker was validated in this pass. + +Implementation and public contracts take precedence over stale design text. The core technical design document still calls version digests private and browser administration a non-goal, while the current public contract and implementation expose content digests and support browser administration. Those design-document discrepancies are outside this docs edit. + +Review foundations: + +- [Repository conventions](../AGENTS.md), [developer guidance](../AI_AGENT_DEVELOPER_GUIDE.md), and neighboring Context Retriever and Agent Memory pages informed navigation, frontmatter, and explanation depth. +- The official [PyPI package](https://pypi.org/project/redis-agent-playbook/0.1.0a2/) supplied supported client names and workflows. The package is an alpha prerelease and requires Python 3.10 or later. +- The [Agent Skills specification](https://agentskills.io/specification) is linked from the skill guide. Service-specific packaging and limits remain Redis-specific. +- [LangSmith skills](https://docs.langchain.com/langsmith/skills) describes skills through concrete tasks and optional helper scripts. The [Deep Agents skills guide](https://docs.langchain.com/oss/python/deepagents/skills) explains descriptions, instructions, and supporting files loaded as needed. The editorial takeaway is to explain what an agent does with a skill and when each file is useful. Redis discovery remains task-based; the docs do not adopt another framework's startup loading behavior. +- Google's [Gemini Enterprise Skill Registry overview](https://docs.cloud.google.com/gemini-enterprise-agent-platform/build/skill-registry) and [skill registration guide](https://docs.cloud.google.com/agent-registry/register-skills) provided references for package structure, prerequisites, and lifecycle explanations. Redis retains its own terms, limits, and explicit publication step. These references do not establish Redis execution, installation, or governance capabilities. + +## Quickstart scope + +FAQ creation and semantic search provide the first useful result without packaging files, connecting an agent framework, or configuring extraction. The quickstart follows one refund-support example through publication, search, filtered discovery, a complete version update, and cleanup. This follows Agent Memory's pattern of one scenario with several useful capabilities. Skill packaging and proposal review remain in their task guides so the first request needs only an existing playbook and a scoped key. + +## Example pattern + +- Use the existing `multitabs` shortcode with Python, TypeScript, and HTTP tabs. Shared task explanations remain outside tabs. All SDK client objects are named `playbook`. +- Keep code in the Markdown pages as the only editable source. `build/agent_playbook_docs/examples.py` assembles those exact blocks into temporary executable scripts for validation and extracts the standalone asynchronous example separately. It also rejects duplicate tab identifiers. Hugo renders the same blocks directly. The two walkthrough downloads and package-preparation script are generated page resources; a check rejects stale files. Refresh them with `python build/agent_playbook_docs/examples.py --refresh-downloads` after changing displayed code. Site builds consume the checked-in resources without requiring generation. +- The direct API guides reuse the quickstart client setup. `PLAYBOOK_API_KEY` has the permissions needed by that guide. SDK retries are disabled. Python snippets use a shared `playbook = Playbook(...)` instance and concatenate directly at top level; complete async and MCP programs retain context managers for their connection lifetimes. TypeScript uses a `.mts` file; HTTP uses Bash, curl, and jq. Skill packaging also uses zip, and HTTP evidence identifiers use openssl. +- Equivalent tabs use the same content and requests, preserve returned entry identifiers and versions, and carry reviewed proposal revisions forward. The tests compare serialized request sequences and query parameters across all three clients. +- The MCP page uses `openai-agents==0.23.1` with `mcp==1.30.0` in Python and `@openai/agents@0.19.0` in TypeScript. It creates a Streamable HTTP connection named `playbook`, attaches it to an agent, and submits a task. The HTTP tab configures hosted MCP through the Responses API. The runtime discovers and calls tools; the application does not hard-code the retrieval sequence. All three tabs now report tool names and arguments alongside the completed customer reply. The page distinguishes application network access from a hosted client's access. +- Overview, Concepts, and the generated API reference keep their existing roles. Code is attached to the seven task and troubleshooting pages. + +## Maintain the API reference + +The service's public Smithy projection generates OpenAPI; the docs consume the versioned public contract used by the SDK. `prepare_reference.py` adds titles, summaries, tags, and ordering only. It does not create a separate hand-authored API contract or fetch a moving upstream branch during a site build. + +From the docs repository: + +```sh +python -m pip install -r build/agent_playbook_docs/requirements.txt +python build/agent_playbook_docs/check_sdk_source.py --sdk-repo ../iris-sdks +npm ci --prefix build/agent_playbook_docs +python build/agent_playbook_docs/examples.py --refresh-downloads +python build/agent_playbook_docs/examples.py build/agent_playbook_docs/.generated +build/agent_playbook_docs/node_modules/.bin/tsc -p build/agent_playbook_docs +python -m pytest build/agent_playbook_docs --rootdir . -q +``` + +To refresh presentation from an approved contract: + +```sh +python build/agent_playbook_docs/prepare_reference.py --data-plane ../iris-sdks/openapi-playbook.json +``` + +For a new release, review contract changes, regenerate, update the package/source lock, and validate together. The scoped workflow checks the public contract, documentation links, TypeScript compilation, and the displayed example sequences. It installs the pinned public SDKs; a private SDK checkout is not needed in CI. + +## Validation and human review + +- All 40 scoped checks pass on 2026-10-12. These cover the public schema and source lock, page relrefs, six direct API guide sequences in all three languages, three additional runs with missing totals, six found/missing lookup cases, three tests preserving earlier proposal edits, six JSON/non-JSON request failure cases, standalone asynchronous Python, two MCP agent runs, two MCP tool-error cases, the hosted MCP request configuration and tool-call output, six downloaded walkthrough executions that retain published samples, and one download-drift check. Paginated examples must continue through an empty page and preserve cursors containing reserved URL characters. +- TypeScript examples compile with strict checking against the published alpha SDK. Python and HTTP examples execute against the same local contract fixture. Each request and JSON response is checked against the vendored public OpenAPI contract. The fixture does not implement a semantic search engine. +- The source lock matches the local SDK contract, usage overlay, and both package versions. The wire contract was not changed. +- MCP checks run the actual agent SDKs against a local MCP fixture and scripted model responses. They verify connection headers, tool discovery, model-selected dispatch, propagation of returned identifiers, and tool failures reaching the model. The hosted HTTP check validates request configuration and output handling; it does not run OpenAI's hosted MCP service or evaluate model choices. Model requests use a local endpoint and test keys; tracing is disabled during checks. +- A fresh full site build passes with Hugo Extended 0.143.0 and `HUGO_TIMEOUT=600s`. The installed Homebrew Hugo 0.167.0 fails on unrelated repository compatibility issues; the build uses the existing 0.143 binary. CI pins 0.143.1. The full build emits warnings in other documentation sections but none for these playbook pages. +- The first-time developer pass renders all ten pages and verifies all 79 internal references, including section anchors; the four dynamic API model anchors are also verified in Chrome. All seven download links resolve in HTML and Markdown, and published file bytes match their sources. The existing unit link test checks pages only; rendered anchor and asset checks remain separate. Markdown exports contain no raw shortcodes, tab separators, or TODO comments. The displayed programs and downloads are validated by the 40-test example audit and TypeScript compilation. +- Earlier browser checks cover all 53 tab groups and 159 code panels, switching each group among Python, TypeScript, and HTTP. The rendered pages and fresh build output contain no removed playbook diagrams. +- The workflow passes `actionlint`. Frontmatter and the public contract are unchanged. The example execution and compilation checks use the final displayed code. + +All 28 walkthrough/download programs and the generated package-preparation script pass against local Iris at `9f7f0da22b10cc9b858cdf7d5da31362adf57ef7`, built from the current checkout. The isolated stack uses separate metadata/content Redis instances, real Identity Service keys, and `text-embedding-3-small` with 1,536 dimensions. Python, TypeScript, and OpenAI hosted HTTP MCP run with `gpt-5-mini` and return the expected Processing reply. A temporary HTTPS proxy made the local data plane reachable for hosted MCP and rewrote the Host header for the loopback listener; the tunnel, services, Redis containers, and temporary model credentials were removed afterward. The MCP prompt now requires loading the selected skill and copying its returned resource URI after a real agent constructed an invalid relative URI. + +`build/agent_playbook_docs/live_examples.py` reproduces the live checks against a disposable local stack. It provisions one playbook per case, uses reader keys for retrieval and MCP, and deletes each test playbook. Select the configured embedding model with its command-line flags, set `OPENAI_API_KEY` for model calls, and supply `--hosted-url` only for a public HTTPS route to that same test data plane. Skipped model/hosted cases are reported explicitly. The 29-case validation record and sanitized outputs are in `/tmp/playbook-docs-live-20261012/validation-summary.json` and its `examples/` directory. + +A live maintenance probe confirms bulk FAQ/skill deletion. It also confirms an unresolved release mismatch: the pinned alpha SDK/public contract retains `ClearPlaybook` on the data plane, but the current service has removed that route (404) and its control-plane clear succeeds. The public alpha contract and package pins remain intact; a TODO in Limits marks the clearing guidance for release-owner review before launch. This is the only operation removed when comparing the current service's generated public operation set to the SDK contract. + +Concepts now links to four browser-verified model anchors in the API reference. The reference enables the built-in schema index and opt-in rendering of its existing Markdown introduction/download links. The introduction and its download link are visible at 1,440 px and 390 px without overflowing the viewport. Another product's reference is checked to retain its presentation. The remaining Concepts diagram belongs to the author's next pass. Alpha pins are intentional until the launch update; extraction availability and operator pages remain deferred. Duplicate-conflict recovery and standard MCP resources still need focused runtime review beyond the normal walkthroughs. Human experts should confirm release claims and defaults before removing the draft notice. The draft PR lists two developer-docs follow-ups: missing diagrams and the separate API/features alignment pass. Familiar acronym expansions (API, SDK, HTTP, FAQ, MCP, and URI) were removed at the author's request. No publication gate or operator pages are included. diff --git a/layouts/_default/apireference.html b/layouts/_default/apireference.html index 844bb5fee2..c5526763e4 100644 --- a/layouts/_default/apireference.html +++ b/layouts/_default/apireference.html @@ -100,9 +100,17 @@ + {{ if .Params.showPageContent }} +
+ {{ .Content }} +
+ {{ end }}
- \ No newline at end of file +