From 721729b75d1a9823d1c8eb58b020f66aeb48edd8 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Mon, 9 Nov 2026 16:39:35 +0600 Subject: [PATCH 1/9] feat: Enhance observability support with Phoenix integration - Added Phoenix observability configuration options in .env.example and agent.py. - Updated tests to validate Phoenix observability settings and ensure compatibility with existing configurations. - Modified setup middleware to bind Phoenix observability when enabled. - Adjusted production template defaults to include memory storage and JWT authentication. - Enhanced documentation for observability features, including Phoenix setup instructions. - Refactored tests to ensure proper handling of storage presets and cache store configurations. --- config-editor-ui/src/pages.jsx | 3 +- .../production_config/.env.production.example | 7 + tenxgraph_api/cli/commands/init.py | 464 +++++------------- tenxgraph_api/cli/commands/init_context.py | 353 +++++++++++++ tenxgraph_api/cli/commands/init_prompts.py | 92 ++++ tenxgraph_api/cli/config_editor/schema.py | 33 +- tenxgraph_api/cli/config_editor/static/app.js | 6 +- tenxgraph_api/cli/config_editor/validation.py | 5 +- tenxgraph_api/cli/main.py | 48 +- tenxgraph_api/cli/templates/prod/.env.example | 24 +- .../cli/templates/prod/graph/agent.py | 52 +- .../cli/templates/prod/tests/conftest.py | 7 + .../publishers-and-runtime-protocols.md | 9 +- .../src/app/core/config/graph_config.py | 7 +- .../src/app/core/config/setup_middleware.py | 10 +- tests/cli/test_group6_cli_templates.py | 17 +- tests/cli/test_init_full.py | 241 +++++++++ tests/cli/test_init_non_interactive.py | 50 +- tests/cli/test_init_prod.py | 123 ++--- tests/unit_tests/test_config_editor.py | 12 + tests/unit_tests/test_setup_middleware.py | 22 +- 21 files changed, 1115 insertions(+), 470 deletions(-) create mode 100644 tenxgraph_api/cli/commands/init_context.py create mode 100644 tenxgraph_api/cli/commands/init_prompts.py create mode 100644 tests/cli/test_init_full.py diff --git a/config-editor-ui/src/pages.jsx b/config-editor-ui/src/pages.jsx index d00fce0d..4e39a412 100644 --- a/config-editor-ui/src/pages.jsx +++ b/config-editor-ui/src/pages.jsx @@ -74,8 +74,9 @@ function traceSummary() { const on = []; if (valueOrDefault(["observability", "logfire", "enabled"])) on.push("Logfire"); if (valueOrDefault(["observability", "langsmith", "enabled"])) on.push("LangSmith"); + if (valueOrDefault(["observability", "phoenix", "enabled"])) on.push("Phoenix"); const level = valueOrDefault(["observability", "level"]); - return { text: `Level ${level}${on.length ? `, sending to ${on.join(" and ")}` : ", no destination chosen yet"}.` }; + return { text: `Level ${level}${on.length ? `, sending to ${on.length > 1 ? `${on.slice(0, -1).join(", ")} and ${on[on.length - 1]}` : on[0]}` : ", no destination chosen yet"}.` }; } function GetRunning() { diff --git a/examples/security/production_config/.env.production.example b/examples/security/production_config/.env.production.example index 2a54da19..f565abe4 100644 --- a/examples/security/production_config/.env.production.example +++ b/examples/security/production_config/.env.production.example @@ -96,6 +96,13 @@ MAX_REQUEST_SIZE=10485760 # LANGCHAIN_API_KEY=your_langsmith_api_key # LANGCHAIN_PROJECT=your_project_name +# ============================================================================= +# PHOENIX (Optional - Arize Phoenix tracing, see the observability block) +# ============================================================================= +# PHOENIX_API_KEY=your_phoenix_api_key +# PHOENIX_COLLECTOR_ENDPOINT=http://localhost:6006 +# PHOENIX_PROJECT_NAME=your_project_name + # ============================================================================= # OPENAI (Optional - if using OpenAI) # ============================================================================= diff --git a/tenxgraph_api/cli/commands/init.py b/tenxgraph_api/cli/commands/init.py index d6d78598..8e653995 100644 --- a/tenxgraph_api/cli/commands/init.py +++ b/tenxgraph_api/cli/commands/init.py @@ -1,14 +1,27 @@ """Init command implementation.""" import json -import re from collections.abc import Callable from pathlib import Path from string import Template from typing import Any from tenxgraph_api.cli.commands import BaseCommand -from tenxgraph_api.cli.core.prompts import Choice +from tenxgraph_api.cli.commands.init_context import ( + DEFAULT_PROVIDER, + PROVIDERS, + STORAGE_PRESETS, + build_config, + build_context, + check_agent_name, + slugify, + strip_blocks, + template_flags, +) +from tenxgraph_api.cli.commands.init_context import ( + template_vars as _template_vars, +) +from tenxgraph_api.cli.commands.init_prompts import ask_questions from tenxgraph_api.cli.exceptions import FileOperationError, ValidationError from tenxgraph_api.cli.templates.defaults import ( GITIGNORE_SECRET_PATTERNS, @@ -19,41 +32,9 @@ _TEMPLATES_DIR = Path(__file__).parent.parent / "templates" _SKIP_DIRS = {"__pycache__", ".ruff_cache"} - -# Directories inside prod/ that are only included based on user choices _AUTH_DIR = "auth" - -# AI provider choices: the core extra that installs the SDK, the model the scaffold -# starts with, the model the eval user simulator uses, and the API key variable. -_PROVIDERS: dict[str, dict[str, str]] = { - "google": { - "label": "Google Gemini", - "extra": "google-genai", - "model": "gemini-2.5-flash", - "sim_model": "gemini/gemini-2.5-flash", - "api_key_env": "GOOGLE_API_KEY", - }, - "openai": { - "label": "OpenAI", - "extra": "openai", - "model": "gpt-4o-mini", - "sim_model": "openai/gpt-4o-mini", - "api_key_env": "OPENAI_API_KEY", - }, - "anthropic": { - "label": "Anthropic Claude", - "extra": "anthropic", - "model": "claude-sonnet-5-5", - "sim_model": "anthropic/claude-sonnet-5-5", - "api_key_env": "ANTHROPIC_API_KEY", - }, -} -_DEFAULT_PROVIDER = "google" -_CACHE_STORES = ("memory", "redis") -_CONFIG_HINT = ( - "Run 10xgraph config for full control: WebSocket, AG-UI, ID generator, thread naming " - "and every other server setting." -) +_OPTIONAL_DIRS = ("evals", "tests") +_CONFIG_HINT = "10xgraph config for more settings." def _escape_agent_name(name: str, filename: str) -> str: @@ -68,79 +49,15 @@ def _escape_agent_name(name: str, filename: str) -> str: def _agent_name_problem(value: str) -> bool | str: """Prompt validator: True when the name is usable, else the message to show.""" - if not value.strip() or not _slugify(value): + if not value.strip() or not slugify(value): return "Use at least one letter or number" try: - _check_agent_name(value) + check_agent_name(value) except ValidationError as exc: return exc.message return True -def _check_agent_name(name: str) -> None: - """Refuse a name with control characters (a newline would add lines to pyproject.toml).""" - if any(ord(ch) < 32 or ord(ch) == 127 for ch in name): # noqa: PLR2004 - raise ValidationError("Agent name cannot contain control characters.", field="name") - - -def _slugify(name: str) -> str: - """Convert 'WeatherBot' → 'weather-bot'.""" - s = re.sub(r"([A-Z])", r"-\1", name).lstrip("-").lower() - s = re.sub(r"[^a-z0-9]+", "-", s) - return s.strip("-") - - -def _strip_env_blocks(content: str, *, keep_redis: bool, keep_jwt: bool) -> str: - """Remove conditional marker blocks that are not needed.""" - if not keep_redis: - content = re.sub(r"##\[IF_REDIS\]##.*?##\[/IF_REDIS\]##\n?", "", content, flags=re.DOTALL) - else: - content = re.sub(r"##\[IF_REDIS\]##\n?", "", content) - content = re.sub(r"##\[/IF_REDIS\]##\n?", "", content) - - if not keep_jwt: - content = re.sub(r"##\[IF_JWT\]##.*?##\[/IF_JWT\]##\n?", "", content, flags=re.DOTALL) - else: - content = re.sub(r"##\[IF_JWT\]##\n?", "", content) - content = re.sub(r"##\[/IF_JWT\]##\n?", "", content) - - return content - - -def _template_vars(context: dict[str, Any]) -> dict[str, Any]: - """Derive the template variables that depend on the user's answers. - - The production pyproject.toml lists only what the answers need: the provider SDK - and the Postgres + Redis checkpointer (``pg_checkpoint``) through the core extras, - PyJWT for JWT auth, redis (always, the checkpointer and the API's cache store use it), - and websockets for WebSocket streaming / realtime. The generated Dockerfile - installs exactly this list. Quick-start has no Postgres or Redis, so its core - requirement is the provider extra alone. - """ - provider = _PROVIDERS[context.get("provider", _DEFAULT_PROVIDER)] - is_prod = context.get("setup_type") == "production" - core_extras = [provider["extra"]] - if is_prod: - core_extras.append("pg_checkpoint") - api_extras = [] - if context.get("auth") == "jwt": - api_extras.append("jwt") - if is_prod or context.get("cache_store") == "redis": - api_extras.append("redis") - if context.get("websocket"): - api_extras.append("websocket") - return { - "llm_provider": context.get("provider", _DEFAULT_PROVIDER), - "llm_model": provider["model"], - "llm_sim_model": provider["sim_model"], - "llm_api_key_env": provider["api_key_env"], - "core_requirement": f"10xgraph[{','.join(core_extras)}]", - "api_requirement": ( - f"10xgraph-api[{','.join(api_extras)}]" if api_extras else "10xgraph-api" - ), - } - - class InitCommand(BaseCommand): """Command to initialize a new agent project interactively.""" @@ -150,12 +67,18 @@ def execute( # noqa: PLR0913 force: bool = False, agent_name: str | None = None, template: str | None = None, + mode: str | None = None, auth: str | None = None, + storage: str | None = None, cache_store: str | None = None, rate_limit: bool | None = None, token_revocation: bool | None = None, provider: str | None = None, websocket: bool = False, + evals: bool | None = None, + tests: bool | None = None, + ag_ui: bool | None = None, + otel: bool | None = None, yes: bool = False, non_interactive: bool = False, dry_run: bool = False, @@ -172,12 +95,18 @@ def execute( # noqa: PLR0913 path=path, agent_name=agent_name, template=template, + mode=mode, auth=auth, + storage=storage, cache_store=cache_store, rate_limit=rate_limit, token_revocation=token_revocation, provider=provider, websocket=websocket, + evals=evals, + tests=tests, + ag_ui=ag_ui, + otel=otel, ) else: context = self._prompt_user() @@ -239,7 +168,7 @@ def execute( # noqa: PLR0913 # Regenerate 10xgraph.json from the built config. Force is safe only # to override the copy this run just made; honor --force for a file # that was already there. - config = self._build_config(context, is_prod) + config = build_config(context, is_prod) self._write_file( config_path, json.dumps(config, indent=2) + "\n", @@ -247,48 +176,7 @@ def execute( # noqa: PLR0913 ) step.detail(str(config_path)) - agent_name = context["agent_name"] - provider_info = _PROVIDERS[context.get("provider", _DEFAULT_PROVIDER)] - # Production lists these in pyproject.toml; quick-start has no pyproject, so it - # needs the install step. - install_step = ( - [] - if is_prod - else [f'pip install "{_template_vars(context)["core_requirement"]}"'] - + (['pip install "10xgraph-api[websocket]"'] if context.get("websocket") else []) - ) - prod_steps = ( - [ - "Set DATABASE_URL (Postgres) and REDIS_URL (Redis) in .env.", - "graph/agent.py keeps a commented InMemoryCheckpointer for local testing " - "without Postgres and Redis.", - "10xgraph config to review auth, rate limiting and the other settings.", - "10xgraph build to generate the Dockerfile for deployment.", - ] - if is_prod - else ["10xgraph config to review the settings in your browser."] - ) - details: dict[str, Any] = { - "Location": base_path.resolve(), - "Template": "production" if is_prod else "quick-start", - "Auth": context["auth"], - "AI provider": provider_info["label"], - } - if context.get("websocket"): - details["WebSocket"] = "on" - self.output.completion_screen( - "Project ready", - f'"{agent_name}" was created successfully', - details=details, - next_steps=[ - f"cd {base_path}", - *install_step, - f"Copy .env.example to .env and set {provider_info['api_key_env']}.", - *prod_steps, - _CONFIG_HINT, - "10xgraph dev", - ], - ) + self._print_completion(context, base_path, is_prod) return 0 @@ -297,96 +185,9 @@ def execute( # noqa: PLR0913 except Exception as e: return self.handle_error(FileOperationError(f"Failed to initialize project: {e}")) - def _non_interactive_context( - self, - *, - path: str, - agent_name: str | None, - template: str | None, - auth: str | None, - cache_store: str | None = None, - rate_limit: bool | None = None, - token_revocation: bool | None = None, - provider: str | None = None, - websocket: bool = False, - ) -> dict[str, Any]: + def _non_interactive_context(self, **options: Any) -> dict[str, Any]: """Resolve a complete, reproducible scaffold recipe without prompting.""" - resolved_template = (template or "quick-start").strip().lower().replace("_", "-") - if resolved_template not in {"quick-start", "production"}: - raise ValidationError( - f"Invalid template '{resolved_template}'. Choose quick-start or production.", - field="template", - ) - - inferred_name = Path(path).resolve().name if path not in {"", "."} else "MyAgent" - resolved_name = (agent_name or inferred_name).strip() - _check_agent_name(resolved_name) - slug = _slugify(resolved_name) - if not resolved_name or not slug: - raise ValidationError( - "Agent name must contain at least one letter or number.", - field="name", - ) - - is_prod_template = resolved_template == "production" - resolved_auth = (auth or ("jwt" if is_prod_template else "none")).strip().lower() - if resolved_auth not in {"none", "jwt", "custom"}: - raise ValidationError( - f"Invalid auth mode '{resolved_auth}'. Choose none, jwt, or custom.", - field="auth", - ) - - resolved_cache_store = (cache_store or "redis").strip().lower() - if is_prod_template and resolved_cache_store not in _CACHE_STORES: - raise ValidationError( - f"Invalid cache store '{resolved_cache_store}'. " - f"Choose {' or '.join(_CACHE_STORES)}.", - field="cache_store", - ) - - resolved_provider = (provider or _DEFAULT_PROVIDER).strip().lower() - if resolved_provider not in _PROVIDERS: - raise ValidationError( - f"Invalid AI provider '{resolved_provider}'. Choose {', '.join(_PROVIDERS)}.", - field="provider", - ) - - if resolved_template == "quick-start" and auth is not None and resolved_auth != "none": - raise ValidationError("--auth requires --template production.", field="template") - - if not is_prod_template: - # The cache store, rate limit and token revocation flags are production settings. - return { - "agent_name": resolved_name, - "agent_name_slug": slug, - "setup_type": "quick_start", - "provider": resolved_provider, - "websocket": websocket, - "auth": "none", - "cache_store": None, - "rate_limit": False, - "token_revocation": False, - } - - auth_on = resolved_auth in {"jwt", "custom"} - return { - "agent_name": resolved_name, - "agent_name_slug": slug, - "setup_type": "production", - "provider": resolved_provider, - "websocket": websocket, - "auth": resolved_auth, - "cache_store": resolved_cache_store, - "rate_limit": rate_limit is not False, - # Ignored with auth off: there is no token to revoke. - "token_revocation": auth_on and token_revocation is not False, - "id_generator": "uuid", - "rl_requests": 100, - "rl_window": 60, - # With auth on, one budget per user; "ip" would share one budget behind a proxy. - "rl_by": "user" if auth_on else "ip", - "rl_trusted_proxy": False, - } + return build_context(**options) # ------------------------------------------------------------------ # Prompts @@ -395,80 +196,85 @@ def _non_interactive_context( def _prompt_user(self) -> dict | None: prompts = self.output.prompts() prompts.require_interactive( - field="template", + field="mode", alternatives=( "Re-run with --yes to accept the defaults, or --non-interactive " - "with --name/--template for a reproducible recipe." + "with --name/--mode for a reproducible recipe." ), ) - - agent_name = prompts.text( - "What is your agent name?", - default="MyAgent", - validate=_agent_name_problem, - ) - if agent_name is None: - return None - - template = prompts.select( - "Which project template?", - [ - Choice( - "quick_start", - "Quick Start", - "Minimal graph, no auth — fastest path to a running agent", - ), - Choice( - "production", - "Production", - "JWT auth, Postgres + Redis, rate limiting, evals, and tests scaffolded in", - ), - ], - default="quick_start", - ) - if template is None: + answers = ask_questions(prompts, _agent_name_problem, DEFAULT_PROVIDER) + if answers is None: return None + return self._non_interactive_context(path=".", **answers) + + def _print_completion(self, context: dict, base_path: Path, is_prod: bool) -> None: + provider_info = PROVIDERS[context.get("provider", DEFAULT_PROVIDER)] + storage = context.get("storage") or "memory" + key_step = f"Copy .env.example to .env and set {provider_info['api_key_env']}" + details: dict[str, Any] = { + "Location": base_path.resolve(), + "Setup": "Full" if is_prod else "Quick", + "Auth": context["auth"], + "AI provider": provider_info["label"], + } + if is_prod: + details["Storage"] = STORAGE_PRESETS[storage]["label"] + if context.get("websocket"): + details["WebSocket"] = "on" - provider = prompts.select( - "Which AI provider?", - [ - Choice("google", "Google Gemini", "Installs google-genai · GOOGLE_API_KEY"), - Choice("openai", "OpenAI", "Installs openai · OPENAI_API_KEY"), - Choice("anthropic", "Anthropic Claude", "Installs anthropic · ANTHROPIC_API_KEY"), - ], - default=_DEFAULT_PROVIDER, + if not is_prod: + install = [f'pip install "{_template_vars(context)["core_requirement"]}"'] + if context.get("websocket"): + install.append('pip install "10xgraph-api[websocket]"') + steps = [ + f"cd {base_path}", + *install, + f"{key_step}.", + "10xgraph dev", + _CONFIG_HINT, + ] + before_production: list[str] = [] + else: + if storage.startswith("postgres"): + key_step += " (and DATABASE_URL, REDIS_URL)" + steps = [f"cd {base_path}", "pip install -e .", f"{key_step}.", "10xgraph dev"] + secret = " and a strong JWT_SECRET_KEY" if context["auth"] == "jwt" else "" + before_production = [ + "10xgraph config to review auth, rate limit, CORS origins and the other " + "settings and make the project production ready.", + f"Set MODE=production, ORIGINS{secret} in the deploy environment.", + "10xgraph build to generate the Dockerfile (--docker-compose / --k8s optional).", + ] + + self.output.completion_screen( + "Project ready", + f'"{context["agent_name"]}" was created successfully', + details=details, + next_steps=steps, ) - if provider is None: - return None - - # Nothing else is asked: Production writes the recommended setup, and - # `10xgraph config` (or the flags) changes any part of it. - return self._non_interactive_context( - path=".", - agent_name=agent_name, - template="production" if template == "production" else "quick-start", - auth=None, - provider=provider, + if before_production: + self.output.print_list(before_production, title="Before production", bullet="→") + self.output.info( + "10xgraph skills installs the 10xGraph skill for your AI coding assistant.", + emoji=False, ) - # ------------------------------------------------------------------ - # Output helpers - # ------------------------------------------------------------------ - def _print_summary(self, context: dict) -> None: is_prod = context["setup_type"] == "production" - setup_label = "Production" if is_prod else "Quick Start" + setup_label = "Full" if is_prod else "Quick" rows: list[tuple[str, str]] = [ ("Agent name", context["agent_name"]), ("Package name", context["agent_name_slug"]), ("Setup", setup_label), - ("AI provider", _PROVIDERS[context.get("provider", _DEFAULT_PROVIDER)]["label"]), + ("AI provider", PROVIDERS[context.get("provider", DEFAULT_PROVIDER)]["label"]), ] if context.get("websocket"): rows.append(("WebSocket", "On")) if is_prod: auth = context["auth"] + storage = context.get("storage") or "memory" + rows.append(("Storage", STORAGE_PRESETS[storage]["label"])) rows.append(("Auth", "None" if auth == "none" else auth.upper())) store = context.get("cache_store") or "memory" @@ -491,6 +297,11 @@ def _print_summary(self, context: dict) -> None: if auth != "none": revocation = "On" if context.get("token_revocation") else "Off" rows.append(("Token revocation", revocation)) + rows.append(("Evaluations", "On" if context.get("evals") else "Off")) + rows.append(("Tests", "On" if context.get("tests") else "Off")) + for key, label in (("ag_ui", "AG-UI"), ("otel", "OpenTelemetry")): + if context.get(key): + rows.append((label, "On")) self.output.print_table( ["Setting", "Value"], @@ -498,62 +309,6 @@ def _print_summary(self, context: dict) -> None: title="Project summary", ) - # ------------------------------------------------------------------ - # Config generation - # ------------------------------------------------------------------ - - def _build_config(self, context: dict, is_prod: bool) -> dict: - config: dict = { - "agent": "graph.agent:app", - "env": ".env", - "auth": None, - "thread_name_generator": None, - # AG-UI endpoint for clients such as CopilotKit. Off until turned on here. - "ag_ui": {"enabled": False}, - # WebSocket streaming (/v1/graph/ws) and realtime audio (/v1/graph/live). - # Off: neither route is mounted and no WebSocket library is needed. - "websocket": {"enabled": bool(context.get("websocket"))}, - } - - if not is_prod: - return config - - auth = context["auth"] - if auth == "jwt": - config["auth"] = {"method": "jwt"} - elif auth == "custom": - config["auth"] = {"method": "custom", "path": "auth.agent_auth:AgentAuth"} - - # Frontend tools are declared here (name and schema fixed by the server) instead of - # being accepted from each request. Add them under "remote_tools". - config["ag_ui"] = {"enabled": False, "allow_client_tools": False} - config["remote_tools"] = [] - - config["thread_name_generator"] = "graph.thread_name_generator:MyNameGenerator" - config["injectq"] = "graph.agent:container" - config["id_generator"] = context.get("id_generator", "uuid") - - # The API's cache store: rate limit counters, run locks and caps, idempotency, - # ownership cache and token revocation. Redis is shared by every worker (empty url: - # the URL comes from REDIS_URL in .env); memory is per process. - cache_store: dict[str, Any] = {"type": context.get("cache_store") or "memory"} - if context.get("rate_limit"): - cache_store["rate_limit"] = { - "enabled": True, - "requests": context.get("rl_requests", 100), - "window": context.get("rl_window", 60), - "by": context.get("rl_by", "user" if auth in ("jwt", "custom") else "ip"), - "trusted_proxy_headers": context.get("rl_trusted_proxy", False), - "fail_open": False, - "exclude_paths": ["/ping", "/docs", "/redocs", "/openapi.json"], - } - if auth in ("jwt", "custom") and context.get("token_revocation"): - # A leaked or logged-out token is rejected before it expires. See `10xgraph revoke`. - cache_store["token_revocation"] = True - config["cache_store"] = cache_store - - return config - # ------------------------------------------------------------------ # File helpers # ------------------------------------------------------------------ @@ -564,20 +319,23 @@ def _should_skip(self, src: Path, template_dir: Path, context: dict, is_prod: bo return True if not is_prod: return False - # auth/ is only included for custom auth; skip for none and jwt - rel_parts = src.relative_to(template_dir).parts - return rel_parts[0] == _AUTH_DIR and context["auth"] != "custom" + # auth/ is only included for custom auth; evals/ and tests/ only when asked for + top = src.relative_to(template_dir).parts[0] + if top == _AUTH_DIR: + return context["auth"] != "custom" + if top in _OPTIONAL_DIRS: + return not context.get(top) + return False def _render(self, src: Path, context: dict, is_prod: bool) -> str: content = src.read_text(encoding="utf-8") - # Strip conditional blocks in .env.example before substitution - if src.name == ".env.example" and is_prod: - content = _strip_env_blocks( - content, - keep_redis=True, - keep_jwt=context["auth"] == "jwt", - ) + # Resolve ##[IF_NAME]## blocks in every production template file + if is_prod: + try: + content = strip_blocks(content, template_flags(context)) + except FileOperationError as exc: + raise FileOperationError(f"{src.name}: {exc.message}", file_path=str(src)) from exc try: values = {**context, **_template_vars(context)} diff --git a/tenxgraph_api/cli/commands/init_context.py b/tenxgraph_api/cli/commands/init_context.py new file mode 100644 index 00000000..c41c699c --- /dev/null +++ b/tenxgraph_api/cli/commands/init_context.py @@ -0,0 +1,353 @@ +"""Answers for `10xgraph init`: provider table, storage presets, context, template blocks.""" + +import re +from pathlib import Path +from typing import Any + +from tenxgraph_api.cli.exceptions import FileOperationError, ValidationError + + +# AI provider choices: the core extra that installs the SDK, the model the scaffold +# starts with, the model the eval user simulator uses, and the API key variable. +PROVIDERS: dict[str, dict[str, str]] = { + "google": { + "label": "Google Gemini", + "extra": "google-genai", + "model": "gemini-2.5-flash", + "sim_model": "gemini/gemini-2.5-flash", + "api_key_env": "GOOGLE_API_KEY", + }, + "openai": { + "label": "OpenAI", + "extra": "openai", + "model": "gpt-4o-mini", + "sim_model": "openai/gpt-4o-mini", + "api_key_env": "OPENAI_API_KEY", + }, + "anthropic": { + "label": "Anthropic Claude", + "extra": "anthropic", + "model": "claude-sonnet-5-5", + "sim_model": "anthropic/claude-sonnet-5-5", + "api_key_env": "ANTHROPIC_API_KEY", + }, +} +DEFAULT_PROVIDER = "google" +CACHE_STORES = ("memory", "redis") +MODES = ("quick", "full") + +# Storage presets: label, cache store that goes with it. The checkpointer and store are +# chosen in the graph template from the preset key. +STORAGE_PRESETS: dict[str, dict[str, str]] = { + "memory": {"label": "Memory", "cache_store": "memory"}, + "sqlite": {"label": "SQLite", "cache_store": "memory"}, + "postgres": {"label": "Postgres + Redis", "cache_store": "redis"}, + "postgres-memory": {"label": "Postgres + Redis + pgvector memory", "cache_store": "redis"}, +} +AUTH_MODES = ("none", "jwt", "custom") + +_MODE_ALIASES = {"quick-start": "quick", "production": "full"} +_MARKER = re.compile( + r"^[ \t]*(?:(?:#|//)[ \t]*)?##\[(/?)IF_([A-Z0-9_]+)\]##[ \t]*(?:\r?\n|$)", re.MULTILINE +) + + +def slugify(name: str) -> str: + """Convert 'WeatherBot' -> 'weather-bot'.""" + s = re.sub(r"([A-Z])", r"-\1", name).lstrip("-").lower() + s = re.sub(r"[^a-z0-9]+", "-", s) + return s.strip("-") + + +def check_agent_name(name: str) -> None: + """Refuse a name with control characters (a newline would add lines to pyproject.toml).""" + if any(ord(ch) < 32 or ord(ch) == 127 for ch in name): # noqa: PLR2004 + raise ValidationError("Agent name cannot contain control characters.", field="name") + + +def _choice(value: str | None, default: str, options: Any, field: str, label: str) -> str: + resolved = (value or default).strip().lower() + if resolved not in options: + raise ValidationError( + f"Invalid {label} '{resolved}'. Choose {', '.join(options)}.", field=field + ) + return resolved + + +def _resolve_mode(mode: str | None, template: str | None) -> str: + """`--mode` wins; `--template quick-start|production` are aliases of quick|full.""" + from_template = None + if template is not None: + key = template.strip().lower().replace("_", "-") + if key not in _MODE_ALIASES: + raise ValidationError( + f"Invalid template '{key}'. Choose quick-start or production.", field="template" + ) + from_template = _MODE_ALIASES[key] + resolved = _choice(mode, from_template or "quick", MODES, "mode", "mode") + if from_template and mode and resolved != from_template: + raise ValidationError("--mode and --template disagree; pass only --mode.", field="mode") + return resolved + + +def build_context( # noqa: PLR0913 + *, + path: str, + agent_name: str | None, + mode: str | None = None, + template: str | None = None, + auth: str | None = None, + storage: str | None = None, + cache_store: str | None = None, + rate_limit: bool | None = None, + token_revocation: bool | None = None, + provider: str | None = None, + websocket: bool = False, + evals: bool | None = None, + tests: bool | None = None, + ag_ui: bool | None = None, + otel: bool | None = None, +) -> dict[str, Any]: + """Resolve a complete, reproducible scaffold recipe without prompting.""" + resolved_mode = _resolve_mode(mode, template) + inferred_name = Path(path).resolve().name if path not in {"", "."} else "MyAgent" + resolved_name = (agent_name or inferred_name).strip() + check_agent_name(resolved_name) + slug = slugify(resolved_name) + if not resolved_name or not slug: + raise ValidationError( + "Agent name must contain at least one letter or number.", field="name" + ) + resolved_provider = _choice(provider, DEFAULT_PROVIDER, PROVIDERS, "provider", "AI provider") + base: dict[str, Any] = { + "agent_name": resolved_name, + "agent_name_slug": slug, + "provider": resolved_provider, + "websocket": websocket, + "id_generator": "uuid", + } + + if resolved_mode == "quick": + full_only = { + "--auth": auth not in (None, "none"), + "--storage": storage, + "--cache-store": cache_store, + "--rate-limit": rate_limit, + "--token-revocation": token_revocation, + "--evals": evals, + "--tests": tests, + "--ag-ui": ag_ui, + "--otel": otel, + } + used = [flag for flag, value in full_only.items() if value not in (None, False)] + if used: + raise ValidationError(f"{', '.join(used)} requires --mode full.", field="mode") + return { + **base, + "setup_type": "quick_start", + "storage": None, + "auth": "none", + "cache_store": None, + "rate_limit": False, + "token_revocation": False, + "evals": False, + "tests": False, + "ag_ui": False, + "otel": False, + } + + resolved_storage = _choice(storage, "memory", STORAGE_PRESETS, "storage", "storage") + resolved_auth = _choice(auth, "jwt", AUTH_MODES, "auth", "auth mode") + resolved_cache = _choice( + cache_store, + STORAGE_PRESETS[resolved_storage]["cache_store"], + CACHE_STORES, + "cache_store", + "cache store", + ) + auth_on = resolved_auth != "none" + return { + **base, + "setup_type": "production", + "storage": resolved_storage, + "auth": resolved_auth, + "cache_store": resolved_cache, + "rate_limit": rate_limit is not False, + # Ignored with auth off: there is no token to revoke. + "token_revocation": auth_on and token_revocation is not False, + "rl_requests": 100, + "rl_window": 60, + # With auth on, one budget per user; "ip" would share one budget behind a proxy. + "rl_by": "user" if auth_on else "ip", + "rl_trusted_proxy": False, + "evals": evals is not False, + "tests": tests is not False, + "ag_ui": bool(ag_ui), + "otel": bool(otel), + } + + +def build_config(context: dict, is_prod: bool) -> dict: + """The 10xgraph.json written next to the scaffold.""" + config: dict = { + "agent": "graph.agent:app", + "env": ".env", + "auth": None, + "thread_name_generator": None, + # AG-UI endpoint for clients such as CopilotKit. + "ag_ui": {"enabled": bool(context.get("ag_ui"))}, + # WebSocket streaming (/v1/graph/ws) and realtime audio (/v1/graph/live). + # Off: neither route is mounted and no WebSocket library is needed. + "websocket": {"enabled": bool(context.get("websocket"))}, + } + + if not is_prod: + return config + + auth = context["auth"] + if auth == "jwt": + config["auth"] = {"method": "jwt"} + elif auth == "custom": + config["auth"] = {"method": "custom", "path": "auth.agent_auth:AgentAuth"} + + # Frontend tools are declared here (name and schema fixed by the server) instead of + # being accepted from each request. Add them under "remote_tools". + config["ag_ui"] = {"enabled": bool(context.get("ag_ui")), "allow_client_tools": False} + config["remote_tools"] = [] + + config["thread_name_generator"] = "graph.thread_name_generator:MyNameGenerator" + config["injectq"] = "graph.agent:container" + if context.get("storage") == "postgres-memory": + config["store"] = "graph.agent:store" + config["id_generator"] = context.get("id_generator", "uuid") + + # The API's cache store: rate limit counters, run locks and caps, idempotency, + # ownership cache and token revocation. Redis is shared by every worker (empty url: + # the URL comes from REDIS_URL in .env); memory is per process. + cache_store: dict[str, Any] = {"type": context.get("cache_store") or "memory"} + if context.get("rate_limit"): + cache_store["rate_limit"] = { + "enabled": True, + "requests": context.get("rl_requests", 100), + "window": context.get("rl_window", 60), + "by": context.get("rl_by", "user" if auth in ("jwt", "custom") else "ip"), + "trusted_proxy_headers": context.get("rl_trusted_proxy", False), + "fail_open": False, + "exclude_paths": ["/ping", "/docs", "/redocs", "/openapi.json"], + } + if auth in ("jwt", "custom") and context.get("token_revocation"): + # A leaked or logged-out token is rejected before it expires. See `10xgraph revoke`. + cache_store["token_revocation"] = True + config["cache_store"] = cache_store + + if context.get("tests"): + config["test"] = {"path": "tests", "coverage": True, "coverage_threshold": 70} + if context.get("evals"): + config["evaluation"] = { + "directory": "evals", + "output_dir": "eval_reports", + "threshold": 0.75, + "parallel": False, + "max_concurrency": 4, + "timestamp_files": True, + } + + return config + + +def template_flags(context: dict[str, Any]) -> dict[str, bool]: + """Flags for the ##[IF_NAME]## blocks of the production template.""" + storage = context.get("storage") or "" + provider = context.get("provider", DEFAULT_PROVIDER) + postgres = storage.startswith("postgres") + store = storage == "postgres-memory" + return { + "jwt": context.get("auth") == "jwt", + "custom_auth": context.get("auth") == "custom", + "redis": postgres or context.get("cache_store") == "redis", + "postgres": postgres, + "sqlite": storage == "sqlite", + "inmem": storage == "memory", + "store": store, + "embed_google": store and provider == "google", + "embed_openai": store and provider in ("openai", "anthropic"), + "embed_key": store and provider == "anthropic", + "otel": bool(context.get("otel")), + "websocket": bool(context.get("websocket")), + "ag_ui": bool(context.get("ag_ui")), + } + + +def strip_blocks(content: str, flags: dict[str, bool]) -> str: + """Resolve ``##[IF_NAME]##`` ... ``##[/IF_NAME]##`` marker lines. + + A marker may follow a ``#`` or ``//`` comment prefix so the template stays valid code. + + A block is kept (marker lines removed) when ``flags[name.lower()]`` is true and dropped + with its markers otherwise. Blocks may nest. An unknown or unbalanced marker raises, so a + typo in a template is loud. + """ + out: list[str] = [] + stack: list[tuple[str, bool]] = [] + pos = 0 + for match in _MARKER.finditer(content): + if all(keep for _, keep in stack): + out.append(content[pos : match.start()]) + pos = match.end() + closing, name = match.group(1) == "/", match.group(2) + if name.lower() not in flags: + raise FileOperationError(f"Unknown template block IF_{name}.") + if not closing: + stack.append((name, flags[name.lower()])) + elif not stack or stack[-1][0] != name: + raise FileOperationError(f"Unexpected end of template block IF_{name}.") + else: + stack.pop() + if stack: + raise FileOperationError(f"Template block IF_{stack[-1][0]} is never closed.") + out.append(content[pos:]) + result = "".join(out) + if "##[IF_" in result or "##[/IF_" in result: + raise FileOperationError("A template block marker must be alone on its line.") + return result + + +def template_vars(context: dict[str, Any]) -> dict[str, Any]: + """Derive the template variables that depend on the user's answers. + + The pyproject.toml lists only what the answers need; the generated Dockerfile + installs exactly this list. Quick start has a single core extra: the provider SDK. + """ + provider = PROVIDERS[context.get("provider", DEFAULT_PROVIDER)] + flags = template_flags(context) + core_extras = [provider["extra"]] + if flags["postgres"]: + core_extras.append("pg_checkpoint") + if flags["sqlite"]: + core_extras.append("sqlite_checkpoint") + if flags["store"]: + core_extras.append("pgvector") + # The generated graph/agent.py imports OpenAIEmbedding for the store. + if flags["embed_openai"] and "openai" not in core_extras: + core_extras.append("openai") + api_extras = [ + extra + for extra, wanted in ( + ("jwt", flags["jwt"]), + ("redis", flags["redis"]), + ("websocket", flags["websocket"]), + ("ag-ui", flags["ag_ui"]), + ("otel", flags["otel"]), + ) + if wanted + ] + return { + "llm_provider": context.get("provider", DEFAULT_PROVIDER), + "llm_model": provider["model"], + "llm_sim_model": provider["sim_model"], + "llm_api_key_env": provider["api_key_env"], + "core_requirement": f"10xgraph[{','.join(core_extras)}]", + "api_requirement": ( + f"10xgraph-api[{','.join(api_extras)}]" if api_extras else "10xgraph-api" + ), + } diff --git a/tenxgraph_api/cli/commands/init_prompts.py b/tenxgraph_api/cli/commands/init_prompts.py new file mode 100644 index 00000000..56927827 --- /dev/null +++ b/tenxgraph_api/cli/commands/init_prompts.py @@ -0,0 +1,92 @@ +"""Interactive questions for `10xgraph init`.""" + +from collections.abc import Callable +from typing import Any + +from tenxgraph_api.cli.core.prompts import Choice, PromptService + + +_EXTRAS = ( + Choice("evals", "Evals", "eval set and runner", checked=True), + Choice("tests", "Tests", "pytest suite for the graph", checked=True), + Choice("websocket", "WebSocket", "streaming and realtime audio"), + Choice("ag_ui", "AG-UI", "endpoint for CopilotKit and other AG-UI clients"), + Choice("otel", "OpenTelemetry", "traces and metrics"), +) + + +def ask_questions( # noqa: PLR0911 + prompts: PromptService, validate_name: Callable[[str], bool | str], default_provider: str +) -> dict[str, Any] | None: + """Ask the init questions. Returns the answers as ``build_context`` arguments, or None + when the user cancels.""" + mode = prompts.select( + "Which setup?", + [ + Choice("quick", "Quick", "Minimal graph, no auth: fastest path to a running agent"), + Choice("full", "Full", "Storage, API security, evals, tests and pre-commit"), + ], + default="quick", + ) + if mode is None: + return None + + agent_name = prompts.text("What is your agent name?", default="MyAgent", validate=validate_name) + if agent_name is None: + return None + + provider = prompts.select( + "Which AI provider?", + [ + Choice("google", "Google Gemini", "Installs google-genai · GOOGLE_API_KEY"), + Choice("openai", "OpenAI", "Installs openai · OPENAI_API_KEY"), + Choice("anthropic", "Anthropic Claude", "Installs anthropic · ANTHROPIC_API_KEY"), + ], + default=default_provider, + ) + if provider is None: + return None + + answers: dict[str, Any] = {"mode": mode, "agent_name": agent_name, "provider": provider} + if mode == "quick": + return answers + + storage = prompts.select( + "Where should state be stored?", + [ + Choice("memory", "Memory", "InMemoryCheckpointer, nothing to run locally"), + Choice("sqlite", "SQLite", "SqliteCheckpointer, one local file"), + Choice("postgres", "Postgres + Redis", "PgCheckpointer, Redis cache store"), + Choice( + "postgres-memory", + "Postgres + Redis + memory", + "PgCheckpointer and PgVectorStore for long-term memory", + ), + ], + default="memory", + ) + if storage is None: + return None + + auth = prompts.select( + "How should the API be secured?", + [ + Choice("jwt", "JWT", "JWT auth, rate limit and token revocation"), + Choice("open", "Open", "No auth; rate limit still on, by IP"), + Choice("custom", "Custom", "Your BaseAuth subclass, rate limit and token revocation"), + ], + default="jwt", + ) + if auth is None: + return None + + extras = prompts.checkbox("Which extras?", _EXTRAS) + if extras is None: + return None + + return { + **answers, + "storage": storage, + "auth": "none" if auth == "open" else auth, + **{choice.value: choice.value in extras for choice in _EXTRAS}, + } diff --git a/tenxgraph_api/cli/config_editor/schema.py b/tenxgraph_api/cli/config_editor/schema.py index 6c83f92c..65755003 100644 --- a/tenxgraph_api/cli/config_editor/schema.py +++ b/tenxgraph_api/cli/config_editor/schema.py @@ -643,7 +643,7 @@ { "id": "observability", "title": "Observability", - "description": "Trace graph runs to Logfire and/or LangSmith.", + "description": "Trace graph runs to Logfire, LangSmith and/or Phoenix.", "key": "observability", "optional": True, "off_summary": "Graph runs are not traced.", @@ -651,6 +651,7 @@ "level": "standard", "logfire": {"enabled": False}, "langsmith": {"enabled": False}, + "phoenix": {"enabled": False}, }, "fields": [ { @@ -733,8 +734,32 @@ "placeholder": "https://api.smith.langchain.com/otel", "show_if": {"path": ["observability", "langsmith", "enabled"], "equals": True}, }, + { + "path": ["observability", "phoenix", "enabled"], + "group": "Phoenix", + "default": False, + "label": "Phoenix", + "type": "bool", + }, + { + "path": ["observability", "phoenix", "project"], + "group": "Phoenix", + "label": "Phoenix project", + "type": "text", + "placeholder": "my-agent", + "show_if": {"path": ["observability", "phoenix", "enabled"], "equals": True}, + }, + { + "path": ["observability", "phoenix", "endpoint"], + "group": "Phoenix", + "label": "Phoenix endpoint", + "type": "text", + "placeholder": "http://localhost:6006", + "help": "Base collector URL. /v1/traces is appended.", + "show_if": {"path": ["observability", "phoenix", "enabled"], "equals": True}, + }, ], - "notes": ["Tokens stay in .env: LOGFIRE_TOKEN and LANGSMITH_API_KEY."], + "notes": ["Tokens stay in .env: LOGFIRE_TOKEN, LANGSMITH_API_KEY and PHOENIX_API_KEY."], "env": [ { "name": "LOGFIRE_TOKEN", @@ -744,6 +769,10 @@ "name": "LANGSMITH_API_KEY", "purpose": "LangSmith API key. Required when LangSmith is enabled.", }, + { + "name": "PHOENIX_API_KEY", + "purpose": "Phoenix API key. Optional; a local Phoenix needs none.", + }, { "name": "TENXGRAPH_TRACE_USER_ID_KEY", "purpose": "Secret that keys the hash of user ids in traces when the mode is hash.", diff --git a/tenxgraph_api/cli/config_editor/static/app.js b/tenxgraph_api/cli/config_editor/static/app.js index ad70d2ec..4f598745 100644 --- a/tenxgraph_api/cli/config_editor/static/app.js +++ b/tenxgraph_api/cli/config_editor/static/app.js @@ -1,3 +1,3 @@ -var $e,b,Ct,Nn,U,vt,$t,St,Ve,xe,ie,Tt,Ke,Be,Je,Dn,ke={},we=[],Ln=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Se=Array.isArray;function D(e,t){for(var o in t)e[o]=t[o];return e}function Ye(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function zn(e,t,o){var r,i,s,a={};for(s in t)s=="key"?r=t[s]:s=="ref"?i=t[s]:a[s]=t[s];if(arguments.length>2&&(a.children=arguments.length>3?$e.call(arguments,2):o),typeof e=="function"&&e.defaultProps!=null)for(s in e.defaultProps)a[s]===void 0&&(a[s]=e.defaultProps[s]);return ve(e,a,r,i,null)}function ve(e,t,o,r,i){var s={type:e,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i??++Ct,__i:-1,__u:0};return i==null&&b.vnode!=null&&b.vnode(s),s}function w(e){return e.children}function ye(e,t){this.props=e,this.context=t}function J(e,t){if(t==null)return e.__?J(e.__,e.__i+1):null;for(var o;tt&&U.sort(St),e=U.shift(),t=U.length,Hn(e)}finally{U.length=Ce.__r=0}}function It(e,t,o,r,i,s,a,d,f,l,p){var h,c,m,x,$,k,T=r&&r.__k||we,v=t.length;for(f=Fn(o,t,T,f,v),h=0;h0?a=e.__k[s]=ve(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[s]=a,f=s+c,a.__=e,a.__b=e.__b+1,d=null,(l=a.__i=qn(a,o,f,h))!=-1&&(h--,(d=o[l])&&(d.__u|=2)),d==null||d.__v==null?(l==-1&&(i>p?c--:if?c--:c++,a.__u|=4))):e.__k[s]=null;if(h)for(s=0;s(p?1:0)){for(i=o-1,s=o+1;i>=0||s=0?i--:s++])!=null&&(2&l.__u)==0&&d==l.key&&f==l.type)return a}return-1}function kt(e,t,o){t[0]=="-"?e.setProperty(t,o??""):e[t]=o==null?"":typeof o!="number"||Ln.test(t)?o:o+"px"}function be(e,t,o,r,i){var s,a;e:if(t=="style")if(typeof o=="string")e.style.cssText=o;else{if(typeof r=="string"&&(e.style.cssText=r=""),r)for(t in r)o&&t in o||kt(e.style,t,"");if(o)for(t in o)r&&o[t]==r[t]||kt(e.style,t,o[t])}else if(t[0]=="o"&&t[1]=="n")s=t!=(t=t.replace(Tt,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+s]=o,o?r?o[ie]=r[ie]:(o[ie]=Ke,e.addEventListener(t,s?Je:Be,s)):e.removeEventListener(t,s?Je:Be,s);else{if(i=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=o??"";break e}catch{}typeof o=="function"||(o==null||o===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&o==1?"":o))}}function wt(e){return function(t){if(this.l){var o=this.l[t.type+e];if(t[xe]==null)t[xe]=Ke++;else if(t[xe]0?e:Se(e)?e.map(Pt):e.constructor!==void 0?null:D({},e)}function Un(e,t,o,r,i,s,a,d,f){var l,p,h,c,m,x,$,k=o.props||ke,T=t.props,v=t.type;if(v=="svg"?i="http://www.w3.org/2000/svg":v=="math"?i="http://www.w3.org/1998/Math/MathML":i||(i="http://www.w3.org/1999/xhtml"),s!=null){for(l=0;l=o.__.length&&o.__.push({}),o.__[e]}function Z(e){return Oe=1,nt(Vt,e)}function nt(e,t,o){var r=tt(ae++,2);if(r.t=e,!r.__c&&(r.__=[o?o(t):Vt(void 0,t),function(d){var f=r.__N?r.__N[0]:r.__[0],l=r.t(f,d);f!==l&&(r.__N=[l,r.__[1]],r.__c.setState({}))}],r.__c=C,!C.__f)){var i=function(d,f,l){if(!r.__c.__H)return!0;var p=!1,h=r.__c.props!==d;if(r.__c.__H.__.some(function(m){if(m.__N){p=!0;var x=m.__[0];m.__=m.__N,m.__N=void 0,x!==m.__[0]&&(h=!0)}}),s){var c=s.call(this,d,f,l);return p?c||h:c}return!p||h};C.__f=!0;var s=C.shouldComponentUpdate,a=C.componentWillUpdate;C.componentWillUpdate=function(d,f,l){if(this.__e){var p=s;s=void 0,i(d,f,l),s=p}a&&a.call(this,d,f,l)},C.shouldComponentUpdate=i}return r.__N||r.__}function L(e,t){var o=tt(ae++,3);!S.__s&&Gt(o.__H,t)&&(o.__=e,o.u=t,C.__H.__h.push(o))}function Ie(e){return Oe=5,Gn(function(){return{current:e}},[])}function Gn(e,t){var o=tt(ae++,7);return Gt(o.__H,t)&&(o.__=e(),o.__H=t,o.__h=e),o.__}function Vn(){for(var e;e=Wt.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Te),t.__h.some(et),t.__h=[]}catch(o){t.__h=[],S.__e(o,e.__v)}}}S.__b=function(e){C=null,Dt&&Dt(e)},S.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),qt&&qt(e,t)},S.__r=function(e){Lt&&Lt(e),ae=0;var t=(C=e.__c).__H;t&&(Xe===C?(t.__h=[],C.__h=[],t.__.some(function(o){o.__N&&(o.__=o.__N),o.u=o.__N=void 0})):(t.__h.some(Te),t.__h.some(et),t.__h=[],ae=0)),Xe=C},S.diffed=function(e){zt&&zt(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(Wt.push(t)!==1&&Nt===S.requestAnimationFrame||((Nt=S.requestAnimationFrame)||Bn)(Vn)),t.__H.__.some(function(o){o.u&&(o.__H=o.u,o.u=void 0)})),Xe=C=null},S.__c=function(e,t){t.some(function(o){try{o.__h.some(Te),o.__h=o.__h.filter(function(r){return!r.__||et(r)})}catch(r){t.some(function(i){i.__h&&(i.__h=[])}),t=[],S.__e(r,o.__v)}}),Ht&&Ht(e,t)},S.unmount=function(e){Ft&&Ft(e);var t,o=e.__c;o&&o.__H&&(o.__H.__.some(function(r){try{Te(r)}catch(i){t=i}}),o.__H=void 0,t&&S.__e(t,o.__v))};var Ut=typeof requestAnimationFrame=="function";function Bn(e){var t,o=function(){clearTimeout(r),Ut&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(o,35);Ut&&(t=requestAnimationFrame(o))}function Te(e){var t=C,o=e.__c;typeof o=="function"&&(e.__c=void 0,o()),C=t}function et(e){var t=C;e.__c=e.__(),C=t}function Gt(e,t){return!e||e.length!==t.length||t.some(function(o,r){return o!==e[r]})}function Vt(e,t){return typeof t=="function"?t(e):t}var Ee=[{label:null,items:["get-running"]},{label:"Before production",items:["auth","cache_store","observability"]},{label:"Runs and streaming",items:["runs","websocket","ag_ui","remote_tools","media"]},{label:"Project wiring",items:["checkpointer","id_generator","thread_name_generator","store","injectq"]},{label:"Developer tools",items:["test","evaluation"]},{label:"Reference",items:["environment"]}],z={"get-running":{title:"Get running",icon:"flag",description:"Only the agent graph is required. Everything else is optional."},environment:{title:"Server environment (.env)",icon:"terminal",description:"Variables the server reads from .env. The editor never reads that file, so values are not shown."}},ot={"auth.path":"auth-path"},Bt="tg-config-token",Jt=!1;function Jn(){let e=new URLSearchParams(location.hash.slice(1)).get("token")||"";try{e&&sessionStorage.setItem(Bt,e);let t=sessionStorage.getItem(Bt)||"";return Jt=t!==""&&t===(e||t),e||t}catch{return e}}var le=Jn(),rt=e=>Jt||!le?`#${e}`:`#token=${encodeURIComponent(le)}&page=${e}`;function Kt(){let e=location.hash.slice(1);if(e.includes("=")){let t=new URLSearchParams(e);return t.get("page")||t.get("section")||""}try{return decodeURIComponent(e)}catch{return e}}var u={schema:null,sections:[],nav:[],fields:new Map,config:{},version:null,exists:!1,path:"",parseError:null,saved:"",validated:null,issues:[],page:"get-running",query:"",sideOpen:!0,sideRender:!0,sideShown:!0,drawerOpen:!1,tab:"json",copied:!1,busy:null,toolParams:{},toasts:[],fatal:null},Yt=()=>{},Qt=e=>{Yt=e};function g(e){e&&e(u),Yt()}var Zt=e=>JSON.parse(JSON.stringify(e)),M=()=>JSON.stringify(u.config),y=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Ae=(e,t)=>t.reduce((o,r)=>y(o)?o[r]:void 0,e),ce=(e,t)=>`${e} ${t}${e===1?"":"s"}`;function je(e,t,o){let r=e;for(let i of t.slice(0,-1))y(r[i])||(r[i]={}),r=r[i];r[t[t.length-1]]=o}var Kn=new Set(["cache_store.rate_limit","cache_store.token_revocation"]);function st(e,t){let o=[],r=e;for(let i of t.slice(0,-1)){if(!y(r))return;o.push([r,i]),r=r[i]}y(r)&&delete r[t[t.length-1]];for(let i=o.length-1;i>=1;i--){let[s,a]=o[i];if(Kn.has(t.slice(0,i+1).join(".")))break;if(y(s[a])&&Object.keys(s[a]).length===0)delete s[a];else break}}function de(e){let t=Ae(u.config,e);return t!==void 0?t:u.fields.get(e.join("."))?.default}var Yn=(e,t)=>JSON.stringify(e)===JSON.stringify(t),E=e=>u.sections.find(t=>t.id===e),H=()=>M()!==u.saved,Pe=e=>!e.optional||e.key in u.config&&u.config[e.key]!==null,X=e=>u.issues.filter(t=>t.section===e),O=e=>u.issues.find(t=>t.path===e);function ue(e=u.issues){let t=e.filter(o=>o.level==="error").length;return{errors:t,warnings:e.length-t}}var Me=e=>ot[e]||`f-${e.replaceAll(".","-")}`,it=e=>{let t=Ae(u.config,e.path);return t!==void 0&&!Yn(t,e.default)},Xt=e=>e.id==="core"?!1:e.id==="auth"?u.config.auth!=null:e.optional?Pe(e):e.fields.some(it);function en(e){let t=X(e);return t.find(o=>o.level==="error")||t[0]||null}var tn=e=>!!e.show_if&&de(e.show_if.path)!==e.show_if.equals;function nn(e){let o=u.fields.get(e.show_if.path.join("."))?.label||e.show_if.path.join("."),r=e.show_if.equals;return`Used when ${o} is ${r===!0?"on":r===!1?"off":String(r)}.`}var pe=e=>{let t=E(e);return z[e]?{id:e,...z[e]}:t?{id:e,title:t.title,subtitle:t.subtitle,description:t.description,icon:null}:null},on=()=>/^[\w.]+:[\w.]+$/.test(String(u.config.agent||""));async function lt(e,t,o){let r={"X-10xGraph-Token":le};o!==void 0&&(r["Content-Type"]="application/json");let i=await fetch(t,{method:e,headers:r,body:o===void 0?void 0:JSON.stringify(o)}),s={};try{s=await i.json()}catch{}return{ok:i.ok,status:i.status,data:s}}var at=(e,t)=>g(o=>{o.fatal={title:e,text:t}});async function an(){if(!le)return at("Open the link printed by 10xgraph config","This page needs the session token included in that link.");let e;try{e=await lt("GET","/api/state")}catch{return at("Cannot reach the config server","Check that 10xgraph config is still running in your terminal, then reload.")}if(!e.ok)return at("This link has expired",e.data.error||"Restart 10xgraph config and open the new link it prints.");let t=e.data;g(o=>{o.schema=t.schema,o.sections=t.schema.sections;let r=new Set(Ee.flatMap(a=>a.items)),i=o.sections.map(a=>a.id).filter(a=>a!=="core"&&!r.has(a));o.nav=Ee.map((a,d)=>({label:a.label,items:dz[f]||E(f)):[...i,...a.items]}));for(let a of o.sections)for(let d of a.fields)o.fields.set(d.path.join("."),d);o.config=t.config,o.version=t.version,o.exists=t.exists,o.path=t.path,o.parseError=t.parse_error,o.issues=t.issues;let s=t.exists&&!t.parse_error;o.saved=s?M():"",o.validated=s?M():null}),Ne({focus:!1}),history.replaceState(null,"",rt(u.page))}var rn=null;async function ln(){let e=M();if(!(!u.schema||u.validated===e||rn===e)&&!Object.values(u.toolParams).some(t=>t.error)){rn=e;try{let t=await lt("POST","/api/validate",{config:JSON.parse(e)});if(!t.ok)return;g(o=>{o.issues=t.data.issues,o.validated=e})}catch{}}}async function Re(){if(!(u.busy||!u.schema)){if(u.exists&&!H()&&!u.parseError)return F("success","No changes to save.");if(Qn()){g(e=>{e.busy="save"});try{let e=await lt("POST","/api/save",{config:u.config,version:u.version});if(e.data.issues&&(u.issues=e.data.issues,u.validated=M()),e.ok){u.version=e.data.version,u.config=e.data.config,u.toolParams={},u.validated=M(),u.saved=M(),u.exists=!0,u.parseError=null;let t=e.data.path.split(/[\\/]/).pop();F("success",e.data.backup?`Saved ${t}. The previous version is in ${t}.bak.`:`Created ${t}.`)}else if(e.status===409)F("error",e.data.error,{label:"Reload",run:()=>location.reload()});else if(e.status===422){let{errors:t}=ue();F("error",`Not saved: ${ce(t,"error")} to fix first.`,{label:"Show",run:Zn})}else F("error",e.data.error||"Save failed.")}catch{F("error","Cannot reach the config server.")}finally{g(e=>{e.busy=null})}}}}function Qn(){let e=Object.values(u.toolParams).some(t=>t.error);return e&&(R("remote_tools"),F("error","A remote tool has invalid parameters JSON.")),!e}function cn(e,t){g(o=>{t?o.config[e.key]=Zt(e.default):delete o.config[e.key],e.key==="remote_tools"&&(o.toolParams={})})}function Zn(){let e=u.issues.find(t=>t.level==="error")||u.issues[0];e&&R(e.section,Me(e.path))}var Xn=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches?"auto":"smooth";function eo(e){return e==="core"?"get-running":z[e]||E(e)?e:"get-running"}function Ne({focus:e=!0}={}){g(t=>{t.page=eo(Kt()),t.query="",t.drawerOpen=!1}),e&&requestAnimationFrame(()=>{window.scrollTo(0,0),document.getElementById("page-title")?.focus({preventScroll:!0})})}function R(e,t){let o=rt(e);location.hash!==o&&history.pushState(null,"",o),Ne(),t&&requestAnimationFrame(()=>requestAnimationFrame(()=>{let r=document.getElementById(t);r&&(r.scrollIntoView({block:"center",behavior:Xn()}),r.focus({preventScroll:!0}))}))}function De(e){g(t=>{t.query=e})}var to=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches,sn;function ee(e,{instant:t=!1}={}){try{localStorage.setItem("tg-sidebar",e?"shown":"hidden")}catch{}clearTimeout(sn);let o=t||to();g(r=>{r.sideOpen=e,e?(r.sideRender=!0,r.sideShown=o):r.sideShown=!1,!e&&o&&(r.sideRender=!1)}),e&&!o&&requestAnimationFrame(()=>requestAnimationFrame(()=>g(r=>{r.sideShown=!0}))),!e&&!o&&(sn=setTimeout(()=>g(r=>{r.sideOpen||(r.sideRender=!1)}),200))}function ct(){u.sideOpen||ee(!0,{instant:!0}),requestAnimationFrame(()=>{let e=document.getElementById("q");e?.focus(),e?.select()})}function dn(e){g(t=>{e&&(t.tab=e),t.drawerOpen=!0})}var Le=()=>g(e=>{e.drawerOpen=!1});async function un(){let e="10xgraph dev";try{await navigator.clipboard.writeText(e)}catch{try{let t=document.createElement("textarea");t.value=e,document.body.append(t),t.select(),document.execCommand("copy"),t.remove()}catch{}}g(t=>{t.copied=!0}),setTimeout(()=>g(t=>{t.copied=!1}),2e3)}async function pn(){try{await navigator.clipboard.writeText(JSON.stringify(u.config,null,2)+` -`),F("success","Copied 10xgraph.json to the clipboard.")}catch{F("error","The clipboard is not available here.")}}var no=0;function F(e,t,o){let r=++no;g(i=>{i.toasts=[...i.toasts,{id:r,kind:e,message:t,action:o}]}),setTimeout(()=>ze(r),o?8e3:4e3)}function ze(e){g(t=>{t.toasts=t.toasts.filter(o=>o.id!==e)})}var fn={box:'',key:'',shield:'',gauge:'',arrows:'',activity:'',layers:'',wrench:'',flask:'',target:'',checkCircle:'',xCircle:'',alert:'',info:'',file:'',copy:'',x:'',plus:'',trash:'',panel:'',chevronDown:'',expand:'',collapse:'',power:'',spinner:'',book:'',flag:'',lock:'',play:'',bolt:'',terminal:'',search:'',chevronRight:'',code:'',circle:'',external:''},K={"get-running":"flag",environment:"terminal",core:"flag",auth:"lock",cache_store:"gauge",observability:"activity",runs:"play",websocket:"bolt",ag_ui:"external",remote_tools:"wrench",media:"file",checkpointer:"layers",id_generator:"key",thread_name_generator:"file",store:"box",injectq:"arrows",test:"flask",evaluation:"target"};var oo=0;function n(e,t,o,r,i,s){t||(t={});var a,d,f=t;if("ref"in f)for(d in f={},t)d=="ref"?a=t[d]:f[d]=t[d];var l={type:e,props:f,key:o,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--oo,__i:-1,__u:0,__source:i,__self:s};if(typeof e=="function"&&(a=e.defaultProps))for(d in a)f[d]===void 0&&(f[d]=a[d]);return b.vnode&&b.vnode(l),l}function _({name:e,class:t="size-4"}){return/(^|\s)size-/.test(t)||(t=`size-4 ${t}`),n("span",{class:`inline-flex shrink-0 ${t}`,"aria-hidden":"true",children:n("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round",class:"size-full",dangerouslySetInnerHTML:{__html:fn[e]}})})}function te({class:e,children:t}){return n("span",{class:`inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-semibold ${e}`,children:t})}function dt({id:e,on:t,label:o,onChange:r}){return n("button",{id:e,type:"button",role:"switch","aria-checked":t?"true":"false","aria-label":o,class:`focus-ring relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200 before:absolute before:-inset-2.5 before:content-[''] ${t?"bg-brand-solid":"bg-line-strong"}`,onClick:()=>r(!t),children:n("span",{class:`pointer-events-none inline-block size-5 rounded-full bg-white shadow-sm ring-1 ring-black/5 transition-transform duration-200 ease-out ${t?"translate-x-[18px]":"translate-x-0.5"}`})})}function Y({id:e,label:t,options:o,value:r,onPick:i}){return n("div",{id:e,role:"radiogroup","aria-label":t,class:"inline-flex max-w-full flex-wrap gap-0.5 rounded-[10px] border border-line-strong bg-raised p-0.5",children:o.map(([s,a])=>n("button",{type:"button",role:"radio","aria-checked":s===r?"true":"false",class:`focus-ring min-h-10 cursor-pointer rounded-lg px-3.5 text-sm font-medium transition-colors duration-150 ${s===r?"bg-surface text-ink shadow-card":"text-muted hover:text-ink"}`,onClick:()=>{s!==r&&i(s)},children:a},s))})}function ne({id:e,options:t,value:o,invalid:r,onChange:i}){return n("div",{class:"relative",children:[n("select",{id:e,value:o,class:`input cursor-pointer appearance-none pr-9 font-mono ${r?"input-invalid":""}`,onChange:s=>i(s.currentTarget.value),children:t.map(([s,a])=>n("option",{value:s,children:a},s))}),n(_,{name:"chevronDown",class:"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-faint"})]})}function W({id:e,values:t,placeholder:o,suggestions:r,onChange:i}){let s=Ie(null),a=f=>{let l=f.trim();s.current&&(s.current.value=""),l&&!t.includes(l)&&i([...t,l])},d=r?`${e}-options`:void 0;return n("div",{class:"flex min-h-11 cursor-text flex-wrap items-center gap-1.5 rounded-lg border border-line-strong bg-sunken px-1.5 py-1.5 transition-[border-color,box-shadow] duration-150 focus-within:border-brand focus-within:ring-4 focus-within:ring-brand-ring sm:py-1",onClick:f=>{f.target===f.currentTarget&&s.current?.focus()},children:[t.map((f,l)=>n("span",{class:"inline-flex h-7 items-center gap-0.5 rounded-md border border-line bg-raised pr-0.5 pl-2 font-mono text-xs text-body sm:h-6",children:[f,n("button",{type:"button","aria-label":`Remove ${f}`,class:"inline-flex size-6 cursor-pointer items-center justify-center rounded text-faint transition-colors hover:bg-hover hover:text-ink sm:size-5",onClick:()=>i(t.filter((p,h)=>h!==l)),children:n(_,{name:"x",class:"size-3"})})]},f)),n("input",{ref:s,id:e,type:"text",list:d,placeholder:t.length?"Add...":o,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:"h-7 min-w-[7rem] flex-1 bg-transparent px-1 font-mono text-base text-ink outline-none placeholder:text-faint sm:text-sm",onKeyDown:f=>{let l=f.currentTarget;(f.key==="Enter"||f.key===",")&&l.value.trim()?(f.preventDefault(),a(l.value)):f.key==="Backspace"&&!l.value&&t.length&&i(t.slice(0,-1))},onChange:f=>{r?.includes(f.currentTarget.value.trim())&&a(f.currentTarget.value)},onBlur:f=>a(f.currentTarget.value)}),r?n("datalist",{id:d,children:r.filter(f=>!t.includes(f)).map(f=>n("option",{value:f},f))}):null]})}function fe({label:e,path:t,help:o,id:r,required:i}){return n("div",{class:"min-w-0",children:[n("label",{for:r,class:"block text-sm font-medium text-ink",children:[e,i?n("span",{class:"text-danger",children:" *"}):null]}),n("div",{class:"mt-0.5 font-mono text-xs text-faint",children:t}),o?n("p",{class:"mt-1.5 text-[13px] leading-5 text-muted",children:o}):null]})}function G({issue:e}){if(!e)return null;let t=e.level==="error";return n("p",{class:`mt-2 flex gap-1.5 text-[13px] leading-5 ${t?"text-danger":"text-warn"}`,role:t?"alert":void 0,children:[n(_,{name:t?"xCircle":"alert",class:"mt-0.5 size-3.5"}),n("span",{children:e.message})]})}function ut({children:e}){return n("div",{class:"grid gap-3 px-5 py-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] sm:gap-8",children:e})}function V({id:e,label:t,path:o,value:r,placeholder:i,onInput:s,issue:a,help:d}){return n(ut,{children:[n(fe,{label:t,path:o,help:d,id:e}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n("input",{id:e,type:"text",value:r??"",placeholder:i,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${a?.level==="error"?"input-invalid":""}`,onInput:f=>s(f.currentTarget.value)}),n(G,{issue:a})]})]})}function pt({name:e,label:t,options:o,value:r,onPick:i}){return n("div",{role:"radiogroup","aria-label":t,class:"grid gap-3 sm:grid-cols-2",children:o.map(([s,a,d])=>{let f=s===r;return n("button",{type:"button",role:"radio","aria-checked":f?"true":"false",id:`${e}-${s}`,class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 ${f?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{f||i(s)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${f?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:f?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:a}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:d})]})]},s)})})}function A({title:e,children:t}){return n("section",{children:[e?n("h4",{class:"eyebrow mb-2.5",children:e}):null,t]})}var ro={info:["info","border-brand/20 bg-brand-soft","text-brand"],warn:["alert","border-warn/25 bg-warn-soft","text-warn"],error:["xCircle","border-danger/25 bg-danger-soft","text-danger"]};function me({kind:e,title:t,lines:o}){let[r,i,s]=ro[e];return n("div",{class:`flex gap-3 rounded-xl border px-4 py-3 ${i}`,role:e==="error"?"alert":void 0,children:[n(_,{name:r,class:`mt-0.5 size-4 ${s}`}),n("div",{class:"min-w-0 space-y-1 text-[13px] leading-5 text-body",children:[t?n("p",{class:"font-medium text-ink",children:t}):null,o.map((a,d)=>n("p",{children:a},d))]})]})}function ft({issue:e}){return n(me,{kind:e.level==="error"?"error":"warn",lines:[n("span",{children:[e.path?n("code",{class:"mr-1.5 font-mono text-xs text-muted",children:e.path}):null,e.message]})]})}function mn({icon:e,tint:t,title:o,text:r,children:i}){return n("div",{class:"px-6 py-14 text-center",children:[n("div",{class:`mx-auto flex size-11 items-center justify-center rounded-full bg-raised ${t}`,children:n(_,{name:e,class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:o}),n("p",{class:"mx-auto mt-1 max-w-xs text-sm leading-6 text-muted",children:r}),i]})}function hn(){let e=Ie(null);L(()=>{let a=e.current;a&&(u.drawerOpen&&!a.open?a.showModal():!u.drawerOpen&&a.open&&a.close())});let t=u.issues.length>0,o=t&&u.tab==="problems"?"problems":"json",{errors:r,warnings:i}=ue(),s=(a,d,f)=>n("button",{type:"button",role:"tab",id:`tab-${a}`,"aria-selected":o===a?"true":"false",class:`focus-ring inline-flex h-10 cursor-pointer items-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors duration-150 ${o===a?"bg-raised text-ink":"text-muted hover:text-ink"}`,onClick:()=>g(l=>{l.tab=a}),children:[d,f]});return n("dialog",{ref:e,"aria-label":"10xgraph.json preview",class:"m-0 ml-auto h-dvh max-h-none w-[min(540px,100vw)] border-0 border-l border-line-strong bg-surface p-0 text-body shadow-pop backdrop:bg-black/45",onClose:()=>{u.drawerOpen&&Le()},onClick:a=>{a.target===a.currentTarget&&Le()},children:n("div",{class:"flex h-full flex-col",children:[n("div",{class:"flex h-14 shrink-0 items-center gap-1 border-b border-line px-3",children:[n("div",{role:"tablist",class:"flex flex-1 gap-1",children:[s("json","10xgraph.json",null),t?s("problems","Problems",n("span",{class:`inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[11px] font-semibold ${r?"bg-danger-soft text-danger":"bg-warn-soft text-warn"}`,children:r+i})):null]}),n("button",{type:"button",class:"icon-btn","aria-label":"Close preview",onClick:Le,children:n(_,{name:"x",class:"size-5"})})]}),n("div",{class:"min-h-0 flex-1 overflow-y-auto overscroll-contain",children:o==="problems"?n(so,{}):n(ao,{})})]})})}function so(){let{errors:e,warnings:t}=ue(),o=u.sections.map(r=>[r,X(r.id)]).filter(([,r])=>r.length);return n("div",{children:[n("div",{class:"border-b border-line px-4 py-3",children:n("p",{class:"text-sm text-muted",children:[n("span",{class:e?"font-medium text-danger":"",children:ce(e,"error")})," \xB7 ",n("span",{class:t?"font-medium text-warn":"",children:ce(t,"warning")})]})}),n("div",{class:"space-y-5 px-3 py-4",children:o.map(([r,i])=>n("div",{children:[n("p",{class:"eyebrow mb-1.5 flex items-center gap-2 px-2",children:[n(_,{name:K[r.id]||"box",class:"size-3.5"}),r.title]}),n("ul",{class:"space-y-0.5",children:i.map((s,a)=>{let d=s.level==="error";return n("li",{children:n("button",{type:"button",onClick:()=>R(r.id,Me(s.path)),class:"focus-ring flex min-h-11 w-full cursor-pointer gap-2.5 rounded-lg px-2 py-2 text-left transition-colors duration-150 hover:bg-raised",children:[n(_,{name:d?"xCircle":"alert",class:`mt-0.5 size-4 ${d?"text-danger":"text-warn"}`}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-[13px] leading-5 text-body",children:s.message}),s.path?n("span",{class:"mt-0.5 block truncate font-mono text-xs text-faint",children:s.path}):null]})]})},a)})})]},r.id))})]})}function io(e){return e.replace(/&/g,"&").replace(//g,">").replace(/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,o=>{let r="var(--af-json-number)";return o.startsWith('"')?r=o.endsWith(":")?"var(--af-json-key)":"var(--af-json-string)":(o==="true"||o==="false"||o==="null")&&(r="var(--af-json-literal)"),`${o}`})}function ao(){let e=JSON.stringify(u.config,null,2).split(` -`);return n("div",{children:[n("div",{class:"sticky top-0 flex items-center justify-between gap-3 border-b border-line bg-surface px-4 py-2.5",children:[n("p",{class:"text-xs text-faint",children:H()||!u.exists?"Preview of what Save will write":"Matches the file on disk"}),n("button",{type:"button",class:"btn btn-ghost h-8 px-2.5 text-xs",onClick:pn,children:[n(_,{name:"copy",class:"size-3.5"}),"Copy"]})]}),n("div",{class:"py-3 pr-4 font-mono text-[12.5px] leading-5",children:e.map((t,o)=>n("div",{class:"flex",children:[n("span",{class:"w-10 shrink-0 pr-3 text-right text-faint/70 select-none",children:o+1}),n("span",{class:"min-w-0 flex-1 break-all whitespace-pre-wrap",dangerouslySetInnerHTML:{__html:io(t)}})]},o))})]})}function ht(e){let t=[];for(let o of e){let r=o.group||"",i=t.find(s=>s.name===r);i||t.push(i={name:r,fields:[]}),i.fields.push(o)}return t}var q=(e,t)=>g(o=>{if(t===void 0){st(o.config,e);return}let r=e.length>1?o.sections.find(i=>i.optional&&i.key===e[0]):null;r&&(o.config[r.key]===void 0||o.config[r.key]===null)&&(o.config[r.key]=JSON.parse(JSON.stringify(r.default))),je(o.config,e,t)}),lo=e=>`f-${e.path.join("-")}`;function co({id:e,value:t,field:o,invalid:r,disabled:i}){let[s,a]=Z(String(t??""));return L(()=>{(s===""?t!==void 0:Number(s)!==t)&&a(String(t??""))},[t]),n("input",{id:e,type:"number",inputmode:"decimal",value:s,disabled:i,min:o.min,step:o.step??"any",placeholder:o.placeholder??o.default??"",class:`input max-w-40 font-mono tabular-nums ${r?"input-invalid":""}`,onInput:d=>{let f=d.currentTarget.value;a(f),f===""?q(o.path,void 0):Number.isNaN(Number(f))||q(o.path,Number(f))}})}function mt({field:e,value:t}){return e.exclusive_min===void 0||typeof t!="number"||t>e.exclusive_min?null:n("p",{class:"mt-1.5 text-[13px] text-warn",children:["Must be greater than ",e.exclusive_min,"."]})}function uo({id:e,field:t,value:o,invalid:r}){let i=t.unlimited_value!==void 0&&o===t.unlimited_value,s=n(co,{id:e,value:i?void 0:o,field:t,invalid:r,disabled:i});if(t.null_label!==void 0){let a=o===null?"n":o===void 0?"d":"s",d=typeof t.default=="number"?t.default:300;return n("div",{children:[n(Y,{id:e,label:t.label,value:a,options:[["d","Default"],["s","Seconds"],["n",t.null_label]],onPick:f=>q(t.path,f==="d"?void 0:f==="n"?null:d)}),a==="s"?n("div",{class:"mt-2.5",children:[s,n(mt,{field:t,value:o})]}):null]})}return t.unlimited_value!==void 0?n("div",{children:[n("div",{class:"flex flex-wrap items-center gap-x-4",children:[s,n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm text-body",children:[n("input",{type:"checkbox",class:"size-[18px] accent-brand-solid",checked:i,onChange:a=>q(t.path,a.currentTarget.checked?t.unlimited_value:void 0)}),"Unlimited"]})]}),n(mt,{field:t,value:o})]}):n("div",{children:[s,n(mt,{field:t,value:o})]})}function po({id:e,field:t,value:o}){if(t.unset_label!==void 0){let r=[["d","Default"]];return t.allow_true!==!1&&r.push(["t","On"]),r.push(["f","Off"]),n("div",{children:[n(Y,{id:e,label:t.label,value:o===void 0?"d":o?"t":"f",options:r,onPick:i=>q(t.path,i==="d"?void 0:i==="t")}),n("p",{class:"mt-1.5 text-xs text-faint",children:["Default: ",t.unset_label]})]})}return n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-semibold text-ink",children:[n("input",{id:e,type:"checkbox",class:"size-[18px] accent-brand-solid",checked:(o??t.default??!1)===!0,onChange:r=>q(t.path,r.currentTarget.checked)}),t.label]})}function fo({id:e,field:t,value:o,invalid:r}){if(t.type==="bool")return n(po,{id:e,field:t,value:o});if(t.type==="number")return n(uo,{id:e,field:t,value:o,invalid:r});if(t.type==="list")return n(W,{id:e,values:Array.isArray(o)?o:[],placeholder:t.placeholder,onChange:i=>q(t.path,i.length?i:void 0)});if(t.type==="select"){let i=t.options.map(s=>Array.isArray(s)?s:[s,s]);return t.unset_label!==void 0&&i.unshift(["",t.unset_label]),n(ne,{id:e,options:i,invalid:r,value:o??(t.unset_label!==void 0?"":t.default??(Array.isArray(t.options[0])?t.options[0][0]:t.options[0])),onChange:s=>q(t.path,s===""?void 0:s)})}return n("input",{id:e,type:"text",value:o??"",placeholder:t.placeholder,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${r?"input-invalid":""}`,onInput:i=>q(t.path,i.currentTarget.value===""?void 0:i.currentTarget.value)})}function I({field:e}){let t=e.path.join("."),o=lo(e),r=Ae(u.config,e.path),i=O(t),s=it(e),a=tn(e),d=e.type==="bool"&&e.unset_label===void 0,f=e.default!==void 0&&!s,l=Array.isArray(e.default)?e.default.join(", "):String(e.default??"");return n("div",{id:`row-${o}`,class:`relative pt-5 pb-1 ${s?"before:absolute before:top-6 before:-left-2.5 before:bottom-2 before:w-[3px] before:rounded before:bg-brand before:content-[''] sm:before:-left-3.5":""}`,children:[n("div",{class:"flex min-h-6 items-center gap-2.5",children:[d?null:n("label",{for:o,class:`text-sm font-semibold text-ink ${a?"opacity-50":""}`,children:[e.label,e.required?n("span",{class:"text-danger",children:" *"}):null]}),d?null:n("code",{class:"hidden font-mono text-[11px] text-faint sm:inline",children:t}),f&&!d?n("span",{class:"rounded border border-line px-1.5 text-[11px] leading-4 font-medium text-faint",children:"default"}):null,n("span",{class:"flex-1"}),s?n("button",{type:"button",class:"focus-ring min-h-8 cursor-pointer rounded px-1 text-[13px] font-medium text-brand hover:underline",title:`Reset to default${e.default!==void 0?` (${l})`:""}`,"aria-label":`Reset ${e.label} to default`,onClick:()=>g(p=>st(p.config,e.path)),children:"Reset"}):null]}),e.help?n("p",{class:`mt-0.5 max-w-prose text-[13px] leading-5 text-muted ${a?"opacity-50":""}`,children:e.help}):null,n("fieldset",{disabled:a,class:`m-0 mt-2.5 min-w-0 max-w-[520px] border-0 p-0 ${a?"opacity-50":""}`,children:n(fo,{id:o,field:e,value:r,invalid:i?.level==="error"})}),a?n("p",{class:"mt-1.5 text-xs text-faint",children:nn(e)}):null,n(G,{issue:i})]})}var mo=[["","Choose reach"],["own","own"],["tenant","tenant"],["all","all"]],bn=e=>Array.isArray(e)?e.length>0:y(e)&&Object.keys(e).length>0;function xn(){let e=u.config.auth;return e==="jwt"?{method:"jwt"}:y(e)?e:{method:"none"}}function N(e){g(t=>{let o={...xn()};e(o),t.config.auth=o.method==="none"?null:o})}function qe(e,t,o){o==null||o===""||typeof o!="string"&&!bn(o)?delete e[t]:e[t]=o}function ho(e){return e.roles_claim!==void 0||bn(e.roles)?"roles":e.permissions_claim!==void 0?"permissions":"default"}var _o=e=>u.issues.filter(t=>t.path===e||t.path.startsWith(`${e}.`));function _t({prefix:e}){return _o(e).map((t,o)=>n(G,{issue:t},o))}function vn({id:e,label:t,name:o,onRename:r}){let[i,s]=Z(o);return L(()=>s(o),[o]),n("div",{children:[n("label",{for:e,class:"sr-only",children:t}),n("input",{id:e,type:"text",value:i,class:"input font-mono",autocomplete:"off",spellcheck:!1,onInput:a=>s(a.currentTarget.value),onChange:a=>{r(a.currentTarget.value.trim())||s(o)}})]})}var Fe=({children:e})=>n("span",{class:"mb-1.5 block text-xs font-medium text-faint",children:e});function yn(e,t,o){return!o||o!==t&&o in e?null:Object.fromEntries(Object.entries(e).map(([r,i])=>[r===t?o:r,i]))}function kn({sec:e}){let t=xn(),o=t.method??"none",r=ho(t);return n("div",{class:"space-y-6",children:[n(A,{title:"Method",children:n(pt,{name:"auth-method",label:"Authentication method",value:o,options:[["none","None","Every request is accepted. For local development."],["jwt","JWT","Verify bearer tokens signed with JWT_SECRET_KEY."],["custom","Custom","Your own BaseAuth subclass decides who is calling."]],onPick:i=>N(s=>{s.method=i,i==="custom"?s.path=s.path??"":delete s.path})})}),o==="custom"?n(A,{title:"Class",children:n("div",{class:"panel",children:n(V,{id:"auth-path",label:"Auth class",path:"auth.path",value:t.path,placeholder:"auth.custom:MyAuth",help:"Import path of a BaseAuth subclass, as module:attribute.",issue:O("auth.path"),onInput:i=>N(s=>{s.path=i})})})}):null,o!=="none"?n(w,{children:[n(A,{title:"Permission source",children:n(pt,{name:"auth-source",label:"Permission source",value:r,options:[["default","Default","Every signed-in user, own data only."],["roles","Roles from a claim","Map the roles in the token to permissions."],["permissions","Permissions from a claim","The token already lists the permissions."]],onPick:i=>N(s=>{delete s.roles_claim,delete s.permissions_claim,delete s.roles,delete s.default_role,i==="roles"&&(s.roles_claim="roles",s.roles={member:{permissions:[],reach:"own"}}),i==="permissions"&&(s.permissions_claim="permissions")})})}),r==="roles"?n(bo,{auth:t,registry:e.registry}):null,r==="permissions"?n(A,{title:"Claim",children:n("div",{class:"panel",children:n(V,{id:"auth-permissions-claim",label:"Permissions claim",path:"auth.permissions_claim",value:t.permissions_claim,placeholder:"permissions",issue:O("auth.permissions_claim"),help:"Token claim holding a list of permissions, such as graph:invoke.",onInput:i=>N(s=>{s.permissions_claim=i})})})}):null,n(A,{title:"Tenants",children:n("div",{class:"panel divide-y divide-line",children:[n("div",{class:"px-5 py-4",children:[n(fe,{label:"Tenant claims",path:"auth.tenant_claims",id:"auth-tenant-claims",help:"Token claims that name the tenant, such as org_id. Rows are separated by them."}),n("div",{class:"mt-3",children:n(W,{id:"auth-tenant-claims",values:t.tenant_claims??[],placeholder:"org_id",onChange:i=>N(s=>qe(s,"tenant_claims",i))})}),n(_t,{prefix:"auth.tenant_claims"})]}),r==="roles"?n(go,{auth:t}):null]})}),n(xo,{auth:t})]}):null]})}function go({auth:e}){let t=Object.keys(y(e.roles)?e.roles:{});return n(ut,{children:[n(fe,{label:"Default role",path:"auth.default_role",id:"auth-default-role",help:"Given to a signed-in user whose token carries no role."}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n(ne,{id:"auth-default-role",value:e.default_role??"",options:[["","None"],...t.map(o=>[o,o])],onChange:o=>N(r=>qe(r,"default_role",o))}),n(G,{issue:O("auth.default_role")})]})]})}function bo({auth:e,registry:t}){let o=y(e.roles)?e.roles:{},r=Object.entries(o),i=Object.keys(o),s=["*",...Object.entries(t||{}).flatMap(([p,h])=>[`${p}:*`,...h.map(c=>`${p}:${c}`)])],a=p=>N(h=>{h.roles=p}),d=(p,h)=>a({...o,[p]:{...o[p],...h}}),f=()=>{let p=r.length+1;for(;`role${p}`in o;)p++;a({...o,[`role${p}`]:{permissions:[],reach:"own"}})},l=(p,h)=>{let c=yn(o,p,h);return c?(N(m=>{m.roles=Object.fromEntries(Object.entries(c).map(([x,$])=>[x,$.inherits?{...$,inherits:$.inherits.map(k=>k===p?h:k)}:$])),m.default_role===p&&(m.default_role=h),y(m.system_identities)&&(m.system_identities=Object.fromEntries(Object.entries(m.system_identities).map(([x,$])=>[x,{...$,roles:($.roles??[]).map(k=>k===p?h:k)}])))}),!0):!1};return n(A,{title:"Roles",children:n("div",{class:"panel",children:[n("div",{class:"border-b border-line",children:n(V,{id:"auth-roles-claim",label:"Roles claim",path:"auth.roles_claim",value:e.roles_claim,placeholder:"roles",issue:O("auth.roles_claim"),help:"Token claim holding the user's list of role names.",onInput:p=>N(h=>{h.roles_claim=p})})}),r.length?n("div",{class:"divide-y divide-line",children:r.map(([p,h],c)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_8rem_auto] items-start gap-3",children:[n(vn,{id:`role-name-${c}`,label:"Role name",name:p,onRename:m=>l(p,m)}),n(ne,{id:`role-reach-${c}`,value:h.reach??"",options:mo,invalid:O(`auth.roles.${p}.reach`)?.level==="error",onChange:m=>d(p,{reach:m})}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove role ${p}`,onClick:()=>a(Object.fromEntries(r.filter(([m])=>m!==p))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Fe,{children:"Permissions"}),n(W,{id:`role-permissions-${c}`,values:h.permissions??[],placeholder:"graph:invoke, threads:*, *",suggestions:s,onChange:m=>d(p,{permissions:m})})]}),n("div",{children:[n(Fe,{children:"Inherits"}),n(W,{id:`role-inherits-${c}`,values:h.inherits??[],placeholder:"another role",suggestions:i.filter(m=>m!==p),onChange:m=>{let x={...h};qe(x,"inherits",m),a({...o,[p]:x})}})]}),n(_t,{prefix:`auth.roles.${p}`})]},c))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"No roles yet. Add one to use roles."}),n("div",{class:"flex items-center justify-between gap-4 border-t border-line px-5 py-3",children:[n("p",{class:"text-[13px] text-muted",children:["Reach: ",n("code",{class:"font-mono",children:"own"})," the user's rows, ",n("code",{class:"font-mono",children:"tenant"})," every row of their tenant, ",n("code",{class:"font-mono",children:"all"})," every row."]}),n("button",{type:"button",class:"btn btn-ghost",onClick:f,children:[n(_,{name:"plus"}),"Add role"]})]})]})})}function xo({auth:e}){let t=y(e.system_identities)?e.system_identities:{},o=Object.entries(t),r=Object.keys(y(e.roles)?e.roles:{}),i=l=>N(p=>qe(p,"system_identities",l)),s=(l,p)=>i({...t,[l]:{...t[l],...p}}),a=()=>{let l=o.length+1;for(;`job${l}`in t;)l++;i({...t,[`job${l}`]:{roles:[],tenant:{}}})},d=l=>Object.entries(l??{}).map(([p,h])=>`${p}=${h}`),f=l=>Object.fromEntries(l.filter(p=>p.includes("=")).map(p=>[p.slice(0,p.indexOf("=")).trim(),p.slice(p.indexOf("=")+1).trim()]));return n(A,{title:"System identities",children:n("div",{class:"panel",children:[o.length?n("div",{class:"divide-y divide-line",children:o.map(([l,p],h)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3",children:[n(vn,{id:`system-name-${h}`,label:"Identity name",name:l,onRename:c=>{let m=yn(t,l,c);return m&&i(m),!!m}}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${l}`,onClick:()=>i(Object.fromEntries(o.filter(([c])=>c!==l))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Fe,{children:"Roles"}),n(W,{id:`system-roles-${h}`,values:p.roles??[],placeholder:"member",suggestions:r,onChange:c=>s(l,{roles:c})})]}),n("div",{children:[n(Fe,{children:"Tenant, as claim=value"}),n(W,{id:`system-tenant-${h}`,values:d(p.tenant),placeholder:"org_id=acme",onChange:c=>s(l,{tenant:f(c)})})]}),n(_t,{prefix:`auth.system_identities.${l}`})]},h))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"None. Scheduled jobs and other non-user callers can run as one."}),n("div",{class:"flex justify-end border-t border-line px-5 py-3",children:n("button",{type:"button",class:"btn btn-ghost",onClick:a,children:[n(_,{name:"plus"}),"Add identity"]})})]})})}var He=[["memory","Memory","This process only"],["redis","Redis","Shared. Redis, Valkey, Dragonfly, KeyDB"],["custom","Custom","Your BaseCacheStore class"]],vo={requests:100,window:60,by:"ip"},Ue=()=>y(u.config.cache_store)?u.config.cache_store:{};function gt(e){g(t=>{let o=y(t.config.cache_store)?t.config.cache_store:{};e(o),Object.keys(o).length?t.config.cache_store=o:delete t.config.cache_store})}var yo=()=>{let e=Ue().rate_limit;return y(e)&&e.enabled!==!1},ko=()=>{let e=Ue().token_revocation;return e===!0||y(e)&&e.enabled!==!1};function wo(e){gt(t=>{delete t.url,delete t.path,e==="memory"?delete t.type:t.type=e})}function Co(e){gt(t=>{e?y(t.rate_limit)?delete t.rate_limit.enabled:t.rate_limit={...vo}:delete t.rate_limit})}function $o(e){gt(t=>{e?y(t.token_revocation)?delete t.token_revocation.enabled:t.token_revocation=!0:delete t.token_revocation})}var _n=(e,t)=>g(o=>{je(o.config,e,t)});function So({value:e,onPick:t}){let o=(r,i)=>{let s=r.key==="ArrowRight"||r.key==="ArrowDown"?1:r.key==="ArrowLeft"||r.key==="ArrowUp"?-1:0;if(!s)return;r.preventDefault();let a=He[(i+s+He.length)%He.length][0];t(a),requestAnimationFrame(()=>document.getElementById(`cache-type-${a}`)?.focus())};return n("div",{role:"radiogroup","aria-label":"Cache store type",class:"grid gap-3 sm:grid-cols-3",children:He.map(([r,i,s],a)=>{let d=r===e;return n("button",{type:"button",role:"radio",id:`cache-type-${r}`,"aria-checked":d?"true":"false",tabIndex:d?0:-1,onKeyDown:f=>o(f,a),class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 motion-reduce:transition-none ${d?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{d||t(r)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${d?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:d?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:i}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:s})]})]},r)})})}function he({warn:e,children:t}){return n("p",{class:`mt-3 flex items-start gap-1.5 text-[13px] leading-5 ${e?"text-warn":"text-muted"}`,children:[e?n(_,{name:"alert",class:"mt-0.5 size-3.5"}):null,n("span",{children:t})]})}function gn({id:e,title:t,text:o,on:r,onChange:i}){return n("div",{class:"flex items-center gap-4",children:[n("div",{class:"min-w-0 flex-1",children:[n("h3",{class:"text-base font-semibold text-ink",children:t}),n("p",{class:"mt-0.5 text-[13px] leading-5 text-muted",children:o})]}),n(dt,{id:e,on:r,label:t,onChange:i})]})}var To=[["ip","IP"],["user","User"],["global","Global"]];function Oo({field:e,memory:t}){let[o,r]=Z(!1),i=Ue().rate_limit,s=y(i)&&i.by||"ip",a=!(y(i)&&i.fail_open===!1),d=["trusted_proxy_headers","trusted_proxy_hops","trusted_proxies","exclude_paths"];return n(w,{children:[t?n(he,{warn:!0,children:"Counts per worker with the Memory store."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:e("cache_store.rate_limit.requests")}),n(I,{field:e("cache_store.rate_limit.window")}),n("div",{class:"pt-5",children:[n("span",{id:"cache-by-label",class:"block text-sm font-semibold text-ink",children:"Limit by"}),n("div",{class:"mt-2.5",children:n(Y,{id:"f-cache_store-rate_limit-by",label:"Limit by",options:To,value:s,onPick:f=>_n(["cache_store","rate_limit","by"],f)})}),n(G,{issue:O("cache_store.rate_limit.by")})]}),n("div",{class:"pt-5",children:[n("span",{id:"cache-fail-open-label",class:"block text-sm font-semibold text-ink",children:"Fail open"}),n("div",{class:"mt-2.5 flex items-center gap-3",children:[n(dt,{id:"f-cache_store-rate_limit-fail_open",on:a,label:"Fail open",onChange:f=>_n(["cache_store","rate_limit","fail_open"],f)}),n("p",{class:"m-0 text-[13px] leading-5 text-muted",children:a?"Allow requests if the store errors.":"Deny with 503 if the store errors."})]})]})]}),n("div",{class:"mt-5 border-t border-line pt-1",children:[n("button",{type:"button","aria-expanded":o?"true":"false","aria-controls":"cache-advanced",class:"focus-ring flex min-h-11 cursor-pointer items-center gap-2 rounded font-medium text-muted hover:text-ink",onClick:()=>r(!o),children:[n(_,{name:"chevronRight",class:`size-4 transition-transform duration-150 motion-reduce:transition-none ${o?"rotate-90":""}`}),"Advanced"]}),o?n("div",{id:"cache-advanced",children:d.map(f=>n(I,{field:e(`cache_store.rate_limit.${f}`)},f))}):null]})]})}var oe=[["rate","Rate limiting"],["revoke","Token revocation"]];function wn({sec:e}){let[t,o]=Z("rate"),r=Ue().type||"memory",i=l=>e.fields.find(p=>p.path.join(".")===l),s={rate:yo(),revoke:ko()},a=r==="memory",d={rate:!!O("cache_store.rate_limit"),revoke:!!O("cache_store.token_revocation")},f=(l,p)=>{let h=l.key==="ArrowRight"?1:l.key==="ArrowLeft"?-1:0,c=l.key==="Home"?0:l.key==="End"?oe.length-1:h?(p+h+oe.length)%oe.length:-1;c<0||(l.preventDefault(),o(oe[c][0]),document.getElementById(`cache-tab-${oe[c][0]}`)?.focus())};return n("div",{children:[n(A,{title:"Store type",children:[n(So,{value:r,onPick:wo}),r==="redis"?n(I,{field:i("cache_store.url")}):null,r==="custom"?n(I,{field:i("cache_store.path")}):null,a?n("p",{class:"mt-3 mb-0 text-[13px] leading-5 text-muted",children:"Not shared between workers: each worker keeps its own locks, limits and revocations."}):null]}),n("div",{role:"tablist","aria-label":"Cache store features",class:"mt-8 flex gap-1 border-b border-line",children:oe.map(([l,p],h)=>{let c=t===l;return n("button",{type:"button",role:"tab",id:`cache-tab-${l}`,"aria-selected":c?"true":"false","aria-controls":`cache-panel-${l}`,tabIndex:c?0:-1,onKeyDown:m=>f(m,h),class:`focus-ring -mb-px flex min-h-11 cursor-pointer items-center gap-2 border-b-2 px-3 text-sm font-medium transition-colors duration-150 motion-reduce:transition-none ${c?"border-brand-solid text-ink":"border-transparent text-muted hover:text-ink"}`,onClick:()=>o(l),children:[p,n(te,{class:s[l]?"bg-brand-soft text-brand":"bg-raised text-muted",children:s[l]?"On":"Off"})]},l)})}),n("div",{role:"tabpanel",id:`cache-panel-${t}`,"aria-labelledby":`cache-tab-${t}`,class:"pt-5",children:t==="rate"?n(w,{children:[n(gn,{id:"cache-rate-limit",title:"Rate limiting",on:s.rate,onChange:Co,text:"Cap how many requests one caller can make."}),s.rate?n(Oo,{field:i,memory:a}):d.rate?n(he,{warn:!0,children:"Off: one caller can exhaust the service."}):null]}):n(w,{children:[n(gn,{id:"cache-token-revocation",title:"Token revocation",on:s.revoke,onChange:$o,text:"Reject revoked tokens before they expire (10xgraph revoke)."}),s.revoke?n(w,{children:[a?n(he,{warn:!0,children:"10xgraph revoke cannot reach an in-memory store; pick Redis or Custom."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:i("cache_store.token_revocation.token_lifetime")}),n(I,{field:i("cache_store.token_revocation.user_ttl")})]}),n(he,{children:"Fails closed: while the cache store is unreachable, authenticated requests get 503 and the server will not start."})]}):d.revoke?n(he,{warn:!0,children:"Off: a leaked token works until it expires."}):null]})})]})}function Cn({sec:e}){let t=e.fields[0].path[0],o=e.choices.map(p=>p[0]),r=u.config[t],i=t==="thread_name_generator"?"random":"",s=r===void 0?i:typeof r=="string"&&o.includes(r)?r:"custom",a=[...t==="id_generator"?[["","Not set (the graph's own generator)"]]:[],...e.choices.map(([p,h])=>[p,t==="thread_name_generator"&&p==="random"?`${h} (default)`:h]),["custom","Custom (module:attr)"]],d=s===""?"The graph keeps the generator it was compiled with.":s==="custom"?"Your own class, as module:attribute.":e.choices.find(p=>p[0]===s)?.[2],f=p=>g(h=>{p===i?delete h.config[t]:h.config[t]=p==="custom"?"":p}),l=`f-${t}`;return n("div",{children:[n(A,{title:"Choice",children:n("div",{class:"max-w-[520px]",children:[n("label",{for:`${l}-select`,class:"sr-only",children:e.title}),n(ne,{id:`${l}-select`,options:a,value:s,onChange:f}),n("p",{class:"mt-1.5 text-[13px] text-muted",children:d})]})}),s==="custom"?n(A,{title:"Class",children:n("div",{class:"panel",children:n(V,{id:l,label:e.fields[0].label,path:t,value:typeof r=="string"?r:"",placeholder:e.fields[0].placeholder,help:e.fields[0].help,issue:O(t),onInput:p=>g(h=>{h.config[t]=p})})})}):null]})}function $n(){let e=Array.isArray(u.config.remote_tools)?u.config.remote_tools:[],t=()=>{g(o=>{e.push({node:"",name:"",description:"",parameters:{type:"object",properties:{},required:[]}}),o.config.remote_tools=e}),requestAnimationFrame(()=>document.getElementById(`tool-${e.length-1}-name`)?.focus())};return e.length?n("div",{class:"space-y-4",children:[e.map((o,r)=>n(Io,{tools:e,tool:o,index:r},r)),n("button",{type:"button",onClick:t,class:"focus-ring flex h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-dashed border-line-strong text-sm font-medium text-muted transition-colors duration-150 hover:border-brand hover:text-brand",children:[n(_,{name:"plus"}),"Add remote tool"]})]}):n("div",{class:"rounded-xl border border-dashed border-line-strong bg-surface px-6 py-10 text-center",children:[n("div",{class:"mx-auto flex size-11 items-center justify-center rounded-full bg-raised text-faint",children:n(_,{name:"wrench",class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:"No remote tools yet"}),n("p",{class:"mx-auto mt-1 max-w-sm text-sm leading-6 text-muted",children:"Declare a tool here and the client that calls the API runs it, for example to read the user's location."}),n("button",{type:"button",class:"btn btn-outline mt-5",onClick:t,children:[n(_,{name:"plus"}),"Add remote tool"]})]})}function Io({tools:e,tool:t,index:o}){let r=u.toolParams[o]??{text:JSON.stringify(t.parameters??{},null,2),error:null},i=a=>g(()=>{e[o]={...e[o],...a}}),s=a=>`remote_tools[${o}].${a}`;return n("section",{class:"panel",children:[n("div",{class:"flex items-center gap-3 border-b border-line px-5 py-3",children:[n(_,{name:"wrench",class:"size-4 text-faint"}),n("span",{class:"min-w-0 flex-1 truncate font-mono text-sm font-medium text-ink",children:t.name||`tool_${o+1}`}),t.node?n("span",{class:"rounded-md border border-line bg-raised px-1.5 py-0.5 font-mono text-xs text-muted",children:t.node}):null,n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${t.name||"tool"}`,onClick:()=>g(a=>{e.splice(o,1),a.toolParams={}}),children:n(_,{name:"trash"})})]}),n("div",{class:"divide-y divide-line",children:[n(V,{id:`tool-${o}-name`,label:"Tool name",path:s("name"),value:t.name,placeholder:"get_location",onInput:a=>i({name:a})}),n(V,{id:`tool-${o}-node`,label:"Graph node",path:s("node"),value:t.node,placeholder:"MAIN",help:"The node whose model may call this tool.",onInput:a=>i({node:a})}),n(V,{id:`tool-${o}-description`,label:"Description",path:s("description"),value:t.description,placeholder:"Returns the user's current city",help:"What the model reads when deciding to call it.",onInput:a=>i({description:a})}),n("div",{class:"px-5 py-4",children:[n(fe,{label:"Parameters",path:s("parameters"),help:"JSON schema of the arguments. type must be object.",id:`tool-${o}-params`}),n("textarea",{id:`tool-${o}-params`,rows:7,spellcheck:!1,value:r.text,class:`input mt-3 h-auto py-2.5 font-mono text-[13px] leading-5 ${r.error?"input-invalid":""}`,onInput:a=>{let d=a.currentTarget.value;g(f=>{try{e[o]={...e[o],parameters:JSON.parse(d)},f.toolParams[o]={text:d,error:null}}catch(l){f.toolParams[o]={text:d,error:l.message}}})}}),n(G,{issue:r.error?{level:"error",message:r.error}:null})]})]})]})}var _e=({children:e,code:t})=>n("h1",{id:"page-title",tabindex:-1,class:"flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-semibold tracking-tight text-ink focus:outline-none",children:[e,t?n("code",{class:"rounded border border-line bg-raised px-1.5 font-mono text-xs font-normal text-faint",children:t}):null]}),bt=({children:e})=>n("p",{class:"mt-1.5 max-w-[62ch] text-[15px] leading-6 text-muted",children:e}),Sn=({children:e})=>n("h2",{class:"mt-10 mb-2 eyebrow",children:e});function Tn({id:e,text:t,warn:o,compact:r}){let i=pe(e);return i?n("a",{href:`#${e}`,onClick:s=>{s.preventDefault(),R(e)},class:`focus-ring group flex items-center gap-3.5 border-t border-line py-2 last:border-b ${r?"min-h-12":"min-h-14"}`,children:[n(_,{name:K[e]||"box",class:"size-[18px] text-faint"}),n("span",{class:`min-w-0 flex-1 ${r?"flex flex-wrap items-baseline gap-x-4":""}`,children:[n("span",{class:`block text-sm text-ink group-hover:text-brand ${r?"min-w-40 font-medium":"font-semibold"}`,children:i.title}),n("span",{class:`block text-[13px] leading-5 ${o?"text-warn":"text-muted"}`,children:t??i.description})]}),n(_,{name:"chevronRight",class:"size-4 text-faint"})]}):null}function In(e){return`Not set. ${E(e)?.off_summary||""}`.trim()}function Eo(){let e=u.config.auth,t=e==="jwt"?"jwt":y(e)?e.method??"none":"none";if(t==="none")return{text:In("auth")};let o=y(e)&&y(e.roles)?Object.keys(e.roles).length:0;return{text:`${t==="jwt"?"JWT bearer tokens":`Custom backend${e.path?`: ${e.path}`:""}`}${o?`, ${o} ${o===1?"role":"roles"}`:""}.`}}function Ao(){let e=y(u.config.cache_store)?u.config.cache_store:{},t=[`${e.type||"memory"} store`],o=e.rate_limit;if(y(o)&&o.enabled!==!1){let i={ip:"per IP",user:"per user",global:"across all clients"}[o.by??"ip"]||o.by;t.push(`${o.requests??100} requests per ${o.window??60} s, ${i}`)}let r=e.token_revocation;return(r===!0||y(r)&&r.enabled!==!1)&&t.push("token revocation on"),{text:`${t.join("; ")}.`}}function jo(){let e=u.config.observability;if(e==null)return{text:In("observability")};let t=[];return de(["observability","logfire","enabled"])&&t.push("Logfire"),de(["observability","langsmith","enabled"])&&t.push("LangSmith"),{text:`Level ${de(["observability","level"])}${t.length?`, sending to ${t.join(" and ")}`:", no destination chosen yet"}.`}}function On(){let e=E("core"),t=on(),o=e?X("core").filter(s=>!e.fields.some(a=>a.path.join(".")===s.path)):[],r=[["auth",Eo()],["cache_store",Ao()],["observability",jo()]],i=u.nav.flatMap(s=>s.items).filter(s=>s!=="get-running");return n(w,{children:[n(_e,{children:t?"You're ready to run":"One step to run your agent"}),n(bt,{children:z["get-running"].description}),n("div",{class:`mt-4 flex items-center gap-2 font-medium ${t?"text-ok":"text-muted"}`,children:[n(_,{name:t?"checkCircle":"circle",class:"size-[18px]"}),"Required: ",t?"1":"0"," of 1 done"]}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((s,a)=>n(ft,{issue:s},a))}):null,n("div",{class:"mt-2",children:e?ht(e.fields).flatMap(s=>s.fields).map(s=>n(I,{field:s},s.path.join("."))):null}),t?n("div",{class:"mt-7 border-t border-line pt-5",children:[n("div",{class:"flex items-center gap-2.5 font-medium text-ok",children:[n(_,{name:"checkCircle",class:"size-[18px]"}),"Ready for 10xgraph dev"]}),n("div",{class:"mt-3 flex max-w-[420px] items-center gap-2 rounded-lg border border-line-strong bg-sunken py-0 pr-1 pl-3.5",children:[n("code",{class:"flex-1 font-mono text-[13px] font-medium text-ink",children:"10xgraph dev"}),u.copied?n("span",{class:"text-xs text-ok",children:"Copied"}):null,n("button",{type:"button",class:"icon-btn",onClick:un,"aria-label":u.copied?"Copied 10xgraph dev":"Copy command 10xgraph dev",children:n(_,{name:"copy",class:"size-[18px]"})})]}),n("p",{class:"mt-3 text-muted",children:"Everything else already has working defaults."})]}):n("p",{class:"mt-4 text-xs text-faint",children:"Add the agent graph above, then run 10xgraph dev."}),n(Sn,{children:"Before production (optional)"}),n("div",{children:r.map(([s,a])=>n(Tn,{id:s,text:a.text,warn:a.warn},s))}),n(Sn,{children:"What you can configure"}),n("div",{children:i.map(s=>n(Tn,{id:s,compact:!0},s))})]})}function We({vars:e}){return n("dl",{class:"mb-3 grid gap-x-4 gap-y-1 sm:grid-cols-[minmax(0,260px)_minmax(0,1fr)]",children:e.map(t=>[n("dt",{class:"py-0.5 font-mono text-xs font-medium break-all text-ink",children:t.name},`${t.name}-n`),n("dd",{class:"m-0 pb-2 text-[13px] text-muted sm:py-0.5",children:t.purpose},`${t.name}-p`)])})}function Po({vars:e,open:t}){return e?.length?n("details",{class:"group mt-9 border-t border-line pt-1",open:t,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform group-open:rotate-90"}),"Set in .env"]}),n("p",{class:"mb-2 text-xs text-faint",children:"Read only. The editor never reads .env, so only names are shown."}),n(We,{vars:e})]}):null}function En({sec:e}){return e.widget==="auth"?n(kn,{sec:e}):e.widget==="remote_tools"?n($n,{}):e.widget==="cache_store"?n(wn,{sec:e}):e.widget==="id_generator"||e.widget==="thread_name_generator"?n(Cn,{sec:e}):null}function Mo(e){let t=new Set([...e.fields.map(i=>i.path.join(".")),...Object.keys(ot)]),o=new Set(["cache_store.rate_limit","cache_store.token_revocation"]),r=i=>i.startsWith("auth.roles.")||i.startsWith("auth.system_identities.");return X(e.id).filter(i=>!t.has(i.path)&&!(e.id==="cache_store"&&o.has(i.path))&&!(e.id==="auth"&&r(i.path)))}function Ro({sec:e}){let t=Pe(e),o=e.widget&&(t||!e.optional);return n(w,{children:[o?n("div",{class:"mt-6",children:n(En,{sec:e})}):null,n("div",{class:e.optional&&!t?"opacity-80":"",children:ht(e.fields.filter(r=>!r.managed)).map((r,i)=>n("div",{children:[r.name?n("h3",{class:`eyebrow ${i===0?"mt-6":"mt-8 border-t border-line pt-4"}`,children:r.name}):null,r.fields.map(s=>n(I,{field:s},s.path.join(".")))]},r.name))})]})}function No({sec:e}){let t=Pe(e),o=Mo(e);return n(w,{children:[n(_e,{code:e.key,children:e.title}),n(bt,{children:e.description}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((r,i)=>n(ft,{issue:r},i))}):null,e.optional?n("div",{class:"mt-5 flex flex-wrap items-center gap-4",children:[n(Y,{label:`Settings for ${e.title}`,value:t?"c":"d",options:[["d","Use defaults"],["c","Customize"]],onPick:r=>cn(e,r==="c")}),n("p",{class:"m-0 min-w-56 flex-1 text-[13px] text-muted",children:t?`Saved in 10xgraph.json under ${e.key}.`:e.off_summary})]}):null,e.notes?.length?n("div",{class:"mt-5",children:n(me,{kind:"info",lines:e.notes})}):null,n(Ro,{sec:e}),n(Po,{vars:e.env})]})}function Do(){let e=u.schema.environment||[];return n(w,{children:[n(_e,{children:z.environment.title}),n(bt,{children:"These variables are set in .env or your deployment environment. The editor never reads that file, so only names and purposes are shown."}),e.map(t=>n("section",{children:[n("h2",{class:"mt-8 mb-2 text-sm font-semibold text-ink",children:t.title}),t.note?n("p",{class:"mb-2 text-xs text-faint",children:t.note}):null,n(We,{vars:t.vars})]},t.title))]})}var An=e=>String(e??"").toLowerCase();function Lo(e){let t=i=>e.every(s=>An(i).includes(s)),o=[];for(let i of u.sections){let s=t(`${i.title} ${i.description} ${i.key||""}`),a=p=>s||t(`${p.label} ${p.help||""} ${p.path.join(".")}`),d=i.fields.filter(p=>!p.managed&&a(p)),f=(i.env||[]).filter(p=>t(`${p.name} ${p.purpose}`)),l=!!i.widget&&(s||i.fields.some(p=>p.managed&&a(p)));(d.length||f.length||l)&&o.push({id:i.id,sec:i,fields:d,env:f,widget:l})}let r=(u.schema.environment||[]).map(i=>({title:i.title,vars:i.vars.filter(s=>t(`${s.name} ${s.purpose} ${i.title}`))})).filter(i=>i.vars.length);return{results:o,envGroups:r}}function zo(){let e=An(u.query).split(/\s+/).filter(Boolean),{results:t,envGroups:o}=Lo(e),r=t.reduce((i,s)=>i+s.fields.length+s.env.length+(s.widget?1:0),0)+o.reduce((i,s)=>i+s.vars.length,0);return r?n(w,{children:[n(_e,{code:u.query,children:[r," ",r===1?"setting matches":"settings match"]}),t.map(({id:i,sec:s,fields:a,env:d,widget:f})=>n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:K[i]||"box",class:"size-4"}),s.title,n("a",{href:`#${i}`,onClick:l=>{l.preventDefault(),R(i)},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),f?n("div",{class:"mt-4",children:n(En,{sec:s})}):null,a.map(l=>n(I,{field:l},l.path.join("."))),d.length?n("details",{class:"group mt-5",open:!0,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform group-open:rotate-90"}),"Set in .env"]}),n(We,{vars:d})]}):null]},i)),o.length?n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:"terminal",class:"size-4"}),z.environment.title,n("a",{href:"#environment",onClick:i=>{i.preventDefault(),R("environment")},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),o.map(i=>n("div",{children:[n("h3",{class:"mt-4 mb-1.5 text-sm font-semibold text-ink",children:i.title}),i.note?n("p",{class:"mb-1.5 text-xs text-faint",children:i.note}):null,n(We,{vars:i.vars})]},i.title))]}):null]}):n(w,{children:[n(_e,{children:"No matches"}),n("p",{class:"mt-8 text-muted",children:['Nothing matches "',u.query,'". Try a shorter word, or press Esc to go back.']})]})}function jn(){if(u.query.trim())return n(zo,{});if(u.page==="get-running")return n(On,{});if(u.page==="environment")return n(Do,{});let e=E(u.page);return e?n(No,{sec:e}):n(On,{})}function Ho(){let e=u.path.split(/[\\/]/).pop()||"10xgraph.json",t=u.path.slice(0,u.path.length-e.length),o=null;return u.schema?u.exists?H()?o=n(te,{class:"bg-warn-soft text-warn",children:"Unsaved"}):o=n(te,{class:"bg-ok-soft text-ok",children:"Saved"}):o=n(te,{class:"bg-brand-soft text-brand",children:"New"}):o=null,n("div",{class:"flex min-w-0 flex-1 items-center gap-2",title:u.path||void 0,children:[n(_,{name:"file",class:"size-4 text-faint"}),n("span",{class:"truncate font-mono text-[13px] font-medium text-ink",children:e}),o,t?n("span",{class:"hidden min-w-0 truncate font-mono text-xs text-faint lg:block",dir:"rtl",children:n("bdi",{children:t})}):null]})}function Pn(){let e=!!u.schema&&!u.fatal,t=u.sideOpen?"Hide sidebar":"Show sidebar";return n("header",{class:"sticky top-0 z-40 flex h-14 items-center gap-2 border-b border-line bg-surface px-3 sm:gap-3 sm:px-4",children:[n("button",{type:"button",class:"icon-btn","aria-controls":"side","aria-expanded":u.sideOpen?"true":"false","aria-label":t,title:`${t} (Ctrl/Cmd+B)`,onClick:()=>ee(!u.sideOpen),children:n(_,{name:"panel",class:"size-[18px]"})}),n("div",{class:"flex shrink-0 items-center gap-2.5",children:[n("svg",{viewBox:"0 0 48 48",class:"size-7 shrink-0 text-ink","aria-hidden":"true",children:[n("rect",{x:"4",y:"4",width:"40",height:"40",rx:"11",fill:"none",stroke:"currentColor","stroke-width":"4.4"}),n("path",{d:"M13 33L21 23L34 15L34 31L21 23",fill:"none",stroke:"currentColor","stroke-width":"3","stroke-linecap":"round","stroke-linejoin":"round"}),n("g",{fill:"currentColor",children:[n("circle",{cx:"21",cy:"23",r:"4"}),n("circle",{cx:"34",cy:"15",r:"4"}),n("circle",{cx:"34",cy:"31",r:"4"})]}),n("circle",{cx:"13",cy:"33",r:"4.6",class:"fill-brand-amber"})]}),n("div",{class:"hidden items-baseline gap-1.5 sm:flex",children:[n("span",{class:"text-[15px] font-semibold tracking-tight text-ink",children:"10xGraph"}),n("span",{class:"text-[15px] text-faint",children:"Config"})]})]}),n("span",{class:"hidden h-6 w-px shrink-0 bg-line md:block","aria-hidden":"true"}),n(Ho,{}),n("button",{type:"button",class:"btn btn-ghost","aria-label":"View 10xgraph.json","aria-haspopup":"dialog",disabled:!e,onClick:()=>dn(),children:[n(_,{name:"code",class:"size-[18px]"}),n("span",{class:"hidden md:inline",children:"View 10xgraph.json"})]}),n("button",{type:"button",class:"btn btn-primary",onClick:Re,disabled:!e||!!u.busy||u.exists&&!H()&&!u.parseError,children:[u.busy==="save"?n(_,{name:"spinner",class:"size-4 animate-spin"}):null,u.exists?"Save":"Create file",n("span",{class:"kbd hidden lg:inline-flex",children:"Ctrl S"})]})]})}function Mn(){return u.fatal?n("div",{class:"panel mt-10",children:n(mn,{icon:"xCircle",tint:"text-danger",title:u.fatal.title,text:u.fatal.text})}):u.schema?n(w,{children:[u.parseError?n("div",{class:"mb-6",children:n(me,{kind:"warn",title:"The existing file is not valid JSON",lines:[`${u.parseError}. The form starts from defaults; saving replaces the file and keeps the old one as 10xgraph.json.bak.`]})}):null,n(jn,{})]}):n("div",{class:"animate-pulse space-y-4 pt-2","aria-label":"Loading",children:[n("div",{class:"h-7 w-48 rounded-md bg-raised"}),n("div",{class:"h-4 w-80 max-w-full rounded-md bg-raised"}),[0,1,2].map(e=>n("div",{class:"h-16 rounded-xl bg-raised"},e))]})}function Fo({id:e}){let t=!!E(e)&&Xt(E(e)),o=en(e);return!t&&!o?null:n("span",{class:"flex shrink-0 gap-1",children:[t?n("span",{class:"size-2 rounded-full bg-brand",title:"Changed from default"}):null,o?n("span",{class:`size-2 rounded-full ${o.level==="error"?"bg-danger":"bg-warn"}`,title:o.level==="error"?"Needs attention":"Has a warning"}):null]})}function qo({id:e}){let t=pe(e);if(!t)return null;let o=!u.query&&u.page===e;return n("a",{href:`#${e}`,"aria-current":o?"page":void 0,onClick:r=>{r.preventDefault(),R(e)},ref:r=>{r&&o&&window.innerWidth<1024&&r.scrollIntoView({block:"nearest",inline:"nearest"})},class:`focus-ring flex min-h-10 items-center gap-2.5 rounded-lg px-3 py-1.5 font-medium transition-colors duration-150 max-lg:min-h-11 max-lg:shrink-0 max-lg:whitespace-nowrap ${o?"bg-brand-soft text-ink":"text-muted hover:bg-raised hover:text-ink"}`,children:[n(_,{name:K[e]||"box",class:`size-[18px] ${o?"text-brand":""}`}),n("span",{class:"min-w-0 flex-1",children:[n("span",{class:"block truncate",children:t.title}),t.subtitle?n("span",{class:"block truncate text-xs font-normal text-faint max-lg:hidden",children:t.subtitle}):null]}),n(Fo,{id:e})]})}function Rn(){return u.sideRender?n("aside",{id:"side","aria-label":"Settings navigation",class:`contents lg:sticky lg:top-14 lg:block lg:h-[calc(100dvh-3.5rem)] lg:overflow-y-auto lg:border-r lg:border-line lg:bg-surface lg:px-3 lg:pt-4 lg:pb-16 lg:transition-[transform,opacity] lg:duration-200 lg:ease-out ${u.sideShown?"lg:translate-x-0 lg:opacity-100":"lg:-translate-x-6 lg:opacity-0"}`,inert:!u.sideOpen,children:[n("div",{role:"search",class:"relative mx-1 mb-3 max-lg:mx-4 max-lg:mt-3",children:[n(_,{name:"search",class:"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-faint"}),n("label",{for:"q",class:"sr-only",children:"Search settings"}),n("input",{id:"q",type:"search",value:u.query,autocomplete:"off",spellcheck:!1,placeholder:"Search settings ( / )",class:"input pl-10",onInput:e=>De(e.currentTarget.value)})]}),n("nav",{"aria-label":"Pages",class:"max-lg:sticky max-lg:top-14 max-lg:z-20 max-lg:mt-3 max-lg:flex max-lg:gap-0.5 max-lg:overflow-x-auto max-lg:border-b max-lg:border-line max-lg:bg-surface max-lg:px-3 max-lg:py-1.5",children:u.nav.map(e=>n("div",{class:"max-lg:contents",children:[e.label?n("div",{class:"mt-4 mb-1 px-3 text-[11px] font-semibold tracking-wider text-faint uppercase max-lg:hidden",children:e.label}):null,e.items.map(t=>n(qo,{id:t},t))]},e.label||"top"))})]}):null}var Uo={success:["checkCircle","text-ok"],warning:["alert","text-warn"],error:["xCircle","text-danger"]};function Wo(){return n("div",{class:"pointer-events-none fixed right-4 bottom-4 left-4 z-50 flex flex-col items-end gap-2 sm:left-auto","aria-live":"polite",children:u.toasts.map(e=>{let[t,o]=Uo[e.kind];return n("div",{role:e.kind==="error"?"alert":"status",class:"pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-xl border border-line bg-surface px-4 py-3 text-sm shadow-pop",children:[n(_,{name:t,class:`mt-0.5 size-4 ${o}`}),n("p",{class:"min-w-0 flex-1 leading-5 text-body",children:e.message}),e.action?n("button",{type:"button",class:"shrink-0 font-medium text-brand hover:underline",onClick:()=>{ze(e.id),e.action.run()},children:e.action.label}):null,n("button",{type:"button",class:"-mr-1 shrink-0 cursor-pointer rounded text-faint hover:text-ink","aria-label":"Dismiss",onClick:()=>ze(e.id),children:n(_,{name:"x"})})]},e.id)})})}var Go=()=>/^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName)||document.activeElement?.isContentEditable;function Vo(){let[,e]=nt(o=>o+1,0);L(()=>{Qt(e);try{localStorage.getItem("tg-sidebar")==="hidden"&&ee(!1,{instant:!0})}catch{}let o=s=>{let a=s.ctrlKey||s.metaKey,d=s.key.toLowerCase();a&&d==="s"?(s.preventDefault(),Re()):a&&d==="b"?(s.preventDefault(),ee(!u.sideOpen)):a&&d==="k"?(s.preventDefault(),ct()):s.key==="/"&&!a&&!s.altKey&&!Go()&&!u.drawerOpen?(s.preventDefault(),ct()):s.key==="Escape"&&u.query&&!u.drawerOpen&&De("")},r=s=>{u.schema&&H()&&s.preventDefault()},i=()=>Ne();return document.addEventListener("keydown",o),window.addEventListener("beforeunload",r),window.addEventListener("hashchange",i),an(),()=>{document.removeEventListener("keydown",o),window.removeEventListener("beforeunload",r),window.removeEventListener("hashchange",i)}},[]),L(()=>{if(!u.schema||u.validated===M())return;let o=setTimeout(ln,400);return()=>clearTimeout(o)});let t=u.sideRender;return n(w,{children:[n("a",{href:"#main",class:"sr-only z-50 rounded-md bg-brand-solid px-3 py-2 text-white focus:not-sr-only focus:fixed focus:top-3 focus:left-3",children:"Skip to content"}),n(Pn,{}),n("div",{class:t?"lg:grid lg:grid-cols-[260px_minmax(0,1fr)] lg:items-start":"",children:[n(Rn,{}),n("main",{id:"main",tabindex:-1,class:"min-w-0 px-4 pt-6 pb-24 focus:outline-none sm:px-8 sm:pt-8",children:n("div",{class:"mx-auto w-full max-w-[760px]",children:n(Mn,{})})})]}),n(hn,{}),n("div",{class:"sr-only",role:"status","aria-live":"polite",children:u.copied?"Copied":""}),n(Wo,{})]})}Rt(n(Vo,{}),document.getElementById("app")); +var $e,b,Ct,Nn,U,vt,$t,St,Ve,xe,ae,Tt,Ke,Be,Je,Dn,ke={},we=[],Ln=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Se=Array.isArray;function D(e,t){for(var o in t)e[o]=t[o];return e}function Ye(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function zn(e,t,o){var r,i,s,a={};for(s in t)s=="key"?r=t[s]:s=="ref"?i=t[s]:a[s]=t[s];if(arguments.length>2&&(a.children=arguments.length>3?$e.call(arguments,2):o),typeof e=="function"&&e.defaultProps!=null)for(s in e.defaultProps)a[s]===void 0&&(a[s]=e.defaultProps[s]);return ve(e,a,r,i,null)}function ve(e,t,o,r,i){var s={type:e,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i??++Ct,__i:-1,__u:0};return i==null&&b.vnode!=null&&b.vnode(s),s}function w(e){return e.children}function ye(e,t){this.props=e,this.context=t}function J(e,t){if(t==null)return e.__?J(e.__,e.__i+1):null;for(var o;tt&&U.sort(St),e=U.shift(),t=U.length,Hn(e)}finally{U.length=Ce.__r=0}}function It(e,t,o,r,i,s,a,d,f,l,p){var h,c,m,x,$,k,T=r&&r.__k||we,v=t.length;for(f=Fn(o,t,T,f,v),h=0;h0?a=e.__k[s]=ve(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[s]=a,f=s+c,a.__=e,a.__b=e.__b+1,d=null,(l=a.__i=qn(a,o,f,h))!=-1&&(h--,(d=o[l])&&(d.__u|=2)),d==null||d.__v==null?(l==-1&&(i>p?c--:if?c--:c++,a.__u|=4))):e.__k[s]=null;if(h)for(s=0;s(p?1:0)){for(i=o-1,s=o+1;i>=0||s=0?i--:s++])!=null&&(2&l.__u)==0&&d==l.key&&f==l.type)return a}return-1}function kt(e,t,o){t[0]=="-"?e.setProperty(t,o??""):e[t]=o==null?"":typeof o!="number"||Ln.test(t)?o:o+"px"}function be(e,t,o,r,i){var s,a;e:if(t=="style")if(typeof o=="string")e.style.cssText=o;else{if(typeof r=="string"&&(e.style.cssText=r=""),r)for(t in r)o&&t in o||kt(e.style,t,"");if(o)for(t in o)r&&o[t]==r[t]||kt(e.style,t,o[t])}else if(t[0]=="o"&&t[1]=="n")s=t!=(t=t.replace(Tt,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+s]=o,o?r?o[ae]=r[ae]:(o[ae]=Ke,e.addEventListener(t,s?Je:Be,s)):e.removeEventListener(t,s?Je:Be,s);else{if(i=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=o??"";break e}catch{}typeof o=="function"||(o==null||o===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&o==1?"":o))}}function wt(e){return function(t){if(this.l){var o=this.l[t.type+e];if(t[xe]==null)t[xe]=Ke++;else if(t[xe]0?e:Se(e)?e.map(jt):e.constructor!==void 0?null:D({},e)}function Un(e,t,o,r,i,s,a,d,f){var l,p,h,c,m,x,$,k=o.props||ke,T=t.props,v=t.type;if(v=="svg"?i="http://www.w3.org/2000/svg":v=="math"?i="http://www.w3.org/1998/Math/MathML":i||(i="http://www.w3.org/1999/xhtml"),s!=null){for(l=0;l=o.__.length&&o.__.push({}),o.__[e]}function Z(e){return Oe=1,nt(Vt,e)}function nt(e,t,o){var r=tt(le++,2);if(r.t=e,!r.__c&&(r.__=[o?o(t):Vt(void 0,t),function(d){var f=r.__N?r.__N[0]:r.__[0],l=r.t(f,d);f!==l&&(r.__N=[l,r.__[1]],r.__c.setState({}))}],r.__c=C,!C.__f)){var i=function(d,f,l){if(!r.__c.__H)return!0;var p=!1,h=r.__c.props!==d;if(r.__c.__H.__.some(function(m){if(m.__N){p=!0;var x=m.__[0];m.__=m.__N,m.__N=void 0,x!==m.__[0]&&(h=!0)}}),s){var c=s.call(this,d,f,l);return p?c||h:c}return!p||h};C.__f=!0;var s=C.shouldComponentUpdate,a=C.componentWillUpdate;C.componentWillUpdate=function(d,f,l){if(this.__e){var p=s;s=void 0,i(d,f,l),s=p}a&&a.call(this,d,f,l)},C.shouldComponentUpdate=i}return r.__N||r.__}function L(e,t){var o=tt(le++,3);!S.__s&&Gt(o.__H,t)&&(o.__=e,o.u=t,C.__H.__h.push(o))}function Ie(e){return Oe=5,Gn(function(){return{current:e}},[])}function Gn(e,t){var o=tt(le++,7);return Gt(o.__H,t)&&(o.__=e(),o.__H=t,o.__h=e),o.__}function Vn(){for(var e;e=Wt.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Te),t.__h.some(et),t.__h=[]}catch(o){t.__h=[],S.__e(o,e.__v)}}}S.__b=function(e){C=null,Dt&&Dt(e)},S.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),qt&&qt(e,t)},S.__r=function(e){Lt&&Lt(e),le=0;var t=(C=e.__c).__H;t&&(Xe===C?(t.__h=[],C.__h=[],t.__.some(function(o){o.__N&&(o.__=o.__N),o.u=o.__N=void 0})):(t.__h.some(Te),t.__h.some(et),t.__h=[],le=0)),Xe=C},S.diffed=function(e){zt&&zt(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(Wt.push(t)!==1&&Nt===S.requestAnimationFrame||((Nt=S.requestAnimationFrame)||Bn)(Vn)),t.__H.__.some(function(o){o.u&&(o.__H=o.u,o.u=void 0)})),Xe=C=null},S.__c=function(e,t){t.some(function(o){try{o.__h.some(Te),o.__h=o.__h.filter(function(r){return!r.__||et(r)})}catch(r){t.some(function(i){i.__h&&(i.__h=[])}),t=[],S.__e(r,o.__v)}}),Ht&&Ht(e,t)},S.unmount=function(e){Ft&&Ft(e);var t,o=e.__c;o&&o.__H&&(o.__H.__.some(function(r){try{Te(r)}catch(i){t=i}}),o.__H=void 0,t&&S.__e(t,o.__v))};var Ut=typeof requestAnimationFrame=="function";function Bn(e){var t,o=function(){clearTimeout(r),Ut&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(o,35);Ut&&(t=requestAnimationFrame(o))}function Te(e){var t=C,o=e.__c;typeof o=="function"&&(e.__c=void 0,o()),C=t}function et(e){var t=C;e.__c=e.__(),C=t}function Gt(e,t){return!e||e.length!==t.length||t.some(function(o,r){return o!==e[r]})}function Vt(e,t){return typeof t=="function"?t(e):t}var Ee=[{label:null,items:["get-running"]},{label:"Before production",items:["auth","cache_store","observability"]},{label:"Runs and streaming",items:["runs","websocket","ag_ui","remote_tools","media"]},{label:"Project wiring",items:["checkpointer","id_generator","thread_name_generator","store","injectq"]},{label:"Developer tools",items:["test","evaluation"]},{label:"Reference",items:["environment"]}],z={"get-running":{title:"Get running",icon:"flag",description:"Only the agent graph is required. Everything else is optional."},environment:{title:"Server environment (.env)",icon:"terminal",description:"Variables the server reads from .env. The editor never reads that file, so values are not shown."}},ot={"auth.path":"auth-path"},Bt="tg-config-token",Jt=!1;function Jn(){let e=new URLSearchParams(location.hash.slice(1)).get("token")||"";try{e&&sessionStorage.setItem(Bt,e);let t=sessionStorage.getItem(Bt)||"";return Jt=t!==""&&t===(e||t),e||t}catch{return e}}var ce=Jn(),rt=e=>Jt||!ce?`#${e}`:`#token=${encodeURIComponent(ce)}&page=${e}`;function Kt(){let e=location.hash.slice(1);if(e.includes("=")){let t=new URLSearchParams(e);return t.get("page")||t.get("section")||""}try{return decodeURIComponent(e)}catch{return e}}var u={schema:null,sections:[],nav:[],fields:new Map,config:{},version:null,exists:!1,path:"",parseError:null,saved:"",validated:null,issues:[],page:"get-running",query:"",sideOpen:!0,sideRender:!0,sideShown:!0,drawerOpen:!1,tab:"json",copied:!1,busy:null,toolParams:{},toasts:[],fatal:null},Yt=()=>{},Qt=e=>{Yt=e};function g(e){e&&e(u),Yt()}var Zt=e=>JSON.parse(JSON.stringify(e)),M=()=>JSON.stringify(u.config),y=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Ae=(e,t)=>t.reduce((o,r)=>y(o)?o[r]:void 0,e),de=(e,t)=>`${e} ${t}${e===1?"":"s"}`;function Pe(e,t,o){let r=e;for(let i of t.slice(0,-1))y(r[i])||(r[i]={}),r=r[i];r[t[t.length-1]]=o}var Kn=new Set(["cache_store.rate_limit","cache_store.token_revocation"]);function st(e,t){let o=[],r=e;for(let i of t.slice(0,-1)){if(!y(r))return;o.push([r,i]),r=r[i]}y(r)&&delete r[t[t.length-1]];for(let i=o.length-1;i>=1;i--){let[s,a]=o[i];if(Kn.has(t.slice(0,i+1).join(".")))break;if(y(s[a])&&Object.keys(s[a]).length===0)delete s[a];else break}}function X(e){let t=Ae(u.config,e);return t!==void 0?t:u.fields.get(e.join("."))?.default}var Yn=(e,t)=>JSON.stringify(e)===JSON.stringify(t),E=e=>u.sections.find(t=>t.id===e),H=()=>M()!==u.saved,je=e=>!e.optional||e.key in u.config&&u.config[e.key]!==null,ee=e=>u.issues.filter(t=>t.section===e),O=e=>u.issues.find(t=>t.path===e);function ue(e=u.issues){let t=e.filter(o=>o.level==="error").length;return{errors:t,warnings:e.length-t}}var Me=e=>ot[e]||`f-${e.replaceAll(".","-")}`,it=e=>{let t=Ae(u.config,e.path);return t!==void 0&&!Yn(t,e.default)},Xt=e=>e.id==="core"?!1:e.id==="auth"?u.config.auth!=null:e.optional?je(e):e.fields.some(it);function en(e){let t=ee(e);return t.find(o=>o.level==="error")||t[0]||null}var tn=e=>!!e.show_if&&X(e.show_if.path)!==e.show_if.equals;function nn(e){let o=u.fields.get(e.show_if.path.join("."))?.label||e.show_if.path.join("."),r=e.show_if.equals;return`Used when ${o} is ${r===!0?"on":r===!1?"off":String(r)}.`}var pe=e=>{let t=E(e);return z[e]?{id:e,...z[e]}:t?{id:e,title:t.title,subtitle:t.subtitle,description:t.description,icon:null}:null},on=()=>/^[\w.]+:[\w.]+$/.test(String(u.config.agent||""));async function lt(e,t,o){let r={"X-10xGraph-Token":ce};o!==void 0&&(r["Content-Type"]="application/json");let i=await fetch(t,{method:e,headers:r,body:o===void 0?void 0:JSON.stringify(o)}),s={};try{s=await i.json()}catch{}return{ok:i.ok,status:i.status,data:s}}var at=(e,t)=>g(o=>{o.fatal={title:e,text:t}});async function an(){if(!ce)return at("Open the link printed by 10xgraph config","This page needs the session token included in that link.");let e;try{e=await lt("GET","/api/state")}catch{return at("Cannot reach the config server","Check that 10xgraph config is still running in your terminal, then reload.")}if(!e.ok)return at("This link has expired",e.data.error||"Restart 10xgraph config and open the new link it prints.");let t=e.data;g(o=>{o.schema=t.schema,o.sections=t.schema.sections;let r=new Set(Ee.flatMap(a=>a.items)),i=o.sections.map(a=>a.id).filter(a=>a!=="core"&&!r.has(a));o.nav=Ee.map((a,d)=>({label:a.label,items:dz[f]||E(f)):[...i,...a.items]}));for(let a of o.sections)for(let d of a.fields)o.fields.set(d.path.join("."),d);o.config=t.config,o.version=t.version,o.exists=t.exists,o.path=t.path,o.parseError=t.parse_error,o.issues=t.issues;let s=t.exists&&!t.parse_error;o.saved=s?M():"",o.validated=s?M():null}),Ne({focus:!1}),history.replaceState(null,"",rt(u.page))}var rn=null;async function ln(){let e=M();if(!(!u.schema||u.validated===e||rn===e)&&!Object.values(u.toolParams).some(t=>t.error)){rn=e;try{let t=await lt("POST","/api/validate",{config:JSON.parse(e)});if(!t.ok)return;g(o=>{o.issues=t.data.issues,o.validated=e})}catch{}}}async function Re(){if(!(u.busy||!u.schema)){if(u.exists&&!H()&&!u.parseError)return F("success","No changes to save.");if(Qn()){g(e=>{e.busy="save"});try{let e=await lt("POST","/api/save",{config:u.config,version:u.version});if(e.data.issues&&(u.issues=e.data.issues,u.validated=M()),e.ok){u.version=e.data.version,u.config=e.data.config,u.toolParams={},u.validated=M(),u.saved=M(),u.exists=!0,u.parseError=null;let t=e.data.path.split(/[\\/]/).pop();F("success",e.data.backup?`Saved ${t}. The previous version is in ${t}.bak.`:`Created ${t}.`)}else if(e.status===409)F("error",e.data.error,{label:"Reload",run:()=>location.reload()});else if(e.status===422){let{errors:t}=ue();F("error",`Not saved: ${de(t,"error")} to fix first.`,{label:"Show",run:Zn})}else F("error",e.data.error||"Save failed.")}catch{F("error","Cannot reach the config server.")}finally{g(e=>{e.busy=null})}}}}function Qn(){let e=Object.values(u.toolParams).some(t=>t.error);return e&&(R("remote_tools"),F("error","A remote tool has invalid parameters JSON.")),!e}function cn(e,t){g(o=>{t?o.config[e.key]=Zt(e.default):delete o.config[e.key],e.key==="remote_tools"&&(o.toolParams={})})}function Zn(){let e=u.issues.find(t=>t.level==="error")||u.issues[0];e&&R(e.section,Me(e.path))}var Xn=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches?"auto":"smooth";function eo(e){return e==="core"?"get-running":z[e]||E(e)?e:"get-running"}function Ne({focus:e=!0}={}){g(t=>{t.page=eo(Kt()),t.query="",t.drawerOpen=!1}),e&&requestAnimationFrame(()=>{window.scrollTo(0,0),document.getElementById("page-title")?.focus({preventScroll:!0})})}function R(e,t){let o=rt(e);location.hash!==o&&history.pushState(null,"",o),Ne(),t&&requestAnimationFrame(()=>requestAnimationFrame(()=>{let r=document.getElementById(t);r&&(r.scrollIntoView({block:"center",behavior:Xn()}),r.focus({preventScroll:!0}))}))}function De(e){g(t=>{t.query=e})}var to=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches,sn;function te(e,{instant:t=!1}={}){try{localStorage.setItem("tg-sidebar",e?"shown":"hidden")}catch{}clearTimeout(sn);let o=t||to();g(r=>{r.sideOpen=e,e?(r.sideRender=!0,r.sideShown=o):r.sideShown=!1,!e&&o&&(r.sideRender=!1)}),e&&!o&&requestAnimationFrame(()=>requestAnimationFrame(()=>g(r=>{r.sideShown=!0}))),!e&&!o&&(sn=setTimeout(()=>g(r=>{r.sideOpen||(r.sideRender=!1)}),200))}function ct(){u.sideOpen||te(!0,{instant:!0}),requestAnimationFrame(()=>{let e=document.getElementById("q");e?.focus(),e?.select()})}function dn(e){g(t=>{e&&(t.tab=e),t.drawerOpen=!0})}var Le=()=>g(e=>{e.drawerOpen=!1});async function un(){let e="10xgraph dev";try{await navigator.clipboard.writeText(e)}catch{try{let t=document.createElement("textarea");t.value=e,document.body.append(t),t.select(),document.execCommand("copy"),t.remove()}catch{}}g(t=>{t.copied=!0}),setTimeout(()=>g(t=>{t.copied=!1}),2e3)}async function pn(){try{await navigator.clipboard.writeText(JSON.stringify(u.config,null,2)+` +`),F("success","Copied 10xgraph.json to the clipboard.")}catch{F("error","The clipboard is not available here.")}}var no=0;function F(e,t,o){let r=++no;g(i=>{i.toasts=[...i.toasts,{id:r,kind:e,message:t,action:o}]}),setTimeout(()=>ze(r),o?8e3:4e3)}function ze(e){g(t=>{t.toasts=t.toasts.filter(o=>o.id!==e)})}var fn={box:'',key:'',shield:'',gauge:'',arrows:'',activity:'',layers:'',wrench:'',flask:'',target:'',checkCircle:'',xCircle:'',alert:'',info:'',file:'',copy:'',x:'',plus:'',trash:'',panel:'',chevronDown:'',expand:'',collapse:'',power:'',spinner:'',book:'',flag:'',lock:'',play:'',bolt:'',terminal:'',search:'',chevronRight:'',code:'',circle:'',external:''},K={"get-running":"flag",environment:"terminal",core:"flag",auth:"lock",cache_store:"gauge",observability:"activity",runs:"play",websocket:"bolt",ag_ui:"external",remote_tools:"wrench",media:"file",checkpointer:"layers",id_generator:"key",thread_name_generator:"file",store:"box",injectq:"arrows",test:"flask",evaluation:"target"};var oo=0;function n(e,t,o,r,i,s){t||(t={});var a,d,f=t;if("ref"in f)for(d in f={},t)d=="ref"?a=t[d]:f[d]=t[d];var l={type:e,props:f,key:o,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--oo,__i:-1,__u:0,__source:i,__self:s};if(typeof e=="function"&&(a=e.defaultProps))for(d in a)f[d]===void 0&&(f[d]=a[d]);return b.vnode&&b.vnode(l),l}function _({name:e,class:t="size-4"}){return/(^|\s)size-/.test(t)||(t=`size-4 ${t}`),n("span",{class:`inline-flex shrink-0 ${t}`,"aria-hidden":"true",children:n("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round",class:"size-full",dangerouslySetInnerHTML:{__html:fn[e]}})})}function ne({class:e,children:t}){return n("span",{class:`inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-semibold ${e}`,children:t})}function dt({id:e,on:t,label:o,onChange:r}){return n("button",{id:e,type:"button",role:"switch","aria-checked":t?"true":"false","aria-label":o,class:`focus-ring relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200 before:absolute before:-inset-2.5 before:content-[''] ${t?"bg-brand-solid":"bg-line-strong"}`,onClick:()=>r(!t),children:n("span",{class:`pointer-events-none inline-block size-5 rounded-full bg-white shadow-sm ring-1 ring-black/5 transition-transform duration-200 ease-out ${t?"translate-x-[18px]":"translate-x-0.5"}`})})}function Y({id:e,label:t,options:o,value:r,onPick:i}){return n("div",{id:e,role:"radiogroup","aria-label":t,class:"inline-flex max-w-full flex-wrap gap-0.5 rounded-[10px] border border-line-strong bg-raised p-0.5",children:o.map(([s,a])=>n("button",{type:"button",role:"radio","aria-checked":s===r?"true":"false",class:`focus-ring min-h-10 cursor-pointer rounded-lg px-3.5 text-sm font-medium transition-colors duration-150 ${s===r?"bg-surface text-ink shadow-card":"text-muted hover:text-ink"}`,onClick:()=>{s!==r&&i(s)},children:a},s))})}function oe({id:e,options:t,value:o,invalid:r,onChange:i}){return n("div",{class:"relative",children:[n("select",{id:e,value:o,class:`input cursor-pointer appearance-none pr-9 font-mono ${r?"input-invalid":""}`,onChange:s=>i(s.currentTarget.value),children:t.map(([s,a])=>n("option",{value:s,children:a},s))}),n(_,{name:"chevronDown",class:"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-faint"})]})}function W({id:e,values:t,placeholder:o,suggestions:r,onChange:i}){let s=Ie(null),a=f=>{let l=f.trim();s.current&&(s.current.value=""),l&&!t.includes(l)&&i([...t,l])},d=r?`${e}-options`:void 0;return n("div",{class:"flex min-h-11 cursor-text flex-wrap items-center gap-1.5 rounded-lg border border-line-strong bg-sunken px-1.5 py-1.5 transition-[border-color,box-shadow] duration-150 focus-within:border-brand focus-within:ring-4 focus-within:ring-brand-ring sm:py-1",onClick:f=>{f.target===f.currentTarget&&s.current?.focus()},children:[t.map((f,l)=>n("span",{class:"inline-flex h-7 items-center gap-0.5 rounded-md border border-line bg-raised pr-0.5 pl-2 font-mono text-xs text-body sm:h-6",children:[f,n("button",{type:"button","aria-label":`Remove ${f}`,class:"inline-flex size-6 cursor-pointer items-center justify-center rounded text-faint transition-colors hover:bg-hover hover:text-ink sm:size-5",onClick:()=>i(t.filter((p,h)=>h!==l)),children:n(_,{name:"x",class:"size-3"})})]},f)),n("input",{ref:s,id:e,type:"text",list:d,placeholder:t.length?"Add...":o,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:"h-7 min-w-[7rem] flex-1 bg-transparent px-1 font-mono text-base text-ink outline-none placeholder:text-faint sm:text-sm",onKeyDown:f=>{let l=f.currentTarget;(f.key==="Enter"||f.key===",")&&l.value.trim()?(f.preventDefault(),a(l.value)):f.key==="Backspace"&&!l.value&&t.length&&i(t.slice(0,-1))},onChange:f=>{r?.includes(f.currentTarget.value.trim())&&a(f.currentTarget.value)},onBlur:f=>a(f.currentTarget.value)}),r?n("datalist",{id:d,children:r.filter(f=>!t.includes(f)).map(f=>n("option",{value:f},f))}):null]})}function fe({label:e,path:t,help:o,id:r,required:i}){return n("div",{class:"min-w-0",children:[n("label",{for:r,class:"block text-sm font-medium text-ink",children:[e,i?n("span",{class:"text-danger",children:" *"}):null]}),n("div",{class:"mt-0.5 font-mono text-xs text-faint",children:t}),o?n("p",{class:"mt-1.5 text-[13px] leading-5 text-muted",children:o}):null]})}function G({issue:e}){if(!e)return null;let t=e.level==="error";return n("p",{class:`mt-2 flex gap-1.5 text-[13px] leading-5 ${t?"text-danger":"text-warn"}`,role:t?"alert":void 0,children:[n(_,{name:t?"xCircle":"alert",class:"mt-0.5 size-3.5"}),n("span",{children:e.message})]})}function ut({children:e}){return n("div",{class:"grid gap-3 px-5 py-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] sm:gap-8",children:e})}function V({id:e,label:t,path:o,value:r,placeholder:i,onInput:s,issue:a,help:d}){return n(ut,{children:[n(fe,{label:t,path:o,help:d,id:e}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n("input",{id:e,type:"text",value:r??"",placeholder:i,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${a?.level==="error"?"input-invalid":""}`,onInput:f=>s(f.currentTarget.value)}),n(G,{issue:a})]})]})}function pt({name:e,label:t,options:o,value:r,onPick:i}){return n("div",{role:"radiogroup","aria-label":t,class:"grid gap-3 sm:grid-cols-2",children:o.map(([s,a,d])=>{let f=s===r;return n("button",{type:"button",role:"radio","aria-checked":f?"true":"false",id:`${e}-${s}`,class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 ${f?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{f||i(s)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${f?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:f?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:a}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:d})]})]},s)})})}function A({title:e,children:t}){return n("section",{children:[e?n("h4",{class:"eyebrow mb-2.5",children:e}):null,t]})}var ro={info:["info","border-brand/20 bg-brand-soft","text-brand"],warn:["alert","border-warn/25 bg-warn-soft","text-warn"],error:["xCircle","border-danger/25 bg-danger-soft","text-danger"]};function me({kind:e,title:t,lines:o}){let[r,i,s]=ro[e];return n("div",{class:`flex gap-3 rounded-xl border px-4 py-3 ${i}`,role:e==="error"?"alert":void 0,children:[n(_,{name:r,class:`mt-0.5 size-4 ${s}`}),n("div",{class:"min-w-0 space-y-1 text-[13px] leading-5 text-body",children:[t?n("p",{class:"font-medium text-ink",children:t}):null,o.map((a,d)=>n("p",{children:a},d))]})]})}function ft({issue:e}){return n(me,{kind:e.level==="error"?"error":"warn",lines:[n("span",{children:[e.path?n("code",{class:"mr-1.5 font-mono text-xs text-muted",children:e.path}):null,e.message]})]})}function mn({icon:e,tint:t,title:o,text:r,children:i}){return n("div",{class:"px-6 py-14 text-center",children:[n("div",{class:`mx-auto flex size-11 items-center justify-center rounded-full bg-raised ${t}`,children:n(_,{name:e,class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:o}),n("p",{class:"mx-auto mt-1 max-w-xs text-sm leading-6 text-muted",children:r}),i]})}function hn(){let e=Ie(null);L(()=>{let a=e.current;a&&(u.drawerOpen&&!a.open?a.showModal():!u.drawerOpen&&a.open&&a.close())});let t=u.issues.length>0,o=t&&u.tab==="problems"?"problems":"json",{errors:r,warnings:i}=ue(),s=(a,d,f)=>n("button",{type:"button",role:"tab",id:`tab-${a}`,"aria-selected":o===a?"true":"false",class:`focus-ring inline-flex h-10 cursor-pointer items-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors duration-150 ${o===a?"bg-raised text-ink":"text-muted hover:text-ink"}`,onClick:()=>g(l=>{l.tab=a}),children:[d,f]});return n("dialog",{ref:e,"aria-label":"10xgraph.json preview",class:"m-0 ml-auto h-dvh max-h-none w-[min(540px,100vw)] border-0 border-l border-line-strong bg-surface p-0 text-body shadow-pop backdrop:bg-black/45",onClose:()=>{u.drawerOpen&&Le()},onClick:a=>{a.target===a.currentTarget&&Le()},children:n("div",{class:"flex h-full flex-col",children:[n("div",{class:"flex h-14 shrink-0 items-center gap-1 border-b border-line px-3",children:[n("div",{role:"tablist",class:"flex flex-1 gap-1",children:[s("json","10xgraph.json",null),t?s("problems","Problems",n("span",{class:`inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[11px] font-semibold ${r?"bg-danger-soft text-danger":"bg-warn-soft text-warn"}`,children:r+i})):null]}),n("button",{type:"button",class:"icon-btn","aria-label":"Close preview",onClick:Le,children:n(_,{name:"x",class:"size-5"})})]}),n("div",{class:"min-h-0 flex-1 overflow-y-auto overscroll-contain",children:o==="problems"?n(so,{}):n(ao,{})})]})})}function so(){let{errors:e,warnings:t}=ue(),o=u.sections.map(r=>[r,ee(r.id)]).filter(([,r])=>r.length);return n("div",{children:[n("div",{class:"border-b border-line px-4 py-3",children:n("p",{class:"text-sm text-muted",children:[n("span",{class:e?"font-medium text-danger":"",children:de(e,"error")})," \xB7 ",n("span",{class:t?"font-medium text-warn":"",children:de(t,"warning")})]})}),n("div",{class:"space-y-5 px-3 py-4",children:o.map(([r,i])=>n("div",{children:[n("p",{class:"eyebrow mb-1.5 flex items-center gap-2 px-2",children:[n(_,{name:K[r.id]||"box",class:"size-3.5"}),r.title]}),n("ul",{class:"space-y-0.5",children:i.map((s,a)=>{let d=s.level==="error";return n("li",{children:n("button",{type:"button",onClick:()=>R(r.id,Me(s.path)),class:"focus-ring flex min-h-11 w-full cursor-pointer gap-2.5 rounded-lg px-2 py-2 text-left transition-colors duration-150 hover:bg-raised",children:[n(_,{name:d?"xCircle":"alert",class:`mt-0.5 size-4 ${d?"text-danger":"text-warn"}`}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-[13px] leading-5 text-body",children:s.message}),s.path?n("span",{class:"mt-0.5 block truncate font-mono text-xs text-faint",children:s.path}):null]})]})},a)})})]},r.id))})]})}function io(e){return e.replace(/&/g,"&").replace(//g,">").replace(/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,o=>{let r="var(--af-json-number)";return o.startsWith('"')?r=o.endsWith(":")?"var(--af-json-key)":"var(--af-json-string)":(o==="true"||o==="false"||o==="null")&&(r="var(--af-json-literal)"),`${o}`})}function ao(){let e=JSON.stringify(u.config,null,2).split(` +`);return n("div",{children:[n("div",{class:"sticky top-0 flex items-center justify-between gap-3 border-b border-line bg-surface px-4 py-2.5",children:[n("p",{class:"text-xs text-faint",children:H()||!u.exists?"Preview of what Save will write":"Matches the file on disk"}),n("button",{type:"button",class:"btn btn-ghost h-8 px-2.5 text-xs",onClick:pn,children:[n(_,{name:"copy",class:"size-3.5"}),"Copy"]})]}),n("div",{class:"py-3 pr-4 font-mono text-[12.5px] leading-5",children:e.map((t,o)=>n("div",{class:"flex",children:[n("span",{class:"w-10 shrink-0 pr-3 text-right text-faint/70 select-none",children:o+1}),n("span",{class:"min-w-0 flex-1 break-all whitespace-pre-wrap",dangerouslySetInnerHTML:{__html:io(t)}})]},o))})]})}function ht(e){let t=[];for(let o of e){let r=o.group||"",i=t.find(s=>s.name===r);i||t.push(i={name:r,fields:[]}),i.fields.push(o)}return t}var q=(e,t)=>g(o=>{if(t===void 0){st(o.config,e);return}let r=e.length>1?o.sections.find(i=>i.optional&&i.key===e[0]):null;r&&(o.config[r.key]===void 0||o.config[r.key]===null)&&(o.config[r.key]=JSON.parse(JSON.stringify(r.default))),Pe(o.config,e,t)}),lo=e=>`f-${e.path.join("-")}`;function co({id:e,value:t,field:o,invalid:r,disabled:i}){let[s,a]=Z(String(t??""));return L(()=>{(s===""?t!==void 0:Number(s)!==t)&&a(String(t??""))},[t]),n("input",{id:e,type:"number",inputmode:"decimal",value:s,disabled:i,min:o.min,step:o.step??"any",placeholder:o.placeholder??o.default??"",class:`input max-w-40 font-mono tabular-nums ${r?"input-invalid":""}`,onInput:d=>{let f=d.currentTarget.value;a(f),f===""?q(o.path,void 0):Number.isNaN(Number(f))||q(o.path,Number(f))}})}function mt({field:e,value:t}){return e.exclusive_min===void 0||typeof t!="number"||t>e.exclusive_min?null:n("p",{class:"mt-1.5 text-[13px] text-warn",children:["Must be greater than ",e.exclusive_min,"."]})}function uo({id:e,field:t,value:o,invalid:r}){let i=t.unlimited_value!==void 0&&o===t.unlimited_value,s=n(co,{id:e,value:i?void 0:o,field:t,invalid:r,disabled:i});if(t.null_label!==void 0){let a=o===null?"n":o===void 0?"d":"s",d=typeof t.default=="number"?t.default:300;return n("div",{children:[n(Y,{id:e,label:t.label,value:a,options:[["d","Default"],["s","Seconds"],["n",t.null_label]],onPick:f=>q(t.path,f==="d"?void 0:f==="n"?null:d)}),a==="s"?n("div",{class:"mt-2.5",children:[s,n(mt,{field:t,value:o})]}):null]})}return t.unlimited_value!==void 0?n("div",{children:[n("div",{class:"flex flex-wrap items-center gap-x-4",children:[s,n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm text-body",children:[n("input",{type:"checkbox",class:"size-[18px] accent-brand-solid",checked:i,onChange:a=>q(t.path,a.currentTarget.checked?t.unlimited_value:void 0)}),"Unlimited"]})]}),n(mt,{field:t,value:o})]}):n("div",{children:[s,n(mt,{field:t,value:o})]})}function po({id:e,field:t,value:o}){if(t.unset_label!==void 0){let r=[["d","Default"]];return t.allow_true!==!1&&r.push(["t","On"]),r.push(["f","Off"]),n("div",{children:[n(Y,{id:e,label:t.label,value:o===void 0?"d":o?"t":"f",options:r,onPick:i=>q(t.path,i==="d"?void 0:i==="t")}),n("p",{class:"mt-1.5 text-xs text-faint",children:["Default: ",t.unset_label]})]})}return n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-semibold text-ink",children:[n("input",{id:e,type:"checkbox",class:"size-[18px] accent-brand-solid",checked:(o??t.default??!1)===!0,onChange:r=>q(t.path,r.currentTarget.checked)}),t.label]})}function fo({id:e,field:t,value:o,invalid:r}){if(t.type==="bool")return n(po,{id:e,field:t,value:o});if(t.type==="number")return n(uo,{id:e,field:t,value:o,invalid:r});if(t.type==="list")return n(W,{id:e,values:Array.isArray(o)?o:[],placeholder:t.placeholder,onChange:i=>q(t.path,i.length?i:void 0)});if(t.type==="select"){let i=t.options.map(s=>Array.isArray(s)?s:[s,s]);return t.unset_label!==void 0&&i.unshift(["",t.unset_label]),n(oe,{id:e,options:i,invalid:r,value:o??(t.unset_label!==void 0?"":t.default??(Array.isArray(t.options[0])?t.options[0][0]:t.options[0])),onChange:s=>q(t.path,s===""?void 0:s)})}return n("input",{id:e,type:"text",value:o??"",placeholder:t.placeholder,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${r?"input-invalid":""}`,onInput:i=>q(t.path,i.currentTarget.value===""?void 0:i.currentTarget.value)})}function I({field:e}){let t=e.path.join("."),o=lo(e),r=Ae(u.config,e.path),i=O(t),s=it(e),a=tn(e),d=e.type==="bool"&&e.unset_label===void 0,f=e.default!==void 0&&!s,l=Array.isArray(e.default)?e.default.join(", "):String(e.default??"");return n("div",{id:`row-${o}`,class:`relative pt-5 pb-1 ${s?"before:absolute before:top-6 before:-left-2.5 before:bottom-2 before:w-[3px] before:rounded before:bg-brand before:content-[''] sm:before:-left-3.5":""}`,children:[n("div",{class:"flex min-h-6 items-center gap-2.5",children:[d?null:n("label",{for:o,class:`text-sm font-semibold text-ink ${a?"opacity-50":""}`,children:[e.label,e.required?n("span",{class:"text-danger",children:" *"}):null]}),d?null:n("code",{class:"hidden font-mono text-[11px] text-faint sm:inline",children:t}),f&&!d?n("span",{class:"rounded border border-line px-1.5 text-[11px] leading-4 font-medium text-faint",children:"default"}):null,n("span",{class:"flex-1"}),s?n("button",{type:"button",class:"focus-ring min-h-8 cursor-pointer rounded px-1 text-[13px] font-medium text-brand hover:underline",title:`Reset to default${e.default!==void 0?` (${l})`:""}`,"aria-label":`Reset ${e.label} to default`,onClick:()=>g(p=>st(p.config,e.path)),children:"Reset"}):null]}),e.help?n("p",{class:`mt-0.5 max-w-prose text-[13px] leading-5 text-muted ${a?"opacity-50":""}`,children:e.help}):null,n("fieldset",{disabled:a,class:`m-0 mt-2.5 min-w-0 max-w-[520px] border-0 p-0 ${a?"opacity-50":""}`,children:n(fo,{id:o,field:e,value:r,invalid:i?.level==="error"})}),a?n("p",{class:"mt-1.5 text-xs text-faint",children:nn(e)}):null,n(G,{issue:i})]})}var mo=[["","Choose reach"],["own","own"],["tenant","tenant"],["all","all"]],bn=e=>Array.isArray(e)?e.length>0:y(e)&&Object.keys(e).length>0;function xn(){let e=u.config.auth;return e==="jwt"?{method:"jwt"}:y(e)?e:{method:"none"}}function N(e){g(t=>{let o={...xn()};e(o),t.config.auth=o.method==="none"?null:o})}function qe(e,t,o){o==null||o===""||typeof o!="string"&&!bn(o)?delete e[t]:e[t]=o}function ho(e){return e.roles_claim!==void 0||bn(e.roles)?"roles":e.permissions_claim!==void 0?"permissions":"default"}var _o=e=>u.issues.filter(t=>t.path===e||t.path.startsWith(`${e}.`));function _t({prefix:e}){return _o(e).map((t,o)=>n(G,{issue:t},o))}function vn({id:e,label:t,name:o,onRename:r}){let[i,s]=Z(o);return L(()=>s(o),[o]),n("div",{children:[n("label",{for:e,class:"sr-only",children:t}),n("input",{id:e,type:"text",value:i,class:"input font-mono",autocomplete:"off",spellcheck:!1,onInput:a=>s(a.currentTarget.value),onChange:a=>{r(a.currentTarget.value.trim())||s(o)}})]})}var Fe=({children:e})=>n("span",{class:"mb-1.5 block text-xs font-medium text-faint",children:e});function yn(e,t,o){return!o||o!==t&&o in e?null:Object.fromEntries(Object.entries(e).map(([r,i])=>[r===t?o:r,i]))}function kn({sec:e}){let t=xn(),o=t.method??"none",r=ho(t);return n("div",{class:"space-y-6",children:[n(A,{title:"Method",children:n(pt,{name:"auth-method",label:"Authentication method",value:o,options:[["none","None","Every request is accepted. For local development."],["jwt","JWT","Verify bearer tokens signed with JWT_SECRET_KEY."],["custom","Custom","Your own BaseAuth subclass decides who is calling."]],onPick:i=>N(s=>{s.method=i,i==="custom"?s.path=s.path??"":delete s.path})})}),o==="custom"?n(A,{title:"Class",children:n("div",{class:"panel",children:n(V,{id:"auth-path",label:"Auth class",path:"auth.path",value:t.path,placeholder:"auth.custom:MyAuth",help:"Import path of a BaseAuth subclass, as module:attribute.",issue:O("auth.path"),onInput:i=>N(s=>{s.path=i})})})}):null,o!=="none"?n(w,{children:[n(A,{title:"Permission source",children:n(pt,{name:"auth-source",label:"Permission source",value:r,options:[["default","Default","Every signed-in user, own data only."],["roles","Roles from a claim","Map the roles in the token to permissions."],["permissions","Permissions from a claim","The token already lists the permissions."]],onPick:i=>N(s=>{delete s.roles_claim,delete s.permissions_claim,delete s.roles,delete s.default_role,i==="roles"&&(s.roles_claim="roles",s.roles={member:{permissions:[],reach:"own"}}),i==="permissions"&&(s.permissions_claim="permissions")})})}),r==="roles"?n(bo,{auth:t,registry:e.registry}):null,r==="permissions"?n(A,{title:"Claim",children:n("div",{class:"panel",children:n(V,{id:"auth-permissions-claim",label:"Permissions claim",path:"auth.permissions_claim",value:t.permissions_claim,placeholder:"permissions",issue:O("auth.permissions_claim"),help:"Token claim holding a list of permissions, such as graph:invoke.",onInput:i=>N(s=>{s.permissions_claim=i})})})}):null,n(A,{title:"Tenants",children:n("div",{class:"panel divide-y divide-line",children:[n("div",{class:"px-5 py-4",children:[n(fe,{label:"Tenant claims",path:"auth.tenant_claims",id:"auth-tenant-claims",help:"Token claims that name the tenant, such as org_id. Rows are separated by them."}),n("div",{class:"mt-3",children:n(W,{id:"auth-tenant-claims",values:t.tenant_claims??[],placeholder:"org_id",onChange:i=>N(s=>qe(s,"tenant_claims",i))})}),n(_t,{prefix:"auth.tenant_claims"})]}),r==="roles"?n(go,{auth:t}):null]})}),n(xo,{auth:t})]}):null]})}function go({auth:e}){let t=Object.keys(y(e.roles)?e.roles:{});return n(ut,{children:[n(fe,{label:"Default role",path:"auth.default_role",id:"auth-default-role",help:"Given to a signed-in user whose token carries no role."}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n(oe,{id:"auth-default-role",value:e.default_role??"",options:[["","None"],...t.map(o=>[o,o])],onChange:o=>N(r=>qe(r,"default_role",o))}),n(G,{issue:O("auth.default_role")})]})]})}function bo({auth:e,registry:t}){let o=y(e.roles)?e.roles:{},r=Object.entries(o),i=Object.keys(o),s=["*",...Object.entries(t||{}).flatMap(([p,h])=>[`${p}:*`,...h.map(c=>`${p}:${c}`)])],a=p=>N(h=>{h.roles=p}),d=(p,h)=>a({...o,[p]:{...o[p],...h}}),f=()=>{let p=r.length+1;for(;`role${p}`in o;)p++;a({...o,[`role${p}`]:{permissions:[],reach:"own"}})},l=(p,h)=>{let c=yn(o,p,h);return c?(N(m=>{m.roles=Object.fromEntries(Object.entries(c).map(([x,$])=>[x,$.inherits?{...$,inherits:$.inherits.map(k=>k===p?h:k)}:$])),m.default_role===p&&(m.default_role=h),y(m.system_identities)&&(m.system_identities=Object.fromEntries(Object.entries(m.system_identities).map(([x,$])=>[x,{...$,roles:($.roles??[]).map(k=>k===p?h:k)}])))}),!0):!1};return n(A,{title:"Roles",children:n("div",{class:"panel",children:[n("div",{class:"border-b border-line",children:n(V,{id:"auth-roles-claim",label:"Roles claim",path:"auth.roles_claim",value:e.roles_claim,placeholder:"roles",issue:O("auth.roles_claim"),help:"Token claim holding the user's list of role names.",onInput:p=>N(h=>{h.roles_claim=p})})}),r.length?n("div",{class:"divide-y divide-line",children:r.map(([p,h],c)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_8rem_auto] items-start gap-3",children:[n(vn,{id:`role-name-${c}`,label:"Role name",name:p,onRename:m=>l(p,m)}),n(oe,{id:`role-reach-${c}`,value:h.reach??"",options:mo,invalid:O(`auth.roles.${p}.reach`)?.level==="error",onChange:m=>d(p,{reach:m})}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove role ${p}`,onClick:()=>a(Object.fromEntries(r.filter(([m])=>m!==p))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Fe,{children:"Permissions"}),n(W,{id:`role-permissions-${c}`,values:h.permissions??[],placeholder:"graph:invoke, threads:*, *",suggestions:s,onChange:m=>d(p,{permissions:m})})]}),n("div",{children:[n(Fe,{children:"Inherits"}),n(W,{id:`role-inherits-${c}`,values:h.inherits??[],placeholder:"another role",suggestions:i.filter(m=>m!==p),onChange:m=>{let x={...h};qe(x,"inherits",m),a({...o,[p]:x})}})]}),n(_t,{prefix:`auth.roles.${p}`})]},c))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"No roles yet. Add one to use roles."}),n("div",{class:"flex items-center justify-between gap-4 border-t border-line px-5 py-3",children:[n("p",{class:"text-[13px] text-muted",children:["Reach: ",n("code",{class:"font-mono",children:"own"})," the user's rows, ",n("code",{class:"font-mono",children:"tenant"})," every row of their tenant, ",n("code",{class:"font-mono",children:"all"})," every row."]}),n("button",{type:"button",class:"btn btn-ghost",onClick:f,children:[n(_,{name:"plus"}),"Add role"]})]})]})})}function xo({auth:e}){let t=y(e.system_identities)?e.system_identities:{},o=Object.entries(t),r=Object.keys(y(e.roles)?e.roles:{}),i=l=>N(p=>qe(p,"system_identities",l)),s=(l,p)=>i({...t,[l]:{...t[l],...p}}),a=()=>{let l=o.length+1;for(;`job${l}`in t;)l++;i({...t,[`job${l}`]:{roles:[],tenant:{}}})},d=l=>Object.entries(l??{}).map(([p,h])=>`${p}=${h}`),f=l=>Object.fromEntries(l.filter(p=>p.includes("=")).map(p=>[p.slice(0,p.indexOf("=")).trim(),p.slice(p.indexOf("=")+1).trim()]));return n(A,{title:"System identities",children:n("div",{class:"panel",children:[o.length?n("div",{class:"divide-y divide-line",children:o.map(([l,p],h)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3",children:[n(vn,{id:`system-name-${h}`,label:"Identity name",name:l,onRename:c=>{let m=yn(t,l,c);return m&&i(m),!!m}}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${l}`,onClick:()=>i(Object.fromEntries(o.filter(([c])=>c!==l))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Fe,{children:"Roles"}),n(W,{id:`system-roles-${h}`,values:p.roles??[],placeholder:"member",suggestions:r,onChange:c=>s(l,{roles:c})})]}),n("div",{children:[n(Fe,{children:"Tenant, as claim=value"}),n(W,{id:`system-tenant-${h}`,values:d(p.tenant),placeholder:"org_id=acme",onChange:c=>s(l,{tenant:f(c)})})]}),n(_t,{prefix:`auth.system_identities.${l}`})]},h))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"None. Scheduled jobs and other non-user callers can run as one."}),n("div",{class:"flex justify-end border-t border-line px-5 py-3",children:n("button",{type:"button",class:"btn btn-ghost",onClick:a,children:[n(_,{name:"plus"}),"Add identity"]})})]})})}var He=[["memory","Memory","This process only"],["redis","Redis","Shared. Redis, Valkey, Dragonfly, KeyDB"],["custom","Custom","Your BaseCacheStore class"]],vo={requests:100,window:60,by:"ip"},Ue=()=>y(u.config.cache_store)?u.config.cache_store:{};function gt(e){g(t=>{let o=y(t.config.cache_store)?t.config.cache_store:{};e(o),Object.keys(o).length?t.config.cache_store=o:delete t.config.cache_store})}var yo=()=>{let e=Ue().rate_limit;return y(e)&&e.enabled!==!1},ko=()=>{let e=Ue().token_revocation;return e===!0||y(e)&&e.enabled!==!1};function wo(e){gt(t=>{delete t.url,delete t.path,e==="memory"?delete t.type:t.type=e})}function Co(e){gt(t=>{e?y(t.rate_limit)?delete t.rate_limit.enabled:t.rate_limit={...vo}:delete t.rate_limit})}function $o(e){gt(t=>{e?y(t.token_revocation)?delete t.token_revocation.enabled:t.token_revocation=!0:delete t.token_revocation})}var _n=(e,t)=>g(o=>{Pe(o.config,e,t)});function So({value:e,onPick:t}){let o=(r,i)=>{let s=r.key==="ArrowRight"||r.key==="ArrowDown"?1:r.key==="ArrowLeft"||r.key==="ArrowUp"?-1:0;if(!s)return;r.preventDefault();let a=He[(i+s+He.length)%He.length][0];t(a),requestAnimationFrame(()=>document.getElementById(`cache-type-${a}`)?.focus())};return n("div",{role:"radiogroup","aria-label":"Cache store type",class:"grid gap-3 sm:grid-cols-3",children:He.map(([r,i,s],a)=>{let d=r===e;return n("button",{type:"button",role:"radio",id:`cache-type-${r}`,"aria-checked":d?"true":"false",tabIndex:d?0:-1,onKeyDown:f=>o(f,a),class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 motion-reduce:transition-none ${d?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{d||t(r)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${d?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:d?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:i}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:s})]})]},r)})})}function he({warn:e,children:t}){return n("p",{class:`mt-3 flex items-start gap-1.5 text-[13px] leading-5 ${e?"text-warn":"text-muted"}`,children:[e?n(_,{name:"alert",class:"mt-0.5 size-3.5"}):null,n("span",{children:t})]})}function gn({id:e,title:t,text:o,on:r,onChange:i}){return n("div",{class:"flex items-center gap-4",children:[n("div",{class:"min-w-0 flex-1",children:[n("h3",{class:"text-base font-semibold text-ink",children:t}),n("p",{class:"mt-0.5 text-[13px] leading-5 text-muted",children:o})]}),n(dt,{id:e,on:r,label:t,onChange:i})]})}var To=[["ip","IP"],["user","User"],["global","Global"]];function Oo({field:e,memory:t}){let[o,r]=Z(!1),i=Ue().rate_limit,s=y(i)&&i.by||"ip",a=!(y(i)&&i.fail_open===!1),d=["trusted_proxy_headers","trusted_proxy_hops","trusted_proxies","exclude_paths"];return n(w,{children:[t?n(he,{warn:!0,children:"Counts per worker with the Memory store."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:e("cache_store.rate_limit.requests")}),n(I,{field:e("cache_store.rate_limit.window")}),n("div",{class:"pt-5",children:[n("span",{id:"cache-by-label",class:"block text-sm font-semibold text-ink",children:"Limit by"}),n("div",{class:"mt-2.5",children:n(Y,{id:"f-cache_store-rate_limit-by",label:"Limit by",options:To,value:s,onPick:f=>_n(["cache_store","rate_limit","by"],f)})}),n(G,{issue:O("cache_store.rate_limit.by")})]}),n("div",{class:"pt-5",children:[n("span",{id:"cache-fail-open-label",class:"block text-sm font-semibold text-ink",children:"Fail open"}),n("div",{class:"mt-2.5 flex items-center gap-3",children:[n(dt,{id:"f-cache_store-rate_limit-fail_open",on:a,label:"Fail open",onChange:f=>_n(["cache_store","rate_limit","fail_open"],f)}),n("p",{class:"m-0 text-[13px] leading-5 text-muted",children:a?"Allow requests if the store errors.":"Deny with 503 if the store errors."})]})]})]}),n("div",{class:"mt-5 border-t border-line pt-1",children:[n("button",{type:"button","aria-expanded":o?"true":"false","aria-controls":"cache-advanced",class:"focus-ring flex min-h-11 cursor-pointer items-center gap-2 rounded font-medium text-muted hover:text-ink",onClick:()=>r(!o),children:[n(_,{name:"chevronRight",class:`size-4 transition-transform duration-150 motion-reduce:transition-none ${o?"rotate-90":""}`}),"Advanced"]}),o?n("div",{id:"cache-advanced",children:d.map(f=>n(I,{field:e(`cache_store.rate_limit.${f}`)},f))}):null]})]})}var re=[["rate","Rate limiting"],["revoke","Token revocation"]];function wn({sec:e}){let[t,o]=Z("rate"),r=Ue().type||"memory",i=l=>e.fields.find(p=>p.path.join(".")===l),s={rate:yo(),revoke:ko()},a=r==="memory",d={rate:!!O("cache_store.rate_limit"),revoke:!!O("cache_store.token_revocation")},f=(l,p)=>{let h=l.key==="ArrowRight"?1:l.key==="ArrowLeft"?-1:0,c=l.key==="Home"?0:l.key==="End"?re.length-1:h?(p+h+re.length)%re.length:-1;c<0||(l.preventDefault(),o(re[c][0]),document.getElementById(`cache-tab-${re[c][0]}`)?.focus())};return n("div",{children:[n(A,{title:"Store type",children:[n(So,{value:r,onPick:wo}),r==="redis"?n(I,{field:i("cache_store.url")}):null,r==="custom"?n(I,{field:i("cache_store.path")}):null,a?n("p",{class:"mt-3 mb-0 text-[13px] leading-5 text-muted",children:"Not shared between workers: each worker keeps its own locks, limits and revocations."}):null]}),n("div",{role:"tablist","aria-label":"Cache store features",class:"mt-8 flex gap-1 border-b border-line",children:re.map(([l,p],h)=>{let c=t===l;return n("button",{type:"button",role:"tab",id:`cache-tab-${l}`,"aria-selected":c?"true":"false","aria-controls":`cache-panel-${l}`,tabIndex:c?0:-1,onKeyDown:m=>f(m,h),class:`focus-ring -mb-px flex min-h-11 cursor-pointer items-center gap-2 border-b-2 px-3 text-sm font-medium transition-colors duration-150 motion-reduce:transition-none ${c?"border-brand-solid text-ink":"border-transparent text-muted hover:text-ink"}`,onClick:()=>o(l),children:[p,n(ne,{class:s[l]?"bg-brand-soft text-brand":"bg-raised text-muted",children:s[l]?"On":"Off"})]},l)})}),n("div",{role:"tabpanel",id:`cache-panel-${t}`,"aria-labelledby":`cache-tab-${t}`,class:"pt-5",children:t==="rate"?n(w,{children:[n(gn,{id:"cache-rate-limit",title:"Rate limiting",on:s.rate,onChange:Co,text:"Cap how many requests one caller can make."}),s.rate?n(Oo,{field:i,memory:a}):d.rate?n(he,{warn:!0,children:"Off: one caller can exhaust the service."}):null]}):n(w,{children:[n(gn,{id:"cache-token-revocation",title:"Token revocation",on:s.revoke,onChange:$o,text:"Reject revoked tokens before they expire (10xgraph revoke)."}),s.revoke?n(w,{children:[a?n(he,{warn:!0,children:"10xgraph revoke cannot reach an in-memory store; pick Redis or Custom."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:i("cache_store.token_revocation.token_lifetime")}),n(I,{field:i("cache_store.token_revocation.user_ttl")})]}),n(he,{children:"Fails closed: while the cache store is unreachable, authenticated requests get 503 and the server will not start."})]}):d.revoke?n(he,{warn:!0,children:"Off: a leaked token works until it expires."}):null]})})]})}function Cn({sec:e}){let t=e.fields[0].path[0],o=e.choices.map(p=>p[0]),r=u.config[t],i=t==="thread_name_generator"?"random":"",s=r===void 0?i:typeof r=="string"&&o.includes(r)?r:"custom",a=[...t==="id_generator"?[["","Not set (the graph's own generator)"]]:[],...e.choices.map(([p,h])=>[p,t==="thread_name_generator"&&p==="random"?`${h} (default)`:h]),["custom","Custom (module:attr)"]],d=s===""?"The graph keeps the generator it was compiled with.":s==="custom"?"Your own class, as module:attribute.":e.choices.find(p=>p[0]===s)?.[2],f=p=>g(h=>{p===i?delete h.config[t]:h.config[t]=p==="custom"?"":p}),l=`f-${t}`;return n("div",{children:[n(A,{title:"Choice",children:n("div",{class:"max-w-[520px]",children:[n("label",{for:`${l}-select`,class:"sr-only",children:e.title}),n(oe,{id:`${l}-select`,options:a,value:s,onChange:f}),n("p",{class:"mt-1.5 text-[13px] text-muted",children:d})]})}),s==="custom"?n(A,{title:"Class",children:n("div",{class:"panel",children:n(V,{id:l,label:e.fields[0].label,path:t,value:typeof r=="string"?r:"",placeholder:e.fields[0].placeholder,help:e.fields[0].help,issue:O(t),onInput:p=>g(h=>{h.config[t]=p})})})}):null]})}function $n(){let e=Array.isArray(u.config.remote_tools)?u.config.remote_tools:[],t=()=>{g(o=>{e.push({node:"",name:"",description:"",parameters:{type:"object",properties:{},required:[]}}),o.config.remote_tools=e}),requestAnimationFrame(()=>document.getElementById(`tool-${e.length-1}-name`)?.focus())};return e.length?n("div",{class:"space-y-4",children:[e.map((o,r)=>n(Io,{tools:e,tool:o,index:r},r)),n("button",{type:"button",onClick:t,class:"focus-ring flex h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-dashed border-line-strong text-sm font-medium text-muted transition-colors duration-150 hover:border-brand hover:text-brand",children:[n(_,{name:"plus"}),"Add remote tool"]})]}):n("div",{class:"rounded-xl border border-dashed border-line-strong bg-surface px-6 py-10 text-center",children:[n("div",{class:"mx-auto flex size-11 items-center justify-center rounded-full bg-raised text-faint",children:n(_,{name:"wrench",class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:"No remote tools yet"}),n("p",{class:"mx-auto mt-1 max-w-sm text-sm leading-6 text-muted",children:"Declare a tool here and the client that calls the API runs it, for example to read the user's location."}),n("button",{type:"button",class:"btn btn-outline mt-5",onClick:t,children:[n(_,{name:"plus"}),"Add remote tool"]})]})}function Io({tools:e,tool:t,index:o}){let r=u.toolParams[o]??{text:JSON.stringify(t.parameters??{},null,2),error:null},i=a=>g(()=>{e[o]={...e[o],...a}}),s=a=>`remote_tools[${o}].${a}`;return n("section",{class:"panel",children:[n("div",{class:"flex items-center gap-3 border-b border-line px-5 py-3",children:[n(_,{name:"wrench",class:"size-4 text-faint"}),n("span",{class:"min-w-0 flex-1 truncate font-mono text-sm font-medium text-ink",children:t.name||`tool_${o+1}`}),t.node?n("span",{class:"rounded-md border border-line bg-raised px-1.5 py-0.5 font-mono text-xs text-muted",children:t.node}):null,n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${t.name||"tool"}`,onClick:()=>g(a=>{e.splice(o,1),a.toolParams={}}),children:n(_,{name:"trash"})})]}),n("div",{class:"divide-y divide-line",children:[n(V,{id:`tool-${o}-name`,label:"Tool name",path:s("name"),value:t.name,placeholder:"get_location",onInput:a=>i({name:a})}),n(V,{id:`tool-${o}-node`,label:"Graph node",path:s("node"),value:t.node,placeholder:"MAIN",help:"The node whose model may call this tool.",onInput:a=>i({node:a})}),n(V,{id:`tool-${o}-description`,label:"Description",path:s("description"),value:t.description,placeholder:"Returns the user's current city",help:"What the model reads when deciding to call it.",onInput:a=>i({description:a})}),n("div",{class:"px-5 py-4",children:[n(fe,{label:"Parameters",path:s("parameters"),help:"JSON schema of the arguments. type must be object.",id:`tool-${o}-params`}),n("textarea",{id:`tool-${o}-params`,rows:7,spellcheck:!1,value:r.text,class:`input mt-3 h-auto py-2.5 font-mono text-[13px] leading-5 ${r.error?"input-invalid":""}`,onInput:a=>{let d=a.currentTarget.value;g(f=>{try{e[o]={...e[o],parameters:JSON.parse(d)},f.toolParams[o]={text:d,error:null}}catch(l){f.toolParams[o]={text:d,error:l.message}}})}}),n(G,{issue:r.error?{level:"error",message:r.error}:null})]})]})]})}var _e=({children:e,code:t})=>n("h1",{id:"page-title",tabindex:-1,class:"flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-semibold tracking-tight text-ink focus:outline-none",children:[e,t?n("code",{class:"rounded border border-line bg-raised px-1.5 font-mono text-xs font-normal text-faint",children:t}):null]}),bt=({children:e})=>n("p",{class:"mt-1.5 max-w-[62ch] text-[15px] leading-6 text-muted",children:e}),Sn=({children:e})=>n("h2",{class:"mt-10 mb-2 eyebrow",children:e});function Tn({id:e,text:t,warn:o,compact:r}){let i=pe(e);return i?n("a",{href:`#${e}`,onClick:s=>{s.preventDefault(),R(e)},class:`focus-ring group flex items-center gap-3.5 border-t border-line py-2 last:border-b ${r?"min-h-12":"min-h-14"}`,children:[n(_,{name:K[e]||"box",class:"size-[18px] text-faint"}),n("span",{class:`min-w-0 flex-1 ${r?"flex flex-wrap items-baseline gap-x-4":""}`,children:[n("span",{class:`block text-sm text-ink group-hover:text-brand ${r?"min-w-40 font-medium":"font-semibold"}`,children:i.title}),n("span",{class:`block text-[13px] leading-5 ${o?"text-warn":"text-muted"}`,children:t??i.description})]}),n(_,{name:"chevronRight",class:"size-4 text-faint"})]}):null}function In(e){return`Not set. ${E(e)?.off_summary||""}`.trim()}function Eo(){let e=u.config.auth,t=e==="jwt"?"jwt":y(e)?e.method??"none":"none";if(t==="none")return{text:In("auth")};let o=y(e)&&y(e.roles)?Object.keys(e.roles).length:0;return{text:`${t==="jwt"?"JWT bearer tokens":`Custom backend${e.path?`: ${e.path}`:""}`}${o?`, ${o} ${o===1?"role":"roles"}`:""}.`}}function Ao(){let e=y(u.config.cache_store)?u.config.cache_store:{},t=[`${e.type||"memory"} store`],o=e.rate_limit;if(y(o)&&o.enabled!==!1){let i={ip:"per IP",user:"per user",global:"across all clients"}[o.by??"ip"]||o.by;t.push(`${o.requests??100} requests per ${o.window??60} s, ${i}`)}let r=e.token_revocation;return(r===!0||y(r)&&r.enabled!==!1)&&t.push("token revocation on"),{text:`${t.join("; ")}.`}}function Po(){let e=u.config.observability;if(e==null)return{text:In("observability")};let t=[];return X(["observability","logfire","enabled"])&&t.push("Logfire"),X(["observability","langsmith","enabled"])&&t.push("LangSmith"),X(["observability","phoenix","enabled"])&&t.push("Phoenix"),{text:`Level ${X(["observability","level"])}${t.length?`, sending to ${t.length>1?`${t.slice(0,-1).join(", ")} and ${t[t.length-1]}`:t[0]}`:", no destination chosen yet"}.`}}function On(){let e=E("core"),t=on(),o=e?ee("core").filter(s=>!e.fields.some(a=>a.path.join(".")===s.path)):[],r=[["auth",Eo()],["cache_store",Ao()],["observability",Po()]],i=u.nav.flatMap(s=>s.items).filter(s=>s!=="get-running");return n(w,{children:[n(_e,{children:t?"You're ready to run":"One step to run your agent"}),n(bt,{children:z["get-running"].description}),n("div",{class:`mt-4 flex items-center gap-2 font-medium ${t?"text-ok":"text-muted"}`,children:[n(_,{name:t?"checkCircle":"circle",class:"size-[18px]"}),"Required: ",t?"1":"0"," of 1 done"]}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((s,a)=>n(ft,{issue:s},a))}):null,n("div",{class:"mt-2",children:e?ht(e.fields).flatMap(s=>s.fields).map(s=>n(I,{field:s},s.path.join("."))):null}),t?n("div",{class:"mt-7 border-t border-line pt-5",children:[n("div",{class:"flex items-center gap-2.5 font-medium text-ok",children:[n(_,{name:"checkCircle",class:"size-[18px]"}),"Ready for 10xgraph dev"]}),n("div",{class:"mt-3 flex max-w-[420px] items-center gap-2 rounded-lg border border-line-strong bg-sunken py-0 pr-1 pl-3.5",children:[n("code",{class:"flex-1 font-mono text-[13px] font-medium text-ink",children:"10xgraph dev"}),u.copied?n("span",{class:"text-xs text-ok",children:"Copied"}):null,n("button",{type:"button",class:"icon-btn",onClick:un,"aria-label":u.copied?"Copied 10xgraph dev":"Copy command 10xgraph dev",children:n(_,{name:"copy",class:"size-[18px]"})})]}),n("p",{class:"mt-3 text-muted",children:"Everything else already has working defaults."})]}):n("p",{class:"mt-4 text-xs text-faint",children:"Add the agent graph above, then run 10xgraph dev."}),n(Sn,{children:"Before production (optional)"}),n("div",{children:r.map(([s,a])=>n(Tn,{id:s,text:a.text,warn:a.warn},s))}),n(Sn,{children:"What you can configure"}),n("div",{children:i.map(s=>n(Tn,{id:s,compact:!0},s))})]})}function We({vars:e}){return n("dl",{class:"mb-3 grid gap-x-4 gap-y-1 sm:grid-cols-[minmax(0,260px)_minmax(0,1fr)]",children:e.map(t=>[n("dt",{class:"py-0.5 font-mono text-xs font-medium break-all text-ink",children:t.name},`${t.name}-n`),n("dd",{class:"m-0 pb-2 text-[13px] text-muted sm:py-0.5",children:t.purpose},`${t.name}-p`)])})}function jo({vars:e,open:t}){return e?.length?n("details",{class:"group mt-9 border-t border-line pt-1",open:t,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform group-open:rotate-90"}),"Set in .env"]}),n("p",{class:"mb-2 text-xs text-faint",children:"Read only. The editor never reads .env, so only names are shown."}),n(We,{vars:e})]}):null}function En({sec:e}){return e.widget==="auth"?n(kn,{sec:e}):e.widget==="remote_tools"?n($n,{}):e.widget==="cache_store"?n(wn,{sec:e}):e.widget==="id_generator"||e.widget==="thread_name_generator"?n(Cn,{sec:e}):null}function Mo(e){let t=new Set([...e.fields.map(i=>i.path.join(".")),...Object.keys(ot)]),o=new Set(["cache_store.rate_limit","cache_store.token_revocation"]),r=i=>i.startsWith("auth.roles.")||i.startsWith("auth.system_identities.");return ee(e.id).filter(i=>!t.has(i.path)&&!(e.id==="cache_store"&&o.has(i.path))&&!(e.id==="auth"&&r(i.path)))}function Ro({sec:e}){let t=je(e),o=e.widget&&(t||!e.optional);return n(w,{children:[o?n("div",{class:"mt-6",children:n(En,{sec:e})}):null,n("div",{class:e.optional&&!t?"opacity-80":"",children:ht(e.fields.filter(r=>!r.managed)).map((r,i)=>n("div",{children:[r.name?n("h3",{class:`eyebrow ${i===0?"mt-6":"mt-8 border-t border-line pt-4"}`,children:r.name}):null,r.fields.map(s=>n(I,{field:s},s.path.join(".")))]},r.name))})]})}function No({sec:e}){let t=je(e),o=Mo(e);return n(w,{children:[n(_e,{code:e.key,children:e.title}),n(bt,{children:e.description}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((r,i)=>n(ft,{issue:r},i))}):null,e.optional?n("div",{class:"mt-5 flex flex-wrap items-center gap-4",children:[n(Y,{label:`Settings for ${e.title}`,value:t?"c":"d",options:[["d","Use defaults"],["c","Customize"]],onPick:r=>cn(e,r==="c")}),n("p",{class:"m-0 min-w-56 flex-1 text-[13px] text-muted",children:t?`Saved in 10xgraph.json under ${e.key}.`:e.off_summary})]}):null,e.notes?.length?n("div",{class:"mt-5",children:n(me,{kind:"info",lines:e.notes})}):null,n(Ro,{sec:e}),n(jo,{vars:e.env})]})}function Do(){let e=u.schema.environment||[];return n(w,{children:[n(_e,{children:z.environment.title}),n(bt,{children:"These variables are set in .env or your deployment environment. The editor never reads that file, so only names and purposes are shown."}),e.map(t=>n("section",{children:[n("h2",{class:"mt-8 mb-2 text-sm font-semibold text-ink",children:t.title}),t.note?n("p",{class:"mb-2 text-xs text-faint",children:t.note}):null,n(We,{vars:t.vars})]},t.title))]})}var An=e=>String(e??"").toLowerCase();function Lo(e){let t=i=>e.every(s=>An(i).includes(s)),o=[];for(let i of u.sections){let s=t(`${i.title} ${i.description} ${i.key||""}`),a=p=>s||t(`${p.label} ${p.help||""} ${p.path.join(".")}`),d=i.fields.filter(p=>!p.managed&&a(p)),f=(i.env||[]).filter(p=>t(`${p.name} ${p.purpose}`)),l=!!i.widget&&(s||i.fields.some(p=>p.managed&&a(p)));(d.length||f.length||l)&&o.push({id:i.id,sec:i,fields:d,env:f,widget:l})}let r=(u.schema.environment||[]).map(i=>({title:i.title,vars:i.vars.filter(s=>t(`${s.name} ${s.purpose} ${i.title}`))})).filter(i=>i.vars.length);return{results:o,envGroups:r}}function zo(){let e=An(u.query).split(/\s+/).filter(Boolean),{results:t,envGroups:o}=Lo(e),r=t.reduce((i,s)=>i+s.fields.length+s.env.length+(s.widget?1:0),0)+o.reduce((i,s)=>i+s.vars.length,0);return r?n(w,{children:[n(_e,{code:u.query,children:[r," ",r===1?"setting matches":"settings match"]}),t.map(({id:i,sec:s,fields:a,env:d,widget:f})=>n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:K[i]||"box",class:"size-4"}),s.title,n("a",{href:`#${i}`,onClick:l=>{l.preventDefault(),R(i)},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),f?n("div",{class:"mt-4",children:n(En,{sec:s})}):null,a.map(l=>n(I,{field:l},l.path.join("."))),d.length?n("details",{class:"group mt-5",open:!0,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform group-open:rotate-90"}),"Set in .env"]}),n(We,{vars:d})]}):null]},i)),o.length?n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:"terminal",class:"size-4"}),z.environment.title,n("a",{href:"#environment",onClick:i=>{i.preventDefault(),R("environment")},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),o.map(i=>n("div",{children:[n("h3",{class:"mt-4 mb-1.5 text-sm font-semibold text-ink",children:i.title}),i.note?n("p",{class:"mb-1.5 text-xs text-faint",children:i.note}):null,n(We,{vars:i.vars})]},i.title))]}):null]}):n(w,{children:[n(_e,{children:"No matches"}),n("p",{class:"mt-8 text-muted",children:['Nothing matches "',u.query,'". Try a shorter word, or press Esc to go back.']})]})}function Pn(){if(u.query.trim())return n(zo,{});if(u.page==="get-running")return n(On,{});if(u.page==="environment")return n(Do,{});let e=E(u.page);return e?n(No,{sec:e}):n(On,{})}function Ho(){let e=u.path.split(/[\\/]/).pop()||"10xgraph.json",t=u.path.slice(0,u.path.length-e.length),o=null;return u.schema?u.exists?H()?o=n(ne,{class:"bg-warn-soft text-warn",children:"Unsaved"}):o=n(ne,{class:"bg-ok-soft text-ok",children:"Saved"}):o=n(ne,{class:"bg-brand-soft text-brand",children:"New"}):o=null,n("div",{class:"flex min-w-0 flex-1 items-center gap-2",title:u.path||void 0,children:[n(_,{name:"file",class:"size-4 text-faint"}),n("span",{class:"truncate font-mono text-[13px] font-medium text-ink",children:e}),o,t?n("span",{class:"hidden min-w-0 truncate font-mono text-xs text-faint lg:block",dir:"rtl",children:n("bdi",{children:t})}):null]})}function jn(){let e=!!u.schema&&!u.fatal,t=u.sideOpen?"Hide sidebar":"Show sidebar";return n("header",{class:"sticky top-0 z-40 flex h-14 items-center gap-2 border-b border-line bg-surface px-3 sm:gap-3 sm:px-4",children:[n("button",{type:"button",class:"icon-btn","aria-controls":"side","aria-expanded":u.sideOpen?"true":"false","aria-label":t,title:`${t} (Ctrl/Cmd+B)`,onClick:()=>te(!u.sideOpen),children:n(_,{name:"panel",class:"size-[18px]"})}),n("div",{class:"flex shrink-0 items-center gap-2.5",children:[n("svg",{viewBox:"0 0 48 48",class:"size-7 shrink-0 text-ink","aria-hidden":"true",children:[n("rect",{x:"4",y:"4",width:"40",height:"40",rx:"11",fill:"none",stroke:"currentColor","stroke-width":"4.4"}),n("path",{d:"M13 33L21 23L34 15L34 31L21 23",fill:"none",stroke:"currentColor","stroke-width":"3","stroke-linecap":"round","stroke-linejoin":"round"}),n("g",{fill:"currentColor",children:[n("circle",{cx:"21",cy:"23",r:"4"}),n("circle",{cx:"34",cy:"15",r:"4"}),n("circle",{cx:"34",cy:"31",r:"4"})]}),n("circle",{cx:"13",cy:"33",r:"4.6",class:"fill-brand-amber"})]}),n("div",{class:"hidden items-baseline gap-1.5 sm:flex",children:[n("span",{class:"text-[15px] font-semibold tracking-tight text-ink",children:"10xGraph"}),n("span",{class:"text-[15px] text-faint",children:"Config"})]})]}),n("span",{class:"hidden h-6 w-px shrink-0 bg-line md:block","aria-hidden":"true"}),n(Ho,{}),n("button",{type:"button",class:"btn btn-ghost","aria-label":"View 10xgraph.json","aria-haspopup":"dialog",disabled:!e,onClick:()=>dn(),children:[n(_,{name:"code",class:"size-[18px]"}),n("span",{class:"hidden md:inline",children:"View 10xgraph.json"})]}),n("button",{type:"button",class:"btn btn-primary",onClick:Re,disabled:!e||!!u.busy||u.exists&&!H()&&!u.parseError,children:[u.busy==="save"?n(_,{name:"spinner",class:"size-4 animate-spin"}):null,u.exists?"Save":"Create file",n("span",{class:"kbd hidden lg:inline-flex",children:"Ctrl S"})]})]})}function Mn(){return u.fatal?n("div",{class:"panel mt-10",children:n(mn,{icon:"xCircle",tint:"text-danger",title:u.fatal.title,text:u.fatal.text})}):u.schema?n(w,{children:[u.parseError?n("div",{class:"mb-6",children:n(me,{kind:"warn",title:"The existing file is not valid JSON",lines:[`${u.parseError}. The form starts from defaults; saving replaces the file and keeps the old one as 10xgraph.json.bak.`]})}):null,n(Pn,{})]}):n("div",{class:"animate-pulse space-y-4 pt-2","aria-label":"Loading",children:[n("div",{class:"h-7 w-48 rounded-md bg-raised"}),n("div",{class:"h-4 w-80 max-w-full rounded-md bg-raised"}),[0,1,2].map(e=>n("div",{class:"h-16 rounded-xl bg-raised"},e))]})}function Fo({id:e}){let t=!!E(e)&&Xt(E(e)),o=en(e);return!t&&!o?null:n("span",{class:"flex shrink-0 gap-1",children:[t?n("span",{class:"size-2 rounded-full bg-brand",title:"Changed from default"}):null,o?n("span",{class:`size-2 rounded-full ${o.level==="error"?"bg-danger":"bg-warn"}`,title:o.level==="error"?"Needs attention":"Has a warning"}):null]})}function qo({id:e}){let t=pe(e);if(!t)return null;let o=!u.query&&u.page===e;return n("a",{href:`#${e}`,"aria-current":o?"page":void 0,onClick:r=>{r.preventDefault(),R(e)},ref:r=>{r&&o&&window.innerWidth<1024&&r.scrollIntoView({block:"nearest",inline:"nearest"})},class:`focus-ring flex min-h-10 items-center gap-2.5 rounded-lg px-3 py-1.5 font-medium transition-colors duration-150 max-lg:min-h-11 max-lg:shrink-0 max-lg:whitespace-nowrap ${o?"bg-brand-soft text-ink":"text-muted hover:bg-raised hover:text-ink"}`,children:[n(_,{name:K[e]||"box",class:`size-[18px] ${o?"text-brand":""}`}),n("span",{class:"min-w-0 flex-1",children:[n("span",{class:"block truncate",children:t.title}),t.subtitle?n("span",{class:"block truncate text-xs font-normal text-faint max-lg:hidden",children:t.subtitle}):null]}),n(Fo,{id:e})]})}function Rn(){return u.sideRender?n("aside",{id:"side","aria-label":"Settings navigation",class:`contents lg:sticky lg:top-14 lg:block lg:h-[calc(100dvh-3.5rem)] lg:overflow-y-auto lg:border-r lg:border-line lg:bg-surface lg:px-3 lg:pt-4 lg:pb-16 lg:transition-[transform,opacity] lg:duration-200 lg:ease-out ${u.sideShown?"lg:translate-x-0 lg:opacity-100":"lg:-translate-x-6 lg:opacity-0"}`,inert:!u.sideOpen,children:[n("div",{role:"search",class:"relative mx-1 mb-3 max-lg:mx-4 max-lg:mt-3",children:[n(_,{name:"search",class:"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-faint"}),n("label",{for:"q",class:"sr-only",children:"Search settings"}),n("input",{id:"q",type:"search",value:u.query,autocomplete:"off",spellcheck:!1,placeholder:"Search settings ( / )",class:"input pl-10",onInput:e=>De(e.currentTarget.value)})]}),n("nav",{"aria-label":"Pages",class:"max-lg:sticky max-lg:top-14 max-lg:z-20 max-lg:mt-3 max-lg:flex max-lg:gap-0.5 max-lg:overflow-x-auto max-lg:border-b max-lg:border-line max-lg:bg-surface max-lg:px-3 max-lg:py-1.5",children:u.nav.map(e=>n("div",{class:"max-lg:contents",children:[e.label?n("div",{class:"mt-4 mb-1 px-3 text-[11px] font-semibold tracking-wider text-faint uppercase max-lg:hidden",children:e.label}):null,e.items.map(t=>n(qo,{id:t},t))]},e.label||"top"))})]}):null}var Uo={success:["checkCircle","text-ok"],warning:["alert","text-warn"],error:["xCircle","text-danger"]};function Wo(){return n("div",{class:"pointer-events-none fixed right-4 bottom-4 left-4 z-50 flex flex-col items-end gap-2 sm:left-auto","aria-live":"polite",children:u.toasts.map(e=>{let[t,o]=Uo[e.kind];return n("div",{role:e.kind==="error"?"alert":"status",class:"pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-xl border border-line bg-surface px-4 py-3 text-sm shadow-pop",children:[n(_,{name:t,class:`mt-0.5 size-4 ${o}`}),n("p",{class:"min-w-0 flex-1 leading-5 text-body",children:e.message}),e.action?n("button",{type:"button",class:"shrink-0 font-medium text-brand hover:underline",onClick:()=>{ze(e.id),e.action.run()},children:e.action.label}):null,n("button",{type:"button",class:"-mr-1 shrink-0 cursor-pointer rounded text-faint hover:text-ink","aria-label":"Dismiss",onClick:()=>ze(e.id),children:n(_,{name:"x"})})]},e.id)})})}var Go=()=>/^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName)||document.activeElement?.isContentEditable;function Vo(){let[,e]=nt(o=>o+1,0);L(()=>{Qt(e);try{localStorage.getItem("tg-sidebar")==="hidden"&&te(!1,{instant:!0})}catch{}let o=s=>{let a=s.ctrlKey||s.metaKey,d=s.key.toLowerCase();a&&d==="s"?(s.preventDefault(),Re()):a&&d==="b"?(s.preventDefault(),te(!u.sideOpen)):a&&d==="k"?(s.preventDefault(),ct()):s.key==="/"&&!a&&!s.altKey&&!Go()&&!u.drawerOpen?(s.preventDefault(),ct()):s.key==="Escape"&&u.query&&!u.drawerOpen&&De("")},r=s=>{u.schema&&H()&&s.preventDefault()},i=()=>Ne();return document.addEventListener("keydown",o),window.addEventListener("beforeunload",r),window.addEventListener("hashchange",i),an(),()=>{document.removeEventListener("keydown",o),window.removeEventListener("beforeunload",r),window.removeEventListener("hashchange",i)}},[]),L(()=>{if(!u.schema||u.validated===M())return;let o=setTimeout(ln,400);return()=>clearTimeout(o)});let t=u.sideRender;return n(w,{children:[n("a",{href:"#main",class:"sr-only z-50 rounded-md bg-brand-solid px-3 py-2 text-white focus:not-sr-only focus:fixed focus:top-3 focus:left-3",children:"Skip to content"}),n(jn,{}),n("div",{class:t?"lg:grid lg:grid-cols-[260px_minmax(0,1fr)] lg:items-start":"",children:[n(Rn,{}),n("main",{id:"main",tabindex:-1,class:"min-w-0 px-4 pt-6 pb-24 focus:outline-none sm:px-8 sm:pt-8",children:n("div",{class:"mx-auto w-full max-w-[760px]",children:n(Mn,{})})})]}),n(hn,{}),n("div",{class:"sr-only",role:"status","aria-live":"polite",children:u.copied?"Copied":""}),n(Wo,{})]})}Rt(n(Vo,{}),document.getElementById("app")); diff --git a/tenxgraph_api/cli/config_editor/validation.py b/tenxgraph_api/cli/config_editor/validation.py index 66494a14..b77e0f0f 100644 --- a/tenxgraph_api/cli/config_editor/validation.py +++ b/tenxgraph_api/cli/config_editor/validation.py @@ -302,7 +302,7 @@ def _check_observability(config: dict, out: _Collector) -> None: f"user_id_mode must be one of {', '.join(_USER_ID_MODES)}.", ) any_enabled = False - for backend in ("logfire", "langsmith"): + for backend in ("logfire", "langsmith", "phoenix"): block = data.get(backend) if block is None: continue @@ -322,7 +322,8 @@ def _check_observability(config: dict, out: _Collector) -> None: ) if not any_enabled: out.warning( - "observability", "Neither Logfire nor LangSmith is enabled, so nothing is traced." + "observability", + "None of Logfire, LangSmith or Phoenix is enabled, so nothing is traced.", ) diff --git a/tenxgraph_api/cli/main.py b/tenxgraph_api/cli/main.py index f102758e..8fa3b6fa 100644 --- a/tenxgraph_api/cli/main.py +++ b/tenxgraph_api/cli/main.py @@ -536,30 +536,40 @@ def init( # noqa: PLR0913 "--name", help="Agent name; required only when a default cannot be inferred.", ), + mode: str | None = typer.Option( + None, + "--mode", + help="Setup: quick (default) or full (storage, API security, evals, tests).", + ), template: str | None = typer.Option( None, "--template", - help="Project template: quick-start or production.", + help="Alias of --mode: quick-start is quick, production is full.", ), auth: str | None = typer.Option( None, "--auth", - help="Production authentication: none, jwt, or custom.", + help="Full mode API authentication: none, jwt (default), or custom.", + ), + storage: str | None = typer.Option( + None, + "--storage", + help="Full mode storage: memory (default), sqlite, postgres, or postgres-memory.", ), cache_store: str | None = typer.Option( None, "--cache-store", - help="Production cache store: memory or redis (default redis, URL from REDIS_URL).", + help="Full mode cache store: memory or redis (default follows --storage).", ), rate_limit: bool | None = typer.Option( None, "--rate-limit/--no-rate-limit", - help="Production rate limiting (default on).", + help="Full mode rate limiting (default on).", ), token_revocation: bool | None = typer.Option( None, "--token-revocation/--no-token-revocation", - help="Production token revocation (default on with jwt or custom auth).", + help="Full mode token revocation (default on with jwt or custom auth).", ), provider: str | None = typer.Option( None, @@ -571,11 +581,31 @@ def init( # noqa: PLR0913 "--websocket/--no-websocket", help="Enable WebSocket streaming and realtime audio (adds the websocket extra).", ), + evals: bool | None = typer.Option( + None, + "--evals/--no-evals", + help="Full mode: scaffold evals (default on).", + ), + tests: bool | None = typer.Option( + None, + "--tests/--no-tests", + help="Full mode: scaffold tests (default on).", + ), + ag_ui: bool | None = typer.Option( + None, + "--ag-ui/--no-ag-ui", + help="Full mode: enable the AG-UI endpoint (default off).", + ), + otel: bool | None = typer.Option( + None, + "--otel/--no-otel", + help="Full mode: add OpenTelemetry (default off).", + ), yes: bool = typer.Option( False, "--yes", "-y", - help="Accept recommended defaults and do not prompt.", + help="Accept recommended defaults and do not prompt (quick unless --mode full).", ), non_interactive: bool = typer.Option( False, @@ -614,12 +644,18 @@ def init( # noqa: PLR0913 force=force, agent_name=name, template=template, + mode=mode, auth=auth, + storage=storage, cache_store=cache_store, rate_limit=rate_limit, token_revocation=token_revocation, provider=provider, websocket=websocket, + evals=evals, + tests=tests, + ag_ui=ag_ui, + otel=otel, yes=yes, non_interactive=non_interactive, dry_run=dry_run, diff --git a/tenxgraph_api/cli/templates/prod/.env.example b/tenxgraph_api/cli/templates/prod/.env.example index 1aacf135..394cca4d 100644 --- a/tenxgraph_api/cli/templates/prod/.env.example +++ b/tenxgraph_api/cli/templates/prod/.env.example @@ -57,18 +57,23 @@ SNOWFLAKE_WORKER_ID=2 ################################# SENTRY_DSN="" +##[IF_POSTGRES]## ################################# ###### Postgres Config ########## ################################# -# Durable thread state (PgCheckpointer). Local example value; replace it in production. +# Durable thread state, plus the long-term memory store when enabled. Local example value; +# replace it in production. DATABASE_URL="postgresql://postgres:postgres@localhost:5432/agent" +##[/IF_POSTGRES]## +##[IF_REDIS]## ################################# ###### Redis Config ############# ################################# -# The checkpointer's hot thread cache and the API's cache store (rate limiting, run locks and -# caps, token revocation). Local example value; replace it in production. +# Hot thread cache for the Postgres checkpointer and the API's cache store (rate limiting, +# run locks and caps, token revocation). Local example value; replace it in production. REDIS_URL="redis://localhost:6379/0" +##[/IF_REDIS]## ##[IF_JWT]## ################################# @@ -82,3 +87,16 @@ JWT_ALGORITHM="HS256" ###### API Keys ################# ################################# $llm_api_key_env="" +##[IF_EMBED_KEY]## +# Embeddings for the memory store use OpenAI. +OPENAI_API_KEY="" +##[/IF_EMBED_KEY]## + +##[IF_OTEL]## +################################# +###### OpenTelemetry ############ +################################# +OTEL_ENABLED="true" +OTEL_SERVICE_NAME="$agent_name_slug" +OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318" +##[/IF_OTEL]## diff --git a/tenxgraph_api/cli/templates/prod/graph/agent.py b/tenxgraph_api/cli/templates/prod/graph/agent.py index 049ac671..54fe3714 100644 --- a/tenxgraph_api/cli/templates/prod/graph/agent.py +++ b/tenxgraph_api/cli/templates/prod/graph/agent.py @@ -1,9 +1,28 @@ +# ##[IF_POSTGRES]## import os +# ##[/IF_POSTGRES]## from datetime import datetime from tenxgraph.core.state import MessageContextManager from tenxgraph.prebuilt.agent import ReactAgent +# ##[IF_POSTGRES]## from tenxgraph.storage.checkpointer import PgCheckpointer +# ##[/IF_POSTGRES]## +# ##[IF_SQLITE]## +from tenxgraph.storage.checkpointer import SqliteCheckpointer +# ##[/IF_SQLITE]## +# ##[IF_INMEM]## +from tenxgraph.storage.checkpointer import InMemoryCheckpointer +# ##[/IF_INMEM]## +# ##[IF_STORE]## +from tenxgraph.storage.store import PgVectorStore +# ##[/IF_STORE]## +# ##[IF_EMBED_GOOGLE]## +from tenxgraph.storage.store import GoogleEmbedding +# ##[/IF_EMBED_GOOGLE]## +# ##[IF_EMBED_OPENAI]## +from tenxgraph.storage.store import OpenAIEmbedding +# ##[/IF_EMBED_OPENAI]## from dotenv import load_dotenv from injectq import InjectQ @@ -14,6 +33,8 @@ load_dotenv() +# Storage is chosen at init; run `10xgraph config` to review or change it. +# ##[IF_POSTGRES]## # Durable thread state: Postgres keeps every thread, Redis caches the hot ones. # Set DATABASE_URL and REDIS_URL in .env (see .env.example). The tables are created (or # upgraded) when the server starts, so a fresh database needs no setup step. @@ -21,12 +42,30 @@ postgres_dsn=os.getenv("DATABASE_URL"), redis_url=os.getenv("REDIS_URL"), ) +# ##[/IF_POSTGRES]## +# ##[IF_SQLITE]## +# Durable thread state in a single local file. Fine for one process; use Postgres to run +# several workers. +checkpointer = SqliteCheckpointer(db_path="agent.db") +# ##[/IF_SQLITE]## +# ##[IF_INMEM]## +# Threads live only in this process: they are lost on restart and not shared between +# workers. Switch to a durable checkpointer before deploying. +checkpointer = InMemoryCheckpointer() +# ##[/IF_INMEM]## +# ##[IF_STORE]## -# Local testing without Postgres and Redis: comment out the block above and use this. -# Threads live only in this process (lost on restart, not shared between workers), -# so switch back to PgCheckpointer once you are ready to deploy. -# from tenxgraph.storage.checkpointer import InMemoryCheckpointer -# checkpointer = InMemoryCheckpointer() +# Long-term memory in the same Postgres (pgvector). +store = PgVectorStore( + # ##[IF_EMBED_GOOGLE]## + embedding=GoogleEmbedding(), + # ##[/IF_EMBED_GOOGLE]## + # ##[IF_EMBED_OPENAI]## + embedding=OpenAIEmbedding(), + # ##[/IF_EMBED_OPENAI]## + dsn=os.getenv("DATABASE_URL"), +) +# ##[/IF_STORE]## # Dependency injection container; 10xgraph.json points at it ("injectq": "graph.agent:container"). container = InjectQ.get_instance() @@ -64,5 +103,8 @@ app = react_agent.compile( checkpointer=checkpointer, + # ##[IF_STORE]## + store=store, + # ##[/IF_STORE]## callback_manager=callback_manager, ) diff --git a/tenxgraph_api/cli/templates/prod/tests/conftest.py b/tenxgraph_api/cli/templates/prod/tests/conftest.py index 1f1d0b94..d934410c 100644 --- a/tenxgraph_api/cli/templates/prod/tests/conftest.py +++ b/tenxgraph_api/cli/templates/prod/tests/conftest.py @@ -4,6 +4,13 @@ os.environ.setdefault("$llm_api_key_env", "test-key") +# ##[IF_POSTGRES]## # PgCheckpointer connects lazily, so these only need to be well-formed to import the graph. os.environ.setdefault("DATABASE_URL", "postgresql://postgres:postgres@localhost:5432/agent") +# ##[/IF_POSTGRES]## +# ##[IF_REDIS]## os.environ.setdefault("REDIS_URL", "redis://localhost:6379/0") +# ##[/IF_REDIS]## +# ##[IF_EMBED_KEY]## +os.environ.setdefault("OPENAI_API_KEY", "test-key") +# ##[/IF_EMBED_KEY]## diff --git a/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md b/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md index 29f60678..3de7574f 100644 --- a/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +++ b/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md @@ -71,9 +71,16 @@ ACP support is in `tenxgraph/runtime/protocols/acp.py`. Treat it as a runtime pr - Events and spans keep content on purpose. Redis, RabbitMQ and Kafka publishers send full events (message text, tool I/O); send them only to trusted systems. OTEL records prompts and tool I/O only at `ObservabilityLevel.FULL`. - TLS is the developer's choice, never forced. Recommend it in production whenever possible, and always on networks the developer does not control; a broker reachable only inside a secured private network (private subnet or VPC, or an encrypting service mesh) can run without it. Redis: `rediss://` URL. RabbitMQ: `amqps://` URL. Kafka: opt-in config keys `security_protocol`, `ssl_context`, `ssl_cafile`, `ssl_certfile`, `ssl_keyfile`, `sasl_mechanism`, `sasl_plain_username`, `sasl_plain_password` (passed to aiokafka only when set; `ssl_context` wins over the file keys). -- `user_id_mode` (`"raw"` default, `"hash"`, `"mask"`) controls `user_id` on OTEL spans, and so on Logfire and LangSmith. It does not apply to Redis, RabbitMQ, Kafka or Console events. `hash` is the first 16 hex chars of SHA-256, or HMAC-SHA256 when env `TENXGRAPH_TRACE_USER_ID_KEY` is set (set it; plain SHA-256 of short ids can be guessed). Options: `OtelPublisher(level=..., user_id_mode=...)`, `setup_tracing`, `setup_logfire`, `setup_langsmith`, `setup_observability(graph, {"user_id_mode": ...})`. Invalid value raises `ValueError`. +- `user_id_mode` (`"raw"` default, `"hash"`, `"mask"`) controls `user_id` on OTEL spans, and so on Logfire, LangSmith and Phoenix. It does not apply to Redis, RabbitMQ, Kafka or Console events. `hash` is the first 16 hex chars of SHA-256, or HMAC-SHA256 when env `TENXGRAPH_TRACE_USER_ID_KEY` is set (set it; plain SHA-256 of short ids can be guessed). Options: `OtelPublisher(level=..., user_id_mode=...)`, `setup_tracing`, `setup_logfire`, `setup_langsmith`, `setup_observability(graph, {"user_id_mode": ...})`. Invalid value raises `ValueError`. - Server: `10xgraph.json` `"observability": {"user_id_mode": "hash"}`; also in the `10xgraph config` editor. +## Phoenix (Arize) + +- Install `pip install '10xgraph[phoenix]'` (also in `10xgraph[observability]`). Enable in `10xgraph.json`: `"observability": {"phoenix": {"enabled": true, "project": "my-agent", "endpoint": null}}`. +- `project` falls back to env `PHOENIX_PROJECT` / `PHOENIX_PROJECT_NAME`; `endpoint` is the base collector URL (env `PHOENIX_COLLECTOR_ENDPOINT`, then `http://localhost:6006`), `/v1/traces` is appended. +- In Python: `setup_phoenix(graph, project=..., endpoint=..., api_key=...)` or `PhoenixPublisher(...)`, both from `tenxgraph.runtime.publisher`, used like `setup_langsmith` / `LangsmithPublisher`. Call before `graph.compile()`. +- `PHOENIX_API_KEY` is env only and optional (a local Phoenix needs none). Never put it in the config file. + ## Rules - Prefer publisher events for observability instead of ad hoc print/log statements in reusable runtime paths. diff --git a/tenxgraph_api/src/app/core/config/graph_config.py b/tenxgraph_api/src/app/core/config/graph_config.py index 25846d5f..90ce9da1 100644 --- a/tenxgraph_api/src/app/core/config/graph_config.py +++ b/tenxgraph_api/src/app/core/config/graph_config.py @@ -903,17 +903,18 @@ def remote_tools(self) -> list[RemoteToolConfig]: @property def observability(self) -> dict | None: - """The declarative ``observability`` block (Logfire / LangSmith). + """The declarative ``observability`` block (Logfire / LangSmith / Phoenix). Shape mirrors ``tenxgraph.runtime.publisher.setup_observability``:: { "level": "standard", "logfire": {"enabled": true, "service_name": "my-agent", ...}, - "langsmith": {"enabled": true, "project": "my-agent", "endpoint": null} + "langsmith": {"enabled": true, "project": "my-agent", "endpoint": null}, + "phoenix": {"enabled": true, "project": "my-agent", "endpoint": null} } - Secrets (``LOGFIRE_TOKEN``, ``LANGSMITH_API_KEY``) stay in the + Secrets (``LOGFIRE_TOKEN``, ``LANGSMITH_API_KEY``, ``PHOENIX_API_KEY``) stay in the environment and are never read from here. """ return self.data.get("observability", None) diff --git a/tenxgraph_api/src/app/core/config/setup_middleware.py b/tenxgraph_api/src/app/core/config/setup_middleware.py index af7e790b..14e8d7cc 100644 --- a/tenxgraph_api/src/app/core/config/setup_middleware.py +++ b/tenxgraph_api/src/app/core/config/setup_middleware.py @@ -296,7 +296,7 @@ def _attach_otel_publisher( def _setup_observability(container: InjectQ | None, graph_config: GraphConfig | None) -> None: - """Wire Logfire/LangSmith from the ``observability`` block of ``10xgraph.json``. + """Wire Logfire/LangSmith/Phoenix from the ``observability`` block of ``10xgraph.json``. Configures the tracer provider(s)/exporters and binds an ``OtelPublisher`` into the DI container. This runs at app-construction time, before the graph @@ -306,7 +306,7 @@ def _setup_observability(container: InjectQ | None, graph_config: GraphConfig | preserves a publisher already present in the container instead of clobbering it with ``None``. - Secrets stay in the environment (``LOGFIRE_TOKEN``, ``LANGSMITH_API_KEY``); + Secrets stay in the environment (``LOGFIRE_TOKEN``, ``LANGSMITH_API_KEY``, ``PHOENIX_API_KEY``); only non-secret settings live in ``10xgraph.json``. """ if container is None or graph_config is None: @@ -318,7 +318,8 @@ def _setup_observability(container: InjectQ | None, graph_config: GraphConfig | logfire_on = bool((obs_cfg.get("logfire") or {}).get("enabled", False)) langsmith_on = bool((obs_cfg.get("langsmith") or {}).get("enabled", False)) - if not logfire_on and not langsmith_on: + phoenix_on = bool((obs_cfg.get("phoenix") or {}).get("enabled", False)) + if not logfire_on and not langsmith_on and not phoenix_on: return try: @@ -362,9 +363,10 @@ def _setup_observability(container: InjectQ | None, graph_config: GraphConfig | _bind_publisher(container, CompositePublisher([existing, otel_publisher])) logger.info( - "Observability enabled (logfire=%s, langsmith=%s, level=%s)", + "Observability enabled (logfire=%s, langsmith=%s, phoenix=%s, level=%s)", logfire_on, langsmith_on, + phoenix_on, level, ) diff --git a/tests/cli/test_group6_cli_templates.py b/tests/cli/test_group6_cli_templates.py index 4df2d813..63975bdc 100644 --- a/tests/cli/test_group6_cli_templates.py +++ b/tests/cli/test_group6_cli_templates.py @@ -8,6 +8,7 @@ from pathlib import Path from tenxgraph_api.cli.commands.init import InitCommand +from tenxgraph_api.cli.commands.init_context import build_config from tenxgraph_api.cli.core.output import OutputFormatter from tenxgraph_api.cli.templates.defaults import generate_k8s_manifest_content @@ -29,29 +30,29 @@ def _context(auth: str, cache_store: str = "redis", **flags) -> dict: def test_production_rate_limit_is_per_user_when_auth_is_on() -> None: for auth in ("jwt", "custom"): - config = _cmd()._build_config(_context(auth), True) + config = build_config(_context(auth), True) assert config["cache_store"]["rate_limit"]["by"] == "user" - config = _cmd()._build_config(_context("none"), True) + config = build_config(_context("none"), True) assert config["cache_store"]["rate_limit"]["by"] == "ip" def test_production_cache_store_follows_the_answers() -> None: - config = _cmd()._build_config(_context("jwt"), True) + config = build_config(_context("jwt"), True) assert config["cache_store"]["type"] == "redis" assert "url" not in config["cache_store"] assert config["cache_store"]["token_revocation"] is True - memory = _cmd()._build_config(_context("jwt", "memory"), True)["cache_store"] + memory = build_config(_context("jwt", "memory"), True)["cache_store"] assert memory["type"] == "memory" assert "url" not in memory assert memory["rate_limit"]["enabled"] is True assert memory["token_revocation"] is True - assert "token_revocation" not in _cmd()._build_config(_context("none"), True)["cache_store"] - off = _cmd()._build_config(_context("jwt", rate_limit=False, token_revocation=False), True) + assert "token_revocation" not in build_config(_context("none"), True)["cache_store"] + off = build_config(_context("jwt", rate_limit=False, token_revocation=False), True) assert off["cache_store"] == {"type": "redis"} for store in ("redis", "memory"): - config = _cmd()._build_config(_context("jwt", store), True) + config = build_config(_context("jwt", store), True) assert not {"redis", "rate_limit", "token_revocation"} & set(config) @@ -90,7 +91,7 @@ def test_static_templates_load_through_graph_config(monkeypatch) -> None: def test_generated_exclude_paths_use_the_real_redoc_path() -> None: - config = _cmd()._build_config(_context("jwt"), True) + config = build_config(_context("jwt"), True) assert "/redocs" in config["cache_store"]["rate_limit"]["exclude_paths"] diff --git a/tests/cli/test_init_full.py b/tests/cli/test_init_full.py new file mode 100644 index 00000000..d10f5440 --- /dev/null +++ b/tests/cli/test_init_full.py @@ -0,0 +1,241 @@ +"""`10xgraph init` Full mode: presets, template blocks, flags.""" + +from __future__ import annotations + +import io +import json +from pathlib import Path + +import pytest + +from tenxgraph_api.cli.commands.init import InitCommand +from tenxgraph_api.cli.commands.init_context import ( + build_config, + build_context, + strip_blocks, + template_flags, + template_vars, +) +from tenxgraph_api.cli.core.output import OutputFormatter +from tenxgraph_api.cli.exceptions import FileOperationError, ValidationError + + +def _full(**options) -> dict: + return build_context(path=".", agent_name="demo", mode="full", **options) + + +def _init(path: Path, **options) -> tuple[int, str]: + stream = io.StringIO() + code = InitCommand(OutputFormatter(stream=stream)).execute( + path=str(path), agent_name="Demo", non_interactive=True, **options + ) + return code, stream.getvalue() + + +# --- storage presets ----------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("storage", "cache", "store_key", "core_extras"), + [ + ("memory", "memory", False, "google-genai"), + ("sqlite", "memory", False, "google-genai,sqlite_checkpoint"), + ("postgres", "redis", False, "google-genai,pg_checkpoint"), + ("postgres-memory", "redis", True, "google-genai,pg_checkpoint,pgvector"), + ], +) +def test_storage_preset(storage: str, cache: str, store_key: bool, core_extras: str) -> None: + context = _full(storage=storage) + assert context["storage"] == storage + assert context["cache_store"] == cache + config = build_config(context, True) + assert config["cache_store"]["type"] == cache + assert ("store" in config) is store_key + if store_key: + assert config["store"] == "graph.agent:store" + assert template_vars(context)["core_requirement"] == f"10xgraph[{core_extras}]" + + +def test_cache_store_flag_overrides_the_preset() -> None: + assert _full(storage="postgres", cache_store="memory")["cache_store"] == "memory" + + +def test_template_flags_follow_storage_and_provider() -> None: + flags = template_flags(_full(storage="postgres-memory", provider="anthropic")) + assert flags["postgres"] and flags["redis"] and flags["store"] + assert flags["embed_openai"] and flags["embed_key"] and not flags["embed_google"] + assert template_flags(_full(provider="google", storage="postgres-memory"))["embed_google"] + flags = template_flags(_full(storage="memory")) + assert flags["inmem"] and not flags["redis"] and not flags["store"] + assert template_flags(_full(storage="sqlite"))["sqlite"] + + +# --- security presets ---------------------------------------------------------------- + + +@pytest.mark.parametrize( + ("auth", "rate_limit", "revocation", "by"), + [("jwt", True, True, "user"), ("none", True, False, "ip"), ("custom", True, True, "user")], +) +def test_security_preset(auth: str, rate_limit: bool, revocation: bool, by: str) -> None: + context = _full(auth=auth) + assert (context["auth"], context["rate_limit"], context["token_revocation"]) == ( + auth, + rate_limit, + revocation, + ) + assert context["rl_by"] == by + store = build_config(context, True)["cache_store"] + assert store["rate_limit"]["by"] == by + assert ("token_revocation" in store) is revocation + + +def test_full_defaults() -> None: + context = _full() + assert (context["storage"], context["auth"], context["cache_store"]) == ( + "memory", + "jwt", + "memory", + ) + assert (context["evals"], context["tests"]) == (True, True) + assert not any(context[k] for k in ("websocket", "ag_ui", "otel")) + + +# --- extras -------------------------------------------------------------------------- + + +def test_extras_flags_skip_template_dirs_and_set_config(tmp_path: Path) -> None: + command = InitCommand(OutputFormatter(stream=io.StringIO())) + off, on = tmp_path / "off", tmp_path / "on" + assert command.execute(path=str(off), mode="full", evals=False, tests=False, yes=True) == 0 + assert not (off / "evals").exists() and not (off / "tests").exists() + assert (off / "graph" / "agent.py").exists() + + assert ( + command.execute(path=str(on), mode="full", websocket=True, ag_ui=True, otel=True, yes=True) + == 0 + ) + assert (on / "evals").is_dir() and (on / "tests").is_dir() + config = json.loads((on / "10xgraph.json").read_text()) + assert config["websocket"] == {"enabled": True} + assert config["ag_ui"]["enabled"] is True + api = template_vars(_full(websocket=True, ag_ui=True, otel=True))["api_requirement"] + assert api == "10xgraph-api[jwt,websocket,ag-ui,otel]" + + +# --- strip_blocks -------------------------------------------------------------------- + + +def test_strip_blocks_keeps_true_and_drops_false() -> None: + text = "a\n##[IF_JWT]##\nb\n##[/IF_JWT]##\n##[IF_REDIS]##\nc\n##[/IF_REDIS]##\nd\n" + assert strip_blocks(text, {"jwt": True, "redis": False}) == "a\nb\nd\n" + + +def test_strip_blocks_handles_comment_prefix_and_nesting() -> None: + text = "# ##[IF_A]##\nx\n# ##[IF_B]##\ny\n# ##[/IF_B]##\n# ##[/IF_A]##\nz\n" + assert strip_blocks(text, {"a": True, "b": True}) == "x\ny\nz\n" + assert strip_blocks(text, {"a": True, "b": False}) == "x\nz\n" + assert strip_blocks(text, {"a": False, "b": True}) == "z\n" + + +@pytest.mark.parametrize( + "text", + [ + "##[IF_TYPO]##\nx\n##[/IF_TYPO]##\n", + "##[IF_JWT]##\nx\n", + "x\n##[/IF_JWT]##\n", + "##[IF_JWT]##\n##[IF_REDIS]##\n##[/IF_JWT]##\n##[/IF_REDIS]##\n", + "value ##[IF_JWT]## inline ##[/IF_JWT]##\n", + ], +) +def test_strip_blocks_rejects_bad_markers(text: str) -> None: + with pytest.raises(FileOperationError): + strip_blocks(text, {"jwt": True, "redis": True}) + + +# --- flags and validation ------------------------------------------------------------ + + +@pytest.mark.parametrize( + ("options", "choices"), + [ + ({"mode": "huge"}, "quick, full"), + ({"storage": "mongo"}, "memory, sqlite, postgres, postgres-memory"), + ({"auth": "basic"}, "none, jwt, custom"), + ({"cache_store": "valkey"}, "memory, redis"), + ({"provider": "mistral"}, "google, openai, anthropic"), + ], +) +def test_invalid_values_list_the_choices(options: dict, choices: str) -> None: + with pytest.raises(ValidationError) as exc: + build_context(path=".", agent_name="demo", **{"mode": "full", **options}) + assert choices in exc.value.message + + +def test_invalid_template_and_conflicting_mode() -> None: + with pytest.raises(ValidationError): + build_context(path=".", agent_name="demo", template="enterprise") + with pytest.raises(ValidationError): + build_context(path=".", agent_name="demo", mode="quick", template="production") + + +def test_quick_rejects_full_only_options() -> None: + for option in ({"storage": "sqlite"}, {"evals": True}, {"ag_ui": True}, {"otel": True}): + with pytest.raises(ValidationError, match="--mode full"): + build_context(path=".", agent_name="demo", **option) + + +def test_template_aliases() -> None: + assert build_context(path=".", agent_name="d", template="production")["setup_type"] == ( + "production" + ) + assert build_context(path=".", agent_name="d", template="quick-start")["setup_type"] == ( + "quick_start" + ) + + +def test_yes_is_quick_unless_mode_full(tmp_path: Path) -> None: + command = InitCommand(OutputFormatter(stream=io.StringIO())) + quick, full = tmp_path / "quick", tmp_path / "full" + assert command.execute(path=str(quick), yes=True) == 0 + assert command.execute(path=str(full), mode="full", yes=True) == 0 + assert not (quick / "evals").exists() and (full / "evals").is_dir() + assert json.loads((quick / "10xgraph.json").read_text())["auth"] is None + assert json.loads((full / "10xgraph.json").read_text())["auth"] == {"method": "jwt"} + + +def test_invalid_storage_fails_the_command(tmp_path: Path) -> None: + code, text = _init(tmp_path / "x", mode="full", storage="mongo") + assert code != 0 + assert "memory, sqlite, postgres, postgres-memory" in text + assert not (tmp_path / "x").exists() + + +def test_dry_run_lists_files_by_extras(tmp_path: Path) -> None: + code, text = _init(tmp_path / "d", mode="full", evals=False, tests=True, dry_run=True) + assert code == 0 + assert "Files that would be created" in text + assert "evals/" not in text and "tests/" in text + assert not (tmp_path / "d").exists() + + +def test_anthropic_store_adds_openai_extra_for_embeddings() -> None: + context = _full(storage="postgres-memory", provider="anthropic") + assert ( + template_vars(context)["core_requirement"] + == "10xgraph[anthropic,pg_checkpoint,pgvector,openai]" + ) + openai = _full(storage="postgres-memory", provider="openai") + assert template_vars(openai)["core_requirement"] == "10xgraph[openai,pg_checkpoint,pgvector]" + + +@pytest.mark.parametrize(("tests", "evals"), [(True, True), (True, False), (False, True)]) +def test_config_test_and_evaluation_sections_follow_flags(tests: bool, evals: bool) -> None: + config = build_config(_full(tests=tests, evals=evals), True) + assert ("test" in config) is tests + assert ("evaluation" in config) is evals + if tests: + assert config["test"] == {"path": "tests", "coverage": True, "coverage_threshold": 70} + if evals: + assert config["evaluation"]["threshold"] == 0.75 + assert config["evaluation"]["directory"] == "evals" diff --git a/tests/cli/test_init_non_interactive.py b/tests/cli/test_init_non_interactive.py index 2e5d1a61..fb9f0b84 100644 --- a/tests/cli/test_init_non_interactive.py +++ b/tests/cli/test_init_non_interactive.py @@ -77,6 +77,7 @@ def test_production_dependencies_follow_the_answers(tmp_path: Path) -> None: agent_name="Secure Agent", template="production", auth="jwt", + storage="postgres", provider="openai", non_interactive=True, ) @@ -109,6 +110,7 @@ def test_production_without_jwt_installs_redis_but_not_jwt(tmp_path: Path) -> No agent_name="Open Agent", template="production", auth="custom", + storage="postgres", cache_store="memory", provider="anthropic", non_interactive=True, @@ -143,7 +145,7 @@ def test_websocket_off_by_default_and_not_installed(tmp_path: Path) -> None: path=str(target), template="production", provider="openai", non_interactive=True ) assert result == 0 - assert _dependencies(target) == ["10xgraph[openai,pg_checkpoint]", "10xgraph-api[jwt,redis]"] + assert _dependencies(target) == ["10xgraph[openai]", "10xgraph-api[jwt]"] config = json.loads((target / "10xgraph.json").read_text()) assert config["websocket"] == {"enabled": False} @@ -162,10 +164,7 @@ def test_websocket_on_adds_the_extra_and_enables_the_routes(tmp_path: Path) -> N non_interactive=True, ) assert result == 0 - assert _dependencies(target) == [ - "10xgraph[anthropic,pg_checkpoint]", - "10xgraph-api[jwt,redis,websocket]", - ] + assert _dependencies(target) == ["10xgraph[anthropic]", "10xgraph-api[jwt,websocket]"] config = json.loads((target / "10xgraph.json").read_text()) assert config["websocket"] == {"enabled": True} @@ -273,8 +272,10 @@ def _written(tmp_path: Path, **kwargs) -> dict: def test_cache_store_choices(tmp_path: Path) -> None: - assert _written(tmp_path)["cache_store"]["type"] == "redis" - assert _written(tmp_path / "m", cache_store="memory")["cache_store"]["type"] == "memory" + assert _written(tmp_path)["cache_store"]["type"] == "memory" + assert _written(tmp_path / "p", storage="postgres")["cache_store"]["type"] == "redis" + override = _written(tmp_path / "m", storage="postgres", cache_store="memory") + assert override["cache_store"]["type"] == "memory" assert "url" not in _written(tmp_path / "r", cache_store="redis")["cache_store"] @@ -287,12 +288,12 @@ def test_invalid_cache_store_lists_the_choices(tmp_path: Path) -> None: non_interactive=True, ) assert result != 0 - assert "memory or redis" in stream.getvalue() + assert "memory, redis" in stream.getvalue() def test_rate_limit_off_and_token_revocation_off(tmp_path: Path) -> None: store = _written(tmp_path / "a", rate_limit=False, token_revocation=False)["cache_store"] - assert store == {"type": "redis"} + assert store == {"type": "memory"} store = _written(tmp_path / "b", token_revocation=False)["cache_store"] assert "token_revocation" not in store and store["rate_limit"]["enabled"] is True @@ -302,22 +303,27 @@ def test_token_revocation_is_ignored_without_auth(tmp_path: Path) -> None: assert "token_revocation" not in store -def test_quick_start_ignores_cache_flags(tmp_path: Path) -> None: - import json - - target = tmp_path / "q" - result = InitCommand(OutputFormatter(stream=io.StringIO())).execute( - path=str(target), cache_store="redis", rate_limit=True, non_interactive=True +def test_quick_start_rejects_full_only_flags(tmp_path: Path) -> None: + stream = io.StringIO() + result = InitCommand(OutputFormatter(stream=stream)).execute( + path=str(tmp_path / "q"), cache_store="redis", rate_limit=True, non_interactive=True ) - assert result == 0 - assert "cache_store" not in json.loads((target / "10xgraph.json").read_text()) + assert result != 0 + assert "--mode full" in stream.getvalue() -def test_next_steps_point_to_config_for_every_template(tmp_path: Path) -> None: - for template in ("quick-start", "production"): +def test_next_steps_point_to_config_for_every_mode(tmp_path: Path) -> None: + texts = {} + for mode in ("quick", "full"): stream = io.StringIO() InitCommand(OutputFormatter(stream=stream)).execute( - path=str(tmp_path / template), template=template, non_interactive=True + path=str(tmp_path / mode), mode=mode, non_interactive=True ) - text = " ".join(stream.getvalue().split()) - assert "Run 10xgraph config for full control" in text + texts[mode] = " ".join(stream.getvalue().split()) + assert "10xgraph config for more settings" in texts["quick"] + assert "Before production" not in texts["quick"] + assert "10xgraph config to review auth" in texts["full"] + assert "Before production" in texts["full"] + assert "10xgraph build" in texts["full"] + assert "JWT_SECRET_KEY" in texts["full"] + assert "10xgraph skills" in texts["full"] diff --git a/tests/cli/test_init_prod.py b/tests/cli/test_init_prod.py index 1dba51f7..7d6c2acb 100644 --- a/tests/cli/test_init_prod.py +++ b/tests/cli/test_init_prod.py @@ -6,6 +6,7 @@ from pathlib import Path from tenxgraph_api.cli.commands.init import InitCommand +from tenxgraph_api.cli.commands.init_context import build_config from tenxgraph_api.cli.core.output import OutputFormatter from tenxgraph_api.src.app.core.auth.auth_config import check_auth_config @@ -80,43 +81,45 @@ def test_init_prod_skips_binary_template_artifacts() -> None: ) -def test_production_template_defaults_to_recommended_setup(tmp_path: Path) -> None: - """With no answers, production scaffolds JWT, ownership, Postgres + Redis and rate limits.""" +def test_full_mode_defaults_to_memory_storage_and_jwt(tmp_path: Path) -> None: + """With no answers, Full scaffolds JWT, rate limits and in-memory storage.""" import json import tomllib - target = tmp_path / "default-prod" + target = tmp_path / "default-full" command = InitCommand(OutputFormatter(stream=io.StringIO())) - result = command.execute( - path=str(target), agent_name="Default Prod", template="production", non_interactive=True - ) + result = command.execute(path=str(target), agent_name="Default Full", mode="full", yes=True) assert result == 0 config = json.loads((target / "10xgraph.json").read_text()) assert config["auth"] == {"method": "jwt"} assert "authorization" not in config + assert "store" not in config assert config["ag_ui"] == {"enabled": False, "allow_client_tools": False} assert config["remote_tools"] == [] assert check_auth_config(config, production=True) == [] - assert config["cache_store"]["type"] == "redis" - assert "url" not in config["cache_store"] + assert config["cache_store"]["type"] == "memory" assert config["cache_store"]["rate_limit"]["enabled"] is True assert config["cache_store"]["token_revocation"] is True + assert "InMemoryCheckpointer()" in (target / "graph" / "agent.py").read_text() + assert (target / "evals").is_dir() and (target / "tests").is_dir() + + dependencies = tomllib.loads((target / "pyproject.toml").read_text())["project"]["dependencies"] + assert dependencies == ["10xgraph[google-genai]", "10xgraph-api[jwt]"] - agent_py = (target / "graph" / "agent.py").read_text() - assert "PgCheckpointer(" in agent_py - memory_lines = [line for line in agent_py.splitlines() if "InMemoryCheckpointer" in line] - assert memory_lines - assert all(line.lstrip().startswith("#") for line in memory_lines) +def test_full_postgres_scaffolds_pg_checkpointer_and_env(tmp_path: Path) -> None: + target = tmp_path / "pg" + command = InitCommand(OutputFormatter(stream=io.StringIO())) + result = command.execute( + path=str(target), agent_name="Pg", mode="full", storage="postgres", non_interactive=True + ) + assert result == 0 + assert "PgCheckpointer(" in (target / "graph" / "agent.py").read_text() env_example = (target / ".env.example").read_text() assert "DATABASE_URL=" in env_example assert "REDIS_URL=" in env_example - dependencies = tomllib.loads((target / "pyproject.toml").read_text())["project"]["dependencies"] - assert any("pg_checkpoint" in dep for dep in dependencies) - assert any(dep.startswith("10xgraph-api[") and "redis" in dep for dep in dependencies) - class _Scripted: """Prompt double: answers by message prefix, else accepts the default; records asks.""" @@ -142,10 +145,11 @@ def confirm(self, message, default=False, **_kwargs): return self._answer(message, default) def select(self, message, _choices, default=None, **_kwargs): - if message == "Which project template?": - return "production" return self._answer(message, default) + def checkbox(self, message, choices, **_kwargs): + return self._answer(message, [c.value for c in choices if c.checked]) + def _run_prompts(monkeypatch, answers=None): output = io.StringIO() @@ -155,45 +159,46 @@ def _run_prompts(monkeypatch, answers=None): return InitCommand(output=formatter)._prompt_user(), scripted, output.getvalue() -def test_interactive_production_asks_only_the_basics(monkeypatch) -> None: - """Name, template and provider are the only questions; Production gets the full setup.""" - context, scripted, text = _run_prompts(monkeypatch) +_FULL = {"Which setup?": "full"} + +def test_interactive_quick_asks_three_questions(monkeypatch) -> None: + context, scripted, _ = _run_prompts(monkeypatch) + assert scripted.asked == ["Which setup?", "What is your agent name?", "Which AI provider?"] + assert context is not None and context["setup_type"] == "quick_start" + assert context["storage"] is None and context["cache_store"] is None + assert context["rate_limit"] is False and context["auth"] == "none" + assert build_config(context, False).keys() >= {"agent"} + assert "cache_store" not in build_config(context, False) + + +def test_interactive_full_asks_three_more_questions(monkeypatch) -> None: + context, scripted, _ = _run_prompts(monkeypatch, _FULL) assert scripted.asked == [ + "Which setup?", "What is your agent name?", "Which AI provider?", + "Where should state be stored?", + "How should the API be secured?", + "Which extras?", ] assert context is not None assert context["setup_type"] == "production" + assert context["storage"] == "memory" assert context["auth"] == "jwt" - assert context["cache_store"] == "redis" + assert context["cache_store"] == "memory" assert context["rate_limit"] is True assert context["token_revocation"] is True assert context["id_generator"] == "uuid" - assert context["websocket"] is False - assert "cannot reach" not in text + assert (context["evals"], context["tests"]) == (True, True) + assert (context["websocket"], context["ag_ui"], context["otel"]) == (False, False, False) -def test_interactive_quick_start_has_no_cache_store(monkeypatch) -> None: - class _Quick(_Scripted): - def select(self, message, _choices, default=None, **_kwargs): - return "quick_start" if message == "Which project template?" else default - - output = io.StringIO() - formatter = OutputFormatter(stream=output) - monkeypatch.setattr(formatter, "prompts", lambda: _Quick()) - context = InitCommand(output=formatter)._prompt_user() - assert context is not None and context["setup_type"] == "quick_start" - assert context["cache_store"] is None and context["rate_limit"] is False - assert InitCommand(output=formatter)._build_config(context, False).keys() >= {"agent"} - assert "cache_store" not in InitCommand(output=formatter)._build_config(context, False) - - -def test_interactive_production_writes_the_recommended_config(monkeypatch, tmp_path) -> None: +def test_interactive_full_writes_the_recommended_config(monkeypatch) -> None: import json - context, _, _ = _run_prompts(monkeypatch) - config = InitCommand(output=SilentOutput())._build_config(context, True) + context, _, _ = _run_prompts(monkeypatch, _FULL) + config = build_config(context, True) assert config["auth"] == {"method": "jwt"} assert "authorization" not in config assert config["ag_ui"] == {"enabled": False, "allow_client_tools": False} @@ -201,7 +206,7 @@ def test_interactive_production_writes_the_recommended_config(monkeypatch, tmp_p assert check_auth_config(config, production=True) == [] assert config["id_generator"] == "uuid" assert config["cache_store"] == { - "type": "redis", + "type": "memory", "rate_limit": { "enabled": True, "requests": 100, @@ -215,23 +220,25 @@ def test_interactive_production_writes_the_recommended_config(monkeypatch, tmp_p } template = Path(__file__).parents[2] / "tenxgraph_api/cli/templates/prod/10xgraph.json" static = json.loads(template.read_text(encoding="utf-8")) - for key in ( - "auth", - "ag_ui", - "remote_tools", - "id_generator", - "cache_store", - "thread_name_generator", - ): + for key in ("auth", "ag_ui", "remote_tools", "id_generator", "thread_name_generator"): assert static[key] == config[key], key -def test_summary_shows_the_production_choices(monkeypatch) -> None: - context, _, _ = _run_prompts(monkeypatch) +def test_summary_shows_the_full_choices(monkeypatch) -> None: + context, _, _ = _run_prompts(monkeypatch, _FULL) stream = io.StringIO() InitCommand(output=OutputFormatter(stream=stream))._print_summary(context) text = stream.getvalue() - for label in ("Auth", "Cache store", "Rate limit", "Token revocation", "ID generator"): + for label in ( + "Storage", + "Auth", + "Cache store", + "Rate limit", + "Token revocation", + "ID generator", + "Evaluations", + "Tests", + ): assert label in text @@ -249,7 +256,11 @@ def test_production_graph_builds_a_pg_checkpointer_without_services(tmp_path: Pa command = InitCommand(OutputFormatter(stream=io.StringIO())) assert ( command.execute( - path=str(target), agent_name="Pg Agent", template="production", non_interactive=True + path=str(target), + agent_name="Pg Agent", + mode="full", + storage="postgres", + non_interactive=True, ) == 0 ) diff --git a/tests/unit_tests/test_config_editor.py b/tests/unit_tests/test_config_editor.py index 84f8637a..97143dec 100644 --- a/tests/unit_tests/test_config_editor.py +++ b/tests/unit_tests/test_config_editor.py @@ -441,6 +441,18 @@ def test_observability_checks() -> None: assert _levels(issues, "observability") == ["warning"] +def test_observability_phoenix_block() -> None: + ok = validate_config( + {"agent": "graph.react:app", "observability": {"phoenix": {"enabled": True}}} + ) + assert _levels(ok, "observability") == [] + bad = validate_config( + {"agent": "graph.react:app", "observability": {"phoenix": {"enabled": "yes"}}} + ) + assert _levels(bad, "observability.phoenix.enabled") == ["error"] + assert "observability.phoenix.project" in _fields() + + def test_observability_user_id_mode() -> None: for mode in ("raw", "hash", "mask"): ok = validate_config({"agent": "graph.react:app", "observability": {"user_id_mode": mode}}) diff --git a/tests/unit_tests/test_setup_middleware.py b/tests/unit_tests/test_setup_middleware.py index 106fe8e9..04adafe5 100644 --- a/tests/unit_tests/test_setup_middleware.py +++ b/tests/unit_tests/test_setup_middleware.py @@ -527,7 +527,11 @@ def test_setup_observability_noop_when_no_backend_enabled(): from tenxgraph_api.src.app.core.config.setup_middleware import _setup_observability container = MagicMock() - cfg = {"logfire": {"enabled": False}, "langsmith": {"enabled": False}} + cfg = { + "logfire": {"enabled": False}, + "langsmith": {"enabled": False}, + "phoenix": {"enabled": False}, + } _setup_observability(container, _graph_config(cfg)) container.bind_instance.assert_not_called() @@ -566,6 +570,22 @@ def test_setup_observability_adds_to_existing_composite(): container.bind_instance.assert_not_called() +def test_setup_observability_binds_for_phoenix_only(): + from tenxgraph_api.src.app.core.config.setup_middleware import _setup_observability + + modules, setup_obs, _base, _comp = _obs_modules() + cfg = {"phoenix": {"enabled": True, "project": "p", "endpoint": None}} + container = MagicMock() + container.try_get.return_value = None + + with patch.dict(sys.modules, modules): + _setup_observability(container, _graph_config(cfg)) + + setup_obs.assert_called_once() + assert setup_obs.call_args[0][1] == cfg + container.bind_instance.assert_called_once() + + def test_setup_observability_wraps_single_existing(): from tenxgraph_api.src.app.core.config.setup_middleware import _setup_observability From d1d14f9d8395552a9ee40f7bd8ed29f56e7a3c71 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Mon, 9 Nov 2026 18:53:24 +0600 Subject: [PATCH 2/9] feat(metrics): Enhance observability and metrics configuration - Introduced metrics configuration in the main application setup, allowing for telemetry data collection. - Updated the middleware setup to include metrics handling and observability features. - Added tests to validate the integration of metrics and observability configurations. - Adjusted route protection to account for public metrics endpoints. - Refactored telemetry flushing to ensure it does not block other operations. - Corrected metric duration recording to align with new naming conventions. - Implemented validation for metrics configuration to ensure proper setup. --- config-editor-ui/src/icons.js | 2 +- config-editor-ui/src/pages.jsx | 30 +- config-editor-ui/src/sidebar.jsx | 2 +- config-editor-ui/src/store.js | 3 +- config-editor-ui/src/telemetry.jsx | 357 ++++++ config-editor-ui/src/ui.jsx | 16 +- examples/observability/README.md | 52 + examples/observability/docker-compose.yml | 52 + .../grafana/10xgraph-dashboard.json | 1114 +++++++++++++++++ .../provisioning/dashboards/dashboards.yml | 7 + .../provisioning/datasources/prometheus.yml | 8 + examples/observability/otel-collector.yaml | 29 + examples/observability/prometheus.yml | 19 + pyproject.toml | 9 + tenxgraph_api/cli/config_editor/schema.py | 184 ++- .../cli/config_editor/static/app.css | 2 +- tenxgraph_api/cli/config_editor/static/app.js | 6 +- tenxgraph_api/cli/config_editor/validation.py | 73 +- .../publishers-and-runtime-protocols.md | 8 + tenxgraph_api/jobs/monitoring.py | 8 +- tenxgraph_api/jobs/worker.py | 18 +- .../src/app/core/config/graph_config.py | 8 + tenxgraph_api/src/app/core/config/metrics.py | 277 ++++ .../src/app/core/config/setup_middleware.py | 116 +- tenxgraph_api/src/app/main.py | 17 +- tests/unit_tests/jobs/test_middleware.py | 2 +- tests/unit_tests/jobs/test_worker.py | 121 ++ tests/unit_tests/test_config_editor.py | 127 ++ tests/unit_tests/test_metrics.py | 599 +++++++++ tests/unit_tests/test_middleware_order.py | 2 + tests/unit_tests/test_setup_middleware.py | 4 +- 31 files changed, 3163 insertions(+), 109 deletions(-) create mode 100644 config-editor-ui/src/telemetry.jsx create mode 100644 examples/observability/README.md create mode 100644 examples/observability/docker-compose.yml create mode 100644 examples/observability/grafana/10xgraph-dashboard.json create mode 100644 examples/observability/grafana/provisioning/dashboards/dashboards.yml create mode 100644 examples/observability/grafana/provisioning/datasources/prometheus.yml create mode 100644 examples/observability/otel-collector.yaml create mode 100644 examples/observability/prometheus.yml create mode 100644 tenxgraph_api/src/app/core/config/metrics.py create mode 100644 tests/unit_tests/test_metrics.py diff --git a/config-editor-ui/src/icons.js b/config-editor-ui/src/icons.js index 6aac838d..10fdc076 100644 --- a/config-editor-ui/src/icons.js +++ b/config-editor-ui/src/icons.js @@ -41,7 +41,7 @@ export const ICONS = { export const SECTION_ICONS = { "get-running": "flag", environment: "terminal", core: "flag", auth: "lock", cache_store: "gauge", - observability: "activity", runs: "play", websocket: "bolt", ag_ui: "external", remote_tools: "wrench", + observability: "activity", metrics: "gauge", runs: "play", websocket: "bolt", ag_ui: "external", remote_tools: "wrench", media: "file", checkpointer: "layers", id_generator: "key", thread_name_generator: "file", store: "box", injectq: "arrows", test: "flask", evaluation: "target", }; diff --git a/config-editor-ui/src/pages.jsx b/config-editor-ui/src/pages.jsx index 4e39a412..85ae1b27 100644 --- a/config-editor-ui/src/pages.jsx +++ b/config-editor-ui/src/pages.jsx @@ -3,12 +3,12 @@ import { Field, fieldGroups } from "./fields.jsx"; import { SECTION_ICONS } from "./icons.js"; import { PAGES, WIDGET_FIELD_IDS, isAgentReady, isObject, isOn, issuesFor, navItem, section, store, - valueOrDefault, } from "./store.js"; -import { Callout, Icon, IssueCallout, Segmented } from "./ui.jsx"; +import { Callout, Disclosure, Icon, IssueCallout, Segmented } from "./ui.jsx"; import { AuthWidget, CacheStoreWidget, ChoiceWidget, RemoteToolsWidget, } from "./widgets.jsx"; +import { MetricsWidget, TracingWidget, metricsSummary, traceSummary } from "./telemetry.jsx"; const Heading = ({ children, code }) => (

@@ -68,22 +68,11 @@ function cacheSummary() { return { text: `${parts.join("; ")}.` }; } -function traceSummary() { - const o = store.config.observability; - if (o === undefined || o === null) return { text: offText("observability") }; - const on = []; - if (valueOrDefault(["observability", "logfire", "enabled"])) on.push("Logfire"); - if (valueOrDefault(["observability", "langsmith", "enabled"])) on.push("LangSmith"); - if (valueOrDefault(["observability", "phoenix", "enabled"])) on.push("Phoenix"); - const level = valueOrDefault(["observability", "level"]); - return { text: `Level ${level}${on.length ? `, sending to ${on.length > 1 ? `${on.slice(0, -1).join(", ")} and ${on[on.length - 1]}` : on[0]}` : ", no destination chosen yet"}.` }; -} - function GetRunning() { const core = section("core"); const ready = isAgentReady(); const general = core ? issuesFor("core").filter((i) => !core.fields.some((f) => f.path.join(".") === i.path)) : []; - const rows = [["auth", authSummary()], ["cache_store", cacheSummary()], ["observability", traceSummary()]]; + const rows = [["auth", authSummary()], ["cache_store", cacheSummary()], ["observability", traceSummary(offText)], ["metrics", metricsSummary(offText)]]; const all = store.nav.flatMap((g) => g.items).filter((id) => id !== "get-running"); return ( <> @@ -141,19 +130,18 @@ export function EnvList({ vars }) { function EnvDisclosure({ vars, open }) { if (!vars?.length) return null; return ( -
- - Set in .env - +

Read only. The editor never reads .env, so only names are shown.

-
+ ); } function SectionWidget({ sec }) { if (sec.widget === "auth") return ; if (sec.widget === "remote_tools") return ; + if (sec.widget === "observability") return ; + if (sec.widget === "metrics") return ; if (sec.widget === "cache_store") return ; if (sec.widget === "id_generator" || sec.widget === "thread_name_generator") return ; return null; @@ -173,7 +161,7 @@ export function generalIssues(sec) { // Fields and widget of a section, no heading. Shared by the page and search results. export function SectionFields({ sec }) { const on = isOn(sec); - const widget = sec.widget && (on || !sec.optional); + const widget = sec.widget && (on || !sec.optional || sec.self_managed); return ( <> {widget ?
: null} @@ -199,7 +187,7 @@ function SectionPage({ sec }) { {sec.title} {sec.description} {general.length ?
{general.map((i, n) => )}
: null} - {sec.optional ? ( + {sec.optional && !sec.self_managed ? (
- {changed ? : null} + {changed ? : null} {issue ? ( { export const sectionChanged = (sec) => { if (sec.id === "core") return false; if (sec.id === "auth") return store.config.auth != null; + if (sec.id === "metrics") return store.config.metrics?.enabled === true; return sec.optional ? isOn(sec) : sec.fields.some(fieldChanged); }; // Highest-severity issue of a section, or null. diff --git a/config-editor-ui/src/telemetry.jsx b/config-editor-ui/src/telemetry.jsx new file mode 100644 index 00000000..9e390fb4 --- /dev/null +++ b/config-editor-ui/src/telemetry.jsx @@ -0,0 +1,357 @@ +import { useState } from "preact/hooks"; +import { commit, deletePath, getPath, isObject, issueAt, setPath, store, valueOrDefault } from "./store.js"; +import { Chips, Disclosure, FieldIssue, Icon, Select, Toggle } from "./ui.jsx"; + +const linkBtn = "focus-ring min-h-8 cursor-pointer rounded px-1.5 text-[13px] font-medium text-brand transition-colors duration-150 hover:underline"; +const note = "mt-2 max-w-prose text-[13px] leading-5 text-muted"; + +const put = (path, value) => commit((s) => { + if (value === undefined || value === "") deletePath(s.config, path); + else setPath(s.config, path, value); +}); + +// ------------------------------------------------------------------ form controls + +function Labeled({ id, label, path, help, children }) { + return ( +
+
+ + {path ? : null} +
+
{children}
+ {help ?

{help}

: null} + +
+ ); +} + +function TextField({ path, label, help, placeholder }) { + const key = path.join("."); + const id = `f-${path.join("-")}`; + const value = getPath(store.config, path); + return ( + + put(path, e.currentTarget.value)} + /> + + ); +} + +function NumberField({ path, label, help }) { + const key = path.join("."); + const id = `f-${path.join("-")}`; + const value = getPath(store.config, path); + return ( + + { const raw = e.currentTarget.value; put(path, raw === "" ? undefined : Number(raw)); }} + /> + + ); +} + +function SelectField({ path, label, help, options, value, onChange }) { + const id = `f-${path.join("-")}`; + return ( + + put(path, e.currentTarget.checked)} /> + {label} + + ); +} + +// ------------------------------------------------------------------ AI tracing + +const DESTINATIONS = [ + { key: "logfire", name: "Logfire", label: "Logfire", text: "Pydantic Logfire.", secret: <>Token: set LOGFIRE_TOKEN in .env. }, + { key: "langsmith", name: "LangSmith", label: "LangSmith", text: "LangChain's tracing service.", secret: <>API key: set LANGSMITH_API_KEY in .env. }, + { key: "phoenix", name: "Phoenix", label: "Phoenix", text: "Arize Phoenix, local or hosted.", secret: <>API key: set PHOENIX_API_KEY in .env. A local Phoenix does not need one. }, + { key: "otlp", name: "OTLP endpoint", label: "OTLP endpoint", text: "Any OpenTelemetry endpoint: a collector, Tempo, Jaeger.", secret: <>Auth headers: set OTEL_EXPORTER_OTLP_HEADERS in .env. }, +]; +const LEVELS = [["standard", "Standard"], ["spans", "Spans only"], ["full", "Full"]]; +const LEVEL_HELP = { + standard: "Adds model name, token counts and request parameters.", + spans: "Timing and structure only. No inputs or outputs.", + full: "Records prompts and tool I/O. May contain PII.", +}; +const USER_IDS = [["raw", "Raw"], ["hash", "Hashed"], ["mask", "Masked"]]; +const USER_ID_HELP = { + raw: "The user id is sent as is.", + hash: "A SHA-256 of the user id, keyed when TENXGRAPH_TRACE_USER_ID_KEY is set.", + mask: "The middle of the user id is hidden.", +}; + +const obs = () => (isObject(store.config.observability) ? store.config.observability : {}); +const isPicked = (key) => isObject(obs()[key]) && obs()[key].enabled === true; + +function addDestination(key) { + commit((s) => { + if (!isObject(s.config.observability)) s.config.observability = { level: "standard", user_id_mode: "raw" }; + s.config.observability[key] = { enabled: true }; + }); +} + +function removeDestination(key) { + commit((s) => { + delete s.config.observability[key]; + if (!DESTINATIONS.some((d) => isPicked(d.key))) delete s.config.observability; + }); +} + +// Base URL of the metrics OTLP endpoint when traces can share it, else null. +function sharedEndpoint() { + const m = store.config.metrics; + const url = isObject(m) && isObject(m.otlp) ? m.otlp.endpoint : null; + if (!isObject(m) || m.enabled !== true || m.export !== "otlp" || !url) return null; + return url.replace(/\/+$/, "").replace(/\/v1\/metrics$/, ""); +} + +function OtlpFields() { + const flag = ["observability", "otlp", "use_metrics_endpoint"]; + const on = getPath(store.config, flag) === true; + const base = sharedEndpoint(); + const id = "f-observability-otlp-use_metrics_endpoint"; + return ( + <> + {base ? ( +
+
+ put(flag, v ? true : undefined)} /> + +
+

+ {on ? <>Traces go to {base}/v1/traces : "Send traces to the same endpoint as the metrics."} +

+ +
+ ) : null} + {on && base ? null : ( + + )} + {!base ? ( +

+ Sharing the metrics endpoint needs Metrics set to OTLP push with an endpoint. + {on ? <> It is on in the file. : null} +

+ ) : null} + +

+ Prometheus stores metrics only. To send traces and metrics to one place, use a collector + (OTel Collector, Grafana Alloy, Grafana Cloud). +

+ + ); +} + +function DestinationFields({ dest }) { + const f = (name) => ["observability", dest.key, name]; + if (dest.key === "logfire") { + const sendPath = f("send_to_logfire"); + const consolePath = f("console"); + return ( + <> + + + put(consolePath, v === "off" ? false : undefined)} + /> + + ); + } + if (dest.key === "otlp") return ; + const phoenix = dest.key === "phoenix"; + return ( +
+ + +
+ ); +} + +function DestinationCard({ dest }) { + return ( +
+
+
+

{dest.name}

+

{dest.text}

+
+ +
+ +

{dest.secret}

+
+ ); +} + +export function TracingWidget() { + const [adding, setAdding] = useState(false); + const chosen = DESTINATIONS.filter((d) => isPicked(d.key)); + const free = DESTINATIONS.filter((d) => !isPicked(d.key)); + const o = obs(); + return ( +
+ {chosen.length === 0 ? ( +
+ + [d.key, d.label])]} + onChange={(v) => { setAdding(false); if (v) addDestination(v); }} + /> +
+ ) : ( + + ) + ) : null} +

Trace settings

+
+ put(["observability", "level"], v)} + help={LEVEL_HELP[o.level ?? "standard"]} + /> + put(["observability", "user_id_mode"], v)} + help={USER_ID_HELP[o.user_id_mode ?? "raw"]} + /> +
+ + )} +
+ ); +} + +// ------------------------------------------------------------------ Metrics + +const EXPORTS = [["", "None"], ["otlp", "OTLP push"], ["prometheus", "Prometheus scrape"]]; +const EXPORT_HELP = { + "": "Metrics are not exported.", + otlp: "Recommended for production. Works with several worker processes.", + prometheus: "One process only. The API serves the scrape path, the worker its own port.", +}; +const BUCKETS = [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000, 120000]; +const metrics = () => (isObject(store.config.metrics) ? store.config.metrics : {}); +const metricsMode = () => (metrics().enabled === true && ["otlp", "prometheus"].includes(metrics().export) ? metrics().export : ""); + +function pickMode(mode) { + commit((s) => { + if (!mode) { delete s.config.metrics; return; } + const m = isObject(s.config.metrics) ? s.config.metrics : {}; + m.enabled = true; + m.export = mode; + if (mode === "otlp" && !isObject(m.otlp)) m.otlp = { endpoint: null }; + if (mode === "prometheus" && !isObject(m.prometheus)) m.prometheus = { path: "/metrics", worker_port: 9464 }; + s.config.metrics = m; + }); +} + +export function MetricsWidget() { + const mode = metricsMode(); + const buckets = metrics().latency_buckets_ms; + const bucketIssue = issueAt("metrics.latency_buckets_ms"); + return ( +
+
+ + onChange(e.currentTarget.value)} > {options.map(([v, label]) => )} @@ -235,3 +235,15 @@ export function EmptyPanel({ icon, tint, title, text, children }) {
); } + +// Chevron disclosure, the same one the sections use for "Set in .env". +export function Disclosure({ title, open, class: cls = "", children }) { + return ( +
+ + {title} + + {children} +
+ ); +} diff --git a/examples/observability/README.md b/examples/observability/README.md new file mode 100644 index 00000000..f22d63ed --- /dev/null +++ b/examples/observability/README.md @@ -0,0 +1,52 @@ +# Local metrics stack + +OpenTelemetry Collector, Prometheus, Grafana and Redis for trying the 10xGraph `metrics` block locally. Local use only: no auth, Grafana allows anonymous admin. Ports are bound to 127.0.0.1. URLs use 127.0.0.1 on purpose: where another service listens on ::1, `localhost` can reach the wrong app. + +| Service | URL | +|---|---| +| Grafana | http://127.0.0.1:3000 | +| Prometheus | http://127.0.0.1:9090 | +| Collector OTLP | http://127.0.0.1:4318 (HTTP), 127.0.0.1:4317 (gRPC) | +| Redis | 127.0.0.1:6379 | + +```bash +docker compose up -d +``` + +## Point 10xGraph at it + +Install `10xgraph-api[metrics]`, then add to `10xgraph.json`. + +Option A, push through the collector (recommended; also takes AI traces): + +```json +{ + "metrics": { "enabled": true, "export": "otlp", "otlp": { "endpoint": "http://127.0.0.1:4318" } }, + "observability": { "otlp": { "enabled": true, "use_metrics_endpoint": true } } +} +``` + +Traces land in the collector's debug log (`docker compose logs -f otel-collector`). To keep them, add a Tempo or Jaeger exporter to `otel-collector.yaml`. + +Option A2, push straight to Prometheus (metrics only, no AI traces): + +```json +{ "metrics": { "enabled": true, "export": "otlp", "otlp": { "endpoint": "http://127.0.0.1:9090/api/v1/otlp" } } } +``` + +Option B, scrape: set `"export": "prometheus"`. `prometheus.yml` already scrapes the API at `host.docker.internal:8000/metrics` and the worker at `host.docker.internal:9464`. Scrape covers one process; use push with several gunicorn workers. + +## Run the app + +```bash +OTEL_SERVICE_NAME=tenxgraph-api 10xgraph dev # API on :8000 +OTEL_SERVICE_NAME=tenxgraph-worker taskiq worker # worker, Redis at redis://127.0.0.1:6379 +``` + +Different `OTEL_SERVICE_NAME` values make `job` separate the API from the worker in Prometheus. + +Tip for local testing: metrics are pushed every 60 seconds by default. Set `OTEL_METRIC_EXPORT_INTERVAL=5000` (milliseconds) on both commands to see data in seconds. + +If port 6379 is already used by another Redis, change the Redis port mapping in `docker-compose.yml` (for example `127.0.0.1:6380:6379`) and point `cache_store` and the worker URLs at `redis://127.0.0.1:6380`. + +Send some requests, then open Grafana at http://127.0.0.1:3000 and the **10xGraph** dashboard. It is provisioned from `grafana/10xgraph-dashboard.json`; the same file can be imported into any Grafana (pick your Prometheus in the Data source variable). diff --git a/examples/observability/docker-compose.yml b/examples/observability/docker-compose.yml new file mode 100644 index 00000000..80897f3a --- /dev/null +++ b/examples/observability/docker-compose.yml @@ -0,0 +1,52 @@ +# Local metrics stack for 10xGraph. Not for production: no auth, anonymous Grafana admin. +# Everything is bound to 127.0.0.1. +services: + otel-collector: + image: otel/opentelemetry-collector-contrib:0.130.0 + command: ["--config=/etc/otelcol/config.yaml"] + volumes: + - ./otel-collector.yaml:/etc/otelcol/config.yaml:ro + ports: + - "127.0.0.1:4317:4317" + - "127.0.0.1:4318:4318" + depends_on: + - prometheus + + prometheus: + image: prom/prometheus:v3.5.0 + command: + - --config.file=/etc/prometheus/prometheus.yml + - --web.enable-otlp-receiver + - --storage.tsdb.retention.time=2d + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + - prometheus-data:/prometheus + ports: + - "127.0.0.1:9090:9090" + extra_hosts: + - "host.docker.internal:host-gateway" + + grafana: + image: grafana/grafana:12.0.2 + environment: + # Local use only: everyone who can reach port 3000 is an admin. + GF_AUTH_ANONYMOUS_ENABLED: "true" + GF_AUTH_ANONYMOUS_ORG_ROLE: Admin + GF_AUTH_DISABLE_LOGIN_FORM: "true" + volumes: + - ./grafana/provisioning:/etc/grafana/provisioning:ro + - ./grafana/10xgraph-dashboard.json:/var/lib/grafana/dashboards/10xgraph-dashboard.json:ro + - grafana-data:/var/lib/grafana + ports: + - "127.0.0.1:3000:3000" + depends_on: + - prometheus + + redis: + image: redis:7.4-alpine + ports: + - "127.0.0.1:6379:6379" + +volumes: + prometheus-data: + grafana-data: diff --git a/examples/observability/grafana/10xgraph-dashboard.json b/examples/observability/grafana/10xgraph-dashboard.json new file mode 100644 index 00000000..f499800a --- /dev/null +++ b/examples/observability/grafana/10xgraph-dashboard.json @@ -0,0 +1,1114 @@ +{ + "title": "10xGraph", + "uid": "tenxgraph-overview", + "description": "API, graph, LLM and task metrics for a 10xGraph deployment.", + "tags": [ + "10xgraph" + ], + "schemaVersion": 41, + "version": 1, + "editable": true, + "graphTooltip": 1, + "time": { + "from": "now-1h", + "to": "now" + }, + "refresh": "30s", + "timezone": "browser", + "templating": { + "list": [ + { + "name": "datasource", + "label": "Data source", + "type": "datasource", + "query": "prometheus", + "current": { + "selected": true, + "text": "Prometheus", + "value": "prometheus" + }, + "refresh": 1 + }, + { + "name": "job", + "label": "Job", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "query": { + "query": "label_values(http_server_request_duration_seconds_count, job)", + "refId": "v" + }, + "definition": "label_values(http_server_request_duration_seconds_count, job)", + "refresh": 2, + "sort": 1, + "includeAll": true, + "multi": true, + "allValue": ".*", + "current": { + "selected": true, + "text": "All", + "value": "$__all" + } + }, + { + "name": "route", + "label": "Route", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "query": { + "query": "label_values(http_server_request_duration_seconds_count{job=~\"$job\"}, http_route)", + "refId": "v" + }, + "definition": "label_values(http_server_request_duration_seconds_count{job=~\"$job\"}, http_route)", + "refresh": 2, + "sort": 1, + "includeAll": true, + "multi": true, + "allValue": ".*", + "current": { + "selected": true, + "text": "All", + "value": "$__all" + } + }, + { + "name": "task", + "label": "Task", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "query": { + "query": "label_values(tenxgraph_task_runs_total{job=~\"$job\"}, task)", + "refId": "v" + }, + "definition": "label_values(tenxgraph_task_runs_total{job=~\"$job\"}, task)", + "refresh": 2, + "sort": 1, + "includeAll": true, + "multi": true, + "allValue": ".*", + "current": { + "selected": true, + "text": "All", + "value": "$__all" + } + }, + { + "name": "model", + "label": "Model", + "type": "query", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "query": { + "query": "label_values(tenxgraph_llm_tokens_total{job=~\"$job\"}, model)", + "refId": "v" + }, + "definition": "label_values(tenxgraph_llm_tokens_total{job=~\"$job\"}, model)", + "refresh": 2, + "sort": 1, + "includeAll": true, + "multi": true, + "allValue": ".*", + "current": { + "selected": true, + "text": "All", + "value": "$__all" + } + } + ] + }, + "annotations": { + "list": [] + }, + "panels": [ + { + "type": "row", + "title": "API", + "id": 1, + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "panels": [] + }, + { + "id": 2, + "type": "timeseries", + "title": "Request rate by route", + "description": "Requests per second, grouped by route template.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 1 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "label_replace(sum by (http_route) (rate(http_server_request_duration_seconds_count{job=~\"$job\", http_route=~\"$route\"}[$__rate_interval])), \"http_route\", \"(unmatched)\", \"http_route\", \"\")", + "legendFormat": "{{http_route}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "reqps", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 3, + "type": "timeseries", + "title": "Latency p50 / p95 / p99 (non-stream)", + "description": "Request latency excluding stream, websocket and live routes, where the whole stream counts as one request.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 1 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "histogram_quantile(0.5, sum by (le) (rate(http_server_request_duration_seconds_bucket{job=~\"$job\", http_route=~\"$route\", http_route!~\".*(stream|ws|live|ag-ui).*\"}[$__rate_interval])))", + "legendFormat": "p50", + "range": true + }, + { + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "histogram_quantile(0.95, sum by (le) (rate(http_server_request_duration_seconds_bucket{job=~\"$job\", http_route=~\"$route\", http_route!~\".*(stream|ws|live|ag-ui).*\"}[$__rate_interval])))", + "legendFormat": "p95", + "range": true + }, + { + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "histogram_quantile(0.99, sum by (le) (rate(http_server_request_duration_seconds_bucket{job=~\"$job\", http_route=~\"$route\", http_route!~\".*(stream|ws|live|ag-ui).*\"}[$__rate_interval])))", + "legendFormat": "p99", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 4, + "type": "timeseries", + "title": "5xx error ratio", + "description": "Share of requests answered with a 5xx status.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 1 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(rate(http_server_request_duration_seconds_count{job=~\"$job\", http_route=~\"$route\", http_response_status_code=~\"5..\"}[$__rate_interval])) / sum(rate(http_server_request_duration_seconds_count{job=~\"$job\", http_route=~\"$route\"}[$__rate_interval]))", + "legendFormat": "5xx ratio", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + }, + "min": 0 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 5, + "type": "timeseries", + "title": "In-flight requests", + "description": "Requests being handled right now, summed over all API processes.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 8 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(http_server_active_requests{job=~\"$job\"})", + "legendFormat": "in flight", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "short", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 6, + "type": "timeseries", + "title": "Stream route latency p50 / p95", + "description": "Duration of whole streams (SSE, websocket, live). Long values are normal here.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 8 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "histogram_quantile(0.5, sum by (le) (rate(http_server_request_duration_seconds_bucket{job=~\"$job\", http_route=~\"$route\", http_route=~\".*(stream|ws|live|ag-ui).*\"}[$__rate_interval])))", + "legendFormat": "p50", + "range": true + }, + { + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "histogram_quantile(0.95, sum by (le) (rate(http_server_request_duration_seconds_bucket{job=~\"$job\", http_route=~\"$route\", http_route=~\".*(stream|ws|live|ag-ui).*\"}[$__rate_interval])))", + "legendFormat": "p95", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "s", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 7, + "type": "timeseries", + "title": "Requests by status code", + "description": "Request rate split by HTTP status code.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 8 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (http_response_status_code) (rate(http_server_request_duration_seconds_count{job=~\"$job\", http_route=~\"$route\"}[$__rate_interval]))", + "legendFormat": "{{http_response_status_code}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "reqps", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "type": "row", + "title": "Graph", + "id": 8, + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "panels": [] + }, + { + "id": 9, + "type": "timeseries", + "title": "Node executions by node", + "description": "How often each graph node starts running.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 16 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (node) (rate(tenxgraph_node_executions_total{job=~\"$job\"}[$__rate_interval]))", + "legendFormat": "{{node}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "ops", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 10, + "type": "timeseries", + "title": "Node duration p95", + "description": "95th percentile run time of each graph node.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 16 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "histogram_quantile(0.95, sum by (le, node) (rate(tenxgraph_node_duration_milliseconds_bucket{job=~\"$job\"}[$__rate_interval])))", + "legendFormat": "{{node}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 11, + "type": "timeseries", + "title": "Node errors, timeouts, stops", + "description": "Nodes that failed, hit their node_timeout, or were stopped by a stop request.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 16 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (node) (rate(tenxgraph_node_errors_total{job=~\"$job\"}[$__rate_interval]))", + "legendFormat": "errors {{node}}", + "range": true + }, + { + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (node) (rate(tenxgraph_node_timeouts_total{job=~\"$job\"}[$__rate_interval]))", + "legendFormat": "timeouts {{node}}", + "range": true + }, + { + "refId": "C", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (node) (rate(tenxgraph_node_stopped_total{job=~\"$job\"}[$__rate_interval]))", + "legendFormat": "stopped {{node}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "ops", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 12, + "type": "timeseries", + "title": "Tool calls by tool", + "description": "Tool calls per second. Names the node does not know are grouped as unknown.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 23 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (tool) (rate(tenxgraph_tool_calls_total{job=~\"$job\"}[$__rate_interval]))", + "legendFormat": "{{tool}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "ops", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 13, + "type": "timeseries", + "title": "Tool errors and timeouts", + "description": "Tool calls that raised or hit tool_timeout.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 23 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (tool) (rate(tenxgraph_tool_errors_total{job=~\"$job\"}[$__rate_interval]))", + "legendFormat": "errors {{tool}}", + "range": true + }, + { + "refId": "B", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (tool) (rate(tenxgraph_tool_timeouts_total{job=~\"$job\"}[$__rate_interval]))", + "legendFormat": "timeouts {{tool}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "ops", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 14, + "type": "timeseries", + "title": "Tool duration p95", + "description": "95th percentile run time of each tool.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 23 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "histogram_quantile(0.95, sum by (le, tool) (rate(tenxgraph_tool_duration_milliseconds_bucket{job=~\"$job\"}[$__rate_interval])))", + "legendFormat": "{{tool}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "type": "row", + "title": "LLM", + "id": 15, + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "panels": [] + }, + { + "id": 16, + "type": "timeseries", + "title": "LLM latency p95 by model", + "description": "95th percentile time of successful LLM calls (streams count until the stream ends).", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 31 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "histogram_quantile(0.95, sum by (le, model) (rate(tenxgraph_llm_duration_milliseconds_bucket{job=~\"$job\", model=~\"$model\", status=\"success\"}[$__rate_interval])))", + "legendFormat": "{{model}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 17, + "type": "timeseries", + "title": "LLM error ratio by model", + "description": "Share of LLM calls that failed, per model.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 31 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (model) (rate(tenxgraph_llm_duration_milliseconds_count{job=~\"$job\", model=~\"$model\", status=\"error\"}[$__rate_interval])) / sum by (model) (rate(tenxgraph_llm_duration_milliseconds_count{job=~\"$job\", model=~\"$model\"}[$__rate_interval]))", + "legendFormat": "{{model}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + }, + "min": 0 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 18, + "type": "timeseries", + "title": "Tokens per minute", + "description": "Input and output tokens per minute, by model.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 31 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (type, model) (rate(tenxgraph_llm_tokens_total{job=~\"$job\", model=~\"$model\"}[$__rate_interval])) * 60", + "legendFormat": "{{type}} {{model}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "short", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "type": "row", + "title": "Tasks", + "id": 19, + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "panels": [] + }, + { + "id": 20, + "type": "timeseries", + "title": "Task runs by status", + "description": "Task runs per second: queued, success, failure, cancelled.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 39 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum by (status) (rate(tenxgraph_task_runs_total{job=~\"$job\", task=~\"$task\"}[$__rate_interval]))", + "legendFormat": "{{status}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "ops", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 21, + "type": "timeseries", + "title": "Task failure ratio", + "description": "Failed runs divided by finished runs (success + failure).", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 39 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "sum(rate(tenxgraph_task_runs_total{job=~\"$job\", task=~\"$task\", status=\"failure\"}[$__rate_interval])) / sum(rate(tenxgraph_task_runs_total{job=~\"$job\", task=~\"$task\", status=~\"success|failure\"}[$__rate_interval]))", + "legendFormat": "failure ratio", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "percentunit", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + }, + "min": 0 + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + }, + { + "id": 22, + "type": "timeseries", + "title": "Task duration p95 by task", + "description": "95th percentile run time of each task.", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 39 + }, + "targets": [ + { + "refId": "A", + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "histogram_quantile(0.95, sum by (le, task) (rate(tenxgraph_task_duration_milliseconds_bucket{job=~\"$job\", task=~\"$task\"}[$__rate_interval])))", + "legendFormat": "{{task}}", + "range": true + } + ], + "fieldConfig": { + "defaults": { + "unit": "ms", + "custom": { + "fillOpacity": 10, + "lineWidth": 1 + } + }, + "overrides": [] + }, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + } + } + ] +} \ No newline at end of file diff --git a/examples/observability/grafana/provisioning/dashboards/dashboards.yml b/examples/observability/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 00000000..1d3353f1 --- /dev/null +++ b/examples/observability/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,7 @@ +apiVersion: 1 +providers: + - name: 10xgraph + type: file + allowUiUpdates: true + options: + path: /var/lib/grafana/dashboards diff --git a/examples/observability/grafana/provisioning/datasources/prometheus.yml b/examples/observability/grafana/provisioning/datasources/prometheus.yml new file mode 100644 index 00000000..2387b2f6 --- /dev/null +++ b/examples/observability/grafana/provisioning/datasources/prometheus.yml @@ -0,0 +1,8 @@ +apiVersion: 1 +datasources: + - name: Prometheus + uid: prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true diff --git a/examples/observability/otel-collector.yaml b/examples/observability/otel-collector.yaml new file mode 100644 index 00000000..e0ef73b6 --- /dev/null +++ b/examples/observability/otel-collector.yaml @@ -0,0 +1,29 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: {} + +exporters: + # Metrics go to Prometheus' OTLP receiver (the exporter appends /v1/metrics). + otlphttp/prometheus: + endpoint: http://prometheus:9090/api/v1/otlp + # Traces are only printed to the collector log. Add a Tempo or Jaeger exporter here to keep them. + debug: + verbosity: basic + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [otlphttp/prometheus] + traces: + receivers: [otlp] + processors: [batch] + exporters: [debug] diff --git a/examples/observability/prometheus.yml b/examples/observability/prometheus.yml new file mode 100644 index 00000000..11e85972 --- /dev/null +++ b/examples/observability/prometheus.yml @@ -0,0 +1,19 @@ +global: + scrape_interval: 15s + +# Used by the OTLP receiver (--web.enable-otlp-receiver). Keeps service.name as the +# `job` label and adds one series per process through service.instance.id. +otlp: + promote_resource_attributes: + - service.instance.id + +# Option B scrape mode: use these when 10xgraph.json has "metrics": {"export": "prometheus"}. +# With OTLP push (option A) leave them alone: the targets just show as down. +scrape_configs: + - job_name: tenxgraph-api + metrics_path: /metrics + static_configs: + - targets: ["host.docker.internal:8000"] + - job_name: tenxgraph-worker + static_configs: + - targets: ["host.docker.internal:9464"] diff --git a/pyproject.toml b/pyproject.toml index 8ff19148..72a22414 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -131,6 +131,15 @@ otel = [ "opentelemetry-instrumentation-fastapi>=0.40b0", "opentelemetry-exporter-otlp>=1.20.0", ] +# Metrics export (OTLP push or Prometheus scrape); floors are the tested versions. +metrics = [ + "opentelemetry-api>=1.45.1", + "opentelemetry-sdk>=1.45.1", + "opentelemetry-instrumentation-fastapi>=0.66b2", + "opentelemetry-exporter-otlp-proto-http>=1.45.2", + "opentelemetry-exporter-prometheus>=0.66b2", + "prometheus-client>=0.26.0", +] ag-ui = [ "ag-ui-protocol>=1.0,<2", ] diff --git a/tenxgraph_api/cli/config_editor/schema.py b/tenxgraph_api/cli/config_editor/schema.py index 65755003..5d8d521d 100644 --- a/tenxgraph_api/cli/config_editor/schema.py +++ b/tenxgraph_api/cli/config_editor/schema.py @@ -12,10 +12,12 @@ "description": "...", "key": "observability", # top-level key, or None for root-level fields "optional": True, # renders an on/off toggle for the whole key + "self_managed": False, # True: no toggle, the widget adds and removes the key itself "off_summary": "...", # what happens while the key is absent "default": {...}, # value written when the toggle is switched on "widget": None, # custom renderer: "auth", "remote_tools", - # "cache_store", "id_generator", "thread_name_generator" + # "cache_store", "id_generator", "thread_name_generator", + # "observability", "metrics" "choices": [["value", "Label", "One line of help"]], # widget sections with a dropdown "fields": [...], # generic fields, paths are relative to the root "notes": ["..."], # extra hints (secrets that belong in .env, ...) @@ -87,6 +89,7 @@ "ag_ui", "media", "observability", + "metrics", "injectq", "store", "store_allow_tenant_mismatch", @@ -642,21 +645,22 @@ }, { "id": "observability", - "title": "Observability", - "description": "Trace graph runs to Logfire, LangSmith and/or Phoenix.", + "title": "AI tracing", + "description": ( + "Traces of graph runs, LLM calls and tools, sent to the services you pick. " + "Never metrics: see Metrics for request rate and latency." + ), "key": "observability", "optional": True, "off_summary": "Graph runs are not traced.", - "default": { - "level": "standard", - "logfire": {"enabled": False}, - "langsmith": {"enabled": False}, - "phoenix": {"enabled": False}, - }, + "self_managed": True, + "default": {"level": "standard", "user_id_mode": "raw"}, + "widget": "observability", "fields": [ { "path": ["observability", "level"], "group": "Tracing", + "managed": True, "default": "standard", "label": "Level", "type": "select", @@ -666,6 +670,7 @@ { "path": ["observability", "user_id_mode"], "group": "Tracing", + "managed": True, "default": "raw", "label": "User id in traces", "type": "select", @@ -675,32 +680,26 @@ "TENXGRAPH_TRACE_USER_ID_KEY is set); mask hides the middle of it." ), }, - { - "path": ["observability", "logfire", "enabled"], - "group": "Logfire", - "default": False, - "label": "Logfire", - "type": "bool", - }, { "path": ["observability", "logfire", "service_name"], "group": "Logfire", - "label": "Logfire service name", + "managed": True, + "label": "Service name", "type": "text", "placeholder": "my-agent", - "show_if": {"path": ["observability", "logfire", "enabled"], "equals": True}, }, { "path": ["observability", "logfire", "send_to_logfire"], "group": "Logfire", + "managed": True, "default": True, "label": "Send to Logfire", "type": "bool", - "show_if": {"path": ["observability", "logfire", "enabled"], "equals": True}, }, { "path": ["observability", "logfire", "console"], "group": "Logfire", + "managed": True, "label": "Print spans to console", "type": "bool", "unset_label": "Default: Logfire's own console behavior", @@ -709,57 +708,63 @@ "Only Default or Off work from this file. Logfire takes False or a " "ConsoleOptions object, which JSON cannot express, so true is rejected." ), - "show_if": {"path": ["observability", "logfire", "enabled"], "equals": True}, - }, - { - "path": ["observability", "langsmith", "enabled"], - "group": "LangSmith", - "default": False, - "label": "LangSmith", - "type": "bool", }, { "path": ["observability", "langsmith", "project"], "group": "LangSmith", - "label": "LangSmith project", + "managed": True, + "label": "Project", "type": "text", "placeholder": "my-agent", - "show_if": {"path": ["observability", "langsmith", "enabled"], "equals": True}, }, { "path": ["observability", "langsmith", "endpoint"], "group": "LangSmith", - "label": "LangSmith endpoint", + "managed": True, + "label": "Endpoint", "type": "text", "placeholder": "https://api.smith.langchain.com/otel", - "show_if": {"path": ["observability", "langsmith", "enabled"], "equals": True}, - }, - { - "path": ["observability", "phoenix", "enabled"], - "group": "Phoenix", - "default": False, - "label": "Phoenix", - "type": "bool", }, { "path": ["observability", "phoenix", "project"], "group": "Phoenix", - "label": "Phoenix project", + "managed": True, + "label": "Project", "type": "text", "placeholder": "my-agent", - "show_if": {"path": ["observability", "phoenix", "enabled"], "equals": True}, }, { "path": ["observability", "phoenix", "endpoint"], "group": "Phoenix", - "label": "Phoenix endpoint", + "managed": True, + "label": "Endpoint", "type": "text", "placeholder": "http://localhost:6006", "help": "Base collector URL. /v1/traces is appended.", - "show_if": {"path": ["observability", "phoenix", "enabled"], "equals": True}, + }, + { + "path": ["observability", "otlp", "use_metrics_endpoint"], + "group": "OTLP", + "managed": True, + "default": False, + "label": "Use the metrics endpoint", + "type": "bool", + "help": "Send traces to the same endpoint as the metrics.", + }, + { + "path": ["observability", "otlp", "endpoint"], + "group": "OTLP", + "managed": True, + "label": "Endpoint", + "type": "text", + "placeholder": "http://collector:4318", + "help": ( + "Base URL of an OTLP HTTP receiver. /v1/traces is appended. Auth headers " + "come from OTEL_EXPORTER_OTLP_HEADERS." + ), }, ], - "notes": ["Tokens stay in .env: LOGFIRE_TOKEN, LANGSMITH_API_KEY and PHOENIX_API_KEY."], + "notes": [], "env": [ { "name": "LOGFIRE_TOKEN", @@ -773,13 +778,21 @@ "name": "PHOENIX_API_KEY", "purpose": "Phoenix API key. Optional; a local Phoenix needs none.", }, + { + "name": "OTEL_EXPORTER_OTLP_HEADERS", + "purpose": "Auth headers for the OTLP destination, as key=value pairs.", + }, + { + "name": "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "purpose": "OTLP traces endpoint used when the OTLP endpoint field is empty.", + }, { "name": "TENXGRAPH_TRACE_USER_ID_KEY", "purpose": "Secret that keys the hash of user ids in traces when the mode is hash.", }, { "name": "OTEL_ENABLED", - "purpose": "Export OpenTelemetry traces from the API server. Defaults to false.", + "purpose": "Export OpenTelemetry traces from the API server (off unless set to true).", }, { "name": "OTEL_LEVEL", @@ -787,7 +800,7 @@ }, { "name": "OTEL_SERVICE_NAME", - "purpose": "Service name on OpenTelemetry traces. Defaults to 10xgraph-api.", + "purpose": "Service name on OpenTelemetry traces (10xgraph-api when unset).", }, { "name": "OTEL_EXPORTER_OTLP_ENDPOINT", @@ -795,6 +808,87 @@ }, ], }, + { + "id": "metrics", + "title": "Metrics", + "description": ( + "Request rate, latency percentiles, errors and task throughput for Prometheus and " + "Grafana, from the API server and the worker. Never AI traces." + ), + "key": "metrics", + "optional": True, + "off_summary": "No metrics are exported.", + "default": {"enabled": True, "export": "otlp", "otlp": {"endpoint": None}}, + "self_managed": True, + "widget": "metrics", + "fields": [ + { + "path": ["metrics", "export"], + "managed": True, + "label": "Export metrics", + "type": "select", + "options": [ + ["otlp", "OTLP push (recommended for production)"], + ["prometheus", "Prometheus scrape (single process)"], + ], + }, + { + "path": ["metrics", "otlp", "endpoint"], + "managed": True, + "label": "OTLP endpoint", + "type": "text", + "placeholder": "http://collector:4318", + "help": ( + "Base URL of an OTLP HTTP receiver. /v1/metrics is appended. For Prometheus " + "use http://prometheus:9090/api/v1/otlp, started with " + "--web.enable-otlp-receiver. Auth headers come from " + "OTEL_EXPORTER_OTLP_HEADERS." + ), + }, + { + "path": ["metrics", "prometheus", "path"], + "managed": True, + "default": "/metrics", + "label": "Scrape path", + "type": "text", + "placeholder": "/metrics", + "help": "Route on the API server that Prometheus scrapes.", + }, + { + "path": ["metrics", "prometheus", "worker_port"], + "managed": True, + "default": 9464, + "label": "Worker port", + "type": "number", + "min": 1, + "step": 1, + "help": "Port where each worker serves its own metrics.", + }, + { + "path": ["metrics", "latency_buckets_ms"], + "managed": True, + "default": [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000, 120000], + "label": "Latency buckets (ms)", + "type": "list", + "placeholder": "250", + "help": "Upper bounds of the latency histogram buckets, ascending.", + }, + ], + "env": [ + { + "name": "OTEL_EXPORTER_OTLP_HEADERS", + "purpose": "Auth headers for OTLP push and OTLP tracing, as key=value pairs.", + }, + { + "name": "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "purpose": "OTLP metrics endpoint used when the endpoint field is empty.", + }, + { + "name": "OTEL_SERVICE_NAME", + "purpose": "Service name on metrics (10xgraph-api when unset).", + }, + ], + }, { "id": "checkpointer", "title": "Checkpointer", diff --git a/tenxgraph_api/cli/config_editor/static/app.css b/tenxgraph_api/cli/config_editor/static/app.css index 6bed4f2b..a1277917 100644 --- a/tenxgraph_api/cli/config_editor/static/app.css +++ b/tenxgraph_api/cli/config_editor/static/app.css @@ -1,2 +1,2 @@ /*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;--font-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-canvas:var(--af-canvas);--color-surface:var(--af-surface);--color-raised:var(--af-raised);--color-hover:var(--af-hover);--color-sunken:var(--af-sunken);--color-line:var(--af-line);--color-line-strong:var(--af-line-strong);--color-ink:var(--af-ink);--color-body:var(--af-body);--color-muted:var(--af-muted);--color-faint:var(--af-faint);--color-brand:var(--af-brand);--color-brand-solid:var(--af-brand-solid);--color-brand-solid-hover:var(--af-brand-solid-hover);--color-brand-soft:var(--af-brand-soft);--color-brand-ring:var(--af-brand-ring);--color-brand-amber:var(--af-brand-amber);--color-ok:var(--af-ok);--color-ok-soft:var(--af-ok-soft);--color-warn:var(--af-warn);--color-warn-soft:var(--af-warn-soft);--color-danger:var(--af-danger);--color-danger-soft:var(--af-danger-soft)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html{scrollbar-gutter:stable}body{background-color:var(--color-canvas);font-family:var(--font-sans);color:var(--color-body);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*{scrollbar-width:thin;scrollbar-color:var(--af-scrollbar) transparent}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--af-scrollbar) padding-box;border:3px solid #0000;border-radius:9999px}::-webkit-scrollbar-thumb:hover{background:var(--af-scrollbar-hover) padding-box}::-webkit-scrollbar-corner{background:0 0}}@layer components{.input{height:calc(var(--spacing) * 11);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line-strong);background-color:var(--color-sunken);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));color:var(--color-ink);transition-property:border-color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s;display:block}.input::placeholder{color:var(--color-faint)}@media (hover:hover){.input:hover{border-color:var(--color-faint)}@supports (color:color-mix(in lab, red, red)){.input:hover{border-color:color-mix(in oklab, var(--color-faint) 60%, transparent)}}}.input:focus{border-color:var(--color-brand);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}.input:disabled{cursor:not-allowed;opacity:.5}@media (min-width:40rem){.input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.input-invalid{border-color:var(--color-danger)}.input-invalid:focus{border-color:var(--color-danger);--tw-ring-color:var(--color-danger-soft)}.btn{height:calc(var(--spacing) * 11);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 3.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;flex-shrink:0;transition-duration:.15s;display:inline-flex}.btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}.btn:disabled{cursor:not-allowed;opacity:.45}.btn-primary{background-color:var(--color-brand-solid);color:var(--color-white);--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (hover:hover){.btn-primary:hover{background-color:var(--color-brand-solid-hover)}}.btn-outline{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line-strong);background-color:var(--color-surface);color:var(--color-body)}@media (hover:hover){.btn-outline:hover{background-color:var(--color-raised)}}.btn-ghost{color:var(--color-muted)}@media (hover:hover){.btn-ghost:hover{background-color:var(--color-raised);color:var(--color-ink)}}.icon-btn{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11);cursor:pointer;border-radius:var(--radius-lg);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;flex-shrink:0;justify-content:center;align-items:center;transition-duration:.15s;display:inline-flex}@media (hover:hover){.icon-btn:hover{background-color:var(--color-raised);color:var(--color-ink)}}.icon-btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}.panel{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line);background-color:var(--color-surface);--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.kbd{height:calc(var(--spacing) * 5);border-style:var(--tw-border-style);border-width:1px;border-color:#ffffff40;border-radius:.25rem;align-items:center;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.kbd{border-color:color-mix(in oklab, var(--color-white) 25%, transparent)}}.kbd{padding-inline:calc(var(--spacing) * 1.5);font-family:var(--font-mono);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:#fffc}@supports (color:color-mix(in lab, red, red)){.kbd{color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.eyebrow{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);color:var(--color-faint);text-transform:uppercase}.focus-ring:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.top-0{top:0}.top-1\/2{top:50%}.right-3{right:calc(var(--spacing) * 3)}.right-4{right:calc(var(--spacing) * 4)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-3{left:calc(var(--spacing) * 3)}.left-4{left:calc(var(--spacing) * 4)}.z-40{z-index:40}.z-50{z-index:50}.m-0{margin:0}.mx-1{margin-inline:var(--spacing)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-7{margin-top:calc(var(--spacing) * 7)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-9{margin-top:calc(var(--spacing) * 9)}.mt-10{margin-top:calc(var(--spacing) * 10)}.-mr-1{margin-right:calc(var(--spacing) * -1)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-0{margin-bottom:0}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.size-\[18px\]{width:18px;height:18px}.size-full{width:100%;height:100%}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-auto{height:auto}.h-dvh{height:100dvh}.h-full{height:100%}.max-h-none{max-height:none}.min-h-0{min-height:0}.min-h-6{min-height:calc(var(--spacing) * 6)}.min-h-8{min-height:calc(var(--spacing) * 8)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-11{min-height:calc(var(--spacing) * 11)}.min-h-12{min-height:calc(var(--spacing) * 12)}.min-h-14{min-height:calc(var(--spacing) * 14)}.w-10{width:calc(var(--spacing) * 10)}.w-48{width:calc(var(--spacing) * 48)}.w-80{width:calc(var(--spacing) * 80)}.w-\[min\(540px\,100vw\)\]{width:min(540px,100vw)}.w-full{width:100%}.w-px{width:1px}.max-w-40{max-width:calc(var(--spacing) * 40)}.max-w-\[62ch\]{max-width:62ch}.max-w-\[420px\]{max-width:420px}.max-w-\[520px\]{max-width:520px}.max-w-\[760px\]{max-width:760px}.max-w-full{max-width:100%}.max-w-prose{max-width:65ch}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-40{min-width:calc(var(--spacing) * 40)}.min-w-56{min-width:calc(var(--spacing) * 56)}.min-w-\[7rem\]{min-width:7rem}.flex-1{flex:1}.shrink-0{flex-shrink:0}.translate-x-0\.5{--tw-translate-x:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-\[18px\]{--tw-translate-x:18px;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-90{rotate:90deg}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.list-none{list-style-type:none}.appearance-none{appearance:none}.grid-cols-\[minmax\(0\,1fr\)_8rem_auto\]{grid-template-columns:minmax(0,1fr) 8rem auto}.grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1{row-gap:var(--spacing)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-line>:not(:last-child)){border-color:var(--color-line)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-\[10px\]{border-radius:10px}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-brand{border-color:var(--color-brand)}.border-brand-solid{border-color:var(--color-brand-solid)}.border-brand\/20{border-color:var(--color-brand)}@supports (color:color-mix(in lab, red, red)){.border-brand\/20{border-color:color-mix(in oklab, var(--color-brand) 20%, transparent)}}.border-danger\/25{border-color:var(--color-danger)}@supports (color:color-mix(in lab, red, red)){.border-danger\/25{border-color:color-mix(in oklab, var(--color-danger) 25%, transparent)}}.border-line{border-color:var(--color-line)}.border-line-strong{border-color:var(--color-line-strong)}.border-transparent{border-color:#0000}.border-warn\/25{border-color:var(--color-warn)}@supports (color:color-mix(in lab, red, red)){.border-warn\/25{border-color:color-mix(in oklab, var(--color-warn) 25%, transparent)}}.bg-brand{background-color:var(--color-brand)}.bg-brand-soft{background-color:var(--color-brand-soft)}.bg-brand-solid{background-color:var(--color-brand-solid)}.bg-danger{background-color:var(--color-danger)}.bg-danger-soft{background-color:var(--color-danger-soft)}.bg-line{background-color:var(--color-line)}.bg-line-strong{background-color:var(--color-line-strong)}.bg-ok-soft{background-color:var(--color-ok-soft)}.bg-raised{background-color:var(--color-raised)}.bg-sunken{background-color:var(--color-sunken)}.bg-surface{background-color:var(--color-surface)}.bg-transparent{background-color:#0000}.bg-warn{background-color:var(--color-warn)}.bg-warn-soft{background-color:var(--color-warn-soft)}.bg-white{background-color:var(--color-white)}.fill-brand-amber{fill:var(--color-brand-amber)}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-4{padding:calc(var(--spacing) * 4)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0{padding-block:0}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-14{padding-block:calc(var(--spacing) * 14)}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-0\.5{padding-right:calc(var(--spacing) * .5)}.pr-1{padding-right:var(--spacing)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-24{padding-bottom:calc(var(--spacing) * 24)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-body{color:var(--color-body)}.text-brand{color:var(--color-brand)}.text-danger{color:var(--color-danger)}.text-faint,.text-faint\/70{color:var(--color-faint)}@supports (color:color-mix(in lab, red, red)){.text-faint\/70{color:color-mix(in oklab, var(--color-faint) 70%, transparent)}}.text-ink{color:var(--color-ink)}.text-muted{color:var(--color-muted)}.text-ok{color:var(--color-ok)}.text-warn{color:var(--color-warn)}.text-white{color:var(--color-white)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.accent-brand-solid{accent-color:var(--color-brand-solid)}.opacity-50{opacity:.5}.opacity-80{opacity:.8}.shadow-card{--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-pop{--tw-shadow:var(--af-shadow-pop);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-\[border-color\,box-shadow\]{transition-property:border-color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-open\:rotate-90:is(:where(.group):is([open],:popover-open,:open) *){rotate:90deg}@media (hover:hover){.group-hover\:text-brand:is(:where(.group):hover *){color:var(--color-brand)}}.placeholder\:text-faint::placeholder{color:var(--color-faint)}.backdrop\:bg-black\/45::backdrop{background-color:#00000073}@supports (color:color-mix(in lab, red, red)){.backdrop\:bg-black\/45::backdrop{background-color:color-mix(in oklab, var(--color-black) 45%, transparent)}}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:-inset-2\.5:before{content:var(--tw-content);inset:calc(var(--spacing) * -2.5)}.before\:top-6:before{content:var(--tw-content);top:calc(var(--spacing) * 6)}.before\:bottom-2:before{content:var(--tw-content);bottom:calc(var(--spacing) * 2)}.before\:-left-2\.5:before{content:var(--tw-content);left:calc(var(--spacing) * -2.5)}.before\:w-\[3px\]:before{content:var(--tw-content);width:3px}.before\:rounded:before{content:var(--tw-content);border-radius:.25rem}.before\:bg-brand:before{content:var(--tw-content);background-color:var(--color-brand)}.before\:content-\[\'\'\]:before{--tw-content:"";content:var(--tw-content)}.last\:border-b:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.focus-within\:border-brand:focus-within{border-color:var(--color-brand)}.focus-within\:ring-4:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-within\:ring-brand-ring:focus-within{--tw-ring-color:var(--color-brand-ring)}@media (hover:hover){.hover\:border-brand:hover{border-color:var(--color-brand)}.hover\:border-line-strong:hover{border-color:var(--color-line-strong)}.hover\:bg-hover:hover{background-color:var(--color-hover)}.hover\:bg-raised:hover{background-color:var(--color-raised)}.hover\:text-brand:hover{color:var(--color-brand)}.hover\:text-danger:hover{color:var(--color-danger)}.hover\:text-ink:hover{color:var(--color-ink)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:fixed:focus{position:fixed}.focus\:top-3:focus{top:calc(var(--spacing) * 3)}.focus\:left-3:focus{left:calc(var(--spacing) * 3)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (prefers-reduced-motion:reduce){.motion-reduce\:transition-none{transition-property:none}}@media not all and (min-width:64rem){.max-lg\:sticky{position:sticky}.max-lg\:top-14{top:calc(var(--spacing) * 14)}.max-lg\:z-20{z-index:20}.max-lg\:mx-4{margin-inline:calc(var(--spacing) * 4)}.max-lg\:mt-3{margin-top:calc(var(--spacing) * 3)}.max-lg\:contents{display:contents}.max-lg\:flex{display:flex}.max-lg\:hidden{display:none}.max-lg\:min-h-11{min-height:calc(var(--spacing) * 11)}.max-lg\:shrink-0{flex-shrink:0}.max-lg\:gap-0\.5{gap:calc(var(--spacing) * .5)}.max-lg\:overflow-x-auto{overflow-x:auto}.max-lg\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.max-lg\:border-line{border-color:var(--color-line)}.max-lg\:bg-surface{background-color:var(--color-surface)}.max-lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.max-lg\:py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.max-lg\:whitespace-nowrap{white-space:nowrap}}@media (min-width:40rem){.sm\:left-auto{left:auto}.sm\:flex{display:flex}.sm\:inline{display:inline}.sm\:size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.sm\:h-6{height:calc(var(--spacing) * 6)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1\.1fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1.1fr)}.sm\:grid-cols-\[minmax\(0\,260px\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,260px) minmax(0,1fr)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-8{gap:calc(var(--spacing) * 8)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:px-8{padding-inline:calc(var(--spacing) * 8)}.sm\:py-0\.5{padding-block:calc(var(--spacing) * .5)}.sm\:py-1{padding-block:var(--spacing)}.sm\:pt-0\.5{padding-top:calc(var(--spacing) * .5)}.sm\:pt-8{padding-top:calc(var(--spacing) * 8)}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:before\:-left-3\.5:before{content:var(--tw-content);left:calc(var(--spacing) * -3.5)}}@media (min-width:48rem){.md\:block{display:block}.md\:inline{display:inline}}@media (min-width:64rem){.lg\:sticky{position:sticky}.lg\:top-14{top:calc(var(--spacing) * 14)}.lg\:block{display:block}.lg\:grid{display:grid}.lg\:inline-flex{display:inline-flex}.lg\:h-\[calc\(100dvh-3\.5rem\)\]{height:calc(100dvh - 3.5rem)}.lg\:-translate-x-6{--tw-translate-x:calc(var(--spacing) * -6);translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:grid-cols-\[260px_minmax\(0\,1fr\)\]{grid-template-columns:260px minmax(0,1fr)}.lg\:items-start{align-items:flex-start}.lg\:overflow-y-auto{overflow-y:auto}.lg\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.lg\:border-line{border-color:var(--color-line)}.lg\:bg-surface{background-color:var(--color-surface)}.lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.lg\:pt-4{padding-top:calc(var(--spacing) * 4)}.lg\:pb-16{padding-bottom:calc(var(--spacing) * 16)}.lg\:opacity-0{opacity:0}.lg\:opacity-100{opacity:1}.lg\:transition-\[transform\,opacity\]{transition-property:transform,opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.lg\:duration-200{--tw-duration:.2s;transition-duration:.2s}.lg\:ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}:root{color-scheme:light;--af-canvas:#f5f7fb;--af-surface:#fff;--af-raised:#f1f5f9;--af-hover:#e8eef6;--af-sunken:#fff;--af-line:#0f172a17;--af-line-strong:#0f172a2b;--af-ink:#0b1220;--af-body:#1e293b;--af-muted:#475569;--af-faint:#64748b;--af-brand:#2563eb;--af-brand-solid:#2563eb;--af-brand-solid-hover:#1d4ed8;--af-brand-soft:#2563eb14;--af-brand-ring:#2563eb38;--af-brand-amber:#c8692f;--af-ok:#047857;--af-ok-soft:#10b9811a;--af-warn:#b45309;--af-warn-soft:#f59e0b1f;--af-danger:#dc2626;--af-danger-soft:#ef444417;--af-shadow:0 1px 2px #0f172a0a, 0 1px 3px #0f172a0a;--af-shadow-pop:0 12px 32px #0f172a24;--af-scrollbar:#0f172a33;--af-scrollbar-hover:#0f172a59;--af-json-key:#1d4ed8;--af-json-string:#047857;--af-json-number:#b45309;--af-json-literal:#7c3aed}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--af-canvas:#08111f;--af-surface:#0c1727;--af-raised:#131e33;--af-hover:#1a2741;--af-sunken:#07101d;--af-line:#94a3b824;--af-line-strong:#94a3b842;--af-ink:#f8fafc;--af-body:#e2e8f0;--af-muted:#a3b1c6;--af-faint:#8391a7;--af-brand:#60a5fa;--af-brand-solid:#2563eb;--af-brand-solid-hover:#3b82f6;--af-brand-soft:#3b82f624;--af-brand-ring:#3b82f659;--af-brand-amber:#e9a57c;--af-ok:#34d399;--af-ok-soft:#10b98124;--af-warn:#fbbf24;--af-warn-soft:#f59e0b21;--af-danger:#f87171;--af-danger-soft:#ef444421;--af-shadow:0 1px 2px #02061766;--af-shadow-pop:0 16px 40px #0206178c;--af-scrollbar:#94a3b838;--af-scrollbar-hover:#94a3b866;--af-json-key:#93c5fd;--af-json-string:#6ee7b7;--af-json-number:#fcd34d;--af-json-literal:#c4b5fd}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:0s!important;animation-duration:0s!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;--font-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-canvas:var(--af-canvas);--color-surface:var(--af-surface);--color-raised:var(--af-raised);--color-hover:var(--af-hover);--color-sunken:var(--af-sunken);--color-line:var(--af-line);--color-line-strong:var(--af-line-strong);--color-ink:var(--af-ink);--color-body:var(--af-body);--color-muted:var(--af-muted);--color-faint:var(--af-faint);--color-brand:var(--af-brand);--color-brand-solid:var(--af-brand-solid);--color-brand-solid-hover:var(--af-brand-solid-hover);--color-brand-soft:var(--af-brand-soft);--color-brand-ring:var(--af-brand-ring);--color-brand-amber:var(--af-brand-amber);--color-ok:var(--af-ok);--color-ok-soft:var(--af-ok-soft);--color-warn:var(--af-warn);--color-warn-soft:var(--af-warn-soft);--color-danger:var(--af-danger);--color-danger-soft:var(--af-danger-soft)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html{scrollbar-gutter:stable}body{background-color:var(--color-canvas);font-family:var(--font-sans);color:var(--color-body);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*{scrollbar-width:thin;scrollbar-color:var(--af-scrollbar) transparent}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--af-scrollbar) padding-box;border:3px solid #0000;border-radius:9999px}::-webkit-scrollbar-thumb:hover{background:var(--af-scrollbar-hover) padding-box}::-webkit-scrollbar-corner{background:0 0}}@layer components{.input{height:calc(var(--spacing) * 11);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line-strong);background-color:var(--color-sunken);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));color:var(--color-ink);transition-property:border-color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s;display:block}.input::placeholder{color:var(--color-faint)}@media (hover:hover){.input:hover{border-color:var(--color-faint)}@supports (color:color-mix(in lab, red, red)){.input:hover{border-color:color-mix(in oklab, var(--color-faint) 60%, transparent)}}}.input:focus{border-color:var(--color-brand);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}.input:disabled{cursor:not-allowed;opacity:.5}@media (min-width:40rem){.input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.input-invalid{border-color:var(--color-danger)}.input-invalid:focus{border-color:var(--color-danger);--tw-ring-color:var(--color-danger-soft)}.btn{height:calc(var(--spacing) * 11);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 3.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;flex-shrink:0;transition-duration:.15s;display:inline-flex}.btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}.btn:disabled{cursor:not-allowed;opacity:.45}.btn-primary{background-color:var(--color-brand-solid);color:var(--color-white);--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (hover:hover){.btn-primary:hover{background-color:var(--color-brand-solid-hover)}}.btn-outline{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line-strong);background-color:var(--color-surface);color:var(--color-body)}@media (hover:hover){.btn-outline:hover{background-color:var(--color-raised)}}.btn-ghost{color:var(--color-muted)}@media (hover:hover){.btn-ghost:hover{background-color:var(--color-raised);color:var(--color-ink)}}.icon-btn{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11);cursor:pointer;border-radius:var(--radius-lg);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;flex-shrink:0;justify-content:center;align-items:center;transition-duration:.15s;display:inline-flex}@media (hover:hover){.icon-btn:hover{background-color:var(--color-raised);color:var(--color-ink)}}.icon-btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}.panel{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line);background-color:var(--color-surface);--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.kbd{height:calc(var(--spacing) * 5);border-style:var(--tw-border-style);border-width:1px;border-color:#ffffff40;border-radius:.25rem;align-items:center;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.kbd{border-color:color-mix(in oklab, var(--color-white) 25%, transparent)}}.kbd{padding-inline:calc(var(--spacing) * 1.5);font-family:var(--font-mono);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:#fffc}@supports (color:color-mix(in lab, red, red)){.kbd{color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.eyebrow{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);color:var(--color-faint);text-transform:uppercase}.focus-ring:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.top-0{top:0}.top-1\/2{top:50%}.right-3{right:calc(var(--spacing) * 3)}.right-4{right:calc(var(--spacing) * 4)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-3{left:calc(var(--spacing) * 3)}.left-4{left:calc(var(--spacing) * 4)}.z-40{z-index:40}.z-50{z-index:50}.m-0{margin:0}.mx-1{margin-inline:var(--spacing)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-7{margin-top:calc(var(--spacing) * 7)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-9{margin-top:calc(var(--spacing) * 9)}.mt-10{margin-top:calc(var(--spacing) * 10)}.-mr-1{margin-right:calc(var(--spacing) * -1)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-0{margin-bottom:0}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.size-\[18px\]{width:18px;height:18px}.size-full{width:100%;height:100%}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-auto{height:auto}.h-dvh{height:100dvh}.h-full{height:100%}.max-h-none{max-height:none}.min-h-0{min-height:0}.min-h-6{min-height:calc(var(--spacing) * 6)}.min-h-8{min-height:calc(var(--spacing) * 8)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-11{min-height:calc(var(--spacing) * 11)}.min-h-12{min-height:calc(var(--spacing) * 12)}.min-h-14{min-height:calc(var(--spacing) * 14)}.w-10{width:calc(var(--spacing) * 10)}.w-48{width:calc(var(--spacing) * 48)}.w-80{width:calc(var(--spacing) * 80)}.w-\[min\(540px\,100vw\)\]{width:min(540px,100vw)}.w-full{width:100%}.w-px{width:1px}.max-w-40{max-width:calc(var(--spacing) * 40)}.max-w-\[62ch\]{max-width:62ch}.max-w-\[320px\]{max-width:320px}.max-w-\[420px\]{max-width:420px}.max-w-\[520px\]{max-width:520px}.max-w-\[760px\]{max-width:760px}.max-w-full{max-width:100%}.max-w-prose{max-width:65ch}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-40{min-width:calc(var(--spacing) * 40)}.min-w-56{min-width:calc(var(--spacing) * 56)}.min-w-\[7rem\]{min-width:7rem}.flex-1{flex:1}.shrink-0{flex-shrink:0}.translate-x-0\.5{--tw-translate-x:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-\[18px\]{--tw-translate-x:18px;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-90{rotate:90deg}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.list-none{list-style-type:none}.appearance-none{appearance:none}.grid-cols-\[minmax\(0\,1fr\)_8rem_auto\]{grid-template-columns:minmax(0,1fr) 8rem auto}.grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1{row-gap:var(--spacing)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-line>:not(:last-child)){border-color:var(--color-line)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-\[10px\]{border-radius:10px}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-brand{border-color:var(--color-brand)}.border-brand-solid{border-color:var(--color-brand-solid)}.border-brand\/20{border-color:var(--color-brand)}@supports (color:color-mix(in lab, red, red)){.border-brand\/20{border-color:color-mix(in oklab, var(--color-brand) 20%, transparent)}}.border-danger\/25{border-color:var(--color-danger)}@supports (color:color-mix(in lab, red, red)){.border-danger\/25{border-color:color-mix(in oklab, var(--color-danger) 25%, transparent)}}.border-line{border-color:var(--color-line)}.border-line-strong{border-color:var(--color-line-strong)}.border-transparent{border-color:#0000}.border-warn\/25{border-color:var(--color-warn)}@supports (color:color-mix(in lab, red, red)){.border-warn\/25{border-color:color-mix(in oklab, var(--color-warn) 25%, transparent)}}.bg-brand{background-color:var(--color-brand)}.bg-brand-soft{background-color:var(--color-brand-soft)}.bg-brand-solid{background-color:var(--color-brand-solid)}.bg-danger{background-color:var(--color-danger)}.bg-danger-soft{background-color:var(--color-danger-soft)}.bg-line{background-color:var(--color-line)}.bg-line-strong{background-color:var(--color-line-strong)}.bg-ok-soft{background-color:var(--color-ok-soft)}.bg-raised{background-color:var(--color-raised)}.bg-sunken{background-color:var(--color-sunken)}.bg-surface{background-color:var(--color-surface)}.bg-transparent{background-color:#0000}.bg-warn{background-color:var(--color-warn)}.bg-warn-soft{background-color:var(--color-warn-soft)}.bg-white{background-color:var(--color-white)}.fill-brand-amber{fill:var(--color-brand-amber)}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0{padding-block:0}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-14{padding-block:calc(var(--spacing) * 14)}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-0\.5{padding-right:calc(var(--spacing) * .5)}.pr-1{padding-right:var(--spacing)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-24{padding-bottom:calc(var(--spacing) * 24)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-body{color:var(--color-body)}.text-brand{color:var(--color-brand)}.text-danger{color:var(--color-danger)}.text-faint,.text-faint\/70{color:var(--color-faint)}@supports (color:color-mix(in lab, red, red)){.text-faint\/70{color:color-mix(in oklab, var(--color-faint) 70%, transparent)}}.text-ink{color:var(--color-ink)}.text-muted{color:var(--color-muted)}.text-ok{color:var(--color-ok)}.text-warn{color:var(--color-warn)}.text-white{color:var(--color-white)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.accent-brand-solid{accent-color:var(--color-brand-solid)}.opacity-50{opacity:.5}.opacity-80{opacity:.8}.shadow-card{--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-pop{--tw-shadow:var(--af-shadow-pop);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-\[border-color\,box-shadow\]{transition-property:border-color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-open\:rotate-90:is(:where(.group):is([open],:popover-open,:open) *){rotate:90deg}@media (hover:hover){.group-hover\:text-brand:is(:where(.group):hover *){color:var(--color-brand)}}.placeholder\:text-faint::placeholder{color:var(--color-faint)}.backdrop\:bg-black\/45::backdrop{background-color:#00000073}@supports (color:color-mix(in lab, red, red)){.backdrop\:bg-black\/45::backdrop{background-color:color-mix(in oklab, var(--color-black) 45%, transparent)}}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:-inset-2\.5:before{content:var(--tw-content);inset:calc(var(--spacing) * -2.5)}.before\:top-6:before{content:var(--tw-content);top:calc(var(--spacing) * 6)}.before\:bottom-2:before{content:var(--tw-content);bottom:calc(var(--spacing) * 2)}.before\:-left-2\.5:before{content:var(--tw-content);left:calc(var(--spacing) * -2.5)}.before\:w-\[3px\]:before{content:var(--tw-content);width:3px}.before\:rounded:before{content:var(--tw-content);border-radius:.25rem}.before\:bg-brand:before{content:var(--tw-content);background-color:var(--color-brand)}.before\:content-\[\'\'\]:before{--tw-content:"";content:var(--tw-content)}.last\:border-b:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.focus-within\:border-brand:focus-within{border-color:var(--color-brand)}.focus-within\:ring-4:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-within\:ring-brand-ring:focus-within{--tw-ring-color:var(--color-brand-ring)}@media (hover:hover){.hover\:border-brand:hover{border-color:var(--color-brand)}.hover\:border-line-strong:hover{border-color:var(--color-line-strong)}.hover\:bg-hover:hover{background-color:var(--color-hover)}.hover\:bg-raised:hover{background-color:var(--color-raised)}.hover\:text-brand:hover{color:var(--color-brand)}.hover\:text-danger:hover{color:var(--color-danger)}.hover\:text-ink:hover{color:var(--color-ink)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:fixed:focus{position:fixed}.focus\:top-3:focus{top:calc(var(--spacing) * 3)}.focus\:left-3:focus{left:calc(var(--spacing) * 3)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (prefers-reduced-motion:reduce){.motion-reduce\:transition-none{transition-property:none}}@media not all and (min-width:64rem){.max-lg\:sticky{position:sticky}.max-lg\:top-14{top:calc(var(--spacing) * 14)}.max-lg\:z-20{z-index:20}.max-lg\:mx-4{margin-inline:calc(var(--spacing) * 4)}.max-lg\:mt-3{margin-top:calc(var(--spacing) * 3)}.max-lg\:contents{display:contents}.max-lg\:flex{display:flex}.max-lg\:hidden{display:none}.max-lg\:min-h-11{min-height:calc(var(--spacing) * 11)}.max-lg\:shrink-0{flex-shrink:0}.max-lg\:gap-0\.5{gap:calc(var(--spacing) * .5)}.max-lg\:overflow-x-auto{overflow-x:auto}.max-lg\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.max-lg\:border-line{border-color:var(--color-line)}.max-lg\:bg-surface{background-color:var(--color-surface)}.max-lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.max-lg\:py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.max-lg\:whitespace-nowrap{white-space:nowrap}}@media (min-width:40rem){.sm\:left-auto{left:auto}.sm\:flex{display:flex}.sm\:inline{display:inline}.sm\:size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.sm\:h-6{height:calc(var(--spacing) * 6)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1\.1fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1.1fr)}.sm\:grid-cols-\[minmax\(0\,260px\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,260px) minmax(0,1fr)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-8{gap:calc(var(--spacing) * 8)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:px-8{padding-inline:calc(var(--spacing) * 8)}.sm\:py-0\.5{padding-block:calc(var(--spacing) * .5)}.sm\:py-1{padding-block:var(--spacing)}.sm\:pt-0\.5{padding-top:calc(var(--spacing) * .5)}.sm\:pt-8{padding-top:calc(var(--spacing) * 8)}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:before\:-left-3\.5:before{content:var(--tw-content);left:calc(var(--spacing) * -3.5)}}@media (min-width:48rem){.md\:block{display:block}.md\:inline{display:inline}}@media (min-width:64rem){.lg\:sticky{position:sticky}.lg\:top-14{top:calc(var(--spacing) * 14)}.lg\:block{display:block}.lg\:grid{display:grid}.lg\:inline-flex{display:inline-flex}.lg\:h-\[calc\(100dvh-3\.5rem\)\]{height:calc(100dvh - 3.5rem)}.lg\:-translate-x-6{--tw-translate-x:calc(var(--spacing) * -6);translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:grid-cols-\[260px_minmax\(0\,1fr\)\]{grid-template-columns:260px minmax(0,1fr)}.lg\:items-start{align-items:flex-start}.lg\:overflow-y-auto{overflow-y:auto}.lg\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.lg\:border-line{border-color:var(--color-line)}.lg\:bg-surface{background-color:var(--color-surface)}.lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.lg\:pt-4{padding-top:calc(var(--spacing) * 4)}.lg\:pb-16{padding-bottom:calc(var(--spacing) * 16)}.lg\:opacity-0{opacity:0}.lg\:opacity-100{opacity:1}.lg\:transition-\[transform\,opacity\]{transition-property:transform,opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.lg\:duration-200{--tw-duration:.2s;transition-duration:.2s}.lg\:ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}:root{color-scheme:light;--af-canvas:#f5f7fb;--af-surface:#fff;--af-raised:#f1f5f9;--af-hover:#e8eef6;--af-sunken:#fff;--af-line:#0f172a17;--af-line-strong:#0f172a2b;--af-ink:#0b1220;--af-body:#1e293b;--af-muted:#475569;--af-faint:#64748b;--af-brand:#2563eb;--af-brand-solid:#2563eb;--af-brand-solid-hover:#1d4ed8;--af-brand-soft:#2563eb14;--af-brand-ring:#2563eb38;--af-brand-amber:#c8692f;--af-ok:#047857;--af-ok-soft:#10b9811a;--af-warn:#b45309;--af-warn-soft:#f59e0b1f;--af-danger:#dc2626;--af-danger-soft:#ef444417;--af-shadow:0 1px 2px #0f172a0a, 0 1px 3px #0f172a0a;--af-shadow-pop:0 12px 32px #0f172a24;--af-scrollbar:#0f172a33;--af-scrollbar-hover:#0f172a59;--af-json-key:#1d4ed8;--af-json-string:#047857;--af-json-number:#b45309;--af-json-literal:#7c3aed}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--af-canvas:#08111f;--af-surface:#0c1727;--af-raised:#131e33;--af-hover:#1a2741;--af-sunken:#07101d;--af-line:#94a3b824;--af-line-strong:#94a3b842;--af-ink:#f8fafc;--af-body:#e2e8f0;--af-muted:#a3b1c6;--af-faint:#8391a7;--af-brand:#60a5fa;--af-brand-solid:#2563eb;--af-brand-solid-hover:#3b82f6;--af-brand-soft:#3b82f624;--af-brand-ring:#3b82f659;--af-brand-amber:#e9a57c;--af-ok:#34d399;--af-ok-soft:#10b98124;--af-warn:#fbbf24;--af-warn-soft:#f59e0b21;--af-danger:#f87171;--af-danger-soft:#ef444421;--af-shadow:0 1px 2px #02061766;--af-shadow-pop:0 16px 40px #0206178c;--af-scrollbar:#94a3b838;--af-scrollbar-hover:#94a3b866;--af-json-key:#93c5fd;--af-json-string:#6ee7b7;--af-json-number:#fcd34d;--af-json-literal:#c4b5fd}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:0s!important;animation-duration:0s!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file diff --git a/tenxgraph_api/cli/config_editor/static/app.js b/tenxgraph_api/cli/config_editor/static/app.js index 4f598745..a73cf247 100644 --- a/tenxgraph_api/cli/config_editor/static/app.js +++ b/tenxgraph_api/cli/config_editor/static/app.js @@ -1,3 +1,3 @@ -var $e,b,Ct,Nn,U,vt,$t,St,Ve,xe,ae,Tt,Ke,Be,Je,Dn,ke={},we=[],Ln=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Se=Array.isArray;function D(e,t){for(var o in t)e[o]=t[o];return e}function Ye(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function zn(e,t,o){var r,i,s,a={};for(s in t)s=="key"?r=t[s]:s=="ref"?i=t[s]:a[s]=t[s];if(arguments.length>2&&(a.children=arguments.length>3?$e.call(arguments,2):o),typeof e=="function"&&e.defaultProps!=null)for(s in e.defaultProps)a[s]===void 0&&(a[s]=e.defaultProps[s]);return ve(e,a,r,i,null)}function ve(e,t,o,r,i){var s={type:e,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i??++Ct,__i:-1,__u:0};return i==null&&b.vnode!=null&&b.vnode(s),s}function w(e){return e.children}function ye(e,t){this.props=e,this.context=t}function J(e,t){if(t==null)return e.__?J(e.__,e.__i+1):null;for(var o;tt&&U.sort(St),e=U.shift(),t=U.length,Hn(e)}finally{U.length=Ce.__r=0}}function It(e,t,o,r,i,s,a,d,f,l,p){var h,c,m,x,$,k,T=r&&r.__k||we,v=t.length;for(f=Fn(o,t,T,f,v),h=0;h0?a=e.__k[s]=ve(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[s]=a,f=s+c,a.__=e,a.__b=e.__b+1,d=null,(l=a.__i=qn(a,o,f,h))!=-1&&(h--,(d=o[l])&&(d.__u|=2)),d==null||d.__v==null?(l==-1&&(i>p?c--:if?c--:c++,a.__u|=4))):e.__k[s]=null;if(h)for(s=0;s(p?1:0)){for(i=o-1,s=o+1;i>=0||s=0?i--:s++])!=null&&(2&l.__u)==0&&d==l.key&&f==l.type)return a}return-1}function kt(e,t,o){t[0]=="-"?e.setProperty(t,o??""):e[t]=o==null?"":typeof o!="number"||Ln.test(t)?o:o+"px"}function be(e,t,o,r,i){var s,a;e:if(t=="style")if(typeof o=="string")e.style.cssText=o;else{if(typeof r=="string"&&(e.style.cssText=r=""),r)for(t in r)o&&t in o||kt(e.style,t,"");if(o)for(t in o)r&&o[t]==r[t]||kt(e.style,t,o[t])}else if(t[0]=="o"&&t[1]=="n")s=t!=(t=t.replace(Tt,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+s]=o,o?r?o[ae]=r[ae]:(o[ae]=Ke,e.addEventListener(t,s?Je:Be,s)):e.removeEventListener(t,s?Je:Be,s);else{if(i=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=o??"";break e}catch{}typeof o=="function"||(o==null||o===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&o==1?"":o))}}function wt(e){return function(t){if(this.l){var o=this.l[t.type+e];if(t[xe]==null)t[xe]=Ke++;else if(t[xe]0?e:Se(e)?e.map(jt):e.constructor!==void 0?null:D({},e)}function Un(e,t,o,r,i,s,a,d,f){var l,p,h,c,m,x,$,k=o.props||ke,T=t.props,v=t.type;if(v=="svg"?i="http://www.w3.org/2000/svg":v=="math"?i="http://www.w3.org/1998/Math/MathML":i||(i="http://www.w3.org/1999/xhtml"),s!=null){for(l=0;l=o.__.length&&o.__.push({}),o.__[e]}function Z(e){return Oe=1,nt(Vt,e)}function nt(e,t,o){var r=tt(le++,2);if(r.t=e,!r.__c&&(r.__=[o?o(t):Vt(void 0,t),function(d){var f=r.__N?r.__N[0]:r.__[0],l=r.t(f,d);f!==l&&(r.__N=[l,r.__[1]],r.__c.setState({}))}],r.__c=C,!C.__f)){var i=function(d,f,l){if(!r.__c.__H)return!0;var p=!1,h=r.__c.props!==d;if(r.__c.__H.__.some(function(m){if(m.__N){p=!0;var x=m.__[0];m.__=m.__N,m.__N=void 0,x!==m.__[0]&&(h=!0)}}),s){var c=s.call(this,d,f,l);return p?c||h:c}return!p||h};C.__f=!0;var s=C.shouldComponentUpdate,a=C.componentWillUpdate;C.componentWillUpdate=function(d,f,l){if(this.__e){var p=s;s=void 0,i(d,f,l),s=p}a&&a.call(this,d,f,l)},C.shouldComponentUpdate=i}return r.__N||r.__}function L(e,t){var o=tt(le++,3);!S.__s&&Gt(o.__H,t)&&(o.__=e,o.u=t,C.__H.__h.push(o))}function Ie(e){return Oe=5,Gn(function(){return{current:e}},[])}function Gn(e,t){var o=tt(le++,7);return Gt(o.__H,t)&&(o.__=e(),o.__H=t,o.__h=e),o.__}function Vn(){for(var e;e=Wt.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Te),t.__h.some(et),t.__h=[]}catch(o){t.__h=[],S.__e(o,e.__v)}}}S.__b=function(e){C=null,Dt&&Dt(e)},S.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),qt&&qt(e,t)},S.__r=function(e){Lt&&Lt(e),le=0;var t=(C=e.__c).__H;t&&(Xe===C?(t.__h=[],C.__h=[],t.__.some(function(o){o.__N&&(o.__=o.__N),o.u=o.__N=void 0})):(t.__h.some(Te),t.__h.some(et),t.__h=[],le=0)),Xe=C},S.diffed=function(e){zt&&zt(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(Wt.push(t)!==1&&Nt===S.requestAnimationFrame||((Nt=S.requestAnimationFrame)||Bn)(Vn)),t.__H.__.some(function(o){o.u&&(o.__H=o.u,o.u=void 0)})),Xe=C=null},S.__c=function(e,t){t.some(function(o){try{o.__h.some(Te),o.__h=o.__h.filter(function(r){return!r.__||et(r)})}catch(r){t.some(function(i){i.__h&&(i.__h=[])}),t=[],S.__e(r,o.__v)}}),Ht&&Ht(e,t)},S.unmount=function(e){Ft&&Ft(e);var t,o=e.__c;o&&o.__H&&(o.__H.__.some(function(r){try{Te(r)}catch(i){t=i}}),o.__H=void 0,t&&S.__e(t,o.__v))};var Ut=typeof requestAnimationFrame=="function";function Bn(e){var t,o=function(){clearTimeout(r),Ut&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(o,35);Ut&&(t=requestAnimationFrame(o))}function Te(e){var t=C,o=e.__c;typeof o=="function"&&(e.__c=void 0,o()),C=t}function et(e){var t=C;e.__c=e.__(),C=t}function Gt(e,t){return!e||e.length!==t.length||t.some(function(o,r){return o!==e[r]})}function Vt(e,t){return typeof t=="function"?t(e):t}var Ee=[{label:null,items:["get-running"]},{label:"Before production",items:["auth","cache_store","observability"]},{label:"Runs and streaming",items:["runs","websocket","ag_ui","remote_tools","media"]},{label:"Project wiring",items:["checkpointer","id_generator","thread_name_generator","store","injectq"]},{label:"Developer tools",items:["test","evaluation"]},{label:"Reference",items:["environment"]}],z={"get-running":{title:"Get running",icon:"flag",description:"Only the agent graph is required. Everything else is optional."},environment:{title:"Server environment (.env)",icon:"terminal",description:"Variables the server reads from .env. The editor never reads that file, so values are not shown."}},ot={"auth.path":"auth-path"},Bt="tg-config-token",Jt=!1;function Jn(){let e=new URLSearchParams(location.hash.slice(1)).get("token")||"";try{e&&sessionStorage.setItem(Bt,e);let t=sessionStorage.getItem(Bt)||"";return Jt=t!==""&&t===(e||t),e||t}catch{return e}}var ce=Jn(),rt=e=>Jt||!ce?`#${e}`:`#token=${encodeURIComponent(ce)}&page=${e}`;function Kt(){let e=location.hash.slice(1);if(e.includes("=")){let t=new URLSearchParams(e);return t.get("page")||t.get("section")||""}try{return decodeURIComponent(e)}catch{return e}}var u={schema:null,sections:[],nav:[],fields:new Map,config:{},version:null,exists:!1,path:"",parseError:null,saved:"",validated:null,issues:[],page:"get-running",query:"",sideOpen:!0,sideRender:!0,sideShown:!0,drawerOpen:!1,tab:"json",copied:!1,busy:null,toolParams:{},toasts:[],fatal:null},Yt=()=>{},Qt=e=>{Yt=e};function g(e){e&&e(u),Yt()}var Zt=e=>JSON.parse(JSON.stringify(e)),M=()=>JSON.stringify(u.config),y=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Ae=(e,t)=>t.reduce((o,r)=>y(o)?o[r]:void 0,e),de=(e,t)=>`${e} ${t}${e===1?"":"s"}`;function Pe(e,t,o){let r=e;for(let i of t.slice(0,-1))y(r[i])||(r[i]={}),r=r[i];r[t[t.length-1]]=o}var Kn=new Set(["cache_store.rate_limit","cache_store.token_revocation"]);function st(e,t){let o=[],r=e;for(let i of t.slice(0,-1)){if(!y(r))return;o.push([r,i]),r=r[i]}y(r)&&delete r[t[t.length-1]];for(let i=o.length-1;i>=1;i--){let[s,a]=o[i];if(Kn.has(t.slice(0,i+1).join(".")))break;if(y(s[a])&&Object.keys(s[a]).length===0)delete s[a];else break}}function X(e){let t=Ae(u.config,e);return t!==void 0?t:u.fields.get(e.join("."))?.default}var Yn=(e,t)=>JSON.stringify(e)===JSON.stringify(t),E=e=>u.sections.find(t=>t.id===e),H=()=>M()!==u.saved,je=e=>!e.optional||e.key in u.config&&u.config[e.key]!==null,ee=e=>u.issues.filter(t=>t.section===e),O=e=>u.issues.find(t=>t.path===e);function ue(e=u.issues){let t=e.filter(o=>o.level==="error").length;return{errors:t,warnings:e.length-t}}var Me=e=>ot[e]||`f-${e.replaceAll(".","-")}`,it=e=>{let t=Ae(u.config,e.path);return t!==void 0&&!Yn(t,e.default)},Xt=e=>e.id==="core"?!1:e.id==="auth"?u.config.auth!=null:e.optional?je(e):e.fields.some(it);function en(e){let t=ee(e);return t.find(o=>o.level==="error")||t[0]||null}var tn=e=>!!e.show_if&&X(e.show_if.path)!==e.show_if.equals;function nn(e){let o=u.fields.get(e.show_if.path.join("."))?.label||e.show_if.path.join("."),r=e.show_if.equals;return`Used when ${o} is ${r===!0?"on":r===!1?"off":String(r)}.`}var pe=e=>{let t=E(e);return z[e]?{id:e,...z[e]}:t?{id:e,title:t.title,subtitle:t.subtitle,description:t.description,icon:null}:null},on=()=>/^[\w.]+:[\w.]+$/.test(String(u.config.agent||""));async function lt(e,t,o){let r={"X-10xGraph-Token":ce};o!==void 0&&(r["Content-Type"]="application/json");let i=await fetch(t,{method:e,headers:r,body:o===void 0?void 0:JSON.stringify(o)}),s={};try{s=await i.json()}catch{}return{ok:i.ok,status:i.status,data:s}}var at=(e,t)=>g(o=>{o.fatal={title:e,text:t}});async function an(){if(!ce)return at("Open the link printed by 10xgraph config","This page needs the session token included in that link.");let e;try{e=await lt("GET","/api/state")}catch{return at("Cannot reach the config server","Check that 10xgraph config is still running in your terminal, then reload.")}if(!e.ok)return at("This link has expired",e.data.error||"Restart 10xgraph config and open the new link it prints.");let t=e.data;g(o=>{o.schema=t.schema,o.sections=t.schema.sections;let r=new Set(Ee.flatMap(a=>a.items)),i=o.sections.map(a=>a.id).filter(a=>a!=="core"&&!r.has(a));o.nav=Ee.map((a,d)=>({label:a.label,items:dz[f]||E(f)):[...i,...a.items]}));for(let a of o.sections)for(let d of a.fields)o.fields.set(d.path.join("."),d);o.config=t.config,o.version=t.version,o.exists=t.exists,o.path=t.path,o.parseError=t.parse_error,o.issues=t.issues;let s=t.exists&&!t.parse_error;o.saved=s?M():"",o.validated=s?M():null}),Ne({focus:!1}),history.replaceState(null,"",rt(u.page))}var rn=null;async function ln(){let e=M();if(!(!u.schema||u.validated===e||rn===e)&&!Object.values(u.toolParams).some(t=>t.error)){rn=e;try{let t=await lt("POST","/api/validate",{config:JSON.parse(e)});if(!t.ok)return;g(o=>{o.issues=t.data.issues,o.validated=e})}catch{}}}async function Re(){if(!(u.busy||!u.schema)){if(u.exists&&!H()&&!u.parseError)return F("success","No changes to save.");if(Qn()){g(e=>{e.busy="save"});try{let e=await lt("POST","/api/save",{config:u.config,version:u.version});if(e.data.issues&&(u.issues=e.data.issues,u.validated=M()),e.ok){u.version=e.data.version,u.config=e.data.config,u.toolParams={},u.validated=M(),u.saved=M(),u.exists=!0,u.parseError=null;let t=e.data.path.split(/[\\/]/).pop();F("success",e.data.backup?`Saved ${t}. The previous version is in ${t}.bak.`:`Created ${t}.`)}else if(e.status===409)F("error",e.data.error,{label:"Reload",run:()=>location.reload()});else if(e.status===422){let{errors:t}=ue();F("error",`Not saved: ${de(t,"error")} to fix first.`,{label:"Show",run:Zn})}else F("error",e.data.error||"Save failed.")}catch{F("error","Cannot reach the config server.")}finally{g(e=>{e.busy=null})}}}}function Qn(){let e=Object.values(u.toolParams).some(t=>t.error);return e&&(R("remote_tools"),F("error","A remote tool has invalid parameters JSON.")),!e}function cn(e,t){g(o=>{t?o.config[e.key]=Zt(e.default):delete o.config[e.key],e.key==="remote_tools"&&(o.toolParams={})})}function Zn(){let e=u.issues.find(t=>t.level==="error")||u.issues[0];e&&R(e.section,Me(e.path))}var Xn=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches?"auto":"smooth";function eo(e){return e==="core"?"get-running":z[e]||E(e)?e:"get-running"}function Ne({focus:e=!0}={}){g(t=>{t.page=eo(Kt()),t.query="",t.drawerOpen=!1}),e&&requestAnimationFrame(()=>{window.scrollTo(0,0),document.getElementById("page-title")?.focus({preventScroll:!0})})}function R(e,t){let o=rt(e);location.hash!==o&&history.pushState(null,"",o),Ne(),t&&requestAnimationFrame(()=>requestAnimationFrame(()=>{let r=document.getElementById(t);r&&(r.scrollIntoView({block:"center",behavior:Xn()}),r.focus({preventScroll:!0}))}))}function De(e){g(t=>{t.query=e})}var to=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches,sn;function te(e,{instant:t=!1}={}){try{localStorage.setItem("tg-sidebar",e?"shown":"hidden")}catch{}clearTimeout(sn);let o=t||to();g(r=>{r.sideOpen=e,e?(r.sideRender=!0,r.sideShown=o):r.sideShown=!1,!e&&o&&(r.sideRender=!1)}),e&&!o&&requestAnimationFrame(()=>requestAnimationFrame(()=>g(r=>{r.sideShown=!0}))),!e&&!o&&(sn=setTimeout(()=>g(r=>{r.sideOpen||(r.sideRender=!1)}),200))}function ct(){u.sideOpen||te(!0,{instant:!0}),requestAnimationFrame(()=>{let e=document.getElementById("q");e?.focus(),e?.select()})}function dn(e){g(t=>{e&&(t.tab=e),t.drawerOpen=!0})}var Le=()=>g(e=>{e.drawerOpen=!1});async function un(){let e="10xgraph dev";try{await navigator.clipboard.writeText(e)}catch{try{let t=document.createElement("textarea");t.value=e,document.body.append(t),t.select(),document.execCommand("copy"),t.remove()}catch{}}g(t=>{t.copied=!0}),setTimeout(()=>g(t=>{t.copied=!1}),2e3)}async function pn(){try{await navigator.clipboard.writeText(JSON.stringify(u.config,null,2)+` -`),F("success","Copied 10xgraph.json to the clipboard.")}catch{F("error","The clipboard is not available here.")}}var no=0;function F(e,t,o){let r=++no;g(i=>{i.toasts=[...i.toasts,{id:r,kind:e,message:t,action:o}]}),setTimeout(()=>ze(r),o?8e3:4e3)}function ze(e){g(t=>{t.toasts=t.toasts.filter(o=>o.id!==e)})}var fn={box:'',key:'',shield:'',gauge:'',arrows:'',activity:'',layers:'',wrench:'',flask:'',target:'',checkCircle:'',xCircle:'',alert:'',info:'',file:'',copy:'',x:'',plus:'',trash:'',panel:'',chevronDown:'',expand:'',collapse:'',power:'',spinner:'',book:'',flag:'',lock:'',play:'',bolt:'',terminal:'',search:'',chevronRight:'',code:'',circle:'',external:''},K={"get-running":"flag",environment:"terminal",core:"flag",auth:"lock",cache_store:"gauge",observability:"activity",runs:"play",websocket:"bolt",ag_ui:"external",remote_tools:"wrench",media:"file",checkpointer:"layers",id_generator:"key",thread_name_generator:"file",store:"box",injectq:"arrows",test:"flask",evaluation:"target"};var oo=0;function n(e,t,o,r,i,s){t||(t={});var a,d,f=t;if("ref"in f)for(d in f={},t)d=="ref"?a=t[d]:f[d]=t[d];var l={type:e,props:f,key:o,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--oo,__i:-1,__u:0,__source:i,__self:s};if(typeof e=="function"&&(a=e.defaultProps))for(d in a)f[d]===void 0&&(f[d]=a[d]);return b.vnode&&b.vnode(l),l}function _({name:e,class:t="size-4"}){return/(^|\s)size-/.test(t)||(t=`size-4 ${t}`),n("span",{class:`inline-flex shrink-0 ${t}`,"aria-hidden":"true",children:n("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round",class:"size-full",dangerouslySetInnerHTML:{__html:fn[e]}})})}function ne({class:e,children:t}){return n("span",{class:`inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-semibold ${e}`,children:t})}function dt({id:e,on:t,label:o,onChange:r}){return n("button",{id:e,type:"button",role:"switch","aria-checked":t?"true":"false","aria-label":o,class:`focus-ring relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200 before:absolute before:-inset-2.5 before:content-[''] ${t?"bg-brand-solid":"bg-line-strong"}`,onClick:()=>r(!t),children:n("span",{class:`pointer-events-none inline-block size-5 rounded-full bg-white shadow-sm ring-1 ring-black/5 transition-transform duration-200 ease-out ${t?"translate-x-[18px]":"translate-x-0.5"}`})})}function Y({id:e,label:t,options:o,value:r,onPick:i}){return n("div",{id:e,role:"radiogroup","aria-label":t,class:"inline-flex max-w-full flex-wrap gap-0.5 rounded-[10px] border border-line-strong bg-raised p-0.5",children:o.map(([s,a])=>n("button",{type:"button",role:"radio","aria-checked":s===r?"true":"false",class:`focus-ring min-h-10 cursor-pointer rounded-lg px-3.5 text-sm font-medium transition-colors duration-150 ${s===r?"bg-surface text-ink shadow-card":"text-muted hover:text-ink"}`,onClick:()=>{s!==r&&i(s)},children:a},s))})}function oe({id:e,options:t,value:o,invalid:r,onChange:i}){return n("div",{class:"relative",children:[n("select",{id:e,value:o,class:`input cursor-pointer appearance-none pr-9 font-mono ${r?"input-invalid":""}`,onChange:s=>i(s.currentTarget.value),children:t.map(([s,a])=>n("option",{value:s,children:a},s))}),n(_,{name:"chevronDown",class:"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-faint"})]})}function W({id:e,values:t,placeholder:o,suggestions:r,onChange:i}){let s=Ie(null),a=f=>{let l=f.trim();s.current&&(s.current.value=""),l&&!t.includes(l)&&i([...t,l])},d=r?`${e}-options`:void 0;return n("div",{class:"flex min-h-11 cursor-text flex-wrap items-center gap-1.5 rounded-lg border border-line-strong bg-sunken px-1.5 py-1.5 transition-[border-color,box-shadow] duration-150 focus-within:border-brand focus-within:ring-4 focus-within:ring-brand-ring sm:py-1",onClick:f=>{f.target===f.currentTarget&&s.current?.focus()},children:[t.map((f,l)=>n("span",{class:"inline-flex h-7 items-center gap-0.5 rounded-md border border-line bg-raised pr-0.5 pl-2 font-mono text-xs text-body sm:h-6",children:[f,n("button",{type:"button","aria-label":`Remove ${f}`,class:"inline-flex size-6 cursor-pointer items-center justify-center rounded text-faint transition-colors hover:bg-hover hover:text-ink sm:size-5",onClick:()=>i(t.filter((p,h)=>h!==l)),children:n(_,{name:"x",class:"size-3"})})]},f)),n("input",{ref:s,id:e,type:"text",list:d,placeholder:t.length?"Add...":o,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:"h-7 min-w-[7rem] flex-1 bg-transparent px-1 font-mono text-base text-ink outline-none placeholder:text-faint sm:text-sm",onKeyDown:f=>{let l=f.currentTarget;(f.key==="Enter"||f.key===",")&&l.value.trim()?(f.preventDefault(),a(l.value)):f.key==="Backspace"&&!l.value&&t.length&&i(t.slice(0,-1))},onChange:f=>{r?.includes(f.currentTarget.value.trim())&&a(f.currentTarget.value)},onBlur:f=>a(f.currentTarget.value)}),r?n("datalist",{id:d,children:r.filter(f=>!t.includes(f)).map(f=>n("option",{value:f},f))}):null]})}function fe({label:e,path:t,help:o,id:r,required:i}){return n("div",{class:"min-w-0",children:[n("label",{for:r,class:"block text-sm font-medium text-ink",children:[e,i?n("span",{class:"text-danger",children:" *"}):null]}),n("div",{class:"mt-0.5 font-mono text-xs text-faint",children:t}),o?n("p",{class:"mt-1.5 text-[13px] leading-5 text-muted",children:o}):null]})}function G({issue:e}){if(!e)return null;let t=e.level==="error";return n("p",{class:`mt-2 flex gap-1.5 text-[13px] leading-5 ${t?"text-danger":"text-warn"}`,role:t?"alert":void 0,children:[n(_,{name:t?"xCircle":"alert",class:"mt-0.5 size-3.5"}),n("span",{children:e.message})]})}function ut({children:e}){return n("div",{class:"grid gap-3 px-5 py-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] sm:gap-8",children:e})}function V({id:e,label:t,path:o,value:r,placeholder:i,onInput:s,issue:a,help:d}){return n(ut,{children:[n(fe,{label:t,path:o,help:d,id:e}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n("input",{id:e,type:"text",value:r??"",placeholder:i,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${a?.level==="error"?"input-invalid":""}`,onInput:f=>s(f.currentTarget.value)}),n(G,{issue:a})]})]})}function pt({name:e,label:t,options:o,value:r,onPick:i}){return n("div",{role:"radiogroup","aria-label":t,class:"grid gap-3 sm:grid-cols-2",children:o.map(([s,a,d])=>{let f=s===r;return n("button",{type:"button",role:"radio","aria-checked":f?"true":"false",id:`${e}-${s}`,class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 ${f?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{f||i(s)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${f?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:f?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:a}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:d})]})]},s)})})}function A({title:e,children:t}){return n("section",{children:[e?n("h4",{class:"eyebrow mb-2.5",children:e}):null,t]})}var ro={info:["info","border-brand/20 bg-brand-soft","text-brand"],warn:["alert","border-warn/25 bg-warn-soft","text-warn"],error:["xCircle","border-danger/25 bg-danger-soft","text-danger"]};function me({kind:e,title:t,lines:o}){let[r,i,s]=ro[e];return n("div",{class:`flex gap-3 rounded-xl border px-4 py-3 ${i}`,role:e==="error"?"alert":void 0,children:[n(_,{name:r,class:`mt-0.5 size-4 ${s}`}),n("div",{class:"min-w-0 space-y-1 text-[13px] leading-5 text-body",children:[t?n("p",{class:"font-medium text-ink",children:t}):null,o.map((a,d)=>n("p",{children:a},d))]})]})}function ft({issue:e}){return n(me,{kind:e.level==="error"?"error":"warn",lines:[n("span",{children:[e.path?n("code",{class:"mr-1.5 font-mono text-xs text-muted",children:e.path}):null,e.message]})]})}function mn({icon:e,tint:t,title:o,text:r,children:i}){return n("div",{class:"px-6 py-14 text-center",children:[n("div",{class:`mx-auto flex size-11 items-center justify-center rounded-full bg-raised ${t}`,children:n(_,{name:e,class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:o}),n("p",{class:"mx-auto mt-1 max-w-xs text-sm leading-6 text-muted",children:r}),i]})}function hn(){let e=Ie(null);L(()=>{let a=e.current;a&&(u.drawerOpen&&!a.open?a.showModal():!u.drawerOpen&&a.open&&a.close())});let t=u.issues.length>0,o=t&&u.tab==="problems"?"problems":"json",{errors:r,warnings:i}=ue(),s=(a,d,f)=>n("button",{type:"button",role:"tab",id:`tab-${a}`,"aria-selected":o===a?"true":"false",class:`focus-ring inline-flex h-10 cursor-pointer items-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors duration-150 ${o===a?"bg-raised text-ink":"text-muted hover:text-ink"}`,onClick:()=>g(l=>{l.tab=a}),children:[d,f]});return n("dialog",{ref:e,"aria-label":"10xgraph.json preview",class:"m-0 ml-auto h-dvh max-h-none w-[min(540px,100vw)] border-0 border-l border-line-strong bg-surface p-0 text-body shadow-pop backdrop:bg-black/45",onClose:()=>{u.drawerOpen&&Le()},onClick:a=>{a.target===a.currentTarget&&Le()},children:n("div",{class:"flex h-full flex-col",children:[n("div",{class:"flex h-14 shrink-0 items-center gap-1 border-b border-line px-3",children:[n("div",{role:"tablist",class:"flex flex-1 gap-1",children:[s("json","10xgraph.json",null),t?s("problems","Problems",n("span",{class:`inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[11px] font-semibold ${r?"bg-danger-soft text-danger":"bg-warn-soft text-warn"}`,children:r+i})):null]}),n("button",{type:"button",class:"icon-btn","aria-label":"Close preview",onClick:Le,children:n(_,{name:"x",class:"size-5"})})]}),n("div",{class:"min-h-0 flex-1 overflow-y-auto overscroll-contain",children:o==="problems"?n(so,{}):n(ao,{})})]})})}function so(){let{errors:e,warnings:t}=ue(),o=u.sections.map(r=>[r,ee(r.id)]).filter(([,r])=>r.length);return n("div",{children:[n("div",{class:"border-b border-line px-4 py-3",children:n("p",{class:"text-sm text-muted",children:[n("span",{class:e?"font-medium text-danger":"",children:de(e,"error")})," \xB7 ",n("span",{class:t?"font-medium text-warn":"",children:de(t,"warning")})]})}),n("div",{class:"space-y-5 px-3 py-4",children:o.map(([r,i])=>n("div",{children:[n("p",{class:"eyebrow mb-1.5 flex items-center gap-2 px-2",children:[n(_,{name:K[r.id]||"box",class:"size-3.5"}),r.title]}),n("ul",{class:"space-y-0.5",children:i.map((s,a)=>{let d=s.level==="error";return n("li",{children:n("button",{type:"button",onClick:()=>R(r.id,Me(s.path)),class:"focus-ring flex min-h-11 w-full cursor-pointer gap-2.5 rounded-lg px-2 py-2 text-left transition-colors duration-150 hover:bg-raised",children:[n(_,{name:d?"xCircle":"alert",class:`mt-0.5 size-4 ${d?"text-danger":"text-warn"}`}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-[13px] leading-5 text-body",children:s.message}),s.path?n("span",{class:"mt-0.5 block truncate font-mono text-xs text-faint",children:s.path}):null]})]})},a)})})]},r.id))})]})}function io(e){return e.replace(/&/g,"&").replace(//g,">").replace(/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,o=>{let r="var(--af-json-number)";return o.startsWith('"')?r=o.endsWith(":")?"var(--af-json-key)":"var(--af-json-string)":(o==="true"||o==="false"||o==="null")&&(r="var(--af-json-literal)"),`${o}`})}function ao(){let e=JSON.stringify(u.config,null,2).split(` -`);return n("div",{children:[n("div",{class:"sticky top-0 flex items-center justify-between gap-3 border-b border-line bg-surface px-4 py-2.5",children:[n("p",{class:"text-xs text-faint",children:H()||!u.exists?"Preview of what Save will write":"Matches the file on disk"}),n("button",{type:"button",class:"btn btn-ghost h-8 px-2.5 text-xs",onClick:pn,children:[n(_,{name:"copy",class:"size-3.5"}),"Copy"]})]}),n("div",{class:"py-3 pr-4 font-mono text-[12.5px] leading-5",children:e.map((t,o)=>n("div",{class:"flex",children:[n("span",{class:"w-10 shrink-0 pr-3 text-right text-faint/70 select-none",children:o+1}),n("span",{class:"min-w-0 flex-1 break-all whitespace-pre-wrap",dangerouslySetInnerHTML:{__html:io(t)}})]},o))})]})}function ht(e){let t=[];for(let o of e){let r=o.group||"",i=t.find(s=>s.name===r);i||t.push(i={name:r,fields:[]}),i.fields.push(o)}return t}var q=(e,t)=>g(o=>{if(t===void 0){st(o.config,e);return}let r=e.length>1?o.sections.find(i=>i.optional&&i.key===e[0]):null;r&&(o.config[r.key]===void 0||o.config[r.key]===null)&&(o.config[r.key]=JSON.parse(JSON.stringify(r.default))),Pe(o.config,e,t)}),lo=e=>`f-${e.path.join("-")}`;function co({id:e,value:t,field:o,invalid:r,disabled:i}){let[s,a]=Z(String(t??""));return L(()=>{(s===""?t!==void 0:Number(s)!==t)&&a(String(t??""))},[t]),n("input",{id:e,type:"number",inputmode:"decimal",value:s,disabled:i,min:o.min,step:o.step??"any",placeholder:o.placeholder??o.default??"",class:`input max-w-40 font-mono tabular-nums ${r?"input-invalid":""}`,onInput:d=>{let f=d.currentTarget.value;a(f),f===""?q(o.path,void 0):Number.isNaN(Number(f))||q(o.path,Number(f))}})}function mt({field:e,value:t}){return e.exclusive_min===void 0||typeof t!="number"||t>e.exclusive_min?null:n("p",{class:"mt-1.5 text-[13px] text-warn",children:["Must be greater than ",e.exclusive_min,"."]})}function uo({id:e,field:t,value:o,invalid:r}){let i=t.unlimited_value!==void 0&&o===t.unlimited_value,s=n(co,{id:e,value:i?void 0:o,field:t,invalid:r,disabled:i});if(t.null_label!==void 0){let a=o===null?"n":o===void 0?"d":"s",d=typeof t.default=="number"?t.default:300;return n("div",{children:[n(Y,{id:e,label:t.label,value:a,options:[["d","Default"],["s","Seconds"],["n",t.null_label]],onPick:f=>q(t.path,f==="d"?void 0:f==="n"?null:d)}),a==="s"?n("div",{class:"mt-2.5",children:[s,n(mt,{field:t,value:o})]}):null]})}return t.unlimited_value!==void 0?n("div",{children:[n("div",{class:"flex flex-wrap items-center gap-x-4",children:[s,n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm text-body",children:[n("input",{type:"checkbox",class:"size-[18px] accent-brand-solid",checked:i,onChange:a=>q(t.path,a.currentTarget.checked?t.unlimited_value:void 0)}),"Unlimited"]})]}),n(mt,{field:t,value:o})]}):n("div",{children:[s,n(mt,{field:t,value:o})]})}function po({id:e,field:t,value:o}){if(t.unset_label!==void 0){let r=[["d","Default"]];return t.allow_true!==!1&&r.push(["t","On"]),r.push(["f","Off"]),n("div",{children:[n(Y,{id:e,label:t.label,value:o===void 0?"d":o?"t":"f",options:r,onPick:i=>q(t.path,i==="d"?void 0:i==="t")}),n("p",{class:"mt-1.5 text-xs text-faint",children:["Default: ",t.unset_label]})]})}return n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-semibold text-ink",children:[n("input",{id:e,type:"checkbox",class:"size-[18px] accent-brand-solid",checked:(o??t.default??!1)===!0,onChange:r=>q(t.path,r.currentTarget.checked)}),t.label]})}function fo({id:e,field:t,value:o,invalid:r}){if(t.type==="bool")return n(po,{id:e,field:t,value:o});if(t.type==="number")return n(uo,{id:e,field:t,value:o,invalid:r});if(t.type==="list")return n(W,{id:e,values:Array.isArray(o)?o:[],placeholder:t.placeholder,onChange:i=>q(t.path,i.length?i:void 0)});if(t.type==="select"){let i=t.options.map(s=>Array.isArray(s)?s:[s,s]);return t.unset_label!==void 0&&i.unshift(["",t.unset_label]),n(oe,{id:e,options:i,invalid:r,value:o??(t.unset_label!==void 0?"":t.default??(Array.isArray(t.options[0])?t.options[0][0]:t.options[0])),onChange:s=>q(t.path,s===""?void 0:s)})}return n("input",{id:e,type:"text",value:o??"",placeholder:t.placeholder,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${r?"input-invalid":""}`,onInput:i=>q(t.path,i.currentTarget.value===""?void 0:i.currentTarget.value)})}function I({field:e}){let t=e.path.join("."),o=lo(e),r=Ae(u.config,e.path),i=O(t),s=it(e),a=tn(e),d=e.type==="bool"&&e.unset_label===void 0,f=e.default!==void 0&&!s,l=Array.isArray(e.default)?e.default.join(", "):String(e.default??"");return n("div",{id:`row-${o}`,class:`relative pt-5 pb-1 ${s?"before:absolute before:top-6 before:-left-2.5 before:bottom-2 before:w-[3px] before:rounded before:bg-brand before:content-[''] sm:before:-left-3.5":""}`,children:[n("div",{class:"flex min-h-6 items-center gap-2.5",children:[d?null:n("label",{for:o,class:`text-sm font-semibold text-ink ${a?"opacity-50":""}`,children:[e.label,e.required?n("span",{class:"text-danger",children:" *"}):null]}),d?null:n("code",{class:"hidden font-mono text-[11px] text-faint sm:inline",children:t}),f&&!d?n("span",{class:"rounded border border-line px-1.5 text-[11px] leading-4 font-medium text-faint",children:"default"}):null,n("span",{class:"flex-1"}),s?n("button",{type:"button",class:"focus-ring min-h-8 cursor-pointer rounded px-1 text-[13px] font-medium text-brand hover:underline",title:`Reset to default${e.default!==void 0?` (${l})`:""}`,"aria-label":`Reset ${e.label} to default`,onClick:()=>g(p=>st(p.config,e.path)),children:"Reset"}):null]}),e.help?n("p",{class:`mt-0.5 max-w-prose text-[13px] leading-5 text-muted ${a?"opacity-50":""}`,children:e.help}):null,n("fieldset",{disabled:a,class:`m-0 mt-2.5 min-w-0 max-w-[520px] border-0 p-0 ${a?"opacity-50":""}`,children:n(fo,{id:o,field:e,value:r,invalid:i?.level==="error"})}),a?n("p",{class:"mt-1.5 text-xs text-faint",children:nn(e)}):null,n(G,{issue:i})]})}var mo=[["","Choose reach"],["own","own"],["tenant","tenant"],["all","all"]],bn=e=>Array.isArray(e)?e.length>0:y(e)&&Object.keys(e).length>0;function xn(){let e=u.config.auth;return e==="jwt"?{method:"jwt"}:y(e)?e:{method:"none"}}function N(e){g(t=>{let o={...xn()};e(o),t.config.auth=o.method==="none"?null:o})}function qe(e,t,o){o==null||o===""||typeof o!="string"&&!bn(o)?delete e[t]:e[t]=o}function ho(e){return e.roles_claim!==void 0||bn(e.roles)?"roles":e.permissions_claim!==void 0?"permissions":"default"}var _o=e=>u.issues.filter(t=>t.path===e||t.path.startsWith(`${e}.`));function _t({prefix:e}){return _o(e).map((t,o)=>n(G,{issue:t},o))}function vn({id:e,label:t,name:o,onRename:r}){let[i,s]=Z(o);return L(()=>s(o),[o]),n("div",{children:[n("label",{for:e,class:"sr-only",children:t}),n("input",{id:e,type:"text",value:i,class:"input font-mono",autocomplete:"off",spellcheck:!1,onInput:a=>s(a.currentTarget.value),onChange:a=>{r(a.currentTarget.value.trim())||s(o)}})]})}var Fe=({children:e})=>n("span",{class:"mb-1.5 block text-xs font-medium text-faint",children:e});function yn(e,t,o){return!o||o!==t&&o in e?null:Object.fromEntries(Object.entries(e).map(([r,i])=>[r===t?o:r,i]))}function kn({sec:e}){let t=xn(),o=t.method??"none",r=ho(t);return n("div",{class:"space-y-6",children:[n(A,{title:"Method",children:n(pt,{name:"auth-method",label:"Authentication method",value:o,options:[["none","None","Every request is accepted. For local development."],["jwt","JWT","Verify bearer tokens signed with JWT_SECRET_KEY."],["custom","Custom","Your own BaseAuth subclass decides who is calling."]],onPick:i=>N(s=>{s.method=i,i==="custom"?s.path=s.path??"":delete s.path})})}),o==="custom"?n(A,{title:"Class",children:n("div",{class:"panel",children:n(V,{id:"auth-path",label:"Auth class",path:"auth.path",value:t.path,placeholder:"auth.custom:MyAuth",help:"Import path of a BaseAuth subclass, as module:attribute.",issue:O("auth.path"),onInput:i=>N(s=>{s.path=i})})})}):null,o!=="none"?n(w,{children:[n(A,{title:"Permission source",children:n(pt,{name:"auth-source",label:"Permission source",value:r,options:[["default","Default","Every signed-in user, own data only."],["roles","Roles from a claim","Map the roles in the token to permissions."],["permissions","Permissions from a claim","The token already lists the permissions."]],onPick:i=>N(s=>{delete s.roles_claim,delete s.permissions_claim,delete s.roles,delete s.default_role,i==="roles"&&(s.roles_claim="roles",s.roles={member:{permissions:[],reach:"own"}}),i==="permissions"&&(s.permissions_claim="permissions")})})}),r==="roles"?n(bo,{auth:t,registry:e.registry}):null,r==="permissions"?n(A,{title:"Claim",children:n("div",{class:"panel",children:n(V,{id:"auth-permissions-claim",label:"Permissions claim",path:"auth.permissions_claim",value:t.permissions_claim,placeholder:"permissions",issue:O("auth.permissions_claim"),help:"Token claim holding a list of permissions, such as graph:invoke.",onInput:i=>N(s=>{s.permissions_claim=i})})})}):null,n(A,{title:"Tenants",children:n("div",{class:"panel divide-y divide-line",children:[n("div",{class:"px-5 py-4",children:[n(fe,{label:"Tenant claims",path:"auth.tenant_claims",id:"auth-tenant-claims",help:"Token claims that name the tenant, such as org_id. Rows are separated by them."}),n("div",{class:"mt-3",children:n(W,{id:"auth-tenant-claims",values:t.tenant_claims??[],placeholder:"org_id",onChange:i=>N(s=>qe(s,"tenant_claims",i))})}),n(_t,{prefix:"auth.tenant_claims"})]}),r==="roles"?n(go,{auth:t}):null]})}),n(xo,{auth:t})]}):null]})}function go({auth:e}){let t=Object.keys(y(e.roles)?e.roles:{});return n(ut,{children:[n(fe,{label:"Default role",path:"auth.default_role",id:"auth-default-role",help:"Given to a signed-in user whose token carries no role."}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n(oe,{id:"auth-default-role",value:e.default_role??"",options:[["","None"],...t.map(o=>[o,o])],onChange:o=>N(r=>qe(r,"default_role",o))}),n(G,{issue:O("auth.default_role")})]})]})}function bo({auth:e,registry:t}){let o=y(e.roles)?e.roles:{},r=Object.entries(o),i=Object.keys(o),s=["*",...Object.entries(t||{}).flatMap(([p,h])=>[`${p}:*`,...h.map(c=>`${p}:${c}`)])],a=p=>N(h=>{h.roles=p}),d=(p,h)=>a({...o,[p]:{...o[p],...h}}),f=()=>{let p=r.length+1;for(;`role${p}`in o;)p++;a({...o,[`role${p}`]:{permissions:[],reach:"own"}})},l=(p,h)=>{let c=yn(o,p,h);return c?(N(m=>{m.roles=Object.fromEntries(Object.entries(c).map(([x,$])=>[x,$.inherits?{...$,inherits:$.inherits.map(k=>k===p?h:k)}:$])),m.default_role===p&&(m.default_role=h),y(m.system_identities)&&(m.system_identities=Object.fromEntries(Object.entries(m.system_identities).map(([x,$])=>[x,{...$,roles:($.roles??[]).map(k=>k===p?h:k)}])))}),!0):!1};return n(A,{title:"Roles",children:n("div",{class:"panel",children:[n("div",{class:"border-b border-line",children:n(V,{id:"auth-roles-claim",label:"Roles claim",path:"auth.roles_claim",value:e.roles_claim,placeholder:"roles",issue:O("auth.roles_claim"),help:"Token claim holding the user's list of role names.",onInput:p=>N(h=>{h.roles_claim=p})})}),r.length?n("div",{class:"divide-y divide-line",children:r.map(([p,h],c)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_8rem_auto] items-start gap-3",children:[n(vn,{id:`role-name-${c}`,label:"Role name",name:p,onRename:m=>l(p,m)}),n(oe,{id:`role-reach-${c}`,value:h.reach??"",options:mo,invalid:O(`auth.roles.${p}.reach`)?.level==="error",onChange:m=>d(p,{reach:m})}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove role ${p}`,onClick:()=>a(Object.fromEntries(r.filter(([m])=>m!==p))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Fe,{children:"Permissions"}),n(W,{id:`role-permissions-${c}`,values:h.permissions??[],placeholder:"graph:invoke, threads:*, *",suggestions:s,onChange:m=>d(p,{permissions:m})})]}),n("div",{children:[n(Fe,{children:"Inherits"}),n(W,{id:`role-inherits-${c}`,values:h.inherits??[],placeholder:"another role",suggestions:i.filter(m=>m!==p),onChange:m=>{let x={...h};qe(x,"inherits",m),a({...o,[p]:x})}})]}),n(_t,{prefix:`auth.roles.${p}`})]},c))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"No roles yet. Add one to use roles."}),n("div",{class:"flex items-center justify-between gap-4 border-t border-line px-5 py-3",children:[n("p",{class:"text-[13px] text-muted",children:["Reach: ",n("code",{class:"font-mono",children:"own"})," the user's rows, ",n("code",{class:"font-mono",children:"tenant"})," every row of their tenant, ",n("code",{class:"font-mono",children:"all"})," every row."]}),n("button",{type:"button",class:"btn btn-ghost",onClick:f,children:[n(_,{name:"plus"}),"Add role"]})]})]})})}function xo({auth:e}){let t=y(e.system_identities)?e.system_identities:{},o=Object.entries(t),r=Object.keys(y(e.roles)?e.roles:{}),i=l=>N(p=>qe(p,"system_identities",l)),s=(l,p)=>i({...t,[l]:{...t[l],...p}}),a=()=>{let l=o.length+1;for(;`job${l}`in t;)l++;i({...t,[`job${l}`]:{roles:[],tenant:{}}})},d=l=>Object.entries(l??{}).map(([p,h])=>`${p}=${h}`),f=l=>Object.fromEntries(l.filter(p=>p.includes("=")).map(p=>[p.slice(0,p.indexOf("=")).trim(),p.slice(p.indexOf("=")+1).trim()]));return n(A,{title:"System identities",children:n("div",{class:"panel",children:[o.length?n("div",{class:"divide-y divide-line",children:o.map(([l,p],h)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3",children:[n(vn,{id:`system-name-${h}`,label:"Identity name",name:l,onRename:c=>{let m=yn(t,l,c);return m&&i(m),!!m}}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${l}`,onClick:()=>i(Object.fromEntries(o.filter(([c])=>c!==l))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Fe,{children:"Roles"}),n(W,{id:`system-roles-${h}`,values:p.roles??[],placeholder:"member",suggestions:r,onChange:c=>s(l,{roles:c})})]}),n("div",{children:[n(Fe,{children:"Tenant, as claim=value"}),n(W,{id:`system-tenant-${h}`,values:d(p.tenant),placeholder:"org_id=acme",onChange:c=>s(l,{tenant:f(c)})})]}),n(_t,{prefix:`auth.system_identities.${l}`})]},h))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"None. Scheduled jobs and other non-user callers can run as one."}),n("div",{class:"flex justify-end border-t border-line px-5 py-3",children:n("button",{type:"button",class:"btn btn-ghost",onClick:a,children:[n(_,{name:"plus"}),"Add identity"]})})]})})}var He=[["memory","Memory","This process only"],["redis","Redis","Shared. Redis, Valkey, Dragonfly, KeyDB"],["custom","Custom","Your BaseCacheStore class"]],vo={requests:100,window:60,by:"ip"},Ue=()=>y(u.config.cache_store)?u.config.cache_store:{};function gt(e){g(t=>{let o=y(t.config.cache_store)?t.config.cache_store:{};e(o),Object.keys(o).length?t.config.cache_store=o:delete t.config.cache_store})}var yo=()=>{let e=Ue().rate_limit;return y(e)&&e.enabled!==!1},ko=()=>{let e=Ue().token_revocation;return e===!0||y(e)&&e.enabled!==!1};function wo(e){gt(t=>{delete t.url,delete t.path,e==="memory"?delete t.type:t.type=e})}function Co(e){gt(t=>{e?y(t.rate_limit)?delete t.rate_limit.enabled:t.rate_limit={...vo}:delete t.rate_limit})}function $o(e){gt(t=>{e?y(t.token_revocation)?delete t.token_revocation.enabled:t.token_revocation=!0:delete t.token_revocation})}var _n=(e,t)=>g(o=>{Pe(o.config,e,t)});function So({value:e,onPick:t}){let o=(r,i)=>{let s=r.key==="ArrowRight"||r.key==="ArrowDown"?1:r.key==="ArrowLeft"||r.key==="ArrowUp"?-1:0;if(!s)return;r.preventDefault();let a=He[(i+s+He.length)%He.length][0];t(a),requestAnimationFrame(()=>document.getElementById(`cache-type-${a}`)?.focus())};return n("div",{role:"radiogroup","aria-label":"Cache store type",class:"grid gap-3 sm:grid-cols-3",children:He.map(([r,i,s],a)=>{let d=r===e;return n("button",{type:"button",role:"radio",id:`cache-type-${r}`,"aria-checked":d?"true":"false",tabIndex:d?0:-1,onKeyDown:f=>o(f,a),class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 motion-reduce:transition-none ${d?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{d||t(r)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${d?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:d?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:i}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:s})]})]},r)})})}function he({warn:e,children:t}){return n("p",{class:`mt-3 flex items-start gap-1.5 text-[13px] leading-5 ${e?"text-warn":"text-muted"}`,children:[e?n(_,{name:"alert",class:"mt-0.5 size-3.5"}):null,n("span",{children:t})]})}function gn({id:e,title:t,text:o,on:r,onChange:i}){return n("div",{class:"flex items-center gap-4",children:[n("div",{class:"min-w-0 flex-1",children:[n("h3",{class:"text-base font-semibold text-ink",children:t}),n("p",{class:"mt-0.5 text-[13px] leading-5 text-muted",children:o})]}),n(dt,{id:e,on:r,label:t,onChange:i})]})}var To=[["ip","IP"],["user","User"],["global","Global"]];function Oo({field:e,memory:t}){let[o,r]=Z(!1),i=Ue().rate_limit,s=y(i)&&i.by||"ip",a=!(y(i)&&i.fail_open===!1),d=["trusted_proxy_headers","trusted_proxy_hops","trusted_proxies","exclude_paths"];return n(w,{children:[t?n(he,{warn:!0,children:"Counts per worker with the Memory store."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:e("cache_store.rate_limit.requests")}),n(I,{field:e("cache_store.rate_limit.window")}),n("div",{class:"pt-5",children:[n("span",{id:"cache-by-label",class:"block text-sm font-semibold text-ink",children:"Limit by"}),n("div",{class:"mt-2.5",children:n(Y,{id:"f-cache_store-rate_limit-by",label:"Limit by",options:To,value:s,onPick:f=>_n(["cache_store","rate_limit","by"],f)})}),n(G,{issue:O("cache_store.rate_limit.by")})]}),n("div",{class:"pt-5",children:[n("span",{id:"cache-fail-open-label",class:"block text-sm font-semibold text-ink",children:"Fail open"}),n("div",{class:"mt-2.5 flex items-center gap-3",children:[n(dt,{id:"f-cache_store-rate_limit-fail_open",on:a,label:"Fail open",onChange:f=>_n(["cache_store","rate_limit","fail_open"],f)}),n("p",{class:"m-0 text-[13px] leading-5 text-muted",children:a?"Allow requests if the store errors.":"Deny with 503 if the store errors."})]})]})]}),n("div",{class:"mt-5 border-t border-line pt-1",children:[n("button",{type:"button","aria-expanded":o?"true":"false","aria-controls":"cache-advanced",class:"focus-ring flex min-h-11 cursor-pointer items-center gap-2 rounded font-medium text-muted hover:text-ink",onClick:()=>r(!o),children:[n(_,{name:"chevronRight",class:`size-4 transition-transform duration-150 motion-reduce:transition-none ${o?"rotate-90":""}`}),"Advanced"]}),o?n("div",{id:"cache-advanced",children:d.map(f=>n(I,{field:e(`cache_store.rate_limit.${f}`)},f))}):null]})]})}var re=[["rate","Rate limiting"],["revoke","Token revocation"]];function wn({sec:e}){let[t,o]=Z("rate"),r=Ue().type||"memory",i=l=>e.fields.find(p=>p.path.join(".")===l),s={rate:yo(),revoke:ko()},a=r==="memory",d={rate:!!O("cache_store.rate_limit"),revoke:!!O("cache_store.token_revocation")},f=(l,p)=>{let h=l.key==="ArrowRight"?1:l.key==="ArrowLeft"?-1:0,c=l.key==="Home"?0:l.key==="End"?re.length-1:h?(p+h+re.length)%re.length:-1;c<0||(l.preventDefault(),o(re[c][0]),document.getElementById(`cache-tab-${re[c][0]}`)?.focus())};return n("div",{children:[n(A,{title:"Store type",children:[n(So,{value:r,onPick:wo}),r==="redis"?n(I,{field:i("cache_store.url")}):null,r==="custom"?n(I,{field:i("cache_store.path")}):null,a?n("p",{class:"mt-3 mb-0 text-[13px] leading-5 text-muted",children:"Not shared between workers: each worker keeps its own locks, limits and revocations."}):null]}),n("div",{role:"tablist","aria-label":"Cache store features",class:"mt-8 flex gap-1 border-b border-line",children:re.map(([l,p],h)=>{let c=t===l;return n("button",{type:"button",role:"tab",id:`cache-tab-${l}`,"aria-selected":c?"true":"false","aria-controls":`cache-panel-${l}`,tabIndex:c?0:-1,onKeyDown:m=>f(m,h),class:`focus-ring -mb-px flex min-h-11 cursor-pointer items-center gap-2 border-b-2 px-3 text-sm font-medium transition-colors duration-150 motion-reduce:transition-none ${c?"border-brand-solid text-ink":"border-transparent text-muted hover:text-ink"}`,onClick:()=>o(l),children:[p,n(ne,{class:s[l]?"bg-brand-soft text-brand":"bg-raised text-muted",children:s[l]?"On":"Off"})]},l)})}),n("div",{role:"tabpanel",id:`cache-panel-${t}`,"aria-labelledby":`cache-tab-${t}`,class:"pt-5",children:t==="rate"?n(w,{children:[n(gn,{id:"cache-rate-limit",title:"Rate limiting",on:s.rate,onChange:Co,text:"Cap how many requests one caller can make."}),s.rate?n(Oo,{field:i,memory:a}):d.rate?n(he,{warn:!0,children:"Off: one caller can exhaust the service."}):null]}):n(w,{children:[n(gn,{id:"cache-token-revocation",title:"Token revocation",on:s.revoke,onChange:$o,text:"Reject revoked tokens before they expire (10xgraph revoke)."}),s.revoke?n(w,{children:[a?n(he,{warn:!0,children:"10xgraph revoke cannot reach an in-memory store; pick Redis or Custom."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:i("cache_store.token_revocation.token_lifetime")}),n(I,{field:i("cache_store.token_revocation.user_ttl")})]}),n(he,{children:"Fails closed: while the cache store is unreachable, authenticated requests get 503 and the server will not start."})]}):d.revoke?n(he,{warn:!0,children:"Off: a leaked token works until it expires."}):null]})})]})}function Cn({sec:e}){let t=e.fields[0].path[0],o=e.choices.map(p=>p[0]),r=u.config[t],i=t==="thread_name_generator"?"random":"",s=r===void 0?i:typeof r=="string"&&o.includes(r)?r:"custom",a=[...t==="id_generator"?[["","Not set (the graph's own generator)"]]:[],...e.choices.map(([p,h])=>[p,t==="thread_name_generator"&&p==="random"?`${h} (default)`:h]),["custom","Custom (module:attr)"]],d=s===""?"The graph keeps the generator it was compiled with.":s==="custom"?"Your own class, as module:attribute.":e.choices.find(p=>p[0]===s)?.[2],f=p=>g(h=>{p===i?delete h.config[t]:h.config[t]=p==="custom"?"":p}),l=`f-${t}`;return n("div",{children:[n(A,{title:"Choice",children:n("div",{class:"max-w-[520px]",children:[n("label",{for:`${l}-select`,class:"sr-only",children:e.title}),n(oe,{id:`${l}-select`,options:a,value:s,onChange:f}),n("p",{class:"mt-1.5 text-[13px] text-muted",children:d})]})}),s==="custom"?n(A,{title:"Class",children:n("div",{class:"panel",children:n(V,{id:l,label:e.fields[0].label,path:t,value:typeof r=="string"?r:"",placeholder:e.fields[0].placeholder,help:e.fields[0].help,issue:O(t),onInput:p=>g(h=>{h.config[t]=p})})})}):null]})}function $n(){let e=Array.isArray(u.config.remote_tools)?u.config.remote_tools:[],t=()=>{g(o=>{e.push({node:"",name:"",description:"",parameters:{type:"object",properties:{},required:[]}}),o.config.remote_tools=e}),requestAnimationFrame(()=>document.getElementById(`tool-${e.length-1}-name`)?.focus())};return e.length?n("div",{class:"space-y-4",children:[e.map((o,r)=>n(Io,{tools:e,tool:o,index:r},r)),n("button",{type:"button",onClick:t,class:"focus-ring flex h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-dashed border-line-strong text-sm font-medium text-muted transition-colors duration-150 hover:border-brand hover:text-brand",children:[n(_,{name:"plus"}),"Add remote tool"]})]}):n("div",{class:"rounded-xl border border-dashed border-line-strong bg-surface px-6 py-10 text-center",children:[n("div",{class:"mx-auto flex size-11 items-center justify-center rounded-full bg-raised text-faint",children:n(_,{name:"wrench",class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:"No remote tools yet"}),n("p",{class:"mx-auto mt-1 max-w-sm text-sm leading-6 text-muted",children:"Declare a tool here and the client that calls the API runs it, for example to read the user's location."}),n("button",{type:"button",class:"btn btn-outline mt-5",onClick:t,children:[n(_,{name:"plus"}),"Add remote tool"]})]})}function Io({tools:e,tool:t,index:o}){let r=u.toolParams[o]??{text:JSON.stringify(t.parameters??{},null,2),error:null},i=a=>g(()=>{e[o]={...e[o],...a}}),s=a=>`remote_tools[${o}].${a}`;return n("section",{class:"panel",children:[n("div",{class:"flex items-center gap-3 border-b border-line px-5 py-3",children:[n(_,{name:"wrench",class:"size-4 text-faint"}),n("span",{class:"min-w-0 flex-1 truncate font-mono text-sm font-medium text-ink",children:t.name||`tool_${o+1}`}),t.node?n("span",{class:"rounded-md border border-line bg-raised px-1.5 py-0.5 font-mono text-xs text-muted",children:t.node}):null,n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${t.name||"tool"}`,onClick:()=>g(a=>{e.splice(o,1),a.toolParams={}}),children:n(_,{name:"trash"})})]}),n("div",{class:"divide-y divide-line",children:[n(V,{id:`tool-${o}-name`,label:"Tool name",path:s("name"),value:t.name,placeholder:"get_location",onInput:a=>i({name:a})}),n(V,{id:`tool-${o}-node`,label:"Graph node",path:s("node"),value:t.node,placeholder:"MAIN",help:"The node whose model may call this tool.",onInput:a=>i({node:a})}),n(V,{id:`tool-${o}-description`,label:"Description",path:s("description"),value:t.description,placeholder:"Returns the user's current city",help:"What the model reads when deciding to call it.",onInput:a=>i({description:a})}),n("div",{class:"px-5 py-4",children:[n(fe,{label:"Parameters",path:s("parameters"),help:"JSON schema of the arguments. type must be object.",id:`tool-${o}-params`}),n("textarea",{id:`tool-${o}-params`,rows:7,spellcheck:!1,value:r.text,class:`input mt-3 h-auto py-2.5 font-mono text-[13px] leading-5 ${r.error?"input-invalid":""}`,onInput:a=>{let d=a.currentTarget.value;g(f=>{try{e[o]={...e[o],parameters:JSON.parse(d)},f.toolParams[o]={text:d,error:null}}catch(l){f.toolParams[o]={text:d,error:l.message}}})}}),n(G,{issue:r.error?{level:"error",message:r.error}:null})]})]})]})}var _e=({children:e,code:t})=>n("h1",{id:"page-title",tabindex:-1,class:"flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-semibold tracking-tight text-ink focus:outline-none",children:[e,t?n("code",{class:"rounded border border-line bg-raised px-1.5 font-mono text-xs font-normal text-faint",children:t}):null]}),bt=({children:e})=>n("p",{class:"mt-1.5 max-w-[62ch] text-[15px] leading-6 text-muted",children:e}),Sn=({children:e})=>n("h2",{class:"mt-10 mb-2 eyebrow",children:e});function Tn({id:e,text:t,warn:o,compact:r}){let i=pe(e);return i?n("a",{href:`#${e}`,onClick:s=>{s.preventDefault(),R(e)},class:`focus-ring group flex items-center gap-3.5 border-t border-line py-2 last:border-b ${r?"min-h-12":"min-h-14"}`,children:[n(_,{name:K[e]||"box",class:"size-[18px] text-faint"}),n("span",{class:`min-w-0 flex-1 ${r?"flex flex-wrap items-baseline gap-x-4":""}`,children:[n("span",{class:`block text-sm text-ink group-hover:text-brand ${r?"min-w-40 font-medium":"font-semibold"}`,children:i.title}),n("span",{class:`block text-[13px] leading-5 ${o?"text-warn":"text-muted"}`,children:t??i.description})]}),n(_,{name:"chevronRight",class:"size-4 text-faint"})]}):null}function In(e){return`Not set. ${E(e)?.off_summary||""}`.trim()}function Eo(){let e=u.config.auth,t=e==="jwt"?"jwt":y(e)?e.method??"none":"none";if(t==="none")return{text:In("auth")};let o=y(e)&&y(e.roles)?Object.keys(e.roles).length:0;return{text:`${t==="jwt"?"JWT bearer tokens":`Custom backend${e.path?`: ${e.path}`:""}`}${o?`, ${o} ${o===1?"role":"roles"}`:""}.`}}function Ao(){let e=y(u.config.cache_store)?u.config.cache_store:{},t=[`${e.type||"memory"} store`],o=e.rate_limit;if(y(o)&&o.enabled!==!1){let i={ip:"per IP",user:"per user",global:"across all clients"}[o.by??"ip"]||o.by;t.push(`${o.requests??100} requests per ${o.window??60} s, ${i}`)}let r=e.token_revocation;return(r===!0||y(r)&&r.enabled!==!1)&&t.push("token revocation on"),{text:`${t.join("; ")}.`}}function Po(){let e=u.config.observability;if(e==null)return{text:In("observability")};let t=[];return X(["observability","logfire","enabled"])&&t.push("Logfire"),X(["observability","langsmith","enabled"])&&t.push("LangSmith"),X(["observability","phoenix","enabled"])&&t.push("Phoenix"),{text:`Level ${X(["observability","level"])}${t.length?`, sending to ${t.length>1?`${t.slice(0,-1).join(", ")} and ${t[t.length-1]}`:t[0]}`:", no destination chosen yet"}.`}}function On(){let e=E("core"),t=on(),o=e?ee("core").filter(s=>!e.fields.some(a=>a.path.join(".")===s.path)):[],r=[["auth",Eo()],["cache_store",Ao()],["observability",Po()]],i=u.nav.flatMap(s=>s.items).filter(s=>s!=="get-running");return n(w,{children:[n(_e,{children:t?"You're ready to run":"One step to run your agent"}),n(bt,{children:z["get-running"].description}),n("div",{class:`mt-4 flex items-center gap-2 font-medium ${t?"text-ok":"text-muted"}`,children:[n(_,{name:t?"checkCircle":"circle",class:"size-[18px]"}),"Required: ",t?"1":"0"," of 1 done"]}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((s,a)=>n(ft,{issue:s},a))}):null,n("div",{class:"mt-2",children:e?ht(e.fields).flatMap(s=>s.fields).map(s=>n(I,{field:s},s.path.join("."))):null}),t?n("div",{class:"mt-7 border-t border-line pt-5",children:[n("div",{class:"flex items-center gap-2.5 font-medium text-ok",children:[n(_,{name:"checkCircle",class:"size-[18px]"}),"Ready for 10xgraph dev"]}),n("div",{class:"mt-3 flex max-w-[420px] items-center gap-2 rounded-lg border border-line-strong bg-sunken py-0 pr-1 pl-3.5",children:[n("code",{class:"flex-1 font-mono text-[13px] font-medium text-ink",children:"10xgraph dev"}),u.copied?n("span",{class:"text-xs text-ok",children:"Copied"}):null,n("button",{type:"button",class:"icon-btn",onClick:un,"aria-label":u.copied?"Copied 10xgraph dev":"Copy command 10xgraph dev",children:n(_,{name:"copy",class:"size-[18px]"})})]}),n("p",{class:"mt-3 text-muted",children:"Everything else already has working defaults."})]}):n("p",{class:"mt-4 text-xs text-faint",children:"Add the agent graph above, then run 10xgraph dev."}),n(Sn,{children:"Before production (optional)"}),n("div",{children:r.map(([s,a])=>n(Tn,{id:s,text:a.text,warn:a.warn},s))}),n(Sn,{children:"What you can configure"}),n("div",{children:i.map(s=>n(Tn,{id:s,compact:!0},s))})]})}function We({vars:e}){return n("dl",{class:"mb-3 grid gap-x-4 gap-y-1 sm:grid-cols-[minmax(0,260px)_minmax(0,1fr)]",children:e.map(t=>[n("dt",{class:"py-0.5 font-mono text-xs font-medium break-all text-ink",children:t.name},`${t.name}-n`),n("dd",{class:"m-0 pb-2 text-[13px] text-muted sm:py-0.5",children:t.purpose},`${t.name}-p`)])})}function jo({vars:e,open:t}){return e?.length?n("details",{class:"group mt-9 border-t border-line pt-1",open:t,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform group-open:rotate-90"}),"Set in .env"]}),n("p",{class:"mb-2 text-xs text-faint",children:"Read only. The editor never reads .env, so only names are shown."}),n(We,{vars:e})]}):null}function En({sec:e}){return e.widget==="auth"?n(kn,{sec:e}):e.widget==="remote_tools"?n($n,{}):e.widget==="cache_store"?n(wn,{sec:e}):e.widget==="id_generator"||e.widget==="thread_name_generator"?n(Cn,{sec:e}):null}function Mo(e){let t=new Set([...e.fields.map(i=>i.path.join(".")),...Object.keys(ot)]),o=new Set(["cache_store.rate_limit","cache_store.token_revocation"]),r=i=>i.startsWith("auth.roles.")||i.startsWith("auth.system_identities.");return ee(e.id).filter(i=>!t.has(i.path)&&!(e.id==="cache_store"&&o.has(i.path))&&!(e.id==="auth"&&r(i.path)))}function Ro({sec:e}){let t=je(e),o=e.widget&&(t||!e.optional);return n(w,{children:[o?n("div",{class:"mt-6",children:n(En,{sec:e})}):null,n("div",{class:e.optional&&!t?"opacity-80":"",children:ht(e.fields.filter(r=>!r.managed)).map((r,i)=>n("div",{children:[r.name?n("h3",{class:`eyebrow ${i===0?"mt-6":"mt-8 border-t border-line pt-4"}`,children:r.name}):null,r.fields.map(s=>n(I,{field:s},s.path.join(".")))]},r.name))})]})}function No({sec:e}){let t=je(e),o=Mo(e);return n(w,{children:[n(_e,{code:e.key,children:e.title}),n(bt,{children:e.description}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((r,i)=>n(ft,{issue:r},i))}):null,e.optional?n("div",{class:"mt-5 flex flex-wrap items-center gap-4",children:[n(Y,{label:`Settings for ${e.title}`,value:t?"c":"d",options:[["d","Use defaults"],["c","Customize"]],onPick:r=>cn(e,r==="c")}),n("p",{class:"m-0 min-w-56 flex-1 text-[13px] text-muted",children:t?`Saved in 10xgraph.json under ${e.key}.`:e.off_summary})]}):null,e.notes?.length?n("div",{class:"mt-5",children:n(me,{kind:"info",lines:e.notes})}):null,n(Ro,{sec:e}),n(jo,{vars:e.env})]})}function Do(){let e=u.schema.environment||[];return n(w,{children:[n(_e,{children:z.environment.title}),n(bt,{children:"These variables are set in .env or your deployment environment. The editor never reads that file, so only names and purposes are shown."}),e.map(t=>n("section",{children:[n("h2",{class:"mt-8 mb-2 text-sm font-semibold text-ink",children:t.title}),t.note?n("p",{class:"mb-2 text-xs text-faint",children:t.note}):null,n(We,{vars:t.vars})]},t.title))]})}var An=e=>String(e??"").toLowerCase();function Lo(e){let t=i=>e.every(s=>An(i).includes(s)),o=[];for(let i of u.sections){let s=t(`${i.title} ${i.description} ${i.key||""}`),a=p=>s||t(`${p.label} ${p.help||""} ${p.path.join(".")}`),d=i.fields.filter(p=>!p.managed&&a(p)),f=(i.env||[]).filter(p=>t(`${p.name} ${p.purpose}`)),l=!!i.widget&&(s||i.fields.some(p=>p.managed&&a(p)));(d.length||f.length||l)&&o.push({id:i.id,sec:i,fields:d,env:f,widget:l})}let r=(u.schema.environment||[]).map(i=>({title:i.title,vars:i.vars.filter(s=>t(`${s.name} ${s.purpose} ${i.title}`))})).filter(i=>i.vars.length);return{results:o,envGroups:r}}function zo(){let e=An(u.query).split(/\s+/).filter(Boolean),{results:t,envGroups:o}=Lo(e),r=t.reduce((i,s)=>i+s.fields.length+s.env.length+(s.widget?1:0),0)+o.reduce((i,s)=>i+s.vars.length,0);return r?n(w,{children:[n(_e,{code:u.query,children:[r," ",r===1?"setting matches":"settings match"]}),t.map(({id:i,sec:s,fields:a,env:d,widget:f})=>n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:K[i]||"box",class:"size-4"}),s.title,n("a",{href:`#${i}`,onClick:l=>{l.preventDefault(),R(i)},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),f?n("div",{class:"mt-4",children:n(En,{sec:s})}):null,a.map(l=>n(I,{field:l},l.path.join("."))),d.length?n("details",{class:"group mt-5",open:!0,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform group-open:rotate-90"}),"Set in .env"]}),n(We,{vars:d})]}):null]},i)),o.length?n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:"terminal",class:"size-4"}),z.environment.title,n("a",{href:"#environment",onClick:i=>{i.preventDefault(),R("environment")},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),o.map(i=>n("div",{children:[n("h3",{class:"mt-4 mb-1.5 text-sm font-semibold text-ink",children:i.title}),i.note?n("p",{class:"mb-1.5 text-xs text-faint",children:i.note}):null,n(We,{vars:i.vars})]},i.title))]}):null]}):n(w,{children:[n(_e,{children:"No matches"}),n("p",{class:"mt-8 text-muted",children:['Nothing matches "',u.query,'". Try a shorter word, or press Esc to go back.']})]})}function Pn(){if(u.query.trim())return n(zo,{});if(u.page==="get-running")return n(On,{});if(u.page==="environment")return n(Do,{});let e=E(u.page);return e?n(No,{sec:e}):n(On,{})}function Ho(){let e=u.path.split(/[\\/]/).pop()||"10xgraph.json",t=u.path.slice(0,u.path.length-e.length),o=null;return u.schema?u.exists?H()?o=n(ne,{class:"bg-warn-soft text-warn",children:"Unsaved"}):o=n(ne,{class:"bg-ok-soft text-ok",children:"Saved"}):o=n(ne,{class:"bg-brand-soft text-brand",children:"New"}):o=null,n("div",{class:"flex min-w-0 flex-1 items-center gap-2",title:u.path||void 0,children:[n(_,{name:"file",class:"size-4 text-faint"}),n("span",{class:"truncate font-mono text-[13px] font-medium text-ink",children:e}),o,t?n("span",{class:"hidden min-w-0 truncate font-mono text-xs text-faint lg:block",dir:"rtl",children:n("bdi",{children:t})}):null]})}function jn(){let e=!!u.schema&&!u.fatal,t=u.sideOpen?"Hide sidebar":"Show sidebar";return n("header",{class:"sticky top-0 z-40 flex h-14 items-center gap-2 border-b border-line bg-surface px-3 sm:gap-3 sm:px-4",children:[n("button",{type:"button",class:"icon-btn","aria-controls":"side","aria-expanded":u.sideOpen?"true":"false","aria-label":t,title:`${t} (Ctrl/Cmd+B)`,onClick:()=>te(!u.sideOpen),children:n(_,{name:"panel",class:"size-[18px]"})}),n("div",{class:"flex shrink-0 items-center gap-2.5",children:[n("svg",{viewBox:"0 0 48 48",class:"size-7 shrink-0 text-ink","aria-hidden":"true",children:[n("rect",{x:"4",y:"4",width:"40",height:"40",rx:"11",fill:"none",stroke:"currentColor","stroke-width":"4.4"}),n("path",{d:"M13 33L21 23L34 15L34 31L21 23",fill:"none",stroke:"currentColor","stroke-width":"3","stroke-linecap":"round","stroke-linejoin":"round"}),n("g",{fill:"currentColor",children:[n("circle",{cx:"21",cy:"23",r:"4"}),n("circle",{cx:"34",cy:"15",r:"4"}),n("circle",{cx:"34",cy:"31",r:"4"})]}),n("circle",{cx:"13",cy:"33",r:"4.6",class:"fill-brand-amber"})]}),n("div",{class:"hidden items-baseline gap-1.5 sm:flex",children:[n("span",{class:"text-[15px] font-semibold tracking-tight text-ink",children:"10xGraph"}),n("span",{class:"text-[15px] text-faint",children:"Config"})]})]}),n("span",{class:"hidden h-6 w-px shrink-0 bg-line md:block","aria-hidden":"true"}),n(Ho,{}),n("button",{type:"button",class:"btn btn-ghost","aria-label":"View 10xgraph.json","aria-haspopup":"dialog",disabled:!e,onClick:()=>dn(),children:[n(_,{name:"code",class:"size-[18px]"}),n("span",{class:"hidden md:inline",children:"View 10xgraph.json"})]}),n("button",{type:"button",class:"btn btn-primary",onClick:Re,disabled:!e||!!u.busy||u.exists&&!H()&&!u.parseError,children:[u.busy==="save"?n(_,{name:"spinner",class:"size-4 animate-spin"}):null,u.exists?"Save":"Create file",n("span",{class:"kbd hidden lg:inline-flex",children:"Ctrl S"})]})]})}function Mn(){return u.fatal?n("div",{class:"panel mt-10",children:n(mn,{icon:"xCircle",tint:"text-danger",title:u.fatal.title,text:u.fatal.text})}):u.schema?n(w,{children:[u.parseError?n("div",{class:"mb-6",children:n(me,{kind:"warn",title:"The existing file is not valid JSON",lines:[`${u.parseError}. The form starts from defaults; saving replaces the file and keeps the old one as 10xgraph.json.bak.`]})}):null,n(Pn,{})]}):n("div",{class:"animate-pulse space-y-4 pt-2","aria-label":"Loading",children:[n("div",{class:"h-7 w-48 rounded-md bg-raised"}),n("div",{class:"h-4 w-80 max-w-full rounded-md bg-raised"}),[0,1,2].map(e=>n("div",{class:"h-16 rounded-xl bg-raised"},e))]})}function Fo({id:e}){let t=!!E(e)&&Xt(E(e)),o=en(e);return!t&&!o?null:n("span",{class:"flex shrink-0 gap-1",children:[t?n("span",{class:"size-2 rounded-full bg-brand",title:"Changed from default"}):null,o?n("span",{class:`size-2 rounded-full ${o.level==="error"?"bg-danger":"bg-warn"}`,title:o.level==="error"?"Needs attention":"Has a warning"}):null]})}function qo({id:e}){let t=pe(e);if(!t)return null;let o=!u.query&&u.page===e;return n("a",{href:`#${e}`,"aria-current":o?"page":void 0,onClick:r=>{r.preventDefault(),R(e)},ref:r=>{r&&o&&window.innerWidth<1024&&r.scrollIntoView({block:"nearest",inline:"nearest"})},class:`focus-ring flex min-h-10 items-center gap-2.5 rounded-lg px-3 py-1.5 font-medium transition-colors duration-150 max-lg:min-h-11 max-lg:shrink-0 max-lg:whitespace-nowrap ${o?"bg-brand-soft text-ink":"text-muted hover:bg-raised hover:text-ink"}`,children:[n(_,{name:K[e]||"box",class:`size-[18px] ${o?"text-brand":""}`}),n("span",{class:"min-w-0 flex-1",children:[n("span",{class:"block truncate",children:t.title}),t.subtitle?n("span",{class:"block truncate text-xs font-normal text-faint max-lg:hidden",children:t.subtitle}):null]}),n(Fo,{id:e})]})}function Rn(){return u.sideRender?n("aside",{id:"side","aria-label":"Settings navigation",class:`contents lg:sticky lg:top-14 lg:block lg:h-[calc(100dvh-3.5rem)] lg:overflow-y-auto lg:border-r lg:border-line lg:bg-surface lg:px-3 lg:pt-4 lg:pb-16 lg:transition-[transform,opacity] lg:duration-200 lg:ease-out ${u.sideShown?"lg:translate-x-0 lg:opacity-100":"lg:-translate-x-6 lg:opacity-0"}`,inert:!u.sideOpen,children:[n("div",{role:"search",class:"relative mx-1 mb-3 max-lg:mx-4 max-lg:mt-3",children:[n(_,{name:"search",class:"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-faint"}),n("label",{for:"q",class:"sr-only",children:"Search settings"}),n("input",{id:"q",type:"search",value:u.query,autocomplete:"off",spellcheck:!1,placeholder:"Search settings ( / )",class:"input pl-10",onInput:e=>De(e.currentTarget.value)})]}),n("nav",{"aria-label":"Pages",class:"max-lg:sticky max-lg:top-14 max-lg:z-20 max-lg:mt-3 max-lg:flex max-lg:gap-0.5 max-lg:overflow-x-auto max-lg:border-b max-lg:border-line max-lg:bg-surface max-lg:px-3 max-lg:py-1.5",children:u.nav.map(e=>n("div",{class:"max-lg:contents",children:[e.label?n("div",{class:"mt-4 mb-1 px-3 text-[11px] font-semibold tracking-wider text-faint uppercase max-lg:hidden",children:e.label}):null,e.items.map(t=>n(qo,{id:t},t))]},e.label||"top"))})]}):null}var Uo={success:["checkCircle","text-ok"],warning:["alert","text-warn"],error:["xCircle","text-danger"]};function Wo(){return n("div",{class:"pointer-events-none fixed right-4 bottom-4 left-4 z-50 flex flex-col items-end gap-2 sm:left-auto","aria-live":"polite",children:u.toasts.map(e=>{let[t,o]=Uo[e.kind];return n("div",{role:e.kind==="error"?"alert":"status",class:"pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-xl border border-line bg-surface px-4 py-3 text-sm shadow-pop",children:[n(_,{name:t,class:`mt-0.5 size-4 ${o}`}),n("p",{class:"min-w-0 flex-1 leading-5 text-body",children:e.message}),e.action?n("button",{type:"button",class:"shrink-0 font-medium text-brand hover:underline",onClick:()=>{ze(e.id),e.action.run()},children:e.action.label}):null,n("button",{type:"button",class:"-mr-1 shrink-0 cursor-pointer rounded text-faint hover:text-ink","aria-label":"Dismiss",onClick:()=>ze(e.id),children:n(_,{name:"x"})})]},e.id)})})}var Go=()=>/^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName)||document.activeElement?.isContentEditable;function Vo(){let[,e]=nt(o=>o+1,0);L(()=>{Qt(e);try{localStorage.getItem("tg-sidebar")==="hidden"&&te(!1,{instant:!0})}catch{}let o=s=>{let a=s.ctrlKey||s.metaKey,d=s.key.toLowerCase();a&&d==="s"?(s.preventDefault(),Re()):a&&d==="b"?(s.preventDefault(),te(!u.sideOpen)):a&&d==="k"?(s.preventDefault(),ct()):s.key==="/"&&!a&&!s.altKey&&!Go()&&!u.drawerOpen?(s.preventDefault(),ct()):s.key==="Escape"&&u.query&&!u.drawerOpen&&De("")},r=s=>{u.schema&&H()&&s.preventDefault()},i=()=>Ne();return document.addEventListener("keydown",o),window.addEventListener("beforeunload",r),window.addEventListener("hashchange",i),an(),()=>{document.removeEventListener("keydown",o),window.removeEventListener("beforeunload",r),window.removeEventListener("hashchange",i)}},[]),L(()=>{if(!u.schema||u.validated===M())return;let o=setTimeout(ln,400);return()=>clearTimeout(o)});let t=u.sideRender;return n(w,{children:[n("a",{href:"#main",class:"sr-only z-50 rounded-md bg-brand-solid px-3 py-2 text-white focus:not-sr-only focus:fixed focus:top-3 focus:left-3",children:"Skip to content"}),n(jn,{}),n("div",{class:t?"lg:grid lg:grid-cols-[260px_minmax(0,1fr)] lg:items-start":"",children:[n(Rn,{}),n("main",{id:"main",tabindex:-1,class:"min-w-0 px-4 pt-6 pb-24 focus:outline-none sm:px-8 sm:pt-8",children:n("div",{class:"mx-auto w-full max-w-[760px]",children:n(Mn,{})})})]}),n(hn,{}),n("div",{class:"sr-only",role:"status","aria-live":"polite",children:u.copied?"Copied":""}),n(Wo,{})]})}Rt(n(Vo,{}),document.getElementById("app")); +var Ie,v,Nt,Zn,J,At,Lt,Dt,et,Se,ue,Ht,ot,tt,nt,eo,Ee={},Oe=[],to=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Ae=Array.isArray;function F(e,t){for(var o in t)e[o]=t[o];return e}function rt(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function no(e,t,o){var r,i,s,a={};for(s in t)s=="key"?r=t[s]:s=="ref"?i=t[s]:a[s]=t[s];if(arguments.length>2&&(a.children=arguments.length>3?Ie.call(arguments,2):o),typeof e=="function"&&e.defaultProps!=null)for(s in e.defaultProps)a[s]===void 0&&(a[s]=e.defaultProps[s]);return $e(e,a,r,i,null)}function $e(e,t,o,r,i){var s={type:e,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i??++Nt,__i:-1,__u:0};return i==null&&v.vnode!=null&&v.vnode(s),s}function x(e){return e.children}function Te(e,t){this.props=e,this.context=t}function Q(e,t){if(t==null)return e.__?Q(e.__,e.__i+1):null;for(var o;tt&&J.sort(Dt),e=J.shift(),t=J.length,oo(e)}finally{J.length=Pe.__r=0}}function Ft(e,t,o,r,i,s,a,u,m,l,p){var h,c,f,y,$,w,E=r&&r.__k||Oe,k=t.length;for(m=ro(o,t,E,m,k),h=0;h0?a=e.__k[s]=$e(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[s]=a,m=s+c,a.__=e,a.__b=e.__b+1,u=null,(l=a.__i=so(a,o,m,h))!=-1&&(h--,(u=o[l])&&(u.__u|=2)),u==null||u.__v==null?(l==-1&&(i>p?c--:im?c--:c++,a.__u|=4))):e.__k[s]=null;if(h)for(s=0;s(p?1:0)){for(i=o-1,s=o+1;i>=0||s=0?i--:s++])!=null&&(2&l.__u)==0&&u==l.key&&m==l.type)return a}return-1}function jt(e,t,o){t[0]=="-"?e.setProperty(t,o??""):e[t]=o==null?"":typeof o!="number"||to.test(t)?o:o+"px"}function Ce(e,t,o,r,i){var s,a;e:if(t=="style")if(typeof o=="string")e.style.cssText=o;else{if(typeof r=="string"&&(e.style.cssText=r=""),r)for(t in r)o&&t in o||jt(e.style,t,"");if(o)for(t in o)r&&o[t]==r[t]||jt(e.style,t,o[t])}else if(t[0]=="o"&&t[1]=="n")s=t!=(t=t.replace(Ht,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+s]=o,o?r?o[ue]=r[ue]:(o[ue]=ot,e.addEventListener(t,s?nt:tt,s)):e.removeEventListener(t,s?nt:tt,s);else{if(i=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=o??"";break e}catch{}typeof o=="function"||(o==null||o===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&o==1?"":o))}}function Mt(e){return function(t){if(this.l){var o=this.l[t.type+e];if(t[Se]==null)t[Se]=ot++;else if(t[Se]0?e:Ae(e)?e.map(Gt):e.constructor!==void 0?null:F({},e)}function io(e,t,o,r,i,s,a,u,m){var l,p,h,c,f,y,$,w=o.props||Ee,E=t.props,k=t.type;if(k=="svg"?i="http://www.w3.org/2000/svg":k=="math"?i="http://www.w3.org/1998/Math/MathML":i||(i="http://www.w3.org/1999/xhtml"),s!=null){for(l=0;l=o.__.length&&o.__.push({}),o.__[e]}function K(e){return je=1,dt(rn,e)}function dt(e,t,o){var r=ct(pe++,2);if(r.t=e,!r.__c&&(r.__=[o?o(t):rn(void 0,t),function(u){var m=r.__N?r.__N[0]:r.__[0],l=r.t(m,u);m!==l&&(r.__N=[l,r.__[1]],r.__c.setState({}))}],r.__c=C,!C.__f)){var i=function(u,m,l){if(!r.__c.__H)return!0;var p=!1,h=r.__c.props!==u;if(r.__c.__H.__.some(function(f){if(f.__N){p=!0;var y=f.__[0];f.__=f.__N,f.__N=void 0,y!==f.__[0]&&(h=!0)}}),s){var c=s.call(this,u,m,l);return p?c||h:c}return!p||h};C.__f=!0;var s=C.shouldComponentUpdate,a=C.componentWillUpdate;C.componentWillUpdate=function(u,m,l){if(this.__e){var p=s;s=void 0,i(u,m,l),s=p}a&&a.call(this,u,m,l)},C.shouldComponentUpdate=i}return r.__N||r.__}function U(e,t){var o=ct(pe++,3);!T.__s&&on(o.__H,t)&&(o.__=e,o.u=t,C.__H.__h.push(o))}function Me(e){return je=5,lo(function(){return{current:e}},[])}function lo(e,t){var o=ct(pe++,7);return on(o.__H,t)&&(o.__=e(),o.__H=t,o.__h=e),o.__}function co(){for(var e;e=nn.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Re),t.__h.some(lt),t.__h=[]}catch(o){t.__h=[],T.__e(o,e.__v)}}}T.__b=function(e){C=null,Kt&&Kt(e)},T.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),en&&en(e,t)},T.__r=function(e){Yt&&Yt(e),pe=0;var t=(C=e.__c).__H;t&&(at===C?(t.__h=[],C.__h=[],t.__.some(function(o){o.__N&&(o.__=o.__N),o.u=o.__N=void 0})):(t.__h.some(Re),t.__h.some(lt),t.__h=[],pe=0)),at=C},T.diffed=function(e){Xt&&Xt(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(nn.push(t)!==1&&Jt===T.requestAnimationFrame||((Jt=T.requestAnimationFrame)||uo)(co)),t.__H.__.some(function(o){o.u&&(o.__H=o.u,o.u=void 0)})),at=C=null},T.__c=function(e,t){t.some(function(o){try{o.__h.some(Re),o.__h=o.__h.filter(function(r){return!r.__||lt(r)})}catch(r){t.some(function(i){i.__h&&(i.__h=[])}),t=[],T.__e(r,o.__v)}}),Qt&&Qt(e,t)},T.unmount=function(e){Zt&&Zt(e);var t,o=e.__c;o&&o.__H&&(o.__H.__.some(function(r){try{Re(r)}catch(i){t=i}}),o.__H=void 0,t&&T.__e(t,o.__v))};var tn=typeof requestAnimationFrame=="function";function uo(e){var t,o=function(){clearTimeout(r),tn&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(o,35);tn&&(t=requestAnimationFrame(o))}function Re(e){var t=C,o=e.__c;typeof o=="function"&&(e.__c=void 0,o()),C=t}function lt(e){var t=C;e.__c=e.__(),C=t}function on(e,t){return!e||e.length!==t.length||t.some(function(o,r){return o!==e[r]})}function rn(e,t){return typeof t=="function"?t(e):t}var Ne=[{label:null,items:["get-running"]},{label:"Before production",items:["auth","cache_store","observability","metrics"]},{label:"Runs and streaming",items:["runs","websocket","ag_ui","remote_tools","media"]},{label:"Project wiring",items:["checkpointer","id_generator","thread_name_generator","store","injectq"]},{label:"Developer tools",items:["test","evaluation"]},{label:"Reference",items:["environment"]}],q={"get-running":{title:"Get running",icon:"flag",description:"Only the agent graph is required. Everything else is optional."},environment:{title:"Server environment (.env)",icon:"terminal",description:"Variables the server reads from .env. The editor never reads that file, so values are not shown."}},ut={"auth.path":"auth-path"},sn="tg-config-token",an=!1;function po(){let e=new URLSearchParams(location.hash.slice(1)).get("token")||"";try{e&&sessionStorage.setItem(sn,e);let t=sessionStorage.getItem(sn)||"";return an=t!==""&&t===(e||t),e||t}catch{return e}}var me=po(),pt=e=>an||!me?`#${e}`:`#token=${encodeURIComponent(me)}&page=${e}`;function ln(){let e=location.hash.slice(1);if(e.includes("=")){let t=new URLSearchParams(e);return t.get("page")||t.get("section")||""}try{return decodeURIComponent(e)}catch{return e}}var d={schema:null,sections:[],nav:[],fields:new Map,config:{},version:null,exists:!1,path:"",parseError:null,saved:"",validated:null,issues:[],page:"get-running",query:"",sideOpen:!0,sideRender:!0,sideShown:!0,drawerOpen:!1,tab:"json",copied:!1,busy:null,toolParams:{},toasts:[],fatal:null},cn=()=>{},dn=e=>{cn=e};function b(e){e&&e(d),cn()}var un=e=>JSON.parse(JSON.stringify(e)),N=()=>JSON.stringify(d.config),g=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),L=(e,t)=>t.reduce((o,r)=>g(o)?o[r]:void 0,e),fe=(e,t)=>`${e} ${t}${e===1?"":"s"}`;function oe(e,t,o){let r=e;for(let i of t.slice(0,-1))g(r[i])||(r[i]={}),r=r[i];r[t[t.length-1]]=o}var mo=new Set(["cache_store.rate_limit","cache_store.token_revocation"]);function he(e,t){let o=[],r=e;for(let i of t.slice(0,-1)){if(!g(r))return;o.push([r,i]),r=r[i]}g(r)&&delete r[t[t.length-1]];for(let i=o.length-1;i>=1;i--){let[s,a]=o[i];if(mo.has(t.slice(0,i+1).join(".")))break;if(g(s[a])&&Object.keys(s[a]).length===0)delete s[a];else break}}function mt(e){let t=L(d.config,e);return t!==void 0?t:d.fields.get(e.join("."))?.default}var fo=(e,t)=>JSON.stringify(e)===JSON.stringify(t),P=e=>d.sections.find(t=>t.id===e),W=()=>N()!==d.saved,Le=e=>!e.optional||e.key in d.config&&d.config[e.key]!==null,re=e=>d.issues.filter(t=>t.section===e),S=e=>d.issues.find(t=>t.path===e);function _e(e=d.issues){let t=e.filter(o=>o.level==="error").length;return{errors:t,warnings:e.length-t}}var De=e=>ut[e]||`f-${e.replaceAll(".","-")}`,ft=e=>{let t=L(d.config,e.path);return t!==void 0&&!fo(t,e.default)},pn=e=>e.id==="core"?!1:e.id==="auth"?d.config.auth!=null:e.id==="metrics"?d.config.metrics?.enabled===!0:e.optional?Le(e):e.fields.some(ft);function mn(e){let t=re(e);return t.find(o=>o.level==="error")||t[0]||null}var fn=e=>!!e.show_if&&mt(e.show_if.path)!==e.show_if.equals;function hn(e){let o=d.fields.get(e.show_if.path.join("."))?.label||e.show_if.path.join("."),r=e.show_if.equals;return`Used when ${o} is ${r===!0?"on":r===!1?"off":String(r)}.`}var ge=e=>{let t=P(e);return q[e]?{id:e,...q[e]}:t?{id:e,title:t.title,subtitle:t.subtitle,description:t.description,icon:null}:null},_n=()=>/^[\w.]+:[\w.]+$/.test(String(d.config.agent||""));async function _t(e,t,o){let r={"X-10xGraph-Token":me};o!==void 0&&(r["Content-Type"]="application/json");let i=await fetch(t,{method:e,headers:r,body:o===void 0?void 0:JSON.stringify(o)}),s={};try{s=await i.json()}catch{}return{ok:i.ok,status:i.status,data:s}}var ht=(e,t)=>b(o=>{o.fatal={title:e,text:t}});async function xn(){if(!me)return ht("Open the link printed by 10xgraph config","This page needs the session token included in that link.");let e;try{e=await _t("GET","/api/state")}catch{return ht("Cannot reach the config server","Check that 10xgraph config is still running in your terminal, then reload.")}if(!e.ok)return ht("This link has expired",e.data.error||"Restart 10xgraph config and open the new link it prints.");let t=e.data;b(o=>{o.schema=t.schema,o.sections=t.schema.sections;let r=new Set(Ne.flatMap(a=>a.items)),i=o.sections.map(a=>a.id).filter(a=>a!=="core"&&!r.has(a));o.nav=Ne.map((a,u)=>({label:a.label,items:uq[m]||P(m)):[...i,...a.items]}));for(let a of o.sections)for(let u of a.fields)o.fields.set(u.path.join("."),u);o.config=t.config,o.version=t.version,o.exists=t.exists,o.path=t.path,o.parseError=t.parse_error,o.issues=t.issues;let s=t.exists&&!t.parse_error;o.saved=s?N():"",o.validated=s?N():null}),ze({focus:!1}),history.replaceState(null,"",pt(d.page))}var gn=null;async function vn(){let e=N();if(!(!d.schema||d.validated===e||gn===e)&&!Object.values(d.toolParams).some(t=>t.error)){gn=e;try{let t=await _t("POST","/api/validate",{config:JSON.parse(e)});if(!t.ok)return;b(o=>{o.issues=t.data.issues,o.validated=e})}catch{}}}async function He(){if(!(d.busy||!d.schema)){if(d.exists&&!W()&&!d.parseError)return G("success","No changes to save.");if(ho()){b(e=>{e.busy="save"});try{let e=await _t("POST","/api/save",{config:d.config,version:d.version});if(e.data.issues&&(d.issues=e.data.issues,d.validated=N()),e.ok){d.version=e.data.version,d.config=e.data.config,d.toolParams={},d.validated=N(),d.saved=N(),d.exists=!0,d.parseError=null;let t=e.data.path.split(/[\\/]/).pop();G("success",e.data.backup?`Saved ${t}. The previous version is in ${t}.bak.`:`Created ${t}.`)}else if(e.status===409)G("error",e.data.error,{label:"Reload",run:()=>location.reload()});else if(e.status===422){let{errors:t}=_e();G("error",`Not saved: ${fe(t,"error")} to fix first.`,{label:"Show",run:_o})}else G("error",e.data.error||"Save failed.")}catch{G("error","Cannot reach the config server.")}finally{b(e=>{e.busy=null})}}}}function ho(){let e=Object.values(d.toolParams).some(t=>t.error);return e&&(D("remote_tools"),G("error","A remote tool has invalid parameters JSON.")),!e}function yn(e,t){b(o=>{t?o.config[e.key]=un(e.default):delete o.config[e.key],e.key==="remote_tools"&&(o.toolParams={})})}function _o(){let e=d.issues.find(t=>t.level==="error")||d.issues[0];e&&D(e.section,De(e.path))}var go=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches?"auto":"smooth";function bo(e){return e==="core"?"get-running":q[e]||P(e)?e:"get-running"}function ze({focus:e=!0}={}){b(t=>{t.page=bo(ln()),t.query="",t.drawerOpen=!1}),e&&requestAnimationFrame(()=>{window.scrollTo(0,0),document.getElementById("page-title")?.focus({preventScroll:!0})})}function D(e,t){let o=pt(e);location.hash!==o&&history.pushState(null,"",o),ze(),t&&requestAnimationFrame(()=>requestAnimationFrame(()=>{let r=document.getElementById(t);r&&(r.scrollIntoView({block:"center",behavior:go()}),r.focus({preventScroll:!0}))}))}function Fe(e){b(t=>{t.query=e})}var xo=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches,bn;function se(e,{instant:t=!1}={}){try{localStorage.setItem("tg-sidebar",e?"shown":"hidden")}catch{}clearTimeout(bn);let o=t||xo();b(r=>{r.sideOpen=e,e?(r.sideRender=!0,r.sideShown=o):r.sideShown=!1,!e&&o&&(r.sideRender=!1)}),e&&!o&&requestAnimationFrame(()=>requestAnimationFrame(()=>b(r=>{r.sideShown=!0}))),!e&&!o&&(bn=setTimeout(()=>b(r=>{r.sideOpen||(r.sideRender=!1)}),200))}function gt(){d.sideOpen||se(!0,{instant:!0}),requestAnimationFrame(()=>{let e=document.getElementById("q");e?.focus(),e?.select()})}function kn(e){b(t=>{e&&(t.tab=e),t.drawerOpen=!0})}var Ue=()=>b(e=>{e.drawerOpen=!1});async function wn(){let e="10xgraph dev";try{await navigator.clipboard.writeText(e)}catch{try{let t=document.createElement("textarea");t.value=e,document.body.append(t),t.select(),document.execCommand("copy"),t.remove()}catch{}}b(t=>{t.copied=!0}),setTimeout(()=>b(t=>{t.copied=!1}),2e3)}async function Cn(){try{await navigator.clipboard.writeText(JSON.stringify(d.config,null,2)+` +`),G("success","Copied 10xgraph.json to the clipboard.")}catch{G("error","The clipboard is not available here.")}}var vo=0;function G(e,t,o){let r=++vo;b(i=>{i.toasts=[...i.toasts,{id:r,kind:e,message:t,action:o}]}),setTimeout(()=>qe(r),o?8e3:4e3)}function qe(e){b(t=>{t.toasts=t.toasts.filter(o=>o.id!==e)})}var Sn={box:'',key:'',shield:'',gauge:'',arrows:'',activity:'',layers:'',wrench:'',flask:'',target:'',checkCircle:'',xCircle:'',alert:'',info:'',file:'',copy:'',x:'',plus:'',trash:'',panel:'',chevronDown:'',expand:'',collapse:'',power:'',spinner:'',book:'',flag:'',lock:'',play:'',bolt:'',terminal:'',search:'',chevronRight:'',code:'',circle:'',external:''},Z={"get-running":"flag",environment:"terminal",core:"flag",auth:"lock",cache_store:"gauge",observability:"activity",metrics:"gauge",runs:"play",websocket:"bolt",ag_ui:"external",remote_tools:"wrench",media:"file",checkpointer:"layers",id_generator:"key",thread_name_generator:"file",store:"box",injectq:"arrows",test:"flask",evaluation:"target"};var yo=0;function n(e,t,o,r,i,s){t||(t={});var a,u,m=t;if("ref"in m)for(u in m={},t)u=="ref"?a=t[u]:m[u]=t[u];var l={type:e,props:m,key:o,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--yo,__i:-1,__u:0,__source:i,__self:s};if(typeof e=="function"&&(a=e.defaultProps))for(u in a)m[u]===void 0&&(m[u]=a[u]);return v.vnode&&v.vnode(l),l}function _({name:e,class:t="size-4"}){return/(^|\s)size-/.test(t)||(t=`size-4 ${t}`),n("span",{class:`inline-flex shrink-0 ${t}`,"aria-hidden":"true",children:n("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round",class:"size-full",dangerouslySetInnerHTML:{__html:Sn[e]}})})}function ie({class:e,children:t}){return n("span",{class:`inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-semibold ${e}`,children:t})}function be({id:e,on:t,label:o,onChange:r}){return n("button",{id:e,type:"button",role:"switch","aria-checked":t?"true":"false","aria-label":o,class:`focus-ring relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200 before:absolute before:-inset-2.5 before:content-[''] ${t?"bg-brand-solid":"bg-line-strong"}`,onClick:()=>r(!t),children:n("span",{class:`pointer-events-none inline-block size-5 rounded-full bg-white shadow-sm ring-1 ring-black/5 transition-transform duration-200 ease-out ${t?"translate-x-[18px]":"translate-x-0.5"}`})})}function ee({id:e,label:t,options:o,value:r,onPick:i}){return n("div",{id:e,role:"radiogroup","aria-label":t,class:"inline-flex max-w-full flex-wrap gap-0.5 rounded-[10px] border border-line-strong bg-raised p-0.5",children:o.map(([s,a])=>n("button",{type:"button",role:"radio","aria-checked":s===r?"true":"false",class:`focus-ring min-h-10 cursor-pointer rounded-lg px-3.5 text-sm font-medium transition-colors duration-150 ${s===r?"bg-surface text-ink shadow-card":"text-muted hover:text-ink"}`,onClick:()=>{s!==r&&i(s)},children:a},s))})}function A({id:e,options:t,value:o,invalid:r,onChange:i,plain:s}){return n("div",{class:"relative",children:[n("select",{id:e,value:o,class:`input cursor-pointer appearance-none pr-9 ${s?"":"font-mono"} ${r?"input-invalid":""}`,onChange:a=>i(a.currentTarget.value),children:t.map(([a,u])=>n("option",{value:a,children:u},a))}),n(_,{name:"chevronDown",class:"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-faint"})]})}function H({id:e,values:t,placeholder:o,suggestions:r,onChange:i}){let s=Me(null),a=m=>{let l=m.trim();s.current&&(s.current.value=""),l&&!t.includes(l)&&i([...t,l])},u=r?`${e}-options`:void 0;return n("div",{class:"flex min-h-11 cursor-text flex-wrap items-center gap-1.5 rounded-lg border border-line-strong bg-sunken px-1.5 py-1.5 transition-[border-color,box-shadow] duration-150 focus-within:border-brand focus-within:ring-4 focus-within:ring-brand-ring sm:py-1",onClick:m=>{m.target===m.currentTarget&&s.current?.focus()},children:[t.map((m,l)=>n("span",{class:"inline-flex h-7 items-center gap-0.5 rounded-md border border-line bg-raised pr-0.5 pl-2 font-mono text-xs text-body sm:h-6",children:[m,n("button",{type:"button","aria-label":`Remove ${m}`,class:"inline-flex size-6 cursor-pointer items-center justify-center rounded text-faint transition-colors hover:bg-hover hover:text-ink sm:size-5",onClick:()=>i(t.filter((p,h)=>h!==l)),children:n(_,{name:"x",class:"size-3"})})]},m)),n("input",{ref:s,id:e,type:"text",list:u,placeholder:t.length?"Add...":o,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:"h-7 min-w-[7rem] flex-1 bg-transparent px-1 font-mono text-base text-ink outline-none placeholder:text-faint sm:text-sm",onKeyDown:m=>{let l=m.currentTarget;(m.key==="Enter"||m.key===",")&&l.value.trim()?(m.preventDefault(),a(l.value)):m.key==="Backspace"&&!l.value&&t.length&&i(t.slice(0,-1))},onChange:m=>{r?.includes(m.currentTarget.value.trim())&&a(m.currentTarget.value)},onBlur:m=>a(m.currentTarget.value)}),r?n("datalist",{id:u,children:r.filter(m=>!t.includes(m)).map(m=>n("option",{value:m},m))}):null]})}function xe({label:e,path:t,help:o,id:r,required:i}){return n("div",{class:"min-w-0",children:[n("label",{for:r,class:"block text-sm font-medium text-ink",children:[e,i?n("span",{class:"text-danger",children:" *"}):null]}),n("div",{class:"mt-0.5 font-mono text-xs text-faint",children:t}),o?n("p",{class:"mt-1.5 text-[13px] leading-5 text-muted",children:o}):null]})}function O({issue:e}){if(!e)return null;let t=e.level==="error";return n("p",{class:`mt-2 flex gap-1.5 text-[13px] leading-5 ${t?"text-danger":"text-warn"}`,role:t?"alert":void 0,children:[n(_,{name:t?"xCircle":"alert",class:"mt-0.5 size-3.5"}),n("span",{children:e.message})]})}function bt({children:e}){return n("div",{class:"grid gap-3 px-5 py-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] sm:gap-8",children:e})}function Y({id:e,label:t,path:o,value:r,placeholder:i,onInput:s,issue:a,help:u}){return n(bt,{children:[n(xe,{label:t,path:o,help:u,id:e}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n("input",{id:e,type:"text",value:r??"",placeholder:i,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${a?.level==="error"?"input-invalid":""}`,onInput:m=>s(m.currentTarget.value)}),n(O,{issue:a})]})]})}function xt({name:e,label:t,options:o,value:r,onPick:i}){return n("div",{role:"radiogroup","aria-label":t,class:"grid gap-3 sm:grid-cols-2",children:o.map(([s,a,u])=>{let m=s===r;return n("button",{type:"button",role:"radio","aria-checked":m?"true":"false",id:`${e}-${s}`,class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 ${m?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{m||i(s)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${m?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:m?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:a}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:u})]})]},s)})})}function R({title:e,children:t}){return n("section",{children:[e?n("h4",{class:"eyebrow mb-2.5",children:e}):null,t]})}var ko={info:["info","border-brand/20 bg-brand-soft","text-brand"],warn:["alert","border-warn/25 bg-warn-soft","text-warn"],error:["xCircle","border-danger/25 bg-danger-soft","text-danger"]};function ve({kind:e,title:t,lines:o}){let[r,i,s]=ko[e];return n("div",{class:`flex gap-3 rounded-xl border px-4 py-3 ${i}`,role:e==="error"?"alert":void 0,children:[n(_,{name:r,class:`mt-0.5 size-4 ${s}`}),n("div",{class:"min-w-0 space-y-1 text-[13px] leading-5 text-body",children:[t?n("p",{class:"font-medium text-ink",children:t}):null,o.map((a,u)=>n("p",{children:a},u))]})]})}function vt({issue:e}){return n(ve,{kind:e.level==="error"?"error":"warn",lines:[n("span",{children:[e.path?n("code",{class:"mr-1.5 font-mono text-xs text-muted",children:e.path}):null,e.message]})]})}function $n({icon:e,tint:t,title:o,text:r,children:i}){return n("div",{class:"px-6 py-14 text-center",children:[n("div",{class:`mx-auto flex size-11 items-center justify-center rounded-full bg-raised ${t}`,children:n(_,{name:e,class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:o}),n("p",{class:"mx-auto mt-1 max-w-xs text-sm leading-6 text-muted",children:r}),i]})}function We({title:e,open:t,class:o="",children:r}){return n("details",{class:`group ${o}`,open:t,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform duration-150 group-open:rotate-90"}),e]}),r]})}function Tn(){let e=Me(null);U(()=>{let a=e.current;a&&(d.drawerOpen&&!a.open?a.showModal():!d.drawerOpen&&a.open&&a.close())});let t=d.issues.length>0,o=t&&d.tab==="problems"?"problems":"json",{errors:r,warnings:i}=_e(),s=(a,u,m)=>n("button",{type:"button",role:"tab",id:`tab-${a}`,"aria-selected":o===a?"true":"false",class:`focus-ring inline-flex h-10 cursor-pointer items-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors duration-150 ${o===a?"bg-raised text-ink":"text-muted hover:text-ink"}`,onClick:()=>b(l=>{l.tab=a}),children:[u,m]});return n("dialog",{ref:e,"aria-label":"10xgraph.json preview",class:"m-0 ml-auto h-dvh max-h-none w-[min(540px,100vw)] border-0 border-l border-line-strong bg-surface p-0 text-body shadow-pop backdrop:bg-black/45",onClose:()=>{d.drawerOpen&&Ue()},onClick:a=>{a.target===a.currentTarget&&Ue()},children:n("div",{class:"flex h-full flex-col",children:[n("div",{class:"flex h-14 shrink-0 items-center gap-1 border-b border-line px-3",children:[n("div",{role:"tablist",class:"flex flex-1 gap-1",children:[s("json","10xgraph.json",null),t?s("problems","Problems",n("span",{class:`inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[11px] font-semibold ${r?"bg-danger-soft text-danger":"bg-warn-soft text-warn"}`,children:r+i})):null]}),n("button",{type:"button",class:"icon-btn","aria-label":"Close preview",onClick:Ue,children:n(_,{name:"x",class:"size-5"})})]}),n("div",{class:"min-h-0 flex-1 overflow-y-auto overscroll-contain",children:o==="problems"?n(wo,{}):n(So,{})})]})})}function wo(){let{errors:e,warnings:t}=_e(),o=d.sections.map(r=>[r,re(r.id)]).filter(([,r])=>r.length);return n("div",{children:[n("div",{class:"border-b border-line px-4 py-3",children:n("p",{class:"text-sm text-muted",children:[n("span",{class:e?"font-medium text-danger":"",children:fe(e,"error")})," \xB7 ",n("span",{class:t?"font-medium text-warn":"",children:fe(t,"warning")})]})}),n("div",{class:"space-y-5 px-3 py-4",children:o.map(([r,i])=>n("div",{children:[n("p",{class:"eyebrow mb-1.5 flex items-center gap-2 px-2",children:[n(_,{name:Z[r.id]||"box",class:"size-3.5"}),r.title]}),n("ul",{class:"space-y-0.5",children:i.map((s,a)=>{let u=s.level==="error";return n("li",{children:n("button",{type:"button",onClick:()=>D(r.id,De(s.path)),class:"focus-ring flex min-h-11 w-full cursor-pointer gap-2.5 rounded-lg px-2 py-2 text-left transition-colors duration-150 hover:bg-raised",children:[n(_,{name:u?"xCircle":"alert",class:`mt-0.5 size-4 ${u?"text-danger":"text-warn"}`}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-[13px] leading-5 text-body",children:s.message}),s.path?n("span",{class:"mt-0.5 block truncate font-mono text-xs text-faint",children:s.path}):null]})]})},a)})})]},r.id))})]})}function Co(e){return e.replace(/&/g,"&").replace(//g,">").replace(/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,o=>{let r="var(--af-json-number)";return o.startsWith('"')?r=o.endsWith(":")?"var(--af-json-key)":"var(--af-json-string)":(o==="true"||o==="false"||o==="null")&&(r="var(--af-json-literal)"),`${o}`})}function So(){let e=JSON.stringify(d.config,null,2).split(` +`);return n("div",{children:[n("div",{class:"sticky top-0 flex items-center justify-between gap-3 border-b border-line bg-surface px-4 py-2.5",children:[n("p",{class:"text-xs text-faint",children:W()||!d.exists?"Preview of what Save will write":"Matches the file on disk"}),n("button",{type:"button",class:"btn btn-ghost h-8 px-2.5 text-xs",onClick:Cn,children:[n(_,{name:"copy",class:"size-3.5"}),"Copy"]})]}),n("div",{class:"py-3 pr-4 font-mono text-[12.5px] leading-5",children:e.map((t,o)=>n("div",{class:"flex",children:[n("span",{class:"w-10 shrink-0 pr-3 text-right text-faint/70 select-none",children:o+1}),n("span",{class:"min-w-0 flex-1 break-all whitespace-pre-wrap",dangerouslySetInnerHTML:{__html:Co(t)}})]},o))})]})}function kt(e){let t=[];for(let o of e){let r=o.group||"",i=t.find(s=>s.name===r);i||t.push(i={name:r,fields:[]}),i.fields.push(o)}return t}var B=(e,t)=>b(o=>{if(t===void 0){he(o.config,e);return}let r=e.length>1?o.sections.find(i=>i.optional&&i.key===e[0]):null;r&&(o.config[r.key]===void 0||o.config[r.key]===null)&&(o.config[r.key]=JSON.parse(JSON.stringify(r.default))),oe(o.config,e,t)}),$o=e=>`f-${e.path.join("-")}`;function To({id:e,value:t,field:o,invalid:r,disabled:i}){let[s,a]=K(String(t??""));return U(()=>{(s===""?t!==void 0:Number(s)!==t)&&a(String(t??""))},[t]),n("input",{id:e,type:"number",inputmode:"decimal",value:s,disabled:i,min:o.min,step:o.step??"any",placeholder:o.placeholder??o.default??"",class:`input max-w-40 font-mono tabular-nums ${r?"input-invalid":""}`,onInput:u=>{let m=u.currentTarget.value;a(m),m===""?B(o.path,void 0):Number.isNaN(Number(m))||B(o.path,Number(m))}})}function yt({field:e,value:t}){return e.exclusive_min===void 0||typeof t!="number"||t>e.exclusive_min?null:n("p",{class:"mt-1.5 text-[13px] text-warn",children:["Must be greater than ",e.exclusive_min,"."]})}function Eo({id:e,field:t,value:o,invalid:r}){let i=t.unlimited_value!==void 0&&o===t.unlimited_value,s=n(To,{id:e,value:i?void 0:o,field:t,invalid:r,disabled:i});if(t.null_label!==void 0){let a=o===null?"n":o===void 0?"d":"s",u=typeof t.default=="number"?t.default:300;return n("div",{children:[n(ee,{id:e,label:t.label,value:a,options:[["d","Default"],["s","Seconds"],["n",t.null_label]],onPick:m=>B(t.path,m==="d"?void 0:m==="n"?null:u)}),a==="s"?n("div",{class:"mt-2.5",children:[s,n(yt,{field:t,value:o})]}):null]})}return t.unlimited_value!==void 0?n("div",{children:[n("div",{class:"flex flex-wrap items-center gap-x-4",children:[s,n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm text-body",children:[n("input",{type:"checkbox",class:"size-[18px] accent-brand-solid",checked:i,onChange:a=>B(t.path,a.currentTarget.checked?t.unlimited_value:void 0)}),"Unlimited"]})]}),n(yt,{field:t,value:o})]}):n("div",{children:[s,n(yt,{field:t,value:o})]})}function Oo({id:e,field:t,value:o}){if(t.unset_label!==void 0){let r=[["d","Default"]];return t.allow_true!==!1&&r.push(["t","On"]),r.push(["f","Off"]),n("div",{children:[n(ee,{id:e,label:t.label,value:o===void 0?"d":o?"t":"f",options:r,onPick:i=>B(t.path,i==="d"?void 0:i==="t")}),n("p",{class:"mt-1.5 text-xs text-faint",children:["Default: ",t.unset_label]})]})}return n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-semibold text-ink",children:[n("input",{id:e,type:"checkbox",class:"size-[18px] accent-brand-solid",checked:(o??t.default??!1)===!0,onChange:r=>B(t.path,r.currentTarget.checked)}),t.label]})}function Po({id:e,field:t,value:o,invalid:r}){if(t.type==="bool")return n(Oo,{id:e,field:t,value:o});if(t.type==="number")return n(Eo,{id:e,field:t,value:o,invalid:r});if(t.type==="list")return n(H,{id:e,values:Array.isArray(o)?o:[],placeholder:t.placeholder,onChange:i=>B(t.path,i.length?i:void 0)});if(t.type==="select"){let i=t.options.map(s=>Array.isArray(s)?s:[s,s]);return t.unset_label!==void 0&&i.unshift(["",t.unset_label]),n(A,{id:e,options:i,invalid:r,value:o??(t.unset_label!==void 0?"":t.default??(Array.isArray(t.options[0])?t.options[0][0]:t.options[0])),onChange:s=>B(t.path,s===""?void 0:s)})}return n("input",{id:e,type:"text",value:o??"",placeholder:t.placeholder,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${r?"input-invalid":""}`,onInput:i=>B(t.path,i.currentTarget.value===""?void 0:i.currentTarget.value)})}function I({field:e}){let t=e.path.join("."),o=$o(e),r=L(d.config,e.path),i=S(t),s=ft(e),a=fn(e),u=e.type==="bool"&&e.unset_label===void 0,m=e.default!==void 0&&!s,l=Array.isArray(e.default)?e.default.join(", "):String(e.default??"");return n("div",{id:`row-${o}`,class:`relative pt-5 pb-1 ${s?"before:absolute before:top-6 before:-left-2.5 before:bottom-2 before:w-[3px] before:rounded before:bg-brand before:content-[''] sm:before:-left-3.5":""}`,children:[n("div",{class:"flex min-h-6 items-center gap-2.5",children:[u?null:n("label",{for:o,class:`text-sm font-semibold text-ink ${a?"opacity-50":""}`,children:[e.label,e.required?n("span",{class:"text-danger",children:" *"}):null]}),u?null:n("code",{class:"hidden font-mono text-[11px] text-faint sm:inline",children:t}),m&&!u?n("span",{class:"rounded border border-line px-1.5 text-[11px] leading-4 font-medium text-faint",children:"default"}):null,n("span",{class:"flex-1"}),s?n("button",{type:"button",class:"focus-ring min-h-8 cursor-pointer rounded px-1 text-[13px] font-medium text-brand hover:underline",title:`Reset to default${e.default!==void 0?` (${l})`:""}`,"aria-label":`Reset ${e.label} to default`,onClick:()=>b(p=>he(p.config,e.path)),children:"Reset"}):null]}),e.help?n("p",{class:`mt-0.5 max-w-prose text-[13px] leading-5 text-muted ${a?"opacity-50":""}`,children:e.help}):null,n("fieldset",{disabled:a,class:`m-0 mt-2.5 min-w-0 max-w-[520px] border-0 p-0 ${a?"opacity-50":""}`,children:n(Po,{id:o,field:e,value:r,invalid:i?.level==="error"})}),a?n("p",{class:"mt-1.5 text-xs text-faint",children:hn(e)}):null,n(O,{issue:i})]})}var Io=[["","Choose reach"],["own","own"],["tenant","tenant"],["all","all"]],Pn=e=>Array.isArray(e)?e.length>0:g(e)&&Object.keys(e).length>0;function In(){let e=d.config.auth;return e==="jwt"?{method:"jwt"}:g(e)?e:{method:"none"}}function z(e){b(t=>{let o={...In()};e(o),t.config.auth=o.method==="none"?null:o})}function Ve(e,t,o){o==null||o===""||typeof o!="string"&&!Pn(o)?delete e[t]:e[t]=o}function Ao(e){return e.roles_claim!==void 0||Pn(e.roles)?"roles":e.permissions_claim!==void 0?"permissions":"default"}var Ro=e=>d.issues.filter(t=>t.path===e||t.path.startsWith(`${e}.`));function wt({prefix:e}){return Ro(e).map((t,o)=>n(O,{issue:t},o))}function An({id:e,label:t,name:o,onRename:r}){let[i,s]=K(o);return U(()=>s(o),[o]),n("div",{children:[n("label",{for:e,class:"sr-only",children:t}),n("input",{id:e,type:"text",value:i,class:"input font-mono",autocomplete:"off",spellcheck:!1,onInput:a=>s(a.currentTarget.value),onChange:a=>{r(a.currentTarget.value.trim())||s(o)}})]})}var Be=({children:e})=>n("span",{class:"mb-1.5 block text-xs font-medium text-faint",children:e});function Rn(e,t,o){return!o||o!==t&&o in e?null:Object.fromEntries(Object.entries(e).map(([r,i])=>[r===t?o:r,i]))}function jn({sec:e}){let t=In(),o=t.method??"none",r=Ao(t);return n("div",{class:"space-y-6",children:[n(R,{title:"Method",children:n(xt,{name:"auth-method",label:"Authentication method",value:o,options:[["none","None","Every request is accepted. For local development."],["jwt","JWT","Verify bearer tokens signed with JWT_SECRET_KEY."],["custom","Custom","Your own BaseAuth subclass decides who is calling."]],onPick:i=>z(s=>{s.method=i,i==="custom"?s.path=s.path??"":delete s.path})})}),o==="custom"?n(R,{title:"Class",children:n("div",{class:"panel",children:n(Y,{id:"auth-path",label:"Auth class",path:"auth.path",value:t.path,placeholder:"auth.custom:MyAuth",help:"Import path of a BaseAuth subclass, as module:attribute.",issue:S("auth.path"),onInput:i=>z(s=>{s.path=i})})})}):null,o!=="none"?n(x,{children:[n(R,{title:"Permission source",children:n(xt,{name:"auth-source",label:"Permission source",value:r,options:[["default","Default","Every signed-in user, own data only."],["roles","Roles from a claim","Map the roles in the token to permissions."],["permissions","Permissions from a claim","The token already lists the permissions."]],onPick:i=>z(s=>{delete s.roles_claim,delete s.permissions_claim,delete s.roles,delete s.default_role,i==="roles"&&(s.roles_claim="roles",s.roles={member:{permissions:[],reach:"own"}}),i==="permissions"&&(s.permissions_claim="permissions")})})}),r==="roles"?n(Mo,{auth:t,registry:e.registry}):null,r==="permissions"?n(R,{title:"Claim",children:n("div",{class:"panel",children:n(Y,{id:"auth-permissions-claim",label:"Permissions claim",path:"auth.permissions_claim",value:t.permissions_claim,placeholder:"permissions",issue:S("auth.permissions_claim"),help:"Token claim holding a list of permissions, such as graph:invoke.",onInput:i=>z(s=>{s.permissions_claim=i})})})}):null,n(R,{title:"Tenants",children:n("div",{class:"panel divide-y divide-line",children:[n("div",{class:"px-5 py-4",children:[n(xe,{label:"Tenant claims",path:"auth.tenant_claims",id:"auth-tenant-claims",help:"Token claims that name the tenant, such as org_id. Rows are separated by them."}),n("div",{class:"mt-3",children:n(H,{id:"auth-tenant-claims",values:t.tenant_claims??[],placeholder:"org_id",onChange:i=>z(s=>Ve(s,"tenant_claims",i))})}),n(wt,{prefix:"auth.tenant_claims"})]}),r==="roles"?n(jo,{auth:t}):null]})}),n(No,{auth:t})]}):null]})}function jo({auth:e}){let t=Object.keys(g(e.roles)?e.roles:{});return n(bt,{children:[n(xe,{label:"Default role",path:"auth.default_role",id:"auth-default-role",help:"Given to a signed-in user whose token carries no role."}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n(A,{id:"auth-default-role",value:e.default_role??"",options:[["","None"],...t.map(o=>[o,o])],onChange:o=>z(r=>Ve(r,"default_role",o))}),n(O,{issue:S("auth.default_role")})]})]})}function Mo({auth:e,registry:t}){let o=g(e.roles)?e.roles:{},r=Object.entries(o),i=Object.keys(o),s=["*",...Object.entries(t||{}).flatMap(([p,h])=>[`${p}:*`,...h.map(c=>`${p}:${c}`)])],a=p=>z(h=>{h.roles=p}),u=(p,h)=>a({...o,[p]:{...o[p],...h}}),m=()=>{let p=r.length+1;for(;`role${p}`in o;)p++;a({...o,[`role${p}`]:{permissions:[],reach:"own"}})},l=(p,h)=>{let c=Rn(o,p,h);return c?(z(f=>{f.roles=Object.fromEntries(Object.entries(c).map(([y,$])=>[y,$.inherits?{...$,inherits:$.inherits.map(w=>w===p?h:w)}:$])),f.default_role===p&&(f.default_role=h),g(f.system_identities)&&(f.system_identities=Object.fromEntries(Object.entries(f.system_identities).map(([y,$])=>[y,{...$,roles:($.roles??[]).map(w=>w===p?h:w)}])))}),!0):!1};return n(R,{title:"Roles",children:n("div",{class:"panel",children:[n("div",{class:"border-b border-line",children:n(Y,{id:"auth-roles-claim",label:"Roles claim",path:"auth.roles_claim",value:e.roles_claim,placeholder:"roles",issue:S("auth.roles_claim"),help:"Token claim holding the user's list of role names.",onInput:p=>z(h=>{h.roles_claim=p})})}),r.length?n("div",{class:"divide-y divide-line",children:r.map(([p,h],c)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_8rem_auto] items-start gap-3",children:[n(An,{id:`role-name-${c}`,label:"Role name",name:p,onRename:f=>l(p,f)}),n(A,{id:`role-reach-${c}`,value:h.reach??"",options:Io,invalid:S(`auth.roles.${p}.reach`)?.level==="error",onChange:f=>u(p,{reach:f})}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove role ${p}`,onClick:()=>a(Object.fromEntries(r.filter(([f])=>f!==p))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Be,{children:"Permissions"}),n(H,{id:`role-permissions-${c}`,values:h.permissions??[],placeholder:"graph:invoke, threads:*, *",suggestions:s,onChange:f=>u(p,{permissions:f})})]}),n("div",{children:[n(Be,{children:"Inherits"}),n(H,{id:`role-inherits-${c}`,values:h.inherits??[],placeholder:"another role",suggestions:i.filter(f=>f!==p),onChange:f=>{let y={...h};Ve(y,"inherits",f),a({...o,[p]:y})}})]}),n(wt,{prefix:`auth.roles.${p}`})]},c))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"No roles yet. Add one to use roles."}),n("div",{class:"flex items-center justify-between gap-4 border-t border-line px-5 py-3",children:[n("p",{class:"text-[13px] text-muted",children:["Reach: ",n("code",{class:"font-mono",children:"own"})," the user's rows, ",n("code",{class:"font-mono",children:"tenant"})," every row of their tenant, ",n("code",{class:"font-mono",children:"all"})," every row."]}),n("button",{type:"button",class:"btn btn-ghost",onClick:m,children:[n(_,{name:"plus"}),"Add role"]})]})]})})}function No({auth:e}){let t=g(e.system_identities)?e.system_identities:{},o=Object.entries(t),r=Object.keys(g(e.roles)?e.roles:{}),i=l=>z(p=>Ve(p,"system_identities",l)),s=(l,p)=>i({...t,[l]:{...t[l],...p}}),a=()=>{let l=o.length+1;for(;`job${l}`in t;)l++;i({...t,[`job${l}`]:{roles:[],tenant:{}}})},u=l=>Object.entries(l??{}).map(([p,h])=>`${p}=${h}`),m=l=>Object.fromEntries(l.filter(p=>p.includes("=")).map(p=>[p.slice(0,p.indexOf("=")).trim(),p.slice(p.indexOf("=")+1).trim()]));return n(R,{title:"System identities",children:n("div",{class:"panel",children:[o.length?n("div",{class:"divide-y divide-line",children:o.map(([l,p],h)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3",children:[n(An,{id:`system-name-${h}`,label:"Identity name",name:l,onRename:c=>{let f=Rn(t,l,c);return f&&i(f),!!f}}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${l}`,onClick:()=>i(Object.fromEntries(o.filter(([c])=>c!==l))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Be,{children:"Roles"}),n(H,{id:`system-roles-${h}`,values:p.roles??[],placeholder:"member",suggestions:r,onChange:c=>s(l,{roles:c})})]}),n("div",{children:[n(Be,{children:"Tenant, as claim=value"}),n(H,{id:`system-tenant-${h}`,values:u(p.tenant),placeholder:"org_id=acme",onChange:c=>s(l,{tenant:m(c)})})]}),n(wt,{prefix:`auth.system_identities.${l}`})]},h))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"None. Scheduled jobs and other non-user callers can run as one."}),n("div",{class:"flex justify-end border-t border-line px-5 py-3",children:n("button",{type:"button",class:"btn btn-ghost",onClick:a,children:[n(_,{name:"plus"}),"Add identity"]})})]})})}var Ge=[["memory","Memory","This process only"],["redis","Redis","Shared. Redis, Valkey, Dragonfly, KeyDB"],["custom","Custom","Your BaseCacheStore class"]],Lo={requests:100,window:60,by:"ip"},Je=()=>g(d.config.cache_store)?d.config.cache_store:{};function Ct(e){b(t=>{let o=g(t.config.cache_store)?t.config.cache_store:{};e(o),Object.keys(o).length?t.config.cache_store=o:delete t.config.cache_store})}var Do=()=>{let e=Je().rate_limit;return g(e)&&e.enabled!==!1},Ho=()=>{let e=Je().token_revocation;return e===!0||g(e)&&e.enabled!==!1};function zo(e){Ct(t=>{delete t.url,delete t.path,e==="memory"?delete t.type:t.type=e})}function Fo(e){Ct(t=>{e?g(t.rate_limit)?delete t.rate_limit.enabled:t.rate_limit={...Lo}:delete t.rate_limit})}function Uo(e){Ct(t=>{e?g(t.token_revocation)?delete t.token_revocation.enabled:t.token_revocation=!0:delete t.token_revocation})}var En=(e,t)=>b(o=>{oe(o.config,e,t)});function qo({value:e,onPick:t}){let o=(r,i)=>{let s=r.key==="ArrowRight"||r.key==="ArrowDown"?1:r.key==="ArrowLeft"||r.key==="ArrowUp"?-1:0;if(!s)return;r.preventDefault();let a=Ge[(i+s+Ge.length)%Ge.length][0];t(a),requestAnimationFrame(()=>document.getElementById(`cache-type-${a}`)?.focus())};return n("div",{role:"radiogroup","aria-label":"Cache store type",class:"grid gap-3 sm:grid-cols-3",children:Ge.map(([r,i,s],a)=>{let u=r===e;return n("button",{type:"button",role:"radio",id:`cache-type-${r}`,"aria-checked":u?"true":"false",tabIndex:u?0:-1,onKeyDown:m=>o(m,a),class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 motion-reduce:transition-none ${u?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{u||t(r)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${u?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:u?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:i}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:s})]})]},r)})})}function ye({warn:e,children:t}){return n("p",{class:`mt-3 flex items-start gap-1.5 text-[13px] leading-5 ${e?"text-warn":"text-muted"}`,children:[e?n(_,{name:"alert",class:"mt-0.5 size-3.5"}):null,n("span",{children:t})]})}function On({id:e,title:t,text:o,on:r,onChange:i}){return n("div",{class:"flex items-center gap-4",children:[n("div",{class:"min-w-0 flex-1",children:[n("h3",{class:"text-base font-semibold text-ink",children:t}),n("p",{class:"mt-0.5 text-[13px] leading-5 text-muted",children:o})]}),n(be,{id:e,on:r,label:t,onChange:i})]})}var Wo=[["ip","IP"],["user","User"],["global","Global"]];function Go({field:e,memory:t}){let[o,r]=K(!1),i=Je().rate_limit,s=g(i)&&i.by||"ip",a=!(g(i)&&i.fail_open===!1),u=["trusted_proxy_headers","trusted_proxy_hops","trusted_proxies","exclude_paths"];return n(x,{children:[t?n(ye,{warn:!0,children:"Counts per worker with the Memory store."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:e("cache_store.rate_limit.requests")}),n(I,{field:e("cache_store.rate_limit.window")}),n("div",{class:"pt-5",children:[n("span",{id:"cache-by-label",class:"block text-sm font-semibold text-ink",children:"Limit by"}),n("div",{class:"mt-2.5",children:n(ee,{id:"f-cache_store-rate_limit-by",label:"Limit by",options:Wo,value:s,onPick:m=>En(["cache_store","rate_limit","by"],m)})}),n(O,{issue:S("cache_store.rate_limit.by")})]}),n("div",{class:"pt-5",children:[n("span",{id:"cache-fail-open-label",class:"block text-sm font-semibold text-ink",children:"Fail open"}),n("div",{class:"mt-2.5 flex items-center gap-3",children:[n(be,{id:"f-cache_store-rate_limit-fail_open",on:a,label:"Fail open",onChange:m=>En(["cache_store","rate_limit","fail_open"],m)}),n("p",{class:"m-0 text-[13px] leading-5 text-muted",children:a?"Allow requests if the store errors.":"Deny with 503 if the store errors."})]})]})]}),n("div",{class:"mt-5 border-t border-line pt-1",children:[n("button",{type:"button","aria-expanded":o?"true":"false","aria-controls":"cache-advanced",class:"focus-ring flex min-h-11 cursor-pointer items-center gap-2 rounded font-medium text-muted hover:text-ink",onClick:()=>r(!o),children:[n(_,{name:"chevronRight",class:`size-4 transition-transform duration-150 motion-reduce:transition-none ${o?"rotate-90":""}`}),"Advanced"]}),o?n("div",{id:"cache-advanced",children:u.map(m=>n(I,{field:e(`cache_store.rate_limit.${m}`)},m))}):null]})]})}var ae=[["rate","Rate limiting"],["revoke","Token revocation"]];function Mn({sec:e}){let[t,o]=K("rate"),r=Je().type||"memory",i=l=>e.fields.find(p=>p.path.join(".")===l),s={rate:Do(),revoke:Ho()},a=r==="memory",u={rate:!!S("cache_store.rate_limit"),revoke:!!S("cache_store.token_revocation")},m=(l,p)=>{let h=l.key==="ArrowRight"?1:l.key==="ArrowLeft"?-1:0,c=l.key==="Home"?0:l.key==="End"?ae.length-1:h?(p+h+ae.length)%ae.length:-1;c<0||(l.preventDefault(),o(ae[c][0]),document.getElementById(`cache-tab-${ae[c][0]}`)?.focus())};return n("div",{children:[n(R,{title:"Store type",children:[n(qo,{value:r,onPick:zo}),r==="redis"?n(I,{field:i("cache_store.url")}):null,r==="custom"?n(I,{field:i("cache_store.path")}):null,a?n("p",{class:"mt-3 mb-0 text-[13px] leading-5 text-muted",children:"Not shared between workers: each worker keeps its own locks, limits and revocations."}):null]}),n("div",{role:"tablist","aria-label":"Cache store features",class:"mt-8 flex gap-1 border-b border-line",children:ae.map(([l,p],h)=>{let c=t===l;return n("button",{type:"button",role:"tab",id:`cache-tab-${l}`,"aria-selected":c?"true":"false","aria-controls":`cache-panel-${l}`,tabIndex:c?0:-1,onKeyDown:f=>m(f,h),class:`focus-ring -mb-px flex min-h-11 cursor-pointer items-center gap-2 border-b-2 px-3 text-sm font-medium transition-colors duration-150 motion-reduce:transition-none ${c?"border-brand-solid text-ink":"border-transparent text-muted hover:text-ink"}`,onClick:()=>o(l),children:[p,n(ie,{class:s[l]?"bg-brand-soft text-brand":"bg-raised text-muted",children:s[l]?"On":"Off"})]},l)})}),n("div",{role:"tabpanel",id:`cache-panel-${t}`,"aria-labelledby":`cache-tab-${t}`,class:"pt-5",children:t==="rate"?n(x,{children:[n(On,{id:"cache-rate-limit",title:"Rate limiting",on:s.rate,onChange:Fo,text:"Cap how many requests one caller can make."}),s.rate?n(Go,{field:i,memory:a}):u.rate?n(ye,{warn:!0,children:"Off: one caller can exhaust the service."}):null]}):n(x,{children:[n(On,{id:"cache-token-revocation",title:"Token revocation",on:s.revoke,onChange:Uo,text:"Reject revoked tokens before they expire (10xgraph revoke)."}),s.revoke?n(x,{children:[a?n(ye,{warn:!0,children:"10xgraph revoke cannot reach an in-memory store; pick Redis or Custom."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:i("cache_store.token_revocation.token_lifetime")}),n(I,{field:i("cache_store.token_revocation.user_ttl")})]}),n(ye,{children:"Fails closed: while the cache store is unreachable, authenticated requests get 503 and the server will not start."})]}):u.revoke?n(ye,{warn:!0,children:"Off: a leaked token works until it expires."}):null]})})]})}function Nn({sec:e}){let t=e.fields[0].path[0],o=e.choices.map(p=>p[0]),r=d.config[t],i=t==="thread_name_generator"?"random":"",s=r===void 0?i:typeof r=="string"&&o.includes(r)?r:"custom",a=[...t==="id_generator"?[["","Not set (the graph's own generator)"]]:[],...e.choices.map(([p,h])=>[p,t==="thread_name_generator"&&p==="random"?`${h} (default)`:h]),["custom","Custom (module:attr)"]],u=s===""?"The graph keeps the generator it was compiled with.":s==="custom"?"Your own class, as module:attribute.":e.choices.find(p=>p[0]===s)?.[2],m=p=>b(h=>{p===i?delete h.config[t]:h.config[t]=p==="custom"?"":p}),l=`f-${t}`;return n("div",{children:[n(R,{title:"Choice",children:n("div",{class:"max-w-[520px]",children:[n("label",{for:`${l}-select`,class:"sr-only",children:e.title}),n(A,{id:`${l}-select`,options:a,value:s,onChange:m}),n("p",{class:"mt-1.5 text-[13px] text-muted",children:u})]})}),s==="custom"?n(R,{title:"Class",children:n("div",{class:"panel",children:n(Y,{id:l,label:e.fields[0].label,path:t,value:typeof r=="string"?r:"",placeholder:e.fields[0].placeholder,help:e.fields[0].help,issue:S(t),onInput:p=>b(h=>{h.config[t]=p})})})}):null]})}function Ln(){let e=Array.isArray(d.config.remote_tools)?d.config.remote_tools:[],t=()=>{b(o=>{e.push({node:"",name:"",description:"",parameters:{type:"object",properties:{},required:[]}}),o.config.remote_tools=e}),requestAnimationFrame(()=>document.getElementById(`tool-${e.length-1}-name`)?.focus())};return e.length?n("div",{class:"space-y-4",children:[e.map((o,r)=>n(Bo,{tools:e,tool:o,index:r},r)),n("button",{type:"button",onClick:t,class:"focus-ring flex h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-dashed border-line-strong text-sm font-medium text-muted transition-colors duration-150 hover:border-brand hover:text-brand",children:[n(_,{name:"plus"}),"Add remote tool"]})]}):n("div",{class:"rounded-xl border border-dashed border-line-strong bg-surface px-6 py-10 text-center",children:[n("div",{class:"mx-auto flex size-11 items-center justify-center rounded-full bg-raised text-faint",children:n(_,{name:"wrench",class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:"No remote tools yet"}),n("p",{class:"mx-auto mt-1 max-w-sm text-sm leading-6 text-muted",children:"Declare a tool here and the client that calls the API runs it, for example to read the user's location."}),n("button",{type:"button",class:"btn btn-outline mt-5",onClick:t,children:[n(_,{name:"plus"}),"Add remote tool"]})]})}function Bo({tools:e,tool:t,index:o}){let r=d.toolParams[o]??{text:JSON.stringify(t.parameters??{},null,2),error:null},i=a=>b(()=>{e[o]={...e[o],...a}}),s=a=>`remote_tools[${o}].${a}`;return n("section",{class:"panel",children:[n("div",{class:"flex items-center gap-3 border-b border-line px-5 py-3",children:[n(_,{name:"wrench",class:"size-4 text-faint"}),n("span",{class:"min-w-0 flex-1 truncate font-mono text-sm font-medium text-ink",children:t.name||`tool_${o+1}`}),t.node?n("span",{class:"rounded-md border border-line bg-raised px-1.5 py-0.5 font-mono text-xs text-muted",children:t.node}):null,n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${t.name||"tool"}`,onClick:()=>b(a=>{e.splice(o,1),a.toolParams={}}),children:n(_,{name:"trash"})})]}),n("div",{class:"divide-y divide-line",children:[n(Y,{id:`tool-${o}-name`,label:"Tool name",path:s("name"),value:t.name,placeholder:"get_location",onInput:a=>i({name:a})}),n(Y,{id:`tool-${o}-node`,label:"Graph node",path:s("node"),value:t.node,placeholder:"MAIN",help:"The node whose model may call this tool.",onInput:a=>i({node:a})}),n(Y,{id:`tool-${o}-description`,label:"Description",path:s("description"),value:t.description,placeholder:"Returns the user's current city",help:"What the model reads when deciding to call it.",onInput:a=>i({description:a})}),n("div",{class:"px-5 py-4",children:[n(xe,{label:"Parameters",path:s("parameters"),help:"JSON schema of the arguments. type must be object.",id:`tool-${o}-params`}),n("textarea",{id:`tool-${o}-params`,rows:7,spellcheck:!1,value:r.text,class:`input mt-3 h-auto py-2.5 font-mono text-[13px] leading-5 ${r.error?"input-invalid":""}`,onInput:a=>{let u=a.currentTarget.value;b(m=>{try{e[o]={...e[o],parameters:JSON.parse(u)},m.toolParams[o]={text:u,error:null}}catch(l){m.toolParams[o]={text:u,error:l.message}}})}}),n(O,{issue:r.error?{level:"error",message:r.error}:null})]})]})]})}var Tt="focus-ring min-h-8 cursor-pointer rounded px-1.5 text-[13px] font-medium text-brand transition-colors duration-150 hover:underline",te="mt-2 max-w-prose text-[13px] leading-5 text-muted",V=(e,t)=>b(o=>{t===void 0||t===""?he(o.config,e):oe(o.config,e,t)});function Et({id:e,label:t,path:o,help:r,children:i}){return n("div",{class:"pt-4",children:[n("div",{class:"flex items-baseline gap-2.5",children:[n("label",{for:e,class:"text-sm font-semibold text-ink",children:t}),o?n("code",{class:"hidden font-mono text-[11px] text-faint sm:inline",children:o}):null]}),n("div",{class:"mt-2 max-w-[520px]",children:i}),r?n("p",{class:te,children:r}):null,n(O,{issue:S(o)})]})}function le({path:e,label:t,help:o,placeholder:r}){let i=e.join("."),s=`f-${e.join("-")}`,a=L(d.config,e);return n(Et,{id:s,label:t,path:i,help:o,children:n("input",{id:s,type:"text",value:a??"",placeholder:r,autocomplete:"off",spellcheck:!1,class:`input font-mono ${S(i)?.level==="error"?"input-invalid":""}`,onInput:u=>V(e,u.currentTarget.value)})})}function Vo({path:e,label:t,help:o}){let r=e.join("."),i=`f-${e.join("-")}`,s=L(d.config,e);return n(Et,{id:i,label:t,path:r,help:o,children:n("input",{id:i,type:"number",inputmode:"numeric",min:"1",step:"1",value:s??"",class:`input max-w-40 font-mono tabular-nums ${S(r)?.level==="error"?"input-invalid":""}`,onInput:a=>{let u=a.currentTarget.value;V(e,u===""?void 0:Number(u))}})})}function St({path:e,label:t,help:o,options:r,value:i,onChange:s}){let a=`f-${e.join("-")}`;return n(Et,{id:a,label:t,path:e.join("."),help:o,children:n(A,{id:a,plain:!0,options:r,value:i,onChange:s})})}function Jo({path:e,label:t,checked:o}){let r=`f-${e.join("-")}`;return n("label",{class:"mt-3 inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-semibold text-ink",children:[n("input",{id:r,type:"checkbox",class:"size-[18px] accent-brand-solid",checked:o,onChange:i=>V(e,i.currentTarget.checked)}),t]})}var Ye=[{key:"logfire",name:"Logfire",label:"Logfire",text:"Pydantic Logfire.",secret:n(x,{children:["Token: set ",n("code",{class:"font-mono text-xs",children:"LOGFIRE_TOKEN"})," in .env."]})},{key:"langsmith",name:"LangSmith",label:"LangSmith",text:"LangChain's tracing service.",secret:n(x,{children:["API key: set ",n("code",{class:"font-mono text-xs",children:"LANGSMITH_API_KEY"})," in .env."]})},{key:"phoenix",name:"Phoenix",label:"Phoenix",text:"Arize Phoenix, local or hosted.",secret:n(x,{children:["API key: set ",n("code",{class:"font-mono text-xs",children:"PHOENIX_API_KEY"})," in .env. A local Phoenix does not need one."]})},{key:"otlp",name:"OTLP endpoint",label:"OTLP endpoint",text:"Any OpenTelemetry endpoint: a collector, Tempo, Jaeger.",secret:n(x,{children:["Auth headers: set ",n("code",{class:"font-mono text-xs",children:"OTEL_EXPORTER_OTLP_HEADERS"})," in .env."]})}],Ko=[["standard","Standard"],["spans","Spans only"],["full","Full"]],Yo={standard:"Adds model name, token counts and request parameters.",spans:"Timing and structure only. No inputs or outputs.",full:"Records prompts and tool I/O. May contain PII."},Xo=[["raw","Raw"],["hash","Hashed"],["mask","Masked"]],Qo={raw:"The user id is sent as is.",hash:"A SHA-256 of the user id, keyed when TENXGRAPH_TRACE_USER_ID_KEY is set.",mask:"The middle of the user id is hidden."},$t=()=>g(d.config.observability)?d.config.observability:{},Xe=e=>g($t()[e])&&$t()[e].enabled===!0;function Dn(e){b(t=>{g(t.config.observability)||(t.config.observability={level:"standard",user_id_mode:"raw"}),t.config.observability[e]={enabled:!0}})}function Zo(e){b(t=>{delete t.config.observability[e],Ye.some(o=>Xe(o.key))||delete t.config.observability})}function er(){let e=d.config.metrics,t=g(e)&&g(e.otlp)?e.otlp.endpoint:null;return!g(e)||e.enabled!==!0||e.export!=="otlp"||!t?null:t.replace(/\/+$/,"").replace(/\/v1\/metrics$/,"")}function tr(){let e=["observability","otlp","use_metrics_endpoint"],t=L(d.config,e)===!0,o=er(),r="f-observability-otlp-use_metrics_endpoint";return n(x,{children:[o?n("div",{class:"pt-4",children:[n("div",{class:"flex items-center gap-3",children:[n(be,{id:r,on:t,label:"Use the metrics endpoint",onChange:i=>V(e,i?!0:void 0)}),n("label",{for:r,class:"cursor-pointer text-sm font-semibold text-ink",children:"Use the metrics endpoint"})]}),n("p",{class:te,children:t?n(x,{children:["Traces go to ",n("code",{class:"font-mono text-xs text-body",children:[o,"/v1/traces"]})]}):"Send traces to the same endpoint as the metrics."}),n(O,{issue:S(e.join("."))})]}):null,t&&o?null:n(le,{path:["observability","otlp","endpoint"],label:"Endpoint",placeholder:"http://collector:4318",help:"Base URL of an OTLP HTTP receiver. /v1/traces is appended."}),o?null:n("p",{class:te,children:["Sharing the metrics endpoint needs Metrics set to OTLP push with an endpoint.",t?n(x,{children:[" It is on in the file. ",n("button",{type:"button",class:Tt,onClick:()=>V(e,void 0),children:"Stop sharing"})]}):null]}),n(O,{issue:t&&!o?S(e.join(".")):null}),n("p",{class:te,children:"Prometheus stores metrics only. To send traces and metrics to one place, use a collector (OTel Collector, Grafana Alloy, Grafana Cloud)."})]})}function nr({dest:e}){let t=r=>["observability",e.key,r];if(e.key==="logfire"){let r=t("send_to_logfire"),i=t("console");return n(x,{children:[n(le,{path:t("service_name"),label:"Service name",placeholder:"my-agent"}),n(Jo,{path:r,label:"Send to Logfire",checked:L(d.config,r)!==!1}),n(St,{path:i,label:"Print spans to console",help:"Logfire also takes console options that JSON cannot express, so only its own behavior or Off is offered.",options:[["","Logfire's own behavior"],["off","Off"]],value:L(d.config,i)===!1?"off":"",onChange:s=>V(i,s==="off"?!1:void 0)})]})}if(e.key==="otlp")return n(tr,{});let o=e.key==="phoenix";return n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(le,{path:t("project"),label:"Project",placeholder:"my-agent"}),n(le,{path:t("endpoint"),label:"Endpoint",placeholder:o?"http://localhost:6006":"https://api.smith.langchain.com/otel",help:o?"Base collector URL. /v1/traces is appended.":void 0})]})}function or({dest:e}){return n("div",{id:`dest-${e.key}`,class:"mt-4 rounded-xl border border-line bg-surface p-5",children:[n("div",{class:"flex items-start gap-3",children:[n("div",{class:"min-w-0 flex-1",children:[n("h3",{class:"text-sm font-semibold text-ink",children:e.name}),n("p",{class:"mt-0.5 text-[13px] text-muted",children:e.text})]}),n("button",{type:"button",class:Tt,"aria-label":`Remove ${e.name}`,onClick:()=>Zo(e.key),children:"Remove"})]}),n(nr,{dest:e}),n("p",{class:"mt-5 border-t border-line pt-3 text-[13px] text-muted",children:e.secret})]})}function Hn(){let[e,t]=K(!1),o=Ye.filter(s=>Xe(s.key)),r=Ye.filter(s=>!Xe(s.key)),i=$t();return n("div",{children:o.length===0?n("div",{class:"max-w-[520px]",children:[n("label",{for:"trace-dest",class:"mb-2 block text-sm font-semibold text-ink",children:"Send AI traces to"}),n(A,{id:"trace-dest",value:"",plain:!0,options:[["","None"],...r.map(s=>[s.key,s.label])],onChange:s=>{s&&Dn(s)}}),n("p",{class:te,children:"Graph runs are not traced."})]}):n(x,{children:[n("h2",{class:"eyebrow",children:"Send AI traces to"}),o.map(s=>n(or,{dest:s},s.key)),r.length?e?n("div",{class:"mt-4 max-w-[320px]",children:[n("label",{for:"trace-add",class:"sr-only",children:"Add another destination"}),n(A,{id:"trace-add",value:"",plain:!0,options:[["","Choose a destination"],...r.map(s=>[s.key,s.label])],onChange:s=>{t(!1),s&&Dn(s)}})]}):n("button",{type:"button",class:`${Tt} mt-3 inline-flex min-h-10 items-center gap-1.5`,onClick:()=>t(!0),children:[n(_,{name:"plus",class:"size-4"}),"Add another destination"]}):null,n("h2",{class:"eyebrow mt-8",children:"Trace settings"}),n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(St,{path:["observability","level"],label:"Level",options:Ko,value:i.level??"standard",onChange:s=>V(["observability","level"],s),help:Yo[i.level??"standard"]}),n(St,{path:["observability","user_id_mode"],label:"User id in traces",options:Xo,value:i.user_id_mode??"raw",onChange:s=>V(["observability","user_id_mode"],s),help:Qo[i.user_id_mode??"raw"]})]})]})})}var rr=[["","None"],["otlp","OTLP push"],["prometheus","Prometheus scrape"]],sr={"":"Metrics are not exported.",otlp:"Recommended for production. Works with several worker processes.",prometheus:"One process only. The API serves the scrape path, the worker its own port."},ir=[50,100,250,500,1e3,2500,5e3,1e4,3e4,6e4,12e4],Ke=()=>g(d.config.metrics)?d.config.metrics:{},zn=()=>Ke().enabled===!0&&["otlp","prometheus"].includes(Ke().export)?Ke().export:"";function ar(e){b(t=>{if(!e){delete t.config.metrics;return}let o=g(t.config.metrics)?t.config.metrics:{};o.enabled=!0,o.export=e,e==="otlp"&&!g(o.otlp)&&(o.otlp={endpoint:null}),e==="prometheus"&&!g(o.prometheus)&&(o.prometheus={path:"/metrics",worker_port:9464}),t.config.metrics=o})}function Fn(){let e=zn(),t=Ke().latency_buckets_ms,o=S("metrics.latency_buckets_ms");return n("div",{children:[n("div",{class:"max-w-[520px]",children:[n("label",{for:"f-metrics-export",class:"mb-2 block text-sm font-semibold text-ink",children:"Export metrics"}),n(A,{id:"f-metrics-export",plain:!0,options:rr,value:e,invalid:S("metrics.export")?.level==="error",onChange:ar}),n("p",{class:te,children:sr[e]}),n(O,{issue:S("metrics.export")})]}),e===""?null:n("div",{class:"mt-5 rounded-xl border border-line bg-surface px-5 pt-1 pb-5",children:[e==="otlp"?n(le,{path:["metrics","otlp","endpoint"],label:"Endpoint",placeholder:"http://collector:4318",help:n(x,{children:["/v1/metrics is appended.",n("br",{}),"Prometheus: ",n("code",{class:"font-mono text-xs",children:"http://prometheus:9090/api/v1/otlp"})," (start it with ",n("code",{class:"font-mono text-xs",children:"--web.enable-otlp-receiver"}),")."]})}):n(x,{children:n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(le,{path:["metrics","prometheus","path"],label:"Scrape path",placeholder:"/metrics",help:"Route on the API server that Prometheus scrapes."}),n(Vo,{path:["metrics","prometheus","worker_port"],label:"Worker port",help:"Each worker serves its own metrics on this port."})]})}),e==="otlp"?n("p",{class:"mt-5 border-t border-line pt-3 text-[13px] text-muted",children:["Auth headers: set ",n("code",{class:"font-mono text-xs",children:"OTEL_EXPORTER_OTLP_HEADERS"})," in .env."]}):null]}),e!==""?n(We,{title:"Advanced",class:"mt-4",open:!!o||t!==void 0,children:n("div",{class:"pt-4",children:[n("label",{for:"f-metrics-latency_buckets_ms",class:"text-sm font-semibold text-ink",children:"Latency buckets (ms)"}),n("div",{class:"mt-2 max-w-[520px]",children:n(H,{id:"f-metrics-latency_buckets_ms",values:(t??ir).map(String),placeholder:"250",onChange:r=>V(["metrics","latency_buckets_ms"],r.length?r.map(Number).filter(Number.isFinite):void 0)})}),n("p",{class:te,children:"Upper bounds of the latency histogram in milliseconds, ascending."}),n(O,{issue:o})]})}):null]})}var lr=e=>e.length>1?`${e.slice(0,-1).join(", ")} and ${e[e.length-1]}`:e[0];function Un(e){let t=Ye.filter(o=>Xe(o.key)).map(o=>o.name);return t.length?{text:`Level ${mt(["observability","level"])}, sending to ${lr(t)}.`}:{text:e("observability")}}function qn(e){let t={otlp:"OTLP push",prometheus:"Prometheus scrape"}[zn()];return t?{text:`${t}.`}:{text:e("metrics")}}var ke=({children:e,code:t})=>n("h1",{id:"page-title",tabindex:-1,class:"flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-semibold tracking-tight text-ink focus:outline-none",children:[e,t?n("code",{class:"rounded border border-line bg-raised px-1.5 font-mono text-xs font-normal text-faint",children:t}):null]}),Pt=({children:e})=>n("p",{class:"mt-1.5 max-w-[62ch] text-[15px] leading-6 text-muted",children:e}),Wn=({children:e})=>n("h2",{class:"mt-10 mb-2 eyebrow",children:e});function Gn({id:e,text:t,warn:o,compact:r}){let i=ge(e);return i?n("a",{href:`#${e}`,onClick:s=>{s.preventDefault(),D(e)},class:`focus-ring group flex items-center gap-3.5 border-t border-line py-2 last:border-b ${r?"min-h-12":"min-h-14"}`,children:[n(_,{name:Z[e]||"box",class:"size-[18px] text-faint"}),n("span",{class:`min-w-0 flex-1 ${r?"flex flex-wrap items-baseline gap-x-4":""}`,children:[n("span",{class:`block text-sm text-ink group-hover:text-brand ${r?"min-w-40 font-medium":"font-semibold"}`,children:i.title}),n("span",{class:`block text-[13px] leading-5 ${o?"text-warn":"text-muted"}`,children:t??i.description})]}),n(_,{name:"chevronRight",class:"size-4 text-faint"})]}):null}function Ot(e){return`Not set. ${P(e)?.off_summary||""}`.trim()}function cr(){let e=d.config.auth,t=e==="jwt"?"jwt":g(e)?e.method??"none":"none";if(t==="none")return{text:Ot("auth")};let o=g(e)&&g(e.roles)?Object.keys(e.roles).length:0;return{text:`${t==="jwt"?"JWT bearer tokens":`Custom backend${e.path?`: ${e.path}`:""}`}${o?`, ${o} ${o===1?"role":"roles"}`:""}.`}}function dr(){let e=g(d.config.cache_store)?d.config.cache_store:{},t=[`${e.type||"memory"} store`],o=e.rate_limit;if(g(o)&&o.enabled!==!1){let i={ip:"per IP",user:"per user",global:"across all clients"}[o.by??"ip"]||o.by;t.push(`${o.requests??100} requests per ${o.window??60} s, ${i}`)}let r=e.token_revocation;return(r===!0||g(r)&&r.enabled!==!1)&&t.push("token revocation on"),{text:`${t.join("; ")}.`}}function Bn(){let e=P("core"),t=_n(),o=e?re("core").filter(s=>!e.fields.some(a=>a.path.join(".")===s.path)):[],r=[["auth",cr()],["cache_store",dr()],["observability",Un(Ot)],["metrics",qn(Ot)]],i=d.nav.flatMap(s=>s.items).filter(s=>s!=="get-running");return n(x,{children:[n(ke,{children:t?"You're ready to run":"One step to run your agent"}),n(Pt,{children:q["get-running"].description}),n("div",{class:`mt-4 flex items-center gap-2 font-medium ${t?"text-ok":"text-muted"}`,children:[n(_,{name:t?"checkCircle":"circle",class:"size-[18px]"}),"Required: ",t?"1":"0"," of 1 done"]}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((s,a)=>n(vt,{issue:s},a))}):null,n("div",{class:"mt-2",children:e?kt(e.fields).flatMap(s=>s.fields).map(s=>n(I,{field:s},s.path.join("."))):null}),t?n("div",{class:"mt-7 border-t border-line pt-5",children:[n("div",{class:"flex items-center gap-2.5 font-medium text-ok",children:[n(_,{name:"checkCircle",class:"size-[18px]"}),"Ready for 10xgraph dev"]}),n("div",{class:"mt-3 flex max-w-[420px] items-center gap-2 rounded-lg border border-line-strong bg-sunken py-0 pr-1 pl-3.5",children:[n("code",{class:"flex-1 font-mono text-[13px] font-medium text-ink",children:"10xgraph dev"}),d.copied?n("span",{class:"text-xs text-ok",children:"Copied"}):null,n("button",{type:"button",class:"icon-btn",onClick:wn,"aria-label":d.copied?"Copied 10xgraph dev":"Copy command 10xgraph dev",children:n(_,{name:"copy",class:"size-[18px]"})})]}),n("p",{class:"mt-3 text-muted",children:"Everything else already has working defaults."})]}):n("p",{class:"mt-4 text-xs text-faint",children:"Add the agent graph above, then run 10xgraph dev."}),n(Wn,{children:"Before production (optional)"}),n("div",{children:r.map(([s,a])=>n(Gn,{id:s,text:a.text,warn:a.warn},s))}),n(Wn,{children:"What you can configure"}),n("div",{children:i.map(s=>n(Gn,{id:s,compact:!0},s))})]})}function Qe({vars:e}){return n("dl",{class:"mb-3 grid gap-x-4 gap-y-1 sm:grid-cols-[minmax(0,260px)_minmax(0,1fr)]",children:e.map(t=>[n("dt",{class:"py-0.5 font-mono text-xs font-medium break-all text-ink",children:t.name},`${t.name}-n`),n("dd",{class:"m-0 pb-2 text-[13px] text-muted sm:py-0.5",children:t.purpose},`${t.name}-p`)])})}function ur({vars:e,open:t}){return e?.length?n(We,{title:"Set in .env",open:t,class:"mt-9 border-t border-line pt-1",children:[n("p",{class:"mb-2 text-xs text-faint",children:"Read only. The editor never reads .env, so only names are shown."}),n(Qe,{vars:e})]}):null}function Vn({sec:e}){return e.widget==="auth"?n(jn,{sec:e}):e.widget==="remote_tools"?n(Ln,{}):e.widget==="observability"?n(Hn,{}):e.widget==="metrics"?n(Fn,{}):e.widget==="cache_store"?n(Mn,{sec:e}):e.widget==="id_generator"||e.widget==="thread_name_generator"?n(Nn,{sec:e}):null}function pr(e){let t=new Set([...e.fields.map(i=>i.path.join(".")),...Object.keys(ut)]),o=new Set(["cache_store.rate_limit","cache_store.token_revocation"]),r=i=>i.startsWith("auth.roles.")||i.startsWith("auth.system_identities.");return re(e.id).filter(i=>!t.has(i.path)&&!(e.id==="cache_store"&&o.has(i.path))&&!(e.id==="auth"&&r(i.path)))}function mr({sec:e}){let t=Le(e),o=e.widget&&(t||!e.optional||e.self_managed);return n(x,{children:[o?n("div",{class:"mt-6",children:n(Vn,{sec:e})}):null,n("div",{class:e.optional&&!t?"opacity-80":"",children:kt(e.fields.filter(r=>!r.managed)).map((r,i)=>n("div",{children:[r.name?n("h3",{class:`eyebrow ${i===0?"mt-6":"mt-8 border-t border-line pt-4"}`,children:r.name}):null,r.fields.map(s=>n(I,{field:s},s.path.join(".")))]},r.name))})]})}function fr({sec:e}){let t=Le(e),o=pr(e);return n(x,{children:[n(ke,{code:e.key,children:e.title}),n(Pt,{children:e.description}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((r,i)=>n(vt,{issue:r},i))}):null,e.optional&&!e.self_managed?n("div",{class:"mt-5 flex flex-wrap items-center gap-4",children:[n(ee,{label:`Settings for ${e.title}`,value:t?"c":"d",options:[["d","Use defaults"],["c","Customize"]],onPick:r=>yn(e,r==="c")}),n("p",{class:"m-0 min-w-56 flex-1 text-[13px] text-muted",children:t?`Saved in 10xgraph.json under ${e.key}.`:e.off_summary})]}):null,e.notes?.length?n("div",{class:"mt-5",children:n(ve,{kind:"info",lines:e.notes})}):null,n(mr,{sec:e}),n(ur,{vars:e.env})]})}function hr(){let e=d.schema.environment||[];return n(x,{children:[n(ke,{children:q.environment.title}),n(Pt,{children:"These variables are set in .env or your deployment environment. The editor never reads that file, so only names and purposes are shown."}),e.map(t=>n("section",{children:[n("h2",{class:"mt-8 mb-2 text-sm font-semibold text-ink",children:t.title}),t.note?n("p",{class:"mb-2 text-xs text-faint",children:t.note}):null,n(Qe,{vars:t.vars})]},t.title))]})}var Jn=e=>String(e??"").toLowerCase();function _r(e){let t=i=>e.every(s=>Jn(i).includes(s)),o=[];for(let i of d.sections){let s=t(`${i.title} ${i.description} ${i.key||""}`),a=p=>s||t(`${p.label} ${p.help||""} ${p.path.join(".")}`),u=i.fields.filter(p=>!p.managed&&a(p)),m=(i.env||[]).filter(p=>t(`${p.name} ${p.purpose}`)),l=!!i.widget&&(s||i.fields.some(p=>p.managed&&a(p)));(u.length||m.length||l)&&o.push({id:i.id,sec:i,fields:u,env:m,widget:l})}let r=(d.schema.environment||[]).map(i=>({title:i.title,vars:i.vars.filter(s=>t(`${s.name} ${s.purpose} ${i.title}`))})).filter(i=>i.vars.length);return{results:o,envGroups:r}}function gr(){let e=Jn(d.query).split(/\s+/).filter(Boolean),{results:t,envGroups:o}=_r(e),r=t.reduce((i,s)=>i+s.fields.length+s.env.length+(s.widget?1:0),0)+o.reduce((i,s)=>i+s.vars.length,0);return r?n(x,{children:[n(ke,{code:d.query,children:[r," ",r===1?"setting matches":"settings match"]}),t.map(({id:i,sec:s,fields:a,env:u,widget:m})=>n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:Z[i]||"box",class:"size-4"}),s.title,n("a",{href:`#${i}`,onClick:l=>{l.preventDefault(),D(i)},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),m?n("div",{class:"mt-4",children:n(Vn,{sec:s})}):null,a.map(l=>n(I,{field:l},l.path.join("."))),u.length?n("details",{class:"group mt-5",open:!0,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform group-open:rotate-90"}),"Set in .env"]}),n(Qe,{vars:u})]}):null]},i)),o.length?n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:"terminal",class:"size-4"}),q.environment.title,n("a",{href:"#environment",onClick:i=>{i.preventDefault(),D("environment")},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),o.map(i=>n("div",{children:[n("h3",{class:"mt-4 mb-1.5 text-sm font-semibold text-ink",children:i.title}),i.note?n("p",{class:"mb-1.5 text-xs text-faint",children:i.note}):null,n(Qe,{vars:i.vars})]},i.title))]}):null]}):n(x,{children:[n(ke,{children:"No matches"}),n("p",{class:"mt-8 text-muted",children:['Nothing matches "',d.query,'". Try a shorter word, or press Esc to go back.']})]})}function Kn(){if(d.query.trim())return n(gr,{});if(d.page==="get-running")return n(Bn,{});if(d.page==="environment")return n(hr,{});let e=P(d.page);return e?n(fr,{sec:e}):n(Bn,{})}function br(){let e=d.path.split(/[\\/]/).pop()||"10xgraph.json",t=d.path.slice(0,d.path.length-e.length),o=null;return d.schema?d.exists?W()?o=n(ie,{class:"bg-warn-soft text-warn",children:"Unsaved"}):o=n(ie,{class:"bg-ok-soft text-ok",children:"Saved"}):o=n(ie,{class:"bg-brand-soft text-brand",children:"New"}):o=null,n("div",{class:"flex min-w-0 flex-1 items-center gap-2",title:d.path||void 0,children:[n(_,{name:"file",class:"size-4 text-faint"}),n("span",{class:"truncate font-mono text-[13px] font-medium text-ink",children:e}),o,t?n("span",{class:"hidden min-w-0 truncate font-mono text-xs text-faint lg:block",dir:"rtl",children:n("bdi",{children:t})}):null]})}function Yn(){let e=!!d.schema&&!d.fatal,t=d.sideOpen?"Hide sidebar":"Show sidebar";return n("header",{class:"sticky top-0 z-40 flex h-14 items-center gap-2 border-b border-line bg-surface px-3 sm:gap-3 sm:px-4",children:[n("button",{type:"button",class:"icon-btn","aria-controls":"side","aria-expanded":d.sideOpen?"true":"false","aria-label":t,title:`${t} (Ctrl/Cmd+B)`,onClick:()=>se(!d.sideOpen),children:n(_,{name:"panel",class:"size-[18px]"})}),n("div",{class:"flex shrink-0 items-center gap-2.5",children:[n("svg",{viewBox:"0 0 48 48",class:"size-7 shrink-0 text-ink","aria-hidden":"true",children:[n("rect",{x:"4",y:"4",width:"40",height:"40",rx:"11",fill:"none",stroke:"currentColor","stroke-width":"4.4"}),n("path",{d:"M13 33L21 23L34 15L34 31L21 23",fill:"none",stroke:"currentColor","stroke-width":"3","stroke-linecap":"round","stroke-linejoin":"round"}),n("g",{fill:"currentColor",children:[n("circle",{cx:"21",cy:"23",r:"4"}),n("circle",{cx:"34",cy:"15",r:"4"}),n("circle",{cx:"34",cy:"31",r:"4"})]}),n("circle",{cx:"13",cy:"33",r:"4.6",class:"fill-brand-amber"})]}),n("div",{class:"hidden items-baseline gap-1.5 sm:flex",children:[n("span",{class:"text-[15px] font-semibold tracking-tight text-ink",children:"10xGraph"}),n("span",{class:"text-[15px] text-faint",children:"Config"})]})]}),n("span",{class:"hidden h-6 w-px shrink-0 bg-line md:block","aria-hidden":"true"}),n(br,{}),n("button",{type:"button",class:"btn btn-ghost","aria-label":"View 10xgraph.json","aria-haspopup":"dialog",disabled:!e,onClick:()=>kn(),children:[n(_,{name:"code",class:"size-[18px]"}),n("span",{class:"hidden md:inline",children:"View 10xgraph.json"})]}),n("button",{type:"button",class:"btn btn-primary",onClick:He,disabled:!e||!!d.busy||d.exists&&!W()&&!d.parseError,children:[d.busy==="save"?n(_,{name:"spinner",class:"size-4 animate-spin"}):null,d.exists?"Save":"Create file",n("span",{class:"kbd hidden lg:inline-flex",children:"Ctrl S"})]})]})}function Xn(){return d.fatal?n("div",{class:"panel mt-10",children:n($n,{icon:"xCircle",tint:"text-danger",title:d.fatal.title,text:d.fatal.text})}):d.schema?n(x,{children:[d.parseError?n("div",{class:"mb-6",children:n(ve,{kind:"warn",title:"The existing file is not valid JSON",lines:[`${d.parseError}. The form starts from defaults; saving replaces the file and keeps the old one as 10xgraph.json.bak.`]})}):null,n(Kn,{})]}):n("div",{class:"animate-pulse space-y-4 pt-2","aria-label":"Loading",children:[n("div",{class:"h-7 w-48 rounded-md bg-raised"}),n("div",{class:"h-4 w-80 max-w-full rounded-md bg-raised"}),[0,1,2].map(e=>n("div",{class:"h-16 rounded-xl bg-raised"},e))]})}function xr({id:e}){let t=!!P(e)&&pn(P(e)),o=mn(e);return!t&&!o?null:n("span",{class:"flex shrink-0 gap-1",children:[t?n("span",{class:"size-2 rounded-full bg-brand",title:P(e).self_managed?"Configured":"Changed from default"}):null,o?n("span",{class:`size-2 rounded-full ${o.level==="error"?"bg-danger":"bg-warn"}`,title:o.level==="error"?"Needs attention":"Has a warning"}):null]})}function vr({id:e}){let t=ge(e);if(!t)return null;let o=!d.query&&d.page===e;return n("a",{href:`#${e}`,"aria-current":o?"page":void 0,onClick:r=>{r.preventDefault(),D(e)},ref:r=>{r&&o&&window.innerWidth<1024&&r.scrollIntoView({block:"nearest",inline:"nearest"})},class:`focus-ring flex min-h-10 items-center gap-2.5 rounded-lg px-3 py-1.5 font-medium transition-colors duration-150 max-lg:min-h-11 max-lg:shrink-0 max-lg:whitespace-nowrap ${o?"bg-brand-soft text-ink":"text-muted hover:bg-raised hover:text-ink"}`,children:[n(_,{name:Z[e]||"box",class:`size-[18px] ${o?"text-brand":""}`}),n("span",{class:"min-w-0 flex-1",children:[n("span",{class:"block truncate",children:t.title}),t.subtitle?n("span",{class:"block truncate text-xs font-normal text-faint max-lg:hidden",children:t.subtitle}):null]}),n(xr,{id:e})]})}function Qn(){return d.sideRender?n("aside",{id:"side","aria-label":"Settings navigation",class:`contents lg:sticky lg:top-14 lg:block lg:h-[calc(100dvh-3.5rem)] lg:overflow-y-auto lg:border-r lg:border-line lg:bg-surface lg:px-3 lg:pt-4 lg:pb-16 lg:transition-[transform,opacity] lg:duration-200 lg:ease-out ${d.sideShown?"lg:translate-x-0 lg:opacity-100":"lg:-translate-x-6 lg:opacity-0"}`,inert:!d.sideOpen,children:[n("div",{role:"search",class:"relative mx-1 mb-3 max-lg:mx-4 max-lg:mt-3",children:[n(_,{name:"search",class:"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-faint"}),n("label",{for:"q",class:"sr-only",children:"Search settings"}),n("input",{id:"q",type:"search",value:d.query,autocomplete:"off",spellcheck:!1,placeholder:"Search settings ( / )",class:"input pl-10",onInput:e=>Fe(e.currentTarget.value)})]}),n("nav",{"aria-label":"Pages",class:"max-lg:sticky max-lg:top-14 max-lg:z-20 max-lg:mt-3 max-lg:flex max-lg:gap-0.5 max-lg:overflow-x-auto max-lg:border-b max-lg:border-line max-lg:bg-surface max-lg:px-3 max-lg:py-1.5",children:d.nav.map(e=>n("div",{class:"max-lg:contents",children:[e.label?n("div",{class:"mt-4 mb-1 px-3 text-[11px] font-semibold tracking-wider text-faint uppercase max-lg:hidden",children:e.label}):null,e.items.map(t=>n(vr,{id:t},t))]},e.label||"top"))})]}):null}var yr={success:["checkCircle","text-ok"],warning:["alert","text-warn"],error:["xCircle","text-danger"]};function kr(){return n("div",{class:"pointer-events-none fixed right-4 bottom-4 left-4 z-50 flex flex-col items-end gap-2 sm:left-auto","aria-live":"polite",children:d.toasts.map(e=>{let[t,o]=yr[e.kind];return n("div",{role:e.kind==="error"?"alert":"status",class:"pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-xl border border-line bg-surface px-4 py-3 text-sm shadow-pop",children:[n(_,{name:t,class:`mt-0.5 size-4 ${o}`}),n("p",{class:"min-w-0 flex-1 leading-5 text-body",children:e.message}),e.action?n("button",{type:"button",class:"shrink-0 font-medium text-brand hover:underline",onClick:()=>{qe(e.id),e.action.run()},children:e.action.label}):null,n("button",{type:"button",class:"-mr-1 shrink-0 cursor-pointer rounded text-faint hover:text-ink","aria-label":"Dismiss",onClick:()=>qe(e.id),children:n(_,{name:"x"})})]},e.id)})})}var wr=()=>/^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName)||document.activeElement?.isContentEditable;function Cr(){let[,e]=dt(o=>o+1,0);U(()=>{dn(e);try{localStorage.getItem("tg-sidebar")==="hidden"&&se(!1,{instant:!0})}catch{}let o=s=>{let a=s.ctrlKey||s.metaKey,u=s.key.toLowerCase();a&&u==="s"?(s.preventDefault(),He()):a&&u==="b"?(s.preventDefault(),se(!d.sideOpen)):a&&u==="k"?(s.preventDefault(),gt()):s.key==="/"&&!a&&!s.altKey&&!wr()&&!d.drawerOpen?(s.preventDefault(),gt()):s.key==="Escape"&&d.query&&!d.drawerOpen&&Fe("")},r=s=>{d.schema&&W()&&s.preventDefault()},i=()=>ze();return document.addEventListener("keydown",o),window.addEventListener("beforeunload",r),window.addEventListener("hashchange",i),xn(),()=>{document.removeEventListener("keydown",o),window.removeEventListener("beforeunload",r),window.removeEventListener("hashchange",i)}},[]),U(()=>{if(!d.schema||d.validated===N())return;let o=setTimeout(vn,400);return()=>clearTimeout(o)});let t=d.sideRender;return n(x,{children:[n("a",{href:"#main",class:"sr-only z-50 rounded-md bg-brand-solid px-3 py-2 text-white focus:not-sr-only focus:fixed focus:top-3 focus:left-3",children:"Skip to content"}),n(Yn,{}),n("div",{class:t?"lg:grid lg:grid-cols-[260px_minmax(0,1fr)] lg:items-start":"",children:[n(Qn,{}),n("main",{id:"main",tabindex:-1,class:"min-w-0 px-4 pt-6 pb-24 focus:outline-none sm:px-8 sm:pt-8",children:n("div",{class:"mx-auto w-full max-w-[760px]",children:n(Xn,{})})})]}),n(Tn,{}),n("div",{class:"sr-only",role:"status","aria-live":"polite",children:d.copied?"Copied":""}),n(kr,{})]})}Vt(n(Cr,{}),document.getElementById("app")); diff --git a/tenxgraph_api/cli/config_editor/validation.py b/tenxgraph_api/cli/config_editor/validation.py index b77e0f0f..d5cc2869 100644 --- a/tenxgraph_api/cli/config_editor/validation.py +++ b/tenxgraph_api/cli/config_editor/validation.py @@ -30,6 +30,7 @@ _ROLE_IN_MESSAGE = re.compile(r"role '([^']+)'") _OBSERVABILITY_LEVELS = ("spans", "standard", "full") _USER_ID_MODES = ("raw", "hash", "mask") +_TRACE_DESTINATIONS = ("logfire", "langsmith", "phoenix", "otlp") _MAX_PERCENT = 100 # Settings that live inside cache_store; at the top level the server refuses them. _CACHE_STORE_SETTINGS = ("redis", "rate_limit", "token_revocation") @@ -79,6 +80,7 @@ def validate_config(config: Any, base_dir: Path | None = None) -> list[Issue]: _check_ag_ui(config, out) _check_media(config, out) _check_observability(config, out) + _check_metrics(config, out) _check_wiring(config, out, base_dir) _check_remote_tools(config, out) _check_test(config, out) @@ -301,8 +303,8 @@ def _check_observability(config: dict, out: _Collector) -> None: "observability.user_id_mode", f"user_id_mode must be one of {', '.join(_USER_ID_MODES)}.", ) - any_enabled = False - for backend in ("logfire", "langsmith", "phoenix"): + enabled_blocks: dict[str, dict] = {} + for backend in _TRACE_DESTINATIONS: block = data.get(backend) if block is None: continue @@ -312,7 +314,8 @@ def _check_observability(config: dict, out: _Collector) -> None: enabled = block.get("enabled", False) if not isinstance(enabled, bool): out.error(f"observability.{backend}.enabled", "enabled must be true or false.") - any_enabled = any_enabled or enabled is True + if enabled is True: + enabled_blocks[backend] = block console = block.get("console") if backend == "logfire" and console is not None and console is not False: out.error( @@ -320,13 +323,73 @@ def _check_observability(config: dict, out: _Collector) -> None: "console must be false or left out. Logfire takes False or a ConsoleOptions " "object, which JSON cannot express.", ) - if not any_enabled: + if "otlp" in enabled_blocks: + _check_otlp_tracing(config, enabled_blocks["otlp"], out) + if not enabled_blocks: out.warning( "observability", - "None of Logfire, LangSmith or Phoenix is enabled, so nothing is traced.", + "None of Logfire, LangSmith, Phoenix or OTLP is enabled, so nothing is traced.", ) +def _check_otlp_tracing(config: dict, otlp: dict, out: _Collector) -> None: + shared = otlp.get("use_metrics_endpoint") + if shared is not None and not isinstance(shared, bool): + out.error( + "observability.otlp.use_metrics_endpoint", "use_metrics_endpoint must be true or false." + ) + return + endpoint = otlp.get("endpoint") + if shared: + metrics = config.get("metrics") + metrics = metrics if isinstance(metrics, dict) else {} + otlp_cfg = metrics.get("otlp") + url = otlp_cfg.get("endpoint") if isinstance(otlp_cfg, dict) else None + if not (metrics.get("enabled") is True and metrics.get("export") == "otlp" and url): + out.warning( + "observability.otlp.use_metrics_endpoint", + "Metrics are not set to OTLP push with an endpoint, so there is no endpoint to " + "share and the OTLP destination stays off.", + ) + return + endpoint = url + elif not endpoint: + if not os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"): + out.error( + "observability.otlp.endpoint", + "OTLP needs an endpoint, or turn on Use the metrics endpoint.", + ) + return + if isinstance(endpoint, str) and "/api/v1/otlp" in endpoint: + out.warning( + "observability.otlp.use_metrics_endpoint" if shared else "observability.otlp.endpoint", + "This is a Prometheus OTLP URL. Prometheus takes metrics only, so trace exports " + "will fail. Use a collector (OTel Collector, Grafana Alloy) that takes both.", + ) + + +def _check_metrics(config: dict, out: _Collector) -> None: + data = config.get("metrics") + if data is None: + return + if not isinstance(data, dict): + out.error("metrics", "metrics must be an object.") + return + from pydantic import ValidationError + + from tenxgraph_api.src.app.core.config.metrics import MetricsConfig + + try: + MetricsConfig.model_validate(data) + except ValidationError as exc: + for err in exc.errors(): + message = err["msg"].removeprefix("Value error, ") + path = ".".join(["metrics", *(str(part) for part in err["loc"])]) + # A rule on the whole model names its field in the message. + named = re.match(r"(metrics\.[\w.]+) ", message) + out.error(named.group(1) if named else path, message) + + def _check_wiring(config: dict, out: _Collector, base_dir: Path | None) -> None: for key in ("injectq", "store", "checkpointer"): value = config.get(key) diff --git a/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md b/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md index 3de7574f..898b5fa1 100644 --- a/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +++ b/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md @@ -81,6 +81,14 @@ ACP support is in `tenxgraph/runtime/protocols/acp.py`. Treat it as a runtime pr - In Python: `setup_phoenix(graph, project=..., endpoint=..., api_key=...)` or `PhoenixPublisher(...)`, both from `tenxgraph.runtime.publisher`, used like `setup_langsmith` / `LangsmithPublisher`. Call before `graph.compile()`. - `PHOENIX_API_KEY` is env only and optional (a local Phoenix needs none). Never put it in the config file. +## OTLP tracing and the metrics block + +- AI traces to any OTLP HTTP receiver (collector, Tempo, Jaeger, Grafana Cloud; not Prometheus): install `pip install '10xgraph[otlp]'`, config `"observability": {"otlp": {"enabled": true, "endpoint": "http://collector:4318"}}`. `/v1/traces` is appended. With a null endpoint, env `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` is required (`OTEL_EXPORTER_OTLP_ENDPOINT` is not used). In Python: `setup_otlp(graph, endpoint=...)` before `graph.compile()`. Auth headers: env `OTEL_EXPORTER_OTLP_HEADERS` only. +- `observability.otlp.use_metrics_endpoint: true` reuses `metrics.otlp.endpoint` (needs metrics on with `export: "otlp"`). The URL must take traces; a Prometheus `/api/v1/otlp` URL does not. +- HTTP request spans never go to AI tracing; the server turns FastAPI's own telemetry off. +- Metrics are separate from tracing: `"metrics": {"enabled": true, "export": "otlp" | "prometheus", "otlp": {"endpoint": ...}, "prometheus": {"path": "/metrics", "worker_port": 9464}, "latency_buckets_ms": [...]}`. Needs `pip install '10xgraph-api[metrics]'`. Prometheus scrape covers one process (API route, worker port); with several gunicorn or TaskIQ processes use OTLP push. The scrape route has no auth, keep it on a private network. +- Core metrics include `tenxgraph.llm.duration` (provider, model, status) and `tenxgraph.llm.tokens` (provider, model, type); the task metrics are `tenxgraph.task.runs` and `tenxgraph.task.duration`. + ## Rules - Prefer publisher events for observability instead of ad hoc print/log statements in reusable runtime paths. diff --git a/tenxgraph_api/jobs/monitoring.py b/tenxgraph_api/jobs/monitoring.py index 22f08501..ff54beec 100644 --- a/tenxgraph_api/jobs/monitoring.py +++ b/tenxgraph_api/jobs/monitoring.py @@ -1,8 +1,8 @@ """``MonitoringMiddleware``: log every TaskIQ hook and send the same as core publisher events when a ``BasePublisher`` is bound in the container. It also records two metrics through core's -metrics facade (exported when OTEL is enabled): ``tenxgraph.task.runs`` (attributes ``task``, -``status`` = queued, success, failure, cancelled) and ``tenxgraph.task.duration_ms`` (attribute -``task``). Never raises.""" +metrics facade (exported by the ``metrics`` block of 10xgraph.json, or the legacy OTEL path): +``tenxgraph.task.runs`` (attributes ``task``, ``status`` = queued, success, failure, cancelled) +and ``tenxgraph.task.duration`` (attribute ``task``). Never raises.""" from __future__ import annotations @@ -19,7 +19,7 @@ logger = logging.getLogger("tenxgraph_api.jobs") MESSAGE_LIMIT = 500 RUNS_METRIC = "tenxgraph.task.runs" -DURATION_METRIC = "tenxgraph.task.duration_ms" +DURATION_METRIC = "tenxgraph.task.duration" class MonitoringMiddleware(TaskiqMiddleware): diff --git a/tenxgraph_api/jobs/worker.py b/tenxgraph_api/jobs/worker.py index 087ee6f6..87d705d7 100644 --- a/tenxgraph_api/jobs/worker.py +++ b/tenxgraph_api/jobs/worker.py @@ -50,9 +50,11 @@ async def worker_startup(state: TaskiqState) -> None: The ``taskiq worker`` CLI has imported the task modules before this event. """ + from tenxgraph_api.src.app.core.config.metrics import setup_metrics from tenxgraph_api.src.app.core.config.settings import get_settings from tenxgraph_api.src.app.core.config.setup_middleware import ( _attach_otel_publisher, + _setup_observability, setup_otel_provider, ) from tenxgraph_api.src.app.loader import attach_all_modules, load_container @@ -63,9 +65,15 @@ async def worker_startup(state: TaskiqState) -> None: container = load_container(graph_config.injectq_path) or InjectQ.get_instance() container.bind_instance(GraphConfig, graph_config) settings = get_settings() - if settings.OTEL_ENABLED: # same traces as the API; before the graph freezes BasePublisher - setup_otel_provider(settings) + metrics_cfg = graph_config.metrics + state.meter_provider = ( + setup_metrics(metrics_cfg, role="worker") if metrics_cfg.enabled else None + ) + # Same traces as the API; before the graph freezes BasePublisher. + if settings.OTEL_ENABLED: + setup_otel_provider(settings, metrics_owned=state.meter_provider is not None) _attach_otel_publisher(container, settings, graph_config) + _setup_observability(container, graph_config) state.container = container state.graph = await attach_all_modules(graph_config, container) if settings.STARTUP_CHECKS: @@ -75,8 +83,10 @@ async def worker_startup(state: TaskiqState) -> None: async def worker_shutdown(state: TaskiqState) -> None: - """Close the graph (and with it the checkpointer and store), then the cache store.""" + """Close the graph (and with it the checkpointer and store), then the cache store, then + send the last metrics and AI spans.""" from tenxgraph_api.src.app.core.cache_store import BaseCacheStore + from tenxgraph_api.src.app.core.config.metrics import flush_telemetry from tenxgraph_api.src.app.core.config.settings import get_settings container = getattr(state, "container", None) or InjectQ.get_instance() @@ -97,6 +107,8 @@ async def worker_shutdown(state: TaskiqState) -> None: await cache_store.aclose() except Exception as exc: logger.warning("Cache store close failed: %s", exc) + # Last: taskiq bounds this hook (--shutdown-timeout), so a slow collector cuts only this. + await flush_telemetry(getattr(state, "meter_provider", None)) if worker_config is not None: diff --git a/tenxgraph_api/src/app/core/config/graph_config.py b/tenxgraph_api/src/app/core/config/graph_config.py index 90ce9da1..a2ff8b91 100644 --- a/tenxgraph_api/src/app/core/config/graph_config.py +++ b/tenxgraph_api/src/app/core/config/graph_config.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: from tenxgraph_api.jobs.config import WorkerConfig from tenxgraph_api.src.app.core.auth.auth_config import AuthConfig + from tenxgraph_api.src.app.core.config.metrics import MetricsConfig logger = logging.getLogger("tenxgraph_api") @@ -976,6 +977,13 @@ def ag_ui(self) -> AgUiConfig: data = self.data.get("ag_ui", None) return AgUiConfig.from_dict({} if data is None else data) + @property + def metrics(self) -> "MetricsConfig": + """The ``metrics`` block (OTLP push or Prometheus scrape); off when absent.""" + from tenxgraph_api.src.app.core.config.metrics import MetricsConfig + + return MetricsConfig.model_validate(self.data.get("metrics") or {}) + @property def media(self) -> MediaConfig: """Uploaded-file lifecycle settings (``media`` key); defaults when absent.""" diff --git a/tenxgraph_api/src/app/core/config/metrics.py b/tenxgraph_api/src/app/core/config/metrics.py new file mode 100644 index 00000000..740c3e78 --- /dev/null +++ b/tenxgraph_api/src/app/core/config/metrics.py @@ -0,0 +1,277 @@ +"""The ``metrics`` block of 10xgraph.json and the MeterProvider it sets up (API and worker). + +Metrics (request rate, latency, task throughput, LLM latency) are separate from AI tracing +(``observability``): they never carry spans, and HTTP spans never reach an AI tracing backend. +""" + +import asyncio +import inspect +import os +import socket +import time +from typing import Literal + +from pydantic import BaseModel, ConfigDict, Field, model_validator + +from .settings import get_settings, logger + + +DEFAULT_BUCKETS_MS = [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000, 120000] +METRICS_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" +INSTALL_HINT = "pip install '10xgraph-api[metrics]'" +# Bound for the final metrics push and AI span flush at shutdown (each, run at the same time). +TELEMETRY_FLUSH_SECONDS = 5.0 + + +class _Section(BaseModel): + model_config = ConfigDict(extra="forbid") + + +class MetricsOtlpConfig(_Section): + """``metrics.otlp``: base URL of an OTLP HTTP receiver; ``/v1/metrics`` is appended.""" + + endpoint: str | None = None + + +class MetricsPrometheusConfig(_Section): + """``metrics.prometheus``: ``path`` is the API scrape route, ``worker_port`` the worker's.""" + + path: str = Field(default="/metrics", pattern=r"^/") + worker_port: int = Field(default=9464, ge=1, le=65535) + + +class MetricsConfig(_Section): + """``metrics`` in 10xgraph.json. Example:: + + "metrics": { + "enabled": true, + "export": "otlp", + "otlp": {"endpoint": "http://collector:4318"}, + "prometheus": {"path": "/metrics", "worker_port": 9464}, + "latency_buckets_ms": [50, 100, 250, 500, 1000, 2500, 5000] + } + + Auth headers for OTLP come from the standard ``OTEL_EXPORTER_OTLP_HEADERS`` env var. + """ + + enabled: bool = False + export: Literal["otlp", "prometheus"] | None = None + otlp: MetricsOtlpConfig = Field(default_factory=MetricsOtlpConfig) + prometheus: MetricsPrometheusConfig = Field(default_factory=MetricsPrometheusConfig) + latency_buckets_ms: list[float] = Field(default_factory=lambda: list(DEFAULT_BUCKETS_MS)) + + @model_validator(mode="after") + def _check(self) -> "MetricsConfig": + buckets = self.latency_buckets_ms + if not buckets or buckets[0] <= 0 or buckets != sorted(set(buckets)): + raise ValueError("metrics.latency_buckets_ms must be positive and strictly ascending") + if self.enabled: + if self.export is None: + raise ValueError('metrics.export is required when enabled: "otlp" or "prometheus"') + if self.export == "otlp" and not ( + self.otlp.endpoint or os.environ.get(METRICS_ENDPOINT_ENV) + ): + raise ValueError( + "metrics.otlp.endpoint is required for export=otlp " + f"(or set {METRICS_ENDPOINT_ENV})" + ) + return self + + @property + def scrape_path(self) -> str | None: + """The API route Prometheus scrapes; None unless metrics are on with export=prometheus.""" + return self.prometheus.path if self.enabled and self.export == "prometheus" else None + + +def _otlp_metrics_url(endpoint: str) -> str: + base = endpoint.rstrip("/") + return base if base.endswith("/v1/metrics") else base + "/v1/metrics" + + +def resolve_use_metrics_endpoint(obs_cfg: dict, metrics_cfg: MetricsConfig) -> dict: + """Copy ``obs_cfg`` with ``otlp.endpoint`` taken from ``metrics.otlp.endpoint`` when + ``observability.otlp.use_metrics_endpoint`` is on. Without a usable metrics endpoint the + otlp destination is turned off in the copy (with a warning) instead of crashing.""" + otlp = obs_cfg.get("otlp") or {} + if not otlp.get("enabled") or not otlp.get("use_metrics_endpoint"): + return obs_cfg + otlp = dict(otlp) + endpoint = ( + metrics_cfg.otlp.endpoint if metrics_cfg.enabled and metrics_cfg.export == "otlp" else None + ) + if endpoint: + # Core appends /v1/traces to the base URL. + base = endpoint.rstrip("/") + otlp["endpoint"] = base.removesuffix("/v1/metrics") + if "/api/v1/otlp" in base: + logger.warning( + "observability.otlp.use_metrics_endpoint points AI traces at a Prometheus OTLP " + "URL (%s); Prometheus accepts metrics only, so trace exports will fail. Use an " + "endpoint that takes both (OTel Collector, Grafana Alloy) or a separate one.", + endpoint, + ) + else: + logger.warning( + "observability.otlp.use_metrics_endpoint needs metrics enabled with export=otlp and " + "metrics.otlp.endpoint set; the otlp trace destination is off." + ) + otlp["enabled"] = False + return {**obs_cfg, "otlp": otlp} + + +def setup_metrics(metrics_cfg: MetricsConfig, *, role: Literal["api", "worker"]): + """Install the global MeterProvider and bridge core's counters and timers to it. + + Returns the provider, or None when the OpenTelemetry packages are missing or the setup + fails (never raises). + """ + # Before any instrumentation: the stable HTTP names (http.server.request.duration, in s). + os.environ.setdefault("OTEL_SEMCONV_STABILITY_OPT_IN", "http") + try: + from opentelemetry import metrics + from tenxgraph.utils.metrics import setup_otel_metrics + + provider = _build_provider(metrics_cfg) + except ImportError: + logger.warning("Metrics are not exported: packages are missing. Install: %s", INSTALL_HINT) + return None + except Exception as exc: + logger.warning("Metrics are not exported: setup failed: %s", exc) + return None + + metrics.set_meter_provider(provider) + if metrics.get_meter_provider() is not provider: + # Set earlier by opentelemetry-instrument, logfire.configure() or an imported module. + logger.warning( + "Another global MeterProvider was installed before the metrics block; 10xgraph " + "and HTTP metrics still use the metrics block, other libraries use the other one." + ) + # The provider itself, not the global one, so a refused set_meter_provider loses nothing. + setup_otel_metrics(provider.get_meter("10xgraph")) + + if metrics_cfg.export == "prometheus": + _announce_scrape(metrics_cfg, role) + logger.info("Metrics enabled (export=%s, role=%s)", metrics_cfg.export, role) + return provider + + +def _build_provider(metrics_cfg: MetricsConfig): + from opentelemetry.sdk.metrics import Histogram, MeterProvider + from opentelemetry.sdk.metrics.view import ExplicitBucketHistogramAggregation, View + from opentelemetry.sdk.resources import Resource + + if metrics_cfg.export == "otlp": + from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter + from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader + + endpoint = metrics_cfg.otlp.endpoint + exporter = OTLPMetricExporter(endpoint=_otlp_metrics_url(endpoint) if endpoint else None) + reader = PeriodicExportingMetricReader(exporter) + else: + from opentelemetry.exporter.prometheus import PrometheusMetricReader + + reader = PrometheusMetricReader() + + buckets_ms = metrics_cfg.latency_buckets_ms + views = [ + View( + instrument_type=Histogram, + instrument_unit=unit, + aggregation=ExplicitBucketHistogramAggregation([b / divisor for b in buckets_ms]), + ) + for unit, divisor in (("ms", 1), ("s", 1000)) + ] + resource = Resource.create( + { + "service.name": get_settings().OTEL_SERVICE_NAME, + # One per process, so pushes from several processes do not overwrite each other. + "service.instance.id": f"{socket.gethostname()}-{os.getpid()}", + } + ) + return MeterProvider(resource=resource, metric_readers=[reader], views=views) + + +def _announce_scrape(metrics_cfg: MetricsConfig, role: str) -> None: + if role == "api": + logger.info( + "Prometheus scrape at %s covers this process only; with several gunicorn " + "workers use OTLP push instead.", + metrics_cfg.prometheus.path, + ) + return + port = metrics_cfg.prometheus.worker_port + try: + from prometheus_client import start_http_server + + start_http_server(port) + except OSError as exc: + # TaskIQ starts several worker processes by default; only the first gets the port. + logger.warning( + "Metrics port %d is not available (%s): this worker process is not scraped. " + "Use OTLP push or run the worker with --workers 1.", + port, + exc, + ) + + +async def flush_telemetry(meter_provider) -> None: + """Last step of the API and worker shutdown: the final metrics push (MeterProvider + shutdown) and a flush of the AI tracing provider (force_flush only, so a provider owned by + Logfire or another library stays usable). Both run in threads at the same time, each + bounded by ``TELEMETRY_FLUSH_SECONDS``. Never raises; a timeout or failure is a WARNING.""" + calls = [] + if meter_provider is not None: + calls.append(_bounded("Metrics shutdown", meter_provider.shutdown)) + tracer_provider = _tracer_provider() + if tracer_provider is not None: + calls.append(_bounded("AI tracing flush", tracer_provider.force_flush)) + await asyncio.gather(*calls) + + +def _tracer_provider(): + try: + from opentelemetry import trace + except ImportError: + return None + provider = trace.get_tracer_provider() + # The default proxy (nothing configured) has no force_flush. + return provider if hasattr(provider, "force_flush") else None + + +async def _bounded(what: str, call) -> None: + started = time.monotonic() + try: + # MeterProvider.shutdown honours timeout_millis; BatchSpanProcessor.force_flush does not + # (it waits for the exporter's own timeout), so wait_for is the real bound there. + result = await asyncio.wait_for( + asyncio.to_thread(call, timeout_millis=int(TELEMETRY_FLUSH_SECONDS * 1000)), + TELEMETRY_FLUSH_SECONDS, + ) + except TimeoutError: + result = False + except Exception as exc: + logger.warning("%s failed: %r", what, exc) + return + # force_flush returns False on timeout; MeterProvider.shutdown returns None at the deadline. + if result is False or time.monotonic() - started >= TELEMETRY_FLUSH_SECONDS: + logger.warning( + "%s did not finish within %ss; the last data may be lost", + what, + TELEMETRY_FLUSH_SECONDS, + ) + + +def native_telemetry_off() -> dict: + """Keyword arguments for ``FastAPI(...)`` that turn its native OpenTelemetry off. + + Newer FastAPI (with a ``telemetry`` parameter) traces, meters and logs every request into + the global providers by default, so HTTP spans would reach an AI tracing backend (Phoenix, + LangSmith, OTLP, Logfire). HTTP spans come only from FastAPIInstrumentor on the legacy + ``OTEL_ENABLED`` path and HTTP metrics only from the ``metrics`` block. Empty on an older + FastAPI. + """ + from fastapi import FastAPI + + if "telemetry" not in inspect.signature(FastAPI.__init__).parameters: + return {} + return {"telemetry": {"tracing": False, "metrics": False, "logs": False}} diff --git a/tenxgraph_api/src/app/core/config/setup_middleware.py b/tenxgraph_api/src/app/core/config/setup_middleware.py index 14e8d7cc..d2d57f8f 100644 --- a/tenxgraph_api/src/app/core/config/setup_middleware.py +++ b/tenxgraph_api/src/app/core/config/setup_middleware.py @@ -1,4 +1,5 @@ import uuid +from dataclasses import replace from datetime import datetime from typing import Any @@ -19,6 +20,7 @@ from tenxgraph_api.src.app.core.middleware.security_headers import SecurityHeadersMiddleware from .graph_config import GraphConfig +from .metrics import resolve_use_metrics_endpoint, setup_metrics from .sentry_config import init_sentry from .settings import get_settings, logger @@ -29,11 +31,16 @@ class HealthCheckAwareTrustedHostMiddleware(TrustedHostMiddleware): Docker and Kubernetes probes call ``/ping`` and ``/health/ready`` on ``localhost`` or the pod IP, which are never in ``ALLOWED_HOST``, so the probe would fail and the container would be restarted. Both return only status words and read nothing from the request, so skipping - the Host check there is safe. + the Host check there is safe. ``open_paths`` adds more such paths (the Prometheus scrape + route, scraped by pod IP or service name). """ + def __init__(self, app, *, open_paths: tuple[str, ...] = (), **kwargs: Any) -> None: + super().__init__(app, **kwargs) + self.open_paths = (*HEALTH_PROBE_PATHS, *open_paths) + async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None: - if scope["type"] == "http" and _is_health_check(scope): + if scope["type"] == "http" and _is_health_check(scope, self.open_paths): await self.app(scope, receive, send) return await super().__call__(scope, receive, send) @@ -58,10 +65,10 @@ def _upload_path_limits(max_request_size: int) -> dict[str, int]: HEALTH_PROBE_PATHS = ("/ping", "/health/ready") -def _is_health_check(scope: Scope) -> bool: +def _is_health_check(scope: Scope, paths: tuple[str, ...] = HEALTH_PROBE_PATHS) -> bool: path = scope.get("path", "") root_path = (scope.get("root_path") or "").rstrip("/") - return any(path in (probe, f"{root_path}{probe}") for probe in HEALTH_PROBE_PATHS) + return any(path in (probe, f"{root_path}{probe}") for probe in paths) # Paths that should be excluded from GZip compression (streaming endpoints) @@ -296,7 +303,7 @@ def _attach_otel_publisher( def _setup_observability(container: InjectQ | None, graph_config: GraphConfig | None) -> None: - """Wire Logfire/LangSmith/Phoenix from the ``observability`` block of ``10xgraph.json``. + """Wire Logfire/LangSmith/Phoenix/OTLP from the ``observability`` block of ``10xgraph.json``. Configures the tracer provider(s)/exporters and binds an ``OtelPublisher`` into the DI container. This runs at app-construction time, before the graph @@ -315,11 +322,13 @@ def _setup_observability(container: InjectQ | None, graph_config: GraphConfig | obs_cfg = graph_config.observability if not obs_cfg: return + obs_cfg = resolve_use_metrics_endpoint(obs_cfg, graph_config.metrics) logfire_on = bool((obs_cfg.get("logfire") or {}).get("enabled", False)) langsmith_on = bool((obs_cfg.get("langsmith") or {}).get("enabled", False)) phoenix_on = bool((obs_cfg.get("phoenix") or {}).get("enabled", False)) - if not logfire_on and not langsmith_on and not phoenix_on: + otlp_on = bool((obs_cfg.get("otlp") or {}).get("enabled", False)) + if not (logfire_on or langsmith_on or phoenix_on or otlp_on): return try: @@ -363,15 +372,16 @@ def _setup_observability(container: InjectQ | None, graph_config: GraphConfig | _bind_publisher(container, CompositePublisher([existing, otel_publisher])) logger.info( - "Observability enabled (logfire=%s, langsmith=%s, phoenix=%s, level=%s)", + "Observability enabled (logfire=%s, langsmith=%s, phoenix=%s, otlp=%s, level=%s)", logfire_on, langsmith_on, phoenix_on, + otlp_on, level, ) -def setup_otel_provider(settings) -> bool: +def setup_otel_provider(settings, *, metrics_owned: bool = False) -> bool: """Configure OpenTelemetry traces and metrics (API and worker share this). Metrics use a MeterProvider with the same resource and endpoint (OTLP gRPC, or the console @@ -385,6 +395,9 @@ def setup_otel_provider(settings) -> bool: configured and the app is not in production, so traces are visible locally without any collector. In production with no endpoint nothing is set up and a warning is logged. Returns True when a provider was installed. + + With ``metrics_owned`` the ``metrics`` block already installed the MeterProvider, so no + second one is created here. """ try: from opentelemetry import trace @@ -429,7 +442,10 @@ def setup_otel_provider(settings) -> bool: logger.info("OTEL: no endpoint configured, printing spans to console") trace.set_tracer_provider(provider) - _setup_otel_metrics(resource, endpoint) + if metrics_owned: + logger.info("OTEL: metrics come from the metrics block") + else: + _setup_otel_metrics(resource, endpoint) return True @@ -462,20 +478,71 @@ def _setup_otel_metrics(resource, endpoint: str | None) -> None: logger.info("OTEL: metrics enabled") -def _setup_otel(app: FastAPI, settings) -> None: - """Configure OpenTelemetry tracing and instrument the FastAPI app.""" - if not setup_otel_provider(settings): - return +def _instrument_fastapi(app: FastAPI, *, tracing: bool, meter_provider=None) -> bool: + """The one ``instrument_app`` call (it can run only once per app). + + HTTP spans go to the global tracer provider only with the legacy ``OTEL_ENABLED`` path; + otherwise they go to a no-op one, so they never reach an AI tracing backend. HTTP metrics + go to ``meter_provider`` when given, else to the global one. + """ try: from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor + from opentelemetry.trace import NoOpTracerProvider except ImportError: logger.warning( - "OTEL_ENABLED=true but opentelemetry packages are not installed. " - "Install with: pip install '10xgraph-api[otel]'" + "FastAPI instrumentation skipped: opentelemetry packages are not installed. " + "Install with: pip install '10xgraph-api[otel]' or '10xgraph-api[metrics]'" ) - return - FastAPIInstrumentor.instrument_app(app) + return False + kwargs: dict[str, Any] = {} + if not tracing: + kwargs["tracer_provider"] = NoOpTracerProvider() + if meter_provider is not None: + kwargs["meter_provider"] = meter_provider + FastAPIInstrumentor.instrument_app(app, **kwargs) + return True + + +def _setup_otel(app: FastAPI, settings, meter_provider=None) -> bool: + """Configure OpenTelemetry tracing and instrument the FastAPI app. + + ``meter_provider`` is the one from the ``metrics`` block; it then owns the metrics. + Returns True when the app was instrumented. + """ + if not setup_otel_provider(settings, metrics_owned=meter_provider is not None): + return False + if not _instrument_fastapi(app, tracing=True, meter_provider=meter_provider): + return False logger.info("OTEL: FastAPI instrumentation active (service=%s)", settings.OTEL_SERVICE_NAME) + return True + + +def _add_metrics_route(app: FastAPI, path: str) -> None: + """Serve the Prometheus registry at ``path``. Public like /ping: scrapers send no tokens, + so keep it on the private network.""" + from prometheus_client import CONTENT_TYPE_LATEST, REGISTRY, generate_latest + + def scrape() -> Response: + return Response(generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST) + + app.add_api_route(path, scrape, methods=["GET"], include_in_schema=False) + + +def _setup_metrics(app: FastAPI, graph_config: GraphConfig | None): + """Start the ``metrics`` block (when on) and return the MeterProvider, else None.""" + metrics_cfg = graph_config.metrics if graph_config is not None else None + if metrics_cfg is None or not metrics_cfg.enabled: + return None + provider = setup_metrics(metrics_cfg, role="api") + if provider is None: + return None + app.state.meter_provider = provider + if metrics_cfg.scrape_path: + try: + _add_metrics_route(app, metrics_cfg.scrape_path) + except ImportError: + logger.warning("Metrics scrape route skipped: install '10xgraph-api[metrics]'") + return provider def setup_middleware( @@ -503,10 +570,16 @@ def setup_middleware( if StaleStateError is not None: app.add_exception_handler(StaleStateError, _stale_state_handler) + meter_provider = _setup_metrics(app, graph_config) + + instrumented = False if settings.OTEL_ENABLED: - _setup_otel(app, settings) + instrumented = _setup_otel(app, settings, meter_provider) if container is not None: _attach_otel_publisher(container, settings, graph_config) + # HTTP metrics once: here unless the legacy path instrumented (it can fail, e.g. no endpoint). + if meter_provider is not None and not instrumented: + _instrument_fastapi(app, tracing=False, meter_provider=meter_provider) # Declarative Logfire/LangSmith wiring from 10xgraph.json (before compile). _setup_observability(container, graph_config) @@ -537,10 +610,16 @@ def setup_middleware( path_limits=_upload_path_limits(settings.MAX_REQUEST_SIZE), ) + scrape_path = graph_config.metrics.scrape_path if graph_config is not None else None # Apply rate limiting only when configured in 10xgraph.json if graph_config is not None: rate_limit_config = graph_config.rate_limit if rate_limit_config is not None: + if scrape_path: # scrapes come every few seconds from one address + rate_limit_config = replace( + rate_limit_config, + exclude_paths=(*rate_limit_config.exclude_paths, scrape_path), + ) # The counters live in the cache store, bound in the container at startup. app.add_middleware( RateLimitMiddleware, @@ -560,6 +639,7 @@ def setup_middleware( app.add_middleware( HealthCheckAwareTrustedHostMiddleware, + open_paths=(scrape_path,) if scrape_path else (), allowed_hosts=[h.strip() for h in settings.ALLOWED_HOST.split(",") if h.strip()], ) diff --git a/tenxgraph_api/src/app/main.py b/tenxgraph_api/src/app/main.py index d26441c5..c9e9907a 100644 --- a/tenxgraph_api/src/app/main.py +++ b/tenxgraph_api/src/app/main.py @@ -17,8 +17,12 @@ init_logger, setup_middleware, ) -from tenxgraph_api.src.app.core.auth.route_guard import assert_all_routes_protected +from tenxgraph_api.src.app.core.auth.route_guard import ( + DEFAULT_PUBLIC_PATHS, + assert_all_routes_protected, +) from tenxgraph_api.src.app.core.config.graph_config import GraphConfig, default_config_path +from tenxgraph_api.src.app.core.config.metrics import flush_telemetry, native_telemetry_off from tenxgraph_api.src.app.loader import attach_all_modules, load_container from tenxgraph_api.src.app.routers import init_routes @@ -256,6 +260,9 @@ async def lifespan(app: FastAPI): except Exception as exc: logger.debug("Auth close failed: %s", exc) + # Last, so a slow collector delays nothing else: the final metrics push and AI spans. + await flush_telemetry(getattr(app.state, "meter_provider", None)) + app = FastAPI( title=settings.APP_NAME, @@ -268,6 +275,7 @@ async def lifespan(app: FastAPI): openapi_url="/openapi.json" if settings.DOCS_PATH or settings.REDOCS_PATH else None, lifespan=lifespan, root_path=settings.ROOT_PATH, + **native_telemetry_off(), ) setup_middleware(app, graph_config=graph_config, container=container) @@ -282,6 +290,7 @@ async def lifespan(app: FastAPI): # A malformed "media" section should stop the boot, not surface on the first delete. graph_config.media # noqa: B018 +graph_config.metrics # noqa: B018 # init routes # The AG-UI endpoint is opt-in through 10xgraph.json; the WebSocket routes can be turned off there. @@ -294,7 +303,11 @@ async def lifespan(app: FastAPI): # Secure by construction: refuse to boot if any non-public route forgot its # RequirePermission guard (a forgotten guard would otherwise ship an open endpoint). -assert_all_routes_protected(app) +# The Prometheus scrape route is public too (scrapers send no tokens). +_scrape_path = graph_config.metrics.scrape_path +assert_all_routes_protected( + app, DEFAULT_PUBLIC_PATHS | {_scrape_path} if _scrape_path else DEFAULT_PUBLIC_PATHS +) # instrumentator = Instrumentator().instrument(app) # Instrument first # instrumentator.expose(app) # Then expose diff --git a/tests/unit_tests/jobs/test_middleware.py b/tests/unit_tests/jobs/test_middleware.py index b8812453..1c6d206a 100644 --- a/tests/unit_tests/jobs/test_middleware.py +++ b/tests/unit_tests/jobs/test_middleware.py @@ -351,7 +351,7 @@ async def stopped() -> None: ("stopped", "queued"), ("stopped", "cancelled"), } - durations = [c for c in meter.calls if c[1] == "tenxgraph.task.duration_ms"] + durations = [c for c in meter.calls if c[1] == "tenxgraph.task.duration"] assert len(durations) == 3 assert all(c[0] == "histogram" and c[2] >= 0 and set(c[3]) == {"task"} for c in durations) diff --git a/tests/unit_tests/jobs/test_worker.py b/tests/unit_tests/jobs/test_worker.py index 804cdf1e..380ff00e 100644 --- a/tests/unit_tests/jobs/test_worker.py +++ b/tests/unit_tests/jobs/test_worker.py @@ -146,3 +146,124 @@ def test_result_ttl_set_from_config(monkeypatch, tmp_path): def _restore_worker_module(): yield sys.modules.pop("tenxgraph_api.jobs.worker", None) + + +METRICS_DATA = { + "agent": "a:b", + "worker": {"broker": {"type": "memory"}}, + "observability": {"otlp": {"enabled": True, "use_metrics_endpoint": True}}, + "metrics": {"enabled": True, "export": "otlp", "otlp": {"endpoint": "http://c:4318"}}, +} + + +@pytest.mark.asyncio +async def test_worker_applies_observability_and_metrics_then_flushes(monkeypatch, tmp_path): + from injectq import InjectQ + + from tenxgraph_api.src.app.core.config import metrics as metrics_module + from tenxgraph_api.src.app.core.config import setup_middleware as sm + + worker = load_worker(monkeypatch, tmp_path, METRICS_DATA) + mine = InjectQ() + order = [] + provider = object() + roles = [] + + class FakeGraph: + async def aclose(self, **kwargs): + order.append("graph closed") + + async def fake_attach(config, cont): + order.append("graph built") + return FakeGraph() + + from tenxgraph_api.src.app import loader + from tenxgraph_api.src.app.core.config.settings import get_settings + + monkeypatch.setattr(loader, "attach_all_modules", fake_attach) + monkeypatch.setattr(loader, "load_container", lambda path: mine) + monkeypatch.setattr(worker, "wrap_tasks", lambda b: None) + monkeypatch.setenv("STARTUP_CHECKS", "false") + get_settings.cache_clear() + monkeypatch.setattr( + metrics_module, "setup_metrics", lambda cfg, role: roles.append(role) or provider + ) + monkeypatch.setattr( + sm, "_setup_observability", lambda c, cfg: order.append(("observability", c is mine)) + ) + + async def fake_flush(p): + order.append(("telemetry flushed", p)) + + monkeypatch.setattr(metrics_module, "flush_telemetry", fake_flush) + + class FakeCacheStore: + async def aclose(self): + order.append("cache store closed") + + from tenxgraph_api.src.app.core.cache_store import BaseCacheStore + + state = SimpleNamespace() + await worker.worker_startup(state) + mine.bind_instance(BaseCacheStore, FakeCacheStore()) + await worker.worker_shutdown(state) + + assert roles == ["worker"] + assert state.meter_provider is provider + assert order == [ + ("observability", True), + "graph built", + "graph closed", + "cache store closed", + ("telemetry flushed", provider), # last: a slow collector cuts nothing else + ] + get_settings.cache_clear() + + +@pytest.mark.asyncio +async def test_worker_with_legacy_otel_and_metrics_installs_one_meter_provider( + monkeypatch, tmp_path +): + from injectq import InjectQ + + from tenxgraph_api.src.app.core.config import metrics as metrics_module + from tenxgraph_api.src.app.core.config import setup_middleware as sm + from tenxgraph_api.src.app.core.config.settings import get_settings + + worker = load_worker(monkeypatch, tmp_path, METRICS_DATA) + _patch_loader(monkeypatch, container=InjectQ()) + monkeypatch.setattr(worker, "wrap_tasks", lambda b: None) + monkeypatch.setenv("OTEL_ENABLED", "true") + get_settings.cache_clear() + provider = object() + monkeypatch.setattr(metrics_module, "setup_metrics", lambda cfg, role: provider) + monkeypatch.setattr(sm, "_setup_observability", lambda c, cfg: None) + monkeypatch.setattr(sm, "_attach_otel_publisher", lambda *a: None) + seen = [] + monkeypatch.setattr( + sm, + "setup_otel_provider", + lambda settings, *, metrics_owned=False: seen.append(metrics_owned), + ) + + await worker.worker_startup(SimpleNamespace()) + + assert seen == [True] + get_settings.cache_clear() + + +@pytest.mark.asyncio +async def test_worker_without_metrics_block_sets_no_provider(monkeypatch, tmp_path): + from injectq import InjectQ + + from tenxgraph_api.src.app.core.config.settings import get_settings + + data = {"agent": "a:b", "worker": {"broker": {"type": "memory"}}} + worker = load_worker(monkeypatch, tmp_path, data) + _patch_loader(monkeypatch, container=InjectQ()) + monkeypatch.setattr(worker, "wrap_tasks", lambda b: None) + state = SimpleNamespace() + await worker.worker_startup(state) + assert state.meter_provider is None + await worker.worker_shutdown(state) + get_settings.cache_clear() diff --git a/tests/unit_tests/test_config_editor.py b/tests/unit_tests/test_config_editor.py index 97143dec..cd3f5845 100644 --- a/tests/unit_tests/test_config_editor.py +++ b/tests/unit_tests/test_config_editor.py @@ -883,3 +883,130 @@ def test_worker_unsupported_type_is_refused(worker: dict, path: str, message: st issues = validate_config({"agent": "a:b", "worker": worker}) matching = [i for i in issues if i["path"] == path] assert [i["message"] for i in matching] == [message] + + +# --------------------------------------------------------------------------- +# AI tracing destinations and metrics +# --------------------------------------------------------------------------- + +_FULL_TELEMETRY = { + "agent": "graph.react:app", + "observability": { + "level": "full", + "logfire": {"enabled": True, "service_name": "svc", "send_to_logfire": False}, + "langsmith": {"enabled": True, "project": "p", "endpoint": "https://smith.example/otel"}, + "phoenix": {"enabled": True, "project": "p", "endpoint": "http://localhost:6006"}, + "otlp": {"enabled": True, "use_metrics_endpoint": True}, + }, + "metrics": { + "enabled": True, + "export": "otlp", + "otlp": {"endpoint": "http://collector:4318"}, + "latency_buckets_ms": [100, 500, 1000], + }, +} + + +def _section(section_id: str) -> dict: + return next(s for s in build_schema()["sections"] if s["id"] == section_id) + + +def test_tracing_and_metrics_sections() -> None: + tracing, metrics = _section("observability"), _section("metrics") + assert tracing["widget"] == "observability" and metrics["widget"] == "metrics" + assert tracing["title"] == "AI tracing" and tracing["key"] == "observability" + assert tracing["self_managed"] is True and metrics["self_managed"] is True + assert tracing["notes"] == [] and tracing["default"]["user_id_mode"] == "raw" + assert all( + s.get("self_managed", False) is False + for s in build_schema()["sections"] + if s["id"] not in ("observability", "metrics") + ) + assert metrics["key"] == "metrics" and metrics["optional"] is True + assert "metrics" in KNOWN_TOP_LEVEL_KEYS + assert section_for_key("metrics") == "metrics" + paths = {".".join(f["path"]) for f in tracing["fields"]} + assert {"observability.otlp.endpoint", "observability.otlp.use_metrics_endpoint"} <= paths + assert all(f["managed"] for f in tracing["fields"] + metrics["fields"]) + assert {".".join(f["path"]) for f in metrics["fields"]} == { + "metrics.export", + "metrics.otlp.endpoint", + "metrics.prometheus.path", + "metrics.prometheus.worker_port", + "metrics.latency_buckets_ms", + } + names = {e["name"] for e in metrics["env"]} + assert names == { + "OTEL_EXPORTER_OTLP_HEADERS", + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", + "OTEL_SERVICE_NAME", + } + from tenxgraph_api.src.app.core.config.metrics import DEFAULT_BUCKETS_MS + + buckets = next(f for f in metrics["fields"] if f["path"][-1] == "latency_buckets_ms") + assert buckets["default"] == DEFAULT_BUCKETS_MS + assert metrics["default"] == {"enabled": True, "export": "otlp", "otlp": {"endpoint": None}} + + +def test_full_telemetry_config_round_trips(tmp_path: Path) -> None: + store = ConfigFileStore(tmp_path / "10xgraph.json") + store.save(_FULL_TELEMETRY, None) + assert json.loads((tmp_path / "10xgraph.json").read_text()) == _FULL_TELEMETRY + issues = validate_config(_FULL_TELEMETRY) + assert [i for i in issues if i["section"] in ("observability", "metrics")] == [] + + +def test_metrics_validation() -> None: + def check(metrics: object) -> list[dict]: + issues = validate_config({"agent": "a:b", "metrics": metrics}) + return [i for i in issues if i["section"] == "metrics"] + + assert [i["path"] for i in check({"enabled": True})] == ["metrics.export"] + assert [i["path"] for i in check({"enabled": True, "export": "otlp"})] == [ + "metrics.otlp.endpoint" + ] + assert check({"enabled": True, "export": "prometheus"}) == [] + assert check({"enabled": False}) == [] + assert check({"latency_buckets_ms": [5, 1]})[0]["path"] == "metrics.latency_buckets_ms" + assert check({"prometheus": {"worker_port": 0}})[0]["path"] == "metrics.prometheus.worker_port" + assert check("on")[0]["path"] == "metrics" + + +def test_otlp_tracing_checks(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.delenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", raising=False) + + def check(otlp: dict, metrics: dict | None = None) -> list[dict]: + config: dict = {"agent": "a:b", "observability": {"otlp": {"enabled": True, **otlp}}} + if metrics is not None: + config["metrics"] = metrics + return [i for i in validate_config(config) if i["path"].startswith("observability")] + + missing = check({}) + assert [(i["level"], i["path"]) for i in missing] == [("error", "observability.otlp.endpoint")] + assert check({"endpoint": "http://collector:4318"}) == [] + + shared = "observability.otlp.use_metrics_endpoint" + unset = check({"use_metrics_endpoint": True}) + assert [(i["level"], i["path"]) for i in unset] == [("warning", shared)] + prom_metrics = {"enabled": True, "export": "prometheus"} + assert [i["level"] for i in check({"use_metrics_endpoint": True}, prom_metrics)] == ["warning"] + good = {"enabled": True, "export": "otlp", "otlp": {"endpoint": "http://collector:4318"}} + assert check({"use_metrics_endpoint": True}, good) == [] + + prom = {"enabled": True, "export": "otlp", "otlp": {"endpoint": "http://p:9090/api/v1/otlp"}} + assert [i["path"] for i in check({"use_metrics_endpoint": True}, prom)] == [shared] + own = check({"endpoint": "http://p:9090/api/v1/otlp"}) + assert [(i["level"], i["path"]) for i in own] == [("warning", "observability.otlp.endpoint")] + + bad = check({"use_metrics_endpoint": "yes"}) + assert _levels(bad, shared) == ["error"] + + +def test_nothing_traced_warning_counts_otlp() -> None: + none = validate_config({"agent": "a:b", "observability": {"otlp": {"enabled": False}}}) + warn = next(i for i in none if i["path"] == "observability") + assert "OTLP" in warn["message"] + on = validate_config( + {"agent": "a:b", "observability": {"otlp": {"enabled": True, "endpoint": "http://c:4318"}}} + ) + assert _levels(on, "observability") == [] diff --git a/tests/unit_tests/test_metrics.py b/tests/unit_tests/test_metrics.py new file mode 100644 index 00000000..187107bd --- /dev/null +++ b/tests/unit_tests/test_metrics.py @@ -0,0 +1,599 @@ +"""The ``metrics`` block: config, MeterProvider setup, API wiring. The venv has no OpenTelemetry, +so the SDK, exporters and instrumentor are replaced by mocks in ``sys.modules``.""" + +import asyncio +import logging +import os +import time +from unittest.mock import MagicMock, patch + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient +from pydantic import ValidationError + +from tenxgraph_api.src.app.core.auth.route_guard import ( + DEFAULT_PUBLIC_PATHS, + find_unprotected_routes, +) +from tenxgraph_api.src.app.core.config import metrics as metrics_module +from tenxgraph_api.src.app.core.config import setup_middleware as sm +from tenxgraph_api.src.app.core.config.graph_config import GraphConfig +from tenxgraph_api.src.app.core.config.metrics import ( + MetricsConfig, + flush_telemetry, + native_telemetry_off, + resolve_use_metrics_endpoint, + setup_metrics, +) + + +HTTP_OK = 200 +HTTP_BAD_REQUEST = 400 +PROMETHEUS_PORT = 9464 + + +def _otel_modules(**extra): + modules = { + name: MagicMock() + for name in ( + "opentelemetry", + "opentelemetry.metrics", + "opentelemetry.trace", + "opentelemetry.sdk", + "opentelemetry.sdk.trace", + "opentelemetry.sdk.trace.export", + "opentelemetry.sdk.metrics", + "opentelemetry.sdk.metrics.view", + "opentelemetry.sdk.metrics.export", + "opentelemetry.sdk.resources", + "opentelemetry.exporter", + "opentelemetry.exporter.otlp", + "opentelemetry.exporter.otlp.proto", + "opentelemetry.exporter.otlp.proto.http", + "opentelemetry.exporter.otlp.proto.http.metric_exporter", + "opentelemetry.exporter.prometheus", + "opentelemetry.instrumentation", + "opentelemetry.instrumentation.fastapi", + ) + } + modules["opentelemetry"].metrics = modules["opentelemetry.metrics"] + modules["opentelemetry"].trace = modules["opentelemetry.trace"] + modules.update(extra) + return modules + + +def _cfg(**kwargs): + return MetricsConfig.model_validate( + {"enabled": True, "export": "otlp", "otlp": {"endpoint": "http://c:4318"}, **kwargs} + ) + + +# ── config ──────────────────────────────────────────────────────────────────── + + +def test_defaults_are_off_and_valid(): + cfg = MetricsConfig() + assert not cfg.enabled + assert cfg.scrape_path is None + assert cfg.prometheus.path == "/metrics" + assert cfg.prometheus.worker_port == PROMETHEUS_PORT + assert cfg.latency_buckets_ms[0] == 50 + + +def test_export_required_when_enabled(): + with pytest.raises(ValidationError, match="export is required"): + MetricsConfig(enabled=True) + + +def test_otlp_endpoint_required_unless_env_set(monkeypatch): + monkeypatch.delenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", raising=False) + with pytest.raises(ValidationError, match=r"otlp\.endpoint is required"): + MetricsConfig(enabled=True, export="otlp") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://x/v1/metrics") + assert MetricsConfig(enabled=True, export="otlp").enabled + + +@pytest.mark.parametrize("buckets", [[], [0, 10], [-5, 10], [100, 50], [10, 10]]) +def test_buckets_must_be_positive_and_sorted(buckets): + with pytest.raises(ValidationError, match="latency_buckets_ms"): + MetricsConfig(latency_buckets_ms=buckets) + + +def test_unknown_key_and_bad_export_rejected(): + with pytest.raises(ValidationError): + MetricsConfig(bogus=1) + with pytest.raises(ValidationError): + MetricsConfig(enabled=True, export="statsd") + + +def test_scrape_path_only_for_enabled_prometheus(): + assert MetricsConfig(enabled=True, export="prometheus").scrape_path == "/metrics" + assert _cfg().scrape_path is None + + +def test_graph_config_exposes_metrics_and_raises_on_invalid(tmp_path): + path = tmp_path / "10xgraph.json" + path.write_text('{"agent": "a:b", "metrics": {"enabled": true}}') + with pytest.raises(ValidationError): + _ = GraphConfig(str(path)).metrics + path.write_text('{"agent": "a:b"}') + assert not GraphConfig(str(path)).metrics.enabled + + +# ── setup_metrics ───────────────────────────────────────────────────────────── + + +def test_setup_otlp_pushes_to_v1_metrics_with_views_and_resource(monkeypatch): + monkeypatch.delenv("OTEL_SEMCONV_STABILITY_OPT_IN", raising=False) + modules = _otel_modules() + with ( + patch.dict("sys.modules", modules), + patch("tenxgraph.utils.metrics.setup_otel_metrics") as bridge, + ): + provider = setup_metrics(_cfg(), role="api") + + sdk = modules["opentelemetry.sdk.metrics"] + assert provider is sdk.MeterProvider.return_value + assert os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] == "http" + exporter = modules["opentelemetry.exporter.otlp.proto.http.metric_exporter"] + exporter.OTLPMetricExporter.assert_called_once_with(endpoint="http://c:4318/v1/metrics") + export = modules["opentelemetry.sdk.metrics.export"] + export.PeriodicExportingMetricReader.assert_called_once_with( + exporter.OTLPMetricExporter.return_value + ) + view = modules["opentelemetry.sdk.metrics.view"] + units = [c.kwargs["instrument_unit"] for c in view.View.call_args_list] + assert units == ["ms", "s"] + seconds = view.ExplicitBucketHistogramAggregation.call_args_list[1].args[0] + assert seconds[0] == pytest.approx(0.05) + resource = modules["opentelemetry.sdk.resources"].Resource.create.call_args.args[0] + assert "service.name" in resource + assert resource["service.instance.id"].endswith(f"-{os.getpid()}") + modules["opentelemetry.metrics"].set_meter_provider.assert_called_once_with(provider) + provider.get_meter.assert_called_once_with("10xgraph") + bridge.assert_called_once_with(provider.get_meter.return_value) + + +def test_global_meter_provider_set_earlier_warns_and_bridges_to_ours(): + modules = _otel_modules() # get_meter_provider returns another object: set was refused + with ( + patch.dict("sys.modules", modules), + patch("tenxgraph.utils.metrics.setup_otel_metrics") as bridge, + patch.object(metrics_module.logger, "warning") as warning, + ): + provider = setup_metrics(_cfg(), role="api") + assert "Another global MeterProvider" in warning.call_args.args[0] + bridge.assert_called_once_with(provider.get_meter.return_value) + + +def test_no_warning_when_our_provider_is_global(): + modules = _otel_modules() + sdk = modules["opentelemetry.sdk.metrics"] + modules[ + "opentelemetry.metrics" + ].get_meter_provider.return_value = sdk.MeterProvider.return_value + with ( + patch.dict("sys.modules", modules), + patch("tenxgraph.utils.metrics.setup_otel_metrics"), + patch.object(metrics_module.logger, "warning") as warning, + ): + setup_metrics(_cfg(), role="api") + warning.assert_not_called() + + +def test_setup_failure_warns_and_returns_none(): + modules = _otel_modules() + exporter = modules["opentelemetry.exporter.otlp.proto.http.metric_exporter"] + exporter.OTLPMetricExporter.side_effect = ValueError("bad endpoint") + with ( + patch.dict("sys.modules", modules), + patch("tenxgraph.utils.metrics.setup_otel_metrics") as bridge, + patch.object(metrics_module.logger, "warning") as warning, + ): + assert setup_metrics(_cfg(), role="api") is None + assert "bad endpoint" in str(warning.call_args.args[1]) + modules["opentelemetry.metrics"].set_meter_provider.assert_not_called() + bridge.assert_not_called() + + +def test_otlp_endpoint_already_ending_in_v1_metrics_is_kept(): + modules = _otel_modules() + cfg = _cfg(otlp={"endpoint": "http://prom:9090/api/v1/otlp/v1/metrics"}) + with patch.dict("sys.modules", modules), patch("tenxgraph.utils.metrics.setup_otel_metrics"): + setup_metrics(cfg, role="api") + exporter = modules["opentelemetry.exporter.otlp.proto.http.metric_exporter"] + exporter.OTLPMetricExporter.assert_called_once_with( + endpoint="http://prom:9090/api/v1/otlp/v1/metrics" + ) + + +def test_otlp_without_endpoint_lets_the_exporter_read_the_env(monkeypatch): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://env/v1/metrics") + modules = _otel_modules() + cfg = MetricsConfig(enabled=True, export="otlp") + with patch.dict("sys.modules", modules), patch("tenxgraph.utils.metrics.setup_otel_metrics"): + setup_metrics(cfg, role="api") + exporter = modules["opentelemetry.exporter.otlp.proto.http.metric_exporter"] + exporter.OTLPMetricExporter.assert_called_once_with(endpoint=None) + + +def test_prometheus_api_logs_info_and_starts_no_server(caplog): + modules = _otel_modules(prometheus_client=MagicMock()) + cfg = MetricsConfig(enabled=True, export="prometheus") + with ( + patch.dict("sys.modules", modules), + patch("tenxgraph.utils.metrics.setup_otel_metrics"), + caplog.at_level(logging.INFO, logger="10xgraph-api"), + ): + setup_metrics(cfg, role="api") + modules["opentelemetry.exporter.prometheus"].PrometheusMetricReader.assert_called_once_with() + modules["prometheus_client"].start_http_server.assert_not_called() + assert any( + "one process" in r.message or "this process only" in r.message for r in caplog.records + ) + + +def test_prometheus_worker_starts_server(): + modules = _otel_modules(prometheus_client=MagicMock()) + cfg = MetricsConfig(enabled=True, export="prometheus", prometheus={"worker_port": 9999}) + with patch.dict("sys.modules", modules), patch("tenxgraph.utils.metrics.setup_otel_metrics"): + setup_metrics(cfg, role="worker") + modules["prometheus_client"].start_http_server.assert_called_once_with(9999) + + +def test_prometheus_worker_port_in_use_warns_and_continues(): + client = MagicMock() + client.start_http_server.side_effect = OSError("Address already in use") + modules = _otel_modules(prometheus_client=client) + cfg = MetricsConfig(enabled=True, export="prometheus") + with ( + patch.dict("sys.modules", modules), + patch("tenxgraph.utils.metrics.setup_otel_metrics"), + patch.object(metrics_module.logger, "warning") as warning, + ): + provider = setup_metrics(cfg, role="worker") + assert provider is not None + message = warning.call_args.args[0] % warning.call_args.args[1:] + assert str(PROMETHEUS_PORT) in message + assert "OTLP push" in message + assert "--workers 1" in message + + +def test_missing_packages_warn_and_return_none(): + with ( + patch.dict("sys.modules", {"opentelemetry": None}), + patch.object(metrics_module.logger, "warning") as warning, + ): + assert setup_metrics(_cfg(), role="api") is None + assert "10xgraph-api[metrics]" in warning.call_args.args[1] + + +def _flush(meter_provider, tracer_provider): + trace = MagicMock() + trace.get_tracer_provider.return_value = tracer_provider + otel = MagicMock(trace=trace) + with patch.dict("sys.modules", {"opentelemetry": otel, "opentelemetry.trace": trace}): + asyncio.run(flush_telemetry(meter_provider)) + + +def test_flush_telemetry_shuts_down_metrics_and_flushes_ai_tracing_with_a_bound(): + meter, tracer = MagicMock(), MagicMock() + tracer.force_flush.return_value = True + with patch.object(metrics_module.logger, "warning") as warning: + _flush(meter, tracer) + meter.shutdown.assert_called_once_with(timeout_millis=5000) + tracer.force_flush.assert_called_once_with(timeout_millis=5000) + tracer.shutdown.assert_not_called() # a provider owned by Logfire stays usable + warning.assert_not_called() + + +def test_flush_telemetry_never_raises_and_warns(): + meter, tracer = MagicMock(), MagicMock() + meter.shutdown.side_effect = RuntimeError("collector down") + tracer.force_flush.return_value = False # the SDK's timeout result + with patch.object(metrics_module.logger, "warning") as warning: + _flush(meter, tracer) + _flush(None, object()) # nothing configured: no force_flush + messages = [c.args[0] % c.args[1:] for c in warning.call_args_list] + assert any("Metrics shutdown failed" in m and "collector down" in m for m in messages) + assert any("AI tracing flush did not finish" in m for m in messages) + assert len(messages) == 2 + + +def test_flush_telemetry_is_bounded_when_a_provider_ignores_the_timeout(monkeypatch): + monkeypatch.setattr(metrics_module, "TELEMETRY_FLUSH_SECONDS", 0.05) + meter = MagicMock() + meter.shutdown.side_effect = lambda **kw: time.sleep(1.5) + started = time.monotonic() + with patch.object(metrics_module.logger, "warning") as warning: + _flush(meter, None) + # asyncio.run joins the leftover thread after the bound, so allow for the 1.5 s sleep. + assert time.monotonic() - started < 2.5 + message = warning.call_args.args[0] % warning.call_args.args[1:] + assert "Metrics shutdown did not finish within 0.05s" in message + + +def test_native_fastapi_telemetry_is_turned_off_when_supported(): + def new_init(self, *, title="", telemetry=None): + """FastAPI with native telemetry.""" + + def old_init(self, *, title=""): + """FastAPI before native telemetry.""" + + with patch.object(FastAPI, "__init__", new_init): + assert native_telemetry_off() == { + "telemetry": {"tracing": False, "metrics": False, "logs": False} + } + with patch.object(FastAPI, "__init__", old_init): + assert native_telemetry_off() == {} + + +# ── API wiring ──────────────────────────────────────────────────────────────── + + +def _graph_config(metrics: MetricsConfig, observability=None): + config = MagicMock() + config.metrics = metrics + config.observability = observability + config.rate_limit = None + return config + + +def _settings(otel_enabled=False): + settings = MagicMock() + settings.OTEL_ENABLED = otel_enabled + settings.ORIGINS = "http://localhost" + settings.CORS_ALLOW_CREDENTIALS = True + settings.MODE = "development" + settings.ALLOWED_HOST = "*" + settings.SECURITY_HEADERS_ENABLED = False + settings.MAX_REQUEST_SIZE = 1_000_000 + return settings + + +def test_single_instrument_call_with_noop_tracer_when_otel_disabled(): + modules = _otel_modules() + provider = MagicMock() + app = FastAPI() + with ( + patch.dict("sys.modules", modules), + patch.object(sm, "get_settings", return_value=_settings(False)), + patch.object(sm, "init_sentry"), + patch.object(sm, "setup_metrics", return_value=provider), + ): + sm.setup_middleware(app, graph_config=_graph_config(_cfg()), container=None) + + instrument = modules["opentelemetry.instrumentation.fastapi"].FastAPIInstrumentor.instrument_app + instrument.assert_called_once() + kwargs = instrument.call_args.kwargs + assert ( + kwargs["tracer_provider"] is modules["opentelemetry.trace"].NoOpTracerProvider.return_value + ) + assert kwargs["meter_provider"] is provider + assert app.state.meter_provider is provider + + +def test_metrics_and_legacy_otel_share_one_instrument_call_and_one_meter_provider(caplog): + modules = _otel_modules() + provider = MagicMock() + app = FastAPI() + settings = _settings(True) + settings.OTEL_SERVICE_NAME = "svc" + settings.OTEL_EXPORTER_OTLP_ENDPOINT = None + with ( + patch.dict("sys.modules", modules), + patch.object(sm, "get_settings", return_value=settings), + patch.object(sm, "init_sentry"), + patch.object(sm, "setup_metrics", return_value=provider), + patch.object(sm, "_setup_otel_metrics") as legacy_metrics, + patch.object(sm, "_attach_otel_publisher"), + caplog.at_level(logging.INFO, logger="10xgraph-api"), + ): + sm.setup_middleware(app, graph_config=_graph_config(_cfg()), container=None) + + legacy_metrics.assert_not_called() + assert any("metrics come from the metrics block" in r.message for r in caplog.records) + instrument = modules["opentelemetry.instrumentation.fastapi"].FastAPIInstrumentor.instrument_app + instrument.assert_called_once() + assert "tracer_provider" not in instrument.call_args.kwargs # the global SDK provider + assert instrument.call_args.kwargs["meter_provider"] is provider + + +@pytest.mark.parametrize("failure", ["no_endpoint", "no_grpc_exporter"]) +def test_failed_legacy_otel_still_gets_http_metrics_from_the_metrics_block(failure): + modules = _otel_modules() + settings = _settings(True) + settings.OTEL_SERVICE_NAME = "svc" + if failure == "no_endpoint": + settings.MODE = "production" + settings.OTEL_EXPORTER_OTLP_ENDPOINT = None + else: + settings.OTEL_EXPORTER_OTLP_ENDPOINT = "http://c:4317" + modules["opentelemetry.exporter.otlp.proto.grpc.trace_exporter"] = None # ImportError + provider = MagicMock() + app = FastAPI() + with ( + patch.dict("sys.modules", modules), + patch.object(sm, "get_settings", return_value=settings), + patch.object(sm, "init_sentry"), + patch.object(sm, "setup_metrics", return_value=provider), + patch.object(sm, "_attach_otel_publisher"), + ): + sm.setup_middleware(app, graph_config=_graph_config(_cfg()), container=None) + + instrument = modules["opentelemetry.instrumentation.fastapi"].FastAPIInstrumentor.instrument_app + instrument.assert_called_once() + kwargs = instrument.call_args.kwargs + assert ( + kwargs["tracer_provider"] is modules["opentelemetry.trace"].NoOpTracerProvider.return_value + ) + assert kwargs["meter_provider"] is provider + + +def test_scrape_path_skips_the_host_check_like_the_probes(): + cfg = MetricsConfig(enabled=True, export="prometheus") + fake_client = MagicMock() + fake_client.CONTENT_TYPE_LATEST = "text/plain" + fake_client.generate_latest.return_value = b"up 1\n" + settings = _settings(False) + settings.ALLOWED_HOST = "api.example.com" + app = FastAPI() + app.get("/other")(lambda: {"ok": True}) + with ( + patch.dict("sys.modules", {**_otel_modules(), "prometheus_client": fake_client}), + patch.object(sm, "get_settings", return_value=settings), + patch.object(sm, "init_sentry"), + patch.object(sm, "setup_metrics", return_value=MagicMock()), + ): + sm.setup_middleware(app, graph_config=_graph_config(cfg), container=None) + client = TestClient(app) + for host in ("10.1.2.3:8000", "api:8000"): + assert client.get("/metrics", headers={"host": host}).status_code == HTTP_OK + assert ( + client.get("/other", headers={"host": "10.1.2.3:8000"}).status_code == HTTP_BAD_REQUEST + ) + assert ( + client.get("/metrics/x", headers={"host": "10.1.2.3:8000"}).status_code + == HTTP_BAD_REQUEST + ) + + +def test_legacy_otel_without_metrics_block_keeps_legacy_metrics(): + settings = MagicMock() + settings.OTEL_SERVICE_NAME = "svc" + settings.OTEL_EXPORTER_OTLP_ENDPOINT = None + settings.MODE = "development" + modules = _otel_modules() + with ( + patch.dict("sys.modules", modules), + patch.object(sm, "_setup_otel_metrics") as legacy_metrics, + ): + assert sm.setup_otel_provider(settings) is True + legacy_metrics.assert_called_once() + + +def test_prometheus_route_serves_registry_without_auth_and_passes_the_guard(): + modules = _otel_modules() + cfg = MetricsConfig(enabled=True, export="prometheus", prometheus={"path": "/scrape"}) + fake_client = MagicMock() + fake_client.CONTENT_TYPE_LATEST = "text/plain; version=0.0.4; charset=utf-8" + fake_client.generate_latest.return_value = b"# HELP up\nup 1\n" + app = FastAPI() + with ( + patch.dict("sys.modules", {**modules, "prometheus_client": fake_client}), + patch.object(sm, "get_settings", return_value=_settings(False)), + patch.object(sm, "init_sentry"), + patch.object(sm, "setup_metrics", return_value=MagicMock()), + ): + sm.setup_middleware(app, graph_config=_graph_config(cfg), container=None) + response = TestClient(app).get("/scrape") + + assert response.status_code == HTTP_OK + assert response.content == b"# HELP up\nup 1\n" + assert response.headers["content-type"].startswith("text/plain") + assert find_unprotected_routes(app, DEFAULT_PUBLIC_PATHS | {"/scrape"}) == [] + assert find_unprotected_routes(app) == ["GET /scrape"] + + +def test_scrape_path_is_excluded_from_rate_limiting(): + from tenxgraph_api.src.app.core.config.graph_config import RateLimitConfig + from tenxgraph_api.src.app.core.middleware.rate_limit import RateLimitMiddleware + + cfg = MetricsConfig(enabled=True, export="prometheus") + graph_config = _graph_config(cfg) + graph_config.rate_limit = RateLimitConfig.from_dict({"exclude_paths": ["/docs"]}) + modules = _otel_modules(prometheus_client=MagicMock()) + app = FastAPI() + with ( + patch.dict("sys.modules", modules), + patch.object(sm, "get_settings", return_value=_settings(False)), + patch.object(sm, "init_sentry"), + patch.object(sm, "setup_metrics", return_value=MagicMock()), + ): + sm.setup_middleware(app, graph_config=graph_config, container=MagicMock()) + + limiter = next(m for m in app.user_middleware if m.cls is RateLimitMiddleware) + assert limiter.kwargs["config"].exclude_paths == ("/docs", "/metrics") + + +def test_metrics_off_does_nothing(): + modules = _otel_modules() + app = FastAPI() + with ( + patch.dict("sys.modules", modules), + patch.object(sm, "get_settings", return_value=_settings(False)), + patch.object(sm, "init_sentry"), + patch.object(sm, "setup_metrics") as setup, + ): + sm.setup_middleware(app, graph_config=_graph_config(MetricsConfig()), container=None) + setup.assert_not_called() + modules[ + "opentelemetry.instrumentation.fastapi" + ].FastAPIInstrumentor.instrument_app.assert_not_called() + + +# ── observability.otlp.use_metrics_endpoint ────────────────────────────────── + + +OBS = {"otlp": {"enabled": True, "endpoint": None, "use_metrics_endpoint": True}} + + +def test_use_metrics_endpoint_copies_the_endpoint_without_mutating(): + resolved = resolve_use_metrics_endpoint(OBS, _cfg()) + assert resolved["otlp"]["endpoint"] == "http://c:4318" + assert resolved["otlp"]["enabled"] is True + assert OBS["otlp"]["endpoint"] is None + + +def test_use_metrics_endpoint_strips_v1_metrics_and_warns_for_prometheus(): + cfg = _cfg(otlp={"endpoint": "http://prom:9090/api/v1/otlp/v1/metrics/"}) + with patch.object(sm.logger, "warning") as warning: + resolved = resolve_use_metrics_endpoint(OBS, cfg) + assert resolved["otlp"]["endpoint"] == "http://prom:9090/api/v1/otlp" + assert "Prometheus accepts metrics only" in warning.call_args.args[0] + + +def test_use_metrics_endpoint_collector_does_not_warn(): + cfg = _cfg(otlp={"endpoint": "http://collector:4318/v1/metrics"}) + with patch.object(sm.logger, "warning") as warning: + resolved = resolve_use_metrics_endpoint(OBS, cfg) + assert resolved["otlp"]["endpoint"] == "http://collector:4318" + warning.assert_not_called() + + +@pytest.mark.parametrize( + "metrics", + [MetricsConfig(), MetricsConfig(enabled=True, export="prometheus")], +) +def test_use_metrics_endpoint_without_otlp_metrics_warns_and_disables(metrics): + with patch.object(sm.logger, "warning") as warning: + resolved = resolve_use_metrics_endpoint(OBS, metrics) + assert resolved["otlp"]["enabled"] is False + assert "use_metrics_endpoint" in warning.call_args.args[0] + assert OBS["otlp"]["enabled"] is True + + +def test_setup_observability_hands_resolved_otlp_to_core(): + container = MagicMock() + container.try_get.return_value = None + graph_config = _graph_config(_cfg(), observability={"level": "standard", **OBS}) + with ( + patch("tenxgraph.runtime.publisher.exporters.setup_observability") as core_setup, + patch.object(sm, "_bind_publisher"), + ): + sm._setup_observability(container, graph_config) + core_setup.assert_called_once() + assert core_setup.call_args.args[1]["otlp"]["endpoint"] == "http://c:4318" + + +def test_setup_observability_runs_for_otlp_alone(): + container = MagicMock() + container.try_get.return_value = None + obs = {"otlp": {"enabled": True, "endpoint": "http://t:4318"}} + with ( + patch("tenxgraph.runtime.publisher.exporters.setup_observability") as core_setup, + patch.object(sm, "_bind_publisher"), + ): + sm._setup_observability(container, _graph_config(MetricsConfig(), observability=obs)) + core_setup.assert_called_once() diff --git a/tests/unit_tests/test_middleware_order.py b/tests/unit_tests/test_middleware_order.py index 370ba639..fa46c1cd 100644 --- a/tests/unit_tests/test_middleware_order.py +++ b/tests/unit_tests/test_middleware_order.py @@ -12,6 +12,7 @@ from tenxgraph_api.src.app.core.config import setup_middleware as sm from tenxgraph_api.src.app.core.config.graph_config import RateLimitConfig +from tenxgraph_api.src.app.core.config.metrics import MetricsConfig from tenxgraph_api.src.app.core.config.settings import Settings @@ -28,6 +29,7 @@ def _app(allowed_host: str = "testserver", max_size: int = 10 * 1024 * 1024) -> graph_config = SimpleNamespace( rate_limit=RateLimitConfig.from_dict({"requests": 1, "window": 60}), observability=None, + metrics=MetricsConfig(), ) app = FastAPI() diff --git a/tests/unit_tests/test_setup_middleware.py b/tests/unit_tests/test_setup_middleware.py index 04adafe5..0c2950d8 100644 --- a/tests/unit_tests/test_setup_middleware.py +++ b/tests/unit_tests/test_setup_middleware.py @@ -6,6 +6,7 @@ from fastapi.testclient import TestClient from tenxgraph_api.src.app.core.config import setup_middleware as setup_middleware_module +from tenxgraph_api.src.app.core.config.metrics import MetricsConfig from tenxgraph_api.src.app.core.config.setup_middleware import ( SelectiveGZipMiddleware, setup_middleware, @@ -431,6 +432,7 @@ def test_setup_middleware_all(): rate_limit_config.exclude_paths = None rate_limit_config.trusted_proxy_headers = True graph_config.rate_limit = rate_limit_config + graph_config.metrics = MetricsConfig() container = MagicMock() @@ -451,7 +453,7 @@ def test_setup_middleware_all(): assert len(rate_limit) == 1 assert rate_limit[0].kwargs == {"config": rate_limit_config, "container": container} assert not hasattr(app.state, "rate_limit_backend") - mock_setup_otel.assert_called_once_with(app, settings) + mock_setup_otel.assert_called_once_with(app, settings, None) mock_attach.assert_called_once_with(container, settings, graph_config) From edc51ee32ae2509888bb05770abae3212080ed81 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Mon, 9 Nov 2026 20:09:34 +0600 Subject: [PATCH 3/9] feat(metrics): Enhance metrics configuration and add scrape path validation --- tenxgraph_api/src/app/core/config/metrics.py | 110 +++++++++-- .../src/app/core/config/setup_middleware.py | 37 +++- tenxgraph_api/src/app/main.py | 9 +- tests/unit_tests/test_metrics.py | 174 ++++++++++++++++++ .../unit_tests/test_metrics_real_packages.py | 77 ++++++++ tests/unit_tests/test_setup_middleware.py | 2 +- 6 files changed, 389 insertions(+), 20 deletions(-) create mode 100644 tests/unit_tests/test_metrics_real_packages.py diff --git a/tenxgraph_api/src/app/core/config/metrics.py b/tenxgraph_api/src/app/core/config/metrics.py index 740c3e78..f01ac69c 100644 --- a/tenxgraph_api/src/app/core/config/metrics.py +++ b/tenxgraph_api/src/app/core/config/metrics.py @@ -6,18 +6,21 @@ import asyncio import inspect +import logging import os import socket import time from typing import Literal -from pydantic import BaseModel, ConfigDict, Field, model_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator from .settings import get_settings, logger DEFAULT_BUCKETS_MS = [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000, 120000] METRICS_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" +OTLP_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT" +SEMCONV_ENV = "OTEL_SEMCONV_STABILITY_OPT_IN" INSTALL_HINT = "pip install '10xgraph-api[metrics]'" # Bound for the final metrics push and AI span flush at shutdown (each, run at the same time). TELEMETRY_FLUSH_SECONDS = 5.0 @@ -39,6 +42,13 @@ class MetricsPrometheusConfig(_Section): path: str = Field(default="/metrics", pattern=r"^/") worker_port: int = Field(default=9464, ge=1, le=65535) + @field_validator("path") + @classmethod + def _not_root(cls, path: str) -> str: + if path == "/": + raise ValueError('metrics.prometheus.path must not be "/"') + return path + class MetricsConfig(_Section): """``metrics`` in 10xgraph.json. Example:: @@ -69,11 +79,13 @@ def _check(self) -> "MetricsConfig": if self.export is None: raise ValueError('metrics.export is required when enabled: "otlp" or "prometheus"') if self.export == "otlp" and not ( - self.otlp.endpoint or os.environ.get(METRICS_ENDPOINT_ENV) + self.otlp.endpoint + or os.environ.get(METRICS_ENDPOINT_ENV) + or os.environ.get(OTLP_ENDPOINT_ENV) ): raise ValueError( "metrics.otlp.endpoint is required for export=otlp " - f"(or set {METRICS_ENDPOINT_ENV})" + f"(or set {METRICS_ENDPOINT_ENV} or {OTLP_ENDPOINT_ENV})" ) return self @@ -96,9 +108,14 @@ def resolve_use_metrics_endpoint(obs_cfg: dict, metrics_cfg: MetricsConfig) -> d if not otlp.get("enabled") or not otlp.get("use_metrics_endpoint"): return obs_cfg otlp = dict(otlp) - endpoint = ( - metrics_cfg.otlp.endpoint if metrics_cfg.enabled and metrics_cfg.export == "otlp" else None - ) + endpoint = None + if metrics_cfg.enabled and metrics_cfg.export == "otlp": + # The exporter reads the same env vars when metrics.otlp.endpoint is not set. + endpoint = ( + metrics_cfg.otlp.endpoint + or os.environ.get(METRICS_ENDPOINT_ENV) + or os.environ.get(OTLP_ENDPOINT_ENV) + ) if endpoint: # Core appends /v1/traces to the base URL. base = endpoint.rstrip("/") @@ -113,20 +130,70 @@ def resolve_use_metrics_endpoint(obs_cfg: dict, metrics_cfg: MetricsConfig) -> d else: logger.warning( "observability.otlp.use_metrics_endpoint needs metrics enabled with export=otlp and " - "metrics.otlp.endpoint set; the otlp trace destination is off." + "an OTLP endpoint (metrics.otlp.endpoint or %s); the otlp trace destination is off.", + METRICS_ENDPOINT_ENV, ) otlp["enabled"] = False return {**obs_cfg, "otlp": otlp} +# The provider of the last setup_metrics call; a rebuild in one process shuts it down first. +_providers: list = [] + + +def _use_stable_http_names() -> None: + """Make sure ``OTEL_SEMCONV_STABILITY_OPT_IN`` has ``http`` (or ``http/dup``), keeping other + values, so HTTP metrics are ``http.server.request.duration`` (seconds). Must run before the + first instrumentation: OpenTelemetry reads the variable once and caches it.""" + values = [v.strip() for v in os.environ.get(SEMCONV_ENV, "").split(",") if v.strip()] + if "http" not in values and "http/dup" not in values: + os.environ[SEMCONV_ENV] = ",".join([*values, "http"]) + try: + from opentelemetry.instrumentation._semconv import ( + _OpenTelemetrySemanticConventionStability as stability, + ) + from opentelemetry.instrumentation._semconv import ( + _OpenTelemetryStabilitySignalType, + _StabilityMode, + ) + + # The mode is cached on first use; a read before our merge may have missed "http". + cached = ( + stability._initialized + and stability._get_opentelemetry_stability_opt_in_mode( + _OpenTelemetryStabilitySignalType.HTTP + ) + == _StabilityMode.DEFAULT + ) + except Exception: # a best-effort diagnostic: never block the setup + return + if cached: + logger.warning( + "OpenTelemetry read %s before the metrics block set it: HTTP metrics will use the " + "old names (http.server.duration, in ms). Set %s=http in the environment.", + SEMCONV_ENV, + SEMCONV_ENV, + ) + + +def _shutdown_previous_provider() -> None: + """Shut down the provider of an earlier setup in this process (app built twice in tests or + when embedding); this also unregisters its Prometheus collector, so families are not doubled.""" + while _providers: + try: + _providers.pop().shutdown() + except Exception as exc: + logger.warning("Previous MeterProvider shutdown failed: %r", exc) + + def setup_metrics(metrics_cfg: MetricsConfig, *, role: Literal["api", "worker"]): """Install the global MeterProvider and bridge core's counters and timers to it. Returns the provider, or None when the OpenTelemetry packages are missing or the setup fails (never raises). """ - # Before any instrumentation: the stable HTTP names (http.server.request.duration, in s). - os.environ.setdefault("OTEL_SEMCONV_STABILITY_OPT_IN", "http") + _use_stable_http_names() + _shutdown_previous_provider() try: from opentelemetry import metrics from tenxgraph.utils.metrics import setup_otel_metrics @@ -139,6 +206,7 @@ def setup_metrics(metrics_cfg: MetricsConfig, *, role: Literal["api", "worker"]) logger.warning("Metrics are not exported: setup failed: %s", exc) return None + _providers.append(provider) metrics.set_meter_provider(provider) if metrics.get_meter_provider() is not provider: # Set earlier by opentelemetry-instrument, logfire.configure() or an imported module. @@ -193,10 +261,15 @@ def _build_provider(metrics_cfg: MetricsConfig): def _announce_scrape(metrics_cfg: MetricsConfig, role: str) -> None: if role == "api": - logger.info( + from tenxgraph_api.src.app.loader import _web_concurrency + + workers = _web_concurrency() + logger.log( + logging.WARNING if workers > 1 else logging.INFO, "Prometheus scrape at %s covers this process only; with several gunicorn " - "workers use OTLP push instead.", + "workers (WEB_CONCURRENCY=%d) use OTLP push instead.", metrics_cfg.prometheus.path, + workers, ) return port = metrics_cfg.prometheus.worker_port @@ -261,6 +334,21 @@ async def _bounded(what: str, call) -> None: ) +def check_scrape_route(app, scrape_route) -> None: + """Refuse to start when another route answers on the scrape path. The scrape path is open + to the auth guard and the rate limit, so a real route there would lose both. Call after all + routers are included. ``scrape_route`` is the route the metrics block added (or None).""" + if scrape_route is None: + return + for route in app.routes: + regex = getattr(route, "path_regex", None) + if route is not scrape_route and regex is not None and regex.match(scrape_route.path): + raise ValueError( + f"metrics.prometheus.path {scrape_route.path!r} clashes with the route " + f"{route.path!r}; choose another path such as /_metrics." + ) + + def native_telemetry_off() -> dict: """Keyword arguments for ``FastAPI(...)`` that turn its native OpenTelemetry off. diff --git a/tenxgraph_api/src/app/core/config/setup_middleware.py b/tenxgraph_api/src/app/core/config/setup_middleware.py index d2d57f8f..545f1d7f 100644 --- a/tenxgraph_api/src/app/core/config/setup_middleware.py +++ b/tenxgraph_api/src/app/core/config/setup_middleware.py @@ -1,3 +1,5 @@ +import os +import re import uuid from dataclasses import replace from datetime import datetime @@ -478,12 +480,26 @@ def _setup_otel_metrics(resource, endpoint: str | None) -> None: logger.info("OTEL: metrics enabled") -def _instrument_fastapi(app: FastAPI, *, tracing: bool, meter_provider=None) -> bool: +def _excluded_urls(app: FastAPI, scrape_path: str) -> str: + """Comma-separated URL regexes the instrumentation skips: the scrape route (so scraping does + not show up in the HTTP metrics) plus whatever the standard env variables already exclude. + Passing ``excluded_urls`` replaces those variables, hence they are merged here.""" + own = rf"^\w+://[^/]*{re.escape(app.root_path)}{re.escape(scrape_path)}(\?|$)" + env = os.environ.get("OTEL_PYTHON_FASTAPI_EXCLUDED_URLS") or os.environ.get( + "OTEL_PYTHON_EXCLUDED_URLS", "" + ) + return f"{env},{own}" if env else own + + +def _instrument_fastapi( + app: FastAPI, *, tracing: bool, meter_provider=None, scrape_path: str | None = None +) -> bool: """The one ``instrument_app`` call (it can run only once per app). HTTP spans go to the global tracer provider only with the legacy ``OTEL_ENABLED`` path; otherwise they go to a no-op one, so they never reach an AI tracing backend. HTTP metrics - go to ``meter_provider`` when given, else to the global one. + go to ``meter_provider`` when given, else to the global one. ``scrape_path`` is left out of + both. """ try: from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor @@ -495,6 +511,8 @@ def _instrument_fastapi(app: FastAPI, *, tracing: bool, meter_provider=None) -> ) return False kwargs: dict[str, Any] = {} + if scrape_path: + kwargs["excluded_urls"] = _excluded_urls(app, scrape_path) if not tracing: kwargs["tracer_provider"] = NoOpTracerProvider() if meter_provider is not None: @@ -503,7 +521,7 @@ def _instrument_fastapi(app: FastAPI, *, tracing: bool, meter_provider=None) -> return True -def _setup_otel(app: FastAPI, settings, meter_provider=None) -> bool: +def _setup_otel(app: FastAPI, settings, meter_provider=None, scrape_path=None) -> bool: """Configure OpenTelemetry tracing and instrument the FastAPI app. ``meter_provider`` is the one from the ``metrics`` block; it then owns the metrics. @@ -511,7 +529,9 @@ def _setup_otel(app: FastAPI, settings, meter_provider=None) -> bool: """ if not setup_otel_provider(settings, metrics_owned=meter_provider is not None): return False - if not _instrument_fastapi(app, tracing=True, meter_provider=meter_provider): + if not _instrument_fastapi( + app, tracing=True, meter_provider=meter_provider, scrape_path=scrape_path + ): return False logger.info("OTEL: FastAPI instrumentation active (service=%s)", settings.OTEL_SERVICE_NAME) return True @@ -526,6 +546,7 @@ def scrape() -> Response: return Response(generate_latest(REGISTRY), media_type=CONTENT_TYPE_LATEST) app.add_api_route(path, scrape, methods=["GET"], include_in_schema=False) + app.state.scrape_route = app.routes[-1] def _setup_metrics(app: FastAPI, graph_config: GraphConfig | None): @@ -571,15 +592,18 @@ def setup_middleware( app.add_exception_handler(StaleStateError, _stale_state_handler) meter_provider = _setup_metrics(app, graph_config) + scrape_path = graph_config.metrics.scrape_path if graph_config is not None else None instrumented = False if settings.OTEL_ENABLED: - instrumented = _setup_otel(app, settings, meter_provider) + instrumented = _setup_otel(app, settings, meter_provider, scrape_path) if container is not None: _attach_otel_publisher(container, settings, graph_config) # HTTP metrics once: here unless the legacy path instrumented (it can fail, e.g. no endpoint). if meter_provider is not None and not instrumented: - _instrument_fastapi(app, tracing=False, meter_provider=meter_provider) + _instrument_fastapi( + app, tracing=False, meter_provider=meter_provider, scrape_path=scrape_path + ) # Declarative Logfire/LangSmith wiring from 10xgraph.json (before compile). _setup_observability(container, graph_config) @@ -610,7 +634,6 @@ def setup_middleware( path_limits=_upload_path_limits(settings.MAX_REQUEST_SIZE), ) - scrape_path = graph_config.metrics.scrape_path if graph_config is not None else None # Apply rate limiting only when configured in 10xgraph.json if graph_config is not None: rate_limit_config = graph_config.rate_limit diff --git a/tenxgraph_api/src/app/main.py b/tenxgraph_api/src/app/main.py index c9e9907a..e1992107 100644 --- a/tenxgraph_api/src/app/main.py +++ b/tenxgraph_api/src/app/main.py @@ -22,7 +22,11 @@ assert_all_routes_protected, ) from tenxgraph_api.src.app.core.config.graph_config import GraphConfig, default_config_path -from tenxgraph_api.src.app.core.config.metrics import flush_telemetry, native_telemetry_off +from tenxgraph_api.src.app.core.config.metrics import ( + check_scrape_route, + flush_telemetry, + native_telemetry_off, +) from tenxgraph_api.src.app.loader import attach_all_modules, load_container from tenxgraph_api.src.app.routers import init_routes @@ -301,6 +305,9 @@ async def lifespan(app: FastAPI): tasks=graph_config.worker is not None, ) +# The scrape path is open to the auth guard and rate limit: no real route may share it. +check_scrape_route(app, getattr(app.state, "scrape_route", None)) + # Secure by construction: refuse to boot if any non-public route forgot its # RequirePermission guard (a forgotten guard would otherwise ship an open endpoint). # The Prometheus scrape route is public too (scrapers send no tokens). diff --git a/tests/unit_tests/test_metrics.py b/tests/unit_tests/test_metrics.py index 187107bd..0793332b 100644 --- a/tests/unit_tests/test_metrics.py +++ b/tests/unit_tests/test_metrics.py @@ -597,3 +597,177 @@ def test_setup_observability_runs_for_otlp_alone(): ): sm._setup_observability(container, _graph_config(MetricsConfig(), observability=obs)) core_setup.assert_called_once() + + +# ── deferred review fixes ──────────────────────────────────────────────────── + + +@pytest.fixture +def semconv_env(monkeypatch): + monkeypatch.delenv("OTEL_SEMCONV_STABILITY_OPT_IN", raising=False) + return monkeypatch + + +@pytest.mark.parametrize( + ("preset", "expected"), + [ + (None, "http"), + ("database", "database,http"), + (" database , gen_ai ", "database,gen_ai,http"), + ("http", "http"), + ("database,http/dup", "database,http/dup"), + ], +) +def test_semconv_opt_in_is_merged_not_replaced(semconv_env, preset, expected): + if preset is not None: + semconv_env.setenv("OTEL_SEMCONV_STABILITY_OPT_IN", preset) + metrics_module._use_stable_http_names() + assert os.environ["OTEL_SEMCONV_STABILITY_OPT_IN"] == expected + + +def _fake_semconv(initialized, *, http_mode="default"): + fake = MagicMock() + fake._StabilityMode.DEFAULT = "default" + stability = fake._OpenTelemetrySemanticConventionStability + stability._initialized = initialized + stability._get_opentelemetry_stability_opt_in_mode.return_value = http_mode + return {"opentelemetry.instrumentation._semconv": fake} + + +def test_semconv_already_read_warns_about_old_names(semconv_env, caplog): + with ( + patch.dict("sys.modules", _fake_semconv(True)), + caplog.at_level(logging.WARNING, logger="10xgraph-api"), + ): + metrics_module._use_stable_http_names() + assert any("old names" in r.message and r.levelno == logging.WARNING for r in caplog.records) + + +def test_semconv_already_read_with_http_mode_does_not_warn(semconv_env, caplog): + with ( + patch.dict("sys.modules", _fake_semconv(True, http_mode="http")), + caplog.at_level(logging.WARNING, logger="10xgraph-api"), + ): + metrics_module._use_stable_http_names() + assert not any("old names" in r.message for r in caplog.records) + + +def test_semconv_not_yet_read_does_not_warn(semconv_env, caplog): + with ( + patch.dict("sys.modules", _fake_semconv(False)), + caplog.at_level(logging.WARNING, logger="10xgraph-api"), + ): + metrics_module._use_stable_http_names() + assert not any("old names" in r.message for r in caplog.records) + + +def test_scrape_path_must_not_be_root_or_relative(): + with pytest.raises(ValidationError): + MetricsConfig(prometheus={"path": "/"}) + with pytest.raises(ValidationError): + MetricsConfig(prometheus={"path": "metrics"}) + + +def _app_with_scrape(path, *real_paths): + app = FastAPI() + for real in real_paths: + app.add_api_route(real, lambda: None, methods=["GET"]) + app.add_api_route(path, lambda: None, methods=["GET"]) + app.state.scrape_route = app.routes[-1] + return app + + +def test_scrape_route_clash_with_real_route_fails_naming_it(): + app = _app_with_scrape("/ping", "/ping") + with pytest.raises(ValueError, match="'/ping'"): + metrics_module.check_scrape_route(app, app.state.scrape_route) + + +def test_scrape_route_clash_with_parameterised_route_fails(): + app = _app_with_scrape("/v1/threads/metrics", "/v1/threads/{thread_id}") + with pytest.raises(ValueError, match=r"/v1/threads/\{thread_id\}"): + metrics_module.check_scrape_route(app, app.state.scrape_route) + + +def test_scrape_route_without_clash_or_without_route_passes(): + app = _app_with_scrape("/metrics", "/ping", "/v1/graph") + metrics_module.check_scrape_route(app, app.state.scrape_route) + metrics_module.check_scrape_route(app, None) + + +def test_prometheus_notice_is_a_warning_with_several_workers(monkeypatch, caplog): + monkeypatch.setenv("WEB_CONCURRENCY", "4") + cfg = MetricsConfig(enabled=True, export="prometheus") + with caplog.at_level(logging.INFO, logger="10xgraph-api"): + metrics_module._announce_scrape(cfg, "api") + record = next(r for r in caplog.records if "this process only" in r.message) + assert record.levelno == logging.WARNING + + +def test_prometheus_notice_is_info_with_one_worker(monkeypatch, caplog): + monkeypatch.delenv("WEB_CONCURRENCY", raising=False) + cfg = MetricsConfig(enabled=True, export="prometheus") + with caplog.at_level(logging.INFO, logger="10xgraph-api"): + metrics_module._announce_scrape(cfg, "api") + record = next(r for r in caplog.records if "this process only" in r.message) + assert record.levelno == logging.INFO + + +def test_scrape_path_is_excluded_from_http_metrics_and_env_exclusions_kept(monkeypatch): + import re + + monkeypatch.setenv("OTEL_PYTHON_FASTAPI_EXCLUDED_URLS", "healthz") + modules = _otel_modules() + app = FastAPI() + with patch.dict("sys.modules", modules): + sm._instrument_fastapi( + app, tracing=False, meter_provider=MagicMock(), scrape_path="/metrics" + ) + instrument = modules["opentelemetry.instrumentation.fastapi"].FastAPIInstrumentor.instrument_app + excluded = instrument.call_args.kwargs["excluded_urls"] + env_part, own = excluded.split(",", 1) + assert env_part == "healthz" + assert re.search(own, "http://host:8000/metrics") + assert re.search(own, "http://host/metrics?x=1") + assert not re.search(own, "http://host/v1/metrics") + assert not re.search(own, "http://host/metrics/extra") + + +def test_rebuild_shuts_down_the_previous_provider(monkeypatch): + monkeypatch.setattr(metrics_module, "_providers", []) + modules = _otel_modules() + first, second = MagicMock(), MagicMock() + modules["opentelemetry.sdk.metrics"].MeterProvider.side_effect = [first, second] + with patch.dict("sys.modules", modules), patch("tenxgraph.utils.metrics.setup_otel_metrics"): + setup_metrics(_cfg(), role="api") + first.shutdown.assert_not_called() + setup_metrics(_cfg(), role="api") + first.shutdown.assert_called_once_with() + second.shutdown.assert_not_called() + + +def test_use_metrics_endpoint_reads_exporter_env_when_endpoint_unset(monkeypatch): + monkeypatch.setenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://m:4318/v1/metrics") + monkeypatch.setenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://shared:4318") + cfg = MetricsConfig(enabled=True, export="otlp") # valid only because the env is set + assert resolve_use_metrics_endpoint(OBS, cfg)["otlp"]["endpoint"] == "http://m:4318" + monkeypatch.delenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT") + assert resolve_use_metrics_endpoint(OBS, cfg)["otlp"]["endpoint"] == "http://shared:4318" + + +def test_use_metrics_endpoint_env_prometheus_url_still_warns(monkeypatch): + monkeypatch.setenv( + "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", "http://p:9090/api/v1/otlp/v1/metrics" + ) + cfg = MetricsConfig(enabled=True, export="otlp") + with patch.object(sm.logger, "warning") as warning: + resolved = resolve_use_metrics_endpoint(OBS, cfg) + assert resolved["otlp"]["endpoint"] == "http://p:9090/api/v1/otlp" + assert "Prometheus accepts metrics only" in warning.call_args.args[0] + + +def test_use_metrics_endpoint_without_any_endpoint_still_disables(monkeypatch): + monkeypatch.delenv("OTEL_EXPORTER_OTLP_METRICS_ENDPOINT", raising=False) + monkeypatch.delenv("OTEL_EXPORTER_OTLP_ENDPOINT", raising=False) + cfg = MetricsConfig.model_construct(enabled=True, export="otlp", otlp=MetricsConfig().otlp) + assert resolve_use_metrics_endpoint(OBS, cfg)["otlp"]["enabled"] is False diff --git a/tests/unit_tests/test_metrics_real_packages.py b/tests/unit_tests/test_metrics_real_packages.py new file mode 100644 index 00000000..137289a9 --- /dev/null +++ b/tests/unit_tests/test_metrics_real_packages.py @@ -0,0 +1,77 @@ +"""End-to-end check with the real OpenTelemetry and Prometheus packages (skipped without them).""" + +from unittest.mock import MagicMock, patch + +import pytest +from fastapi import FastAPI +from fastapi.testclient import TestClient + + +pytest.importorskip("opentelemetry.sdk") +pytest.importorskip("opentelemetry.exporter.prometheus") +pytest.importorskip("opentelemetry.instrumentation.fastapi") +prometheus_client = pytest.importorskip("prometheus_client") + +from tenxgraph_api.src.app.core.config import metrics as metrics_module +from tenxgraph_api.src.app.core.config import setup_middleware as sm +from tenxgraph_api.src.app.core.config.metrics import MetricsConfig + + +HTTP_OK = 200 + + +def _build_app(): + graph_config = MagicMock() + graph_config.metrics = MetricsConfig( + enabled=True, export="prometheus", prometheus={"path": "/_scrape"} + ) + graph_config.rate_limit = None + graph_config.observability = None + settings = MagicMock() + settings.OTEL_ENABLED = False + settings.ORIGINS = "http://localhost" + settings.CORS_ALLOW_CREDENTIALS = True + settings.MODE = "development" + settings.ALLOWED_HOST = "*" + settings.SECURITY_HEADERS_ENABLED = False + settings.MAX_REQUEST_SIZE = 1_000_000 + app = FastAPI() + + @app.get("/items/{item_id}") + def item(item_id: int) -> dict: + return {"id": item_id} + + with ( + patch.object(sm, "get_settings", return_value=settings), + patch.object(sm, "init_sentry"), + patch("tenxgraph.utils.metrics.setup_otel_metrics"), + ): + sm.setup_middleware(app, graph_config=graph_config, container=None) + return app + + +@pytest.fixture(autouse=True) +def _clean_registry(): + yield + metrics_module._shutdown_previous_provider() # unregisters the Prometheus collector + + +def test_scrape_has_http_route_histogram_without_the_scrape_route(): + client = TestClient(_build_app()) + assert client.get("/items/7").status_code == HTTP_OK + assert client.get("/_scrape").status_code == HTTP_OK # not itself measured + body = client.get("/_scrape").text + + assert "http_server_request_duration_seconds_bucket" in body + assert 'http_route="/items/{item_id}"' in body + assert "/_scrape" not in body + + +def test_building_twice_does_not_duplicate_families(): + TestClient(_build_app()).get("/items/1") # the first app has data when the second is built + TestClient(_build_app()).get("/items/1") + text = prometheus_client.generate_latest(prometheus_client.REGISTRY).decode() + helps = [ + line for line in text.splitlines() if line.startswith("# HELP http_server_request_duration") + ] + assert len(helps) == 1 diff --git a/tests/unit_tests/test_setup_middleware.py b/tests/unit_tests/test_setup_middleware.py index 0c2950d8..a8c13aa2 100644 --- a/tests/unit_tests/test_setup_middleware.py +++ b/tests/unit_tests/test_setup_middleware.py @@ -453,7 +453,7 @@ def test_setup_middleware_all(): assert len(rate_limit) == 1 assert rate_limit[0].kwargs == {"config": rate_limit_config, "container": container} assert not hasattr(app.state, "rate_limit_backend") - mock_setup_otel.assert_called_once_with(app, settings, None) + mock_setup_otel.assert_called_once_with(app, settings, None, None) mock_attach.assert_called_once_with(container, settings, graph_config) From 652961b4a4bd037e19463affed090624d2cbd72b Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Mon, 9 Nov 2026 20:38:03 +0600 Subject: [PATCH 4/9] Enhance MonitoringMiddleware to log task lifecycle events and publish only to event publishers - Updated MonitoringMiddleware to ensure task failures are logged as ERROR and counted, but not published as events. - Modified event publishing to restrict to event publishers only, using the `only="events"` argument. - Adjusted test cases to verify the correct behavior of event publishing and logging, ensuring failures are not published. - Added a fixture to track the `only` argument in publish_event calls for testing purposes. --- config-editor-ui/src/telemetry.jsx | 13 ++++++ tenxgraph_api/cli/config_editor/schema.py | 3 +- .../cli/config_editor/static/app.css | 2 +- tenxgraph_api/cli/config_editor/static/app.js | 6 +-- .../publishers-and-runtime-protocols.md | 7 ++++ tenxgraph_api/jobs/monitoring.py | 29 +++++-------- tests/unit_tests/jobs/test_middleware.py | 42 ++++++++++++------- 7 files changed, 65 insertions(+), 37 deletions(-) diff --git a/config-editor-ui/src/telemetry.jsx b/config-editor-ui/src/telemetry.jsx index 9e390fb4..fe618737 100644 --- a/config-editor-ui/src/telemetry.jsx +++ b/config-editor-ui/src/telemetry.jsx @@ -210,6 +210,17 @@ function DestinationCard({ dest }) { ); } +// Shown with and without a destination, so the difference is visible while picking one. +function KindNote() { + return ( +

+ These are observability publishers. They get graph, node, LLM and tool spans, AI errors included. + Event publishers (Kafka, Redis, RabbitMQ) are set in code and also get task events. + API and worker errors go to Metrics and logs. +

+ ); +} + export function TracingWidget() { const [adding, setAdding] = useState(false); const chosen = DESTINATIONS.filter((d) => isPicked(d.key)); @@ -226,6 +237,7 @@ export function TracingWidget() { onChange={(v) => { if (v) addDestination(v); }} />

Graph runs are not traced.

+
) : ( <> @@ -247,6 +259,7 @@ export function TracingWidget() { ) ) : null} +

Trace settings

:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1{row-gap:var(--spacing)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-line>:not(:last-child)){border-color:var(--color-line)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-\[10px\]{border-radius:10px}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-brand{border-color:var(--color-brand)}.border-brand-solid{border-color:var(--color-brand-solid)}.border-brand\/20{border-color:var(--color-brand)}@supports (color:color-mix(in lab, red, red)){.border-brand\/20{border-color:color-mix(in oklab, var(--color-brand) 20%, transparent)}}.border-danger\/25{border-color:var(--color-danger)}@supports (color:color-mix(in lab, red, red)){.border-danger\/25{border-color:color-mix(in oklab, var(--color-danger) 25%, transparent)}}.border-line{border-color:var(--color-line)}.border-line-strong{border-color:var(--color-line-strong)}.border-transparent{border-color:#0000}.border-warn\/25{border-color:var(--color-warn)}@supports (color:color-mix(in lab, red, red)){.border-warn\/25{border-color:color-mix(in oklab, var(--color-warn) 25%, transparent)}}.bg-brand{background-color:var(--color-brand)}.bg-brand-soft{background-color:var(--color-brand-soft)}.bg-brand-solid{background-color:var(--color-brand-solid)}.bg-danger{background-color:var(--color-danger)}.bg-danger-soft{background-color:var(--color-danger-soft)}.bg-line{background-color:var(--color-line)}.bg-line-strong{background-color:var(--color-line-strong)}.bg-ok-soft{background-color:var(--color-ok-soft)}.bg-raised{background-color:var(--color-raised)}.bg-sunken{background-color:var(--color-sunken)}.bg-surface{background-color:var(--color-surface)}.bg-transparent{background-color:#0000}.bg-warn{background-color:var(--color-warn)}.bg-warn-soft{background-color:var(--color-warn-soft)}.bg-white{background-color:var(--color-white)}.fill-brand-amber{fill:var(--color-brand-amber)}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0{padding-block:0}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-14{padding-block:calc(var(--spacing) * 14)}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-0\.5{padding-right:calc(var(--spacing) * .5)}.pr-1{padding-right:var(--spacing)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-24{padding-bottom:calc(var(--spacing) * 24)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-body{color:var(--color-body)}.text-brand{color:var(--color-brand)}.text-danger{color:var(--color-danger)}.text-faint,.text-faint\/70{color:var(--color-faint)}@supports (color:color-mix(in lab, red, red)){.text-faint\/70{color:color-mix(in oklab, var(--color-faint) 70%, transparent)}}.text-ink{color:var(--color-ink)}.text-muted{color:var(--color-muted)}.text-ok{color:var(--color-ok)}.text-warn{color:var(--color-warn)}.text-white{color:var(--color-white)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.accent-brand-solid{accent-color:var(--color-brand-solid)}.opacity-50{opacity:.5}.opacity-80{opacity:.8}.shadow-card{--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-pop{--tw-shadow:var(--af-shadow-pop);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-\[border-color\,box-shadow\]{transition-property:border-color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-open\:rotate-90:is(:where(.group):is([open],:popover-open,:open) *){rotate:90deg}@media (hover:hover){.group-hover\:text-brand:is(:where(.group):hover *){color:var(--color-brand)}}.placeholder\:text-faint::placeholder{color:var(--color-faint)}.backdrop\:bg-black\/45::backdrop{background-color:#00000073}@supports (color:color-mix(in lab, red, red)){.backdrop\:bg-black\/45::backdrop{background-color:color-mix(in oklab, var(--color-black) 45%, transparent)}}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:-inset-2\.5:before{content:var(--tw-content);inset:calc(var(--spacing) * -2.5)}.before\:top-6:before{content:var(--tw-content);top:calc(var(--spacing) * 6)}.before\:bottom-2:before{content:var(--tw-content);bottom:calc(var(--spacing) * 2)}.before\:-left-2\.5:before{content:var(--tw-content);left:calc(var(--spacing) * -2.5)}.before\:w-\[3px\]:before{content:var(--tw-content);width:3px}.before\:rounded:before{content:var(--tw-content);border-radius:.25rem}.before\:bg-brand:before{content:var(--tw-content);background-color:var(--color-brand)}.before\:content-\[\'\'\]:before{--tw-content:"";content:var(--tw-content)}.last\:border-b:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.focus-within\:border-brand:focus-within{border-color:var(--color-brand)}.focus-within\:ring-4:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-within\:ring-brand-ring:focus-within{--tw-ring-color:var(--color-brand-ring)}@media (hover:hover){.hover\:border-brand:hover{border-color:var(--color-brand)}.hover\:border-line-strong:hover{border-color:var(--color-line-strong)}.hover\:bg-hover:hover{background-color:var(--color-hover)}.hover\:bg-raised:hover{background-color:var(--color-raised)}.hover\:text-brand:hover{color:var(--color-brand)}.hover\:text-danger:hover{color:var(--color-danger)}.hover\:text-ink:hover{color:var(--color-ink)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:fixed:focus{position:fixed}.focus\:top-3:focus{top:calc(var(--spacing) * 3)}.focus\:left-3:focus{left:calc(var(--spacing) * 3)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (prefers-reduced-motion:reduce){.motion-reduce\:transition-none{transition-property:none}}@media not all and (min-width:64rem){.max-lg\:sticky{position:sticky}.max-lg\:top-14{top:calc(var(--spacing) * 14)}.max-lg\:z-20{z-index:20}.max-lg\:mx-4{margin-inline:calc(var(--spacing) * 4)}.max-lg\:mt-3{margin-top:calc(var(--spacing) * 3)}.max-lg\:contents{display:contents}.max-lg\:flex{display:flex}.max-lg\:hidden{display:none}.max-lg\:min-h-11{min-height:calc(var(--spacing) * 11)}.max-lg\:shrink-0{flex-shrink:0}.max-lg\:gap-0\.5{gap:calc(var(--spacing) * .5)}.max-lg\:overflow-x-auto{overflow-x:auto}.max-lg\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.max-lg\:border-line{border-color:var(--color-line)}.max-lg\:bg-surface{background-color:var(--color-surface)}.max-lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.max-lg\:py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.max-lg\:whitespace-nowrap{white-space:nowrap}}@media (min-width:40rem){.sm\:left-auto{left:auto}.sm\:flex{display:flex}.sm\:inline{display:inline}.sm\:size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.sm\:h-6{height:calc(var(--spacing) * 6)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1\.1fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1.1fr)}.sm\:grid-cols-\[minmax\(0\,260px\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,260px) minmax(0,1fr)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-8{gap:calc(var(--spacing) * 8)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:px-8{padding-inline:calc(var(--spacing) * 8)}.sm\:py-0\.5{padding-block:calc(var(--spacing) * .5)}.sm\:py-1{padding-block:var(--spacing)}.sm\:pt-0\.5{padding-top:calc(var(--spacing) * .5)}.sm\:pt-8{padding-top:calc(var(--spacing) * 8)}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:before\:-left-3\.5:before{content:var(--tw-content);left:calc(var(--spacing) * -3.5)}}@media (min-width:48rem){.md\:block{display:block}.md\:inline{display:inline}}@media (min-width:64rem){.lg\:sticky{position:sticky}.lg\:top-14{top:calc(var(--spacing) * 14)}.lg\:block{display:block}.lg\:grid{display:grid}.lg\:inline-flex{display:inline-flex}.lg\:h-\[calc\(100dvh-3\.5rem\)\]{height:calc(100dvh - 3.5rem)}.lg\:-translate-x-6{--tw-translate-x:calc(var(--spacing) * -6);translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:grid-cols-\[260px_minmax\(0\,1fr\)\]{grid-template-columns:260px minmax(0,1fr)}.lg\:items-start{align-items:flex-start}.lg\:overflow-y-auto{overflow-y:auto}.lg\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.lg\:border-line{border-color:var(--color-line)}.lg\:bg-surface{background-color:var(--color-surface)}.lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.lg\:pt-4{padding-top:calc(var(--spacing) * 4)}.lg\:pb-16{padding-bottom:calc(var(--spacing) * 16)}.lg\:opacity-0{opacity:0}.lg\:opacity-100{opacity:1}.lg\:transition-\[transform\,opacity\]{transition-property:transform,opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.lg\:duration-200{--tw-duration:.2s;transition-duration:.2s}.lg\:ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}:root{color-scheme:light;--af-canvas:#f5f7fb;--af-surface:#fff;--af-raised:#f1f5f9;--af-hover:#e8eef6;--af-sunken:#fff;--af-line:#0f172a17;--af-line-strong:#0f172a2b;--af-ink:#0b1220;--af-body:#1e293b;--af-muted:#475569;--af-faint:#64748b;--af-brand:#2563eb;--af-brand-solid:#2563eb;--af-brand-solid-hover:#1d4ed8;--af-brand-soft:#2563eb14;--af-brand-ring:#2563eb38;--af-brand-amber:#c8692f;--af-ok:#047857;--af-ok-soft:#10b9811a;--af-warn:#b45309;--af-warn-soft:#f59e0b1f;--af-danger:#dc2626;--af-danger-soft:#ef444417;--af-shadow:0 1px 2px #0f172a0a, 0 1px 3px #0f172a0a;--af-shadow-pop:0 12px 32px #0f172a24;--af-scrollbar:#0f172a33;--af-scrollbar-hover:#0f172a59;--af-json-key:#1d4ed8;--af-json-string:#047857;--af-json-number:#b45309;--af-json-literal:#7c3aed}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--af-canvas:#08111f;--af-surface:#0c1727;--af-raised:#131e33;--af-hover:#1a2741;--af-sunken:#07101d;--af-line:#94a3b824;--af-line-strong:#94a3b842;--af-ink:#f8fafc;--af-body:#e2e8f0;--af-muted:#a3b1c6;--af-faint:#8391a7;--af-brand:#60a5fa;--af-brand-solid:#2563eb;--af-brand-solid-hover:#3b82f6;--af-brand-soft:#3b82f624;--af-brand-ring:#3b82f659;--af-brand-amber:#e9a57c;--af-ok:#34d399;--af-ok-soft:#10b98124;--af-warn:#fbbf24;--af-warn-soft:#f59e0b21;--af-danger:#f87171;--af-danger-soft:#ef444421;--af-shadow:0 1px 2px #02061766;--af-shadow-pop:0 16px 40px #0206178c;--af-scrollbar:#94a3b838;--af-scrollbar-hover:#94a3b866;--af-json-key:#93c5fd;--af-json-string:#6ee7b7;--af-json-number:#fcd34d;--af-json-literal:#c4b5fd}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:0s!important;animation-duration:0s!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-space-y-reverse:0;--tw-divide-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-duration:initial;--tw-ease:initial;--tw-content:""}}}@layer theme{:root,:host{--font-sans:"Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;--font-mono:"JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-xs:20rem;--container-sm:24rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--tracking-tight:-.025em;--tracking-normal:0em;--tracking-wide:.025em;--tracking-wider:.05em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--ease-out:cubic-bezier(0, 0, .2, 1);--animate-spin:spin 1s linear infinite;--animate-pulse:pulse 2s cubic-bezier(.4, 0, .6, 1) infinite;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-canvas:var(--af-canvas);--color-surface:var(--af-surface);--color-raised:var(--af-raised);--color-hover:var(--af-hover);--color-sunken:var(--af-sunken);--color-line:var(--af-line);--color-line-strong:var(--af-line-strong);--color-ink:var(--af-ink);--color-body:var(--af-body);--color-muted:var(--af-muted);--color-faint:var(--af-faint);--color-brand:var(--af-brand);--color-brand-solid:var(--af-brand-solid);--color-brand-solid-hover:var(--af-brand-solid-hover);--color-brand-soft:var(--af-brand-soft);--color-brand-ring:var(--af-brand-ring);--color-brand-amber:var(--af-brand-amber);--color-ok:var(--af-ok);--color-ok-soft:var(--af-ok-soft);--color-warn:var(--af-warn);--color-warn-soft:var(--af-warn-soft);--color-danger:var(--af-danger);--color-danger-soft:var(--af-danger-soft)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring:where(:not(iframe)){outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html{scrollbar-gutter:stable}body{background-color:var(--color-canvas);font-family:var(--font-sans);color:var(--color-body);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*{scrollbar-width:thin;scrollbar-color:var(--af-scrollbar) transparent}::-webkit-scrollbar{width:10px;height:10px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:var(--af-scrollbar) padding-box;border:3px solid #0000;border-radius:9999px}::-webkit-scrollbar-thumb:hover{background:var(--af-scrollbar-hover) padding-box}::-webkit-scrollbar-corner{background:0 0}}@layer components{.input{height:calc(var(--spacing) * 11);border-radius:var(--radius-lg);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line-strong);background-color:var(--color-sunken);width:100%;padding-inline:calc(var(--spacing) * 3);font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height));color:var(--color-ink);transition-property:border-color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;transition-duration:.15s;display:block}.input::placeholder{color:var(--color-faint)}@media (hover:hover){.input:hover{border-color:var(--color-faint)}@supports (color:color-mix(in lab, red, red)){.input:hover{border-color:color-mix(in oklab, var(--color-faint) 60%, transparent)}}}.input:focus{border-color:var(--color-brand);--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}.input:disabled{cursor:not-allowed;opacity:.5}@media (min-width:40rem){.input{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}}.input-invalid{border-color:var(--color-danger)}.input-invalid:focus{border-color:var(--color-danger);--tw-ring-color:var(--color-danger-soft)}.btn{height:calc(var(--spacing) * 11);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing) * 2);border-radius:var(--radius-lg);padding-inline:calc(var(--spacing) * 3.5);font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height));--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium);white-space:nowrap;transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;flex-shrink:0;transition-duration:.15s;display:inline-flex}.btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}.btn:disabled{cursor:not-allowed;opacity:.45}.btn-primary{background-color:var(--color-brand-solid);color:var(--color-white);--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}@media (hover:hover){.btn-primary:hover{background-color:var(--color-brand-solid-hover)}}.btn-outline{border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line-strong);background-color:var(--color-surface);color:var(--color-body)}@media (hover:hover){.btn-outline:hover{background-color:var(--color-raised)}}.btn-ghost{color:var(--color-muted)}@media (hover:hover){.btn-ghost:hover{background-color:var(--color-raised);color:var(--color-ink)}}.icon-btn{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11);cursor:pointer;border-radius:var(--radius-lg);color:var(--color-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration));--tw-duration:.15s;flex-shrink:0;justify-content:center;align-items:center;transition-duration:.15s;display:inline-flex}@media (hover:hover){.icon-btn:hover{background-color:var(--color-raised);color:var(--color-ink)}}.icon-btn:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}.panel{border-radius:var(--radius-xl);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-line);background-color:var(--color-surface);--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.kbd{height:calc(var(--spacing) * 5);border-style:var(--tw-border-style);border-width:1px;border-color:#ffffff40;border-radius:.25rem;align-items:center;display:inline-flex}@supports (color:color-mix(in lab, red, red)){.kbd{border-color:color-mix(in oklab, var(--color-white) 25%, transparent)}}.kbd{padding-inline:calc(var(--spacing) * 1.5);font-family:var(--font-mono);--tw-font-weight:var(--font-weight-medium);font-size:11px;font-weight:var(--font-weight-medium);color:#fffc}@supports (color:color-mix(in lab, red, red)){.kbd{color:color-mix(in oklab, var(--color-white) 80%, transparent)}}.eyebrow{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height));--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold);--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide);color:var(--color-faint);text-transform:uppercase}.focus-ring:focus-visible{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);--tw-ring-color:var(--color-brand-ring);--tw-outline-style:none;outline-style:none}}@layer utilities{.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.sticky{position:sticky}.top-0{top:0}.top-1\/2{top:50%}.right-3{right:calc(var(--spacing) * 3)}.right-4{right:calc(var(--spacing) * 4)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-3{left:calc(var(--spacing) * 3)}.left-4{left:calc(var(--spacing) * 4)}.z-40{z-index:40}.z-50{z-index:50}.m-0{margin:0}.mx-1{margin-inline:var(--spacing)}.mx-auto{margin-inline:auto}.mt-0\.5{margin-top:calc(var(--spacing) * .5)}.mt-1{margin-top:var(--spacing)}.mt-1\.5{margin-top:calc(var(--spacing) * 1.5)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-2\.5{margin-top:calc(var(--spacing) * 2.5)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-5{margin-top:calc(var(--spacing) * 5)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-7{margin-top:calc(var(--spacing) * 7)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-9{margin-top:calc(var(--spacing) * 9)}.mt-10{margin-top:calc(var(--spacing) * 10)}.-mr-1{margin-right:calc(var(--spacing) * -1)}.mr-1\.5{margin-right:calc(var(--spacing) * 1.5)}.-mb-px{margin-bottom:-1px}.mb-0{margin-bottom:0}.mb-1{margin-bottom:var(--spacing)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-2\.5{margin-bottom:calc(var(--spacing) * 2.5)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-auto{margin-left:auto}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.size-1\.5{width:calc(var(--spacing) * 1.5);height:calc(var(--spacing) * 1.5)}.size-2{width:calc(var(--spacing) * 2);height:calc(var(--spacing) * 2)}.size-3{width:calc(var(--spacing) * 3);height:calc(var(--spacing) * 3)}.size-3\.5{width:calc(var(--spacing) * 3.5);height:calc(var(--spacing) * 3.5)}.size-4{width:calc(var(--spacing) * 4);height:calc(var(--spacing) * 4)}.size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.size-6{width:calc(var(--spacing) * 6);height:calc(var(--spacing) * 6)}.size-7{width:calc(var(--spacing) * 7);height:calc(var(--spacing) * 7)}.size-11{width:calc(var(--spacing) * 11);height:calc(var(--spacing) * 11)}.size-\[18px\]{width:18px;height:18px}.size-full{width:100%;height:100%}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-6{height:calc(var(--spacing) * 6)}.h-7{height:calc(var(--spacing) * 7)}.h-8{height:calc(var(--spacing) * 8)}.h-10{height:calc(var(--spacing) * 10)}.h-12{height:calc(var(--spacing) * 12)}.h-14{height:calc(var(--spacing) * 14)}.h-16{height:calc(var(--spacing) * 16)}.h-auto{height:auto}.h-dvh{height:100dvh}.h-full{height:100%}.max-h-none{max-height:none}.min-h-0{min-height:0}.min-h-6{min-height:calc(var(--spacing) * 6)}.min-h-8{min-height:calc(var(--spacing) * 8)}.min-h-10{min-height:calc(var(--spacing) * 10)}.min-h-11{min-height:calc(var(--spacing) * 11)}.min-h-12{min-height:calc(var(--spacing) * 12)}.min-h-14{min-height:calc(var(--spacing) * 14)}.w-10{width:calc(var(--spacing) * 10)}.w-48{width:calc(var(--spacing) * 48)}.w-80{width:calc(var(--spacing) * 80)}.w-\[min\(540px\,100vw\)\]{width:min(540px,100vw)}.w-full{width:100%}.w-px{width:1px}.max-w-40{max-width:calc(var(--spacing) * 40)}.max-w-\[62ch\]{max-width:62ch}.max-w-\[320px\]{max-width:320px}.max-w-\[420px\]{max-width:420px}.max-w-\[520px\]{max-width:520px}.max-w-\[760px\]{max-width:760px}.max-w-full{max-width:100%}.max-w-prose{max-width:65ch}.max-w-sm{max-width:var(--container-sm)}.max-w-xs{max-width:var(--container-xs)}.min-w-0{min-width:0}.min-w-5{min-width:calc(var(--spacing) * 5)}.min-w-40{min-width:calc(var(--spacing) * 40)}.min-w-56{min-width:calc(var(--spacing) * 56)}.min-w-\[7rem\]{min-width:7rem}.flex-1{flex:1}.shrink-0{flex-shrink:0}.translate-x-0\.5{--tw-translate-x:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-\[18px\]{--tw-translate-x:18px;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.rotate-90{rotate:90deg}.animate-pulse{animation:var(--animate-pulse)}.animate-spin{animation:var(--animate-spin)}.cursor-pointer{cursor:pointer}.cursor-text{cursor:text}.list-none{list-style-type:none}.appearance-none{appearance:none}.grid-cols-\[minmax\(0\,1fr\)_8rem_auto\]{grid-template-columns:minmax(0,1fr) 8rem auto}.grid-cols-\[minmax\(0\,1fr\)_auto\]{grid-template-columns:minmax(0,1fr) auto}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-end{align-items:flex-end}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:var(--spacing)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-2\.5{gap:calc(var(--spacing) * 2.5)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-3\.5{gap:calc(var(--spacing) * 3.5)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(var(--spacing) * var(--tw-space-y-reverse));margin-block-end:calc(var(--spacing) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-3{column-gap:calc(var(--spacing) * 3)}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-x-6{column-gap:calc(var(--spacing) * 6)}.gap-y-1{row-gap:var(--spacing)}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-line>:not(:last-child)){border-color:var(--color-line)}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-y-auto{overflow-y:auto}.overscroll-contain{overscroll-behavior:contain}.rounded{border-radius:.25rem}.rounded-\[10px\]{border-radius:10px}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-b-2{border-bottom-style:var(--tw-border-style);border-bottom-width:2px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-brand{border-color:var(--color-brand)}.border-brand-solid{border-color:var(--color-brand-solid)}.border-brand\/20{border-color:var(--color-brand)}@supports (color:color-mix(in lab, red, red)){.border-brand\/20{border-color:color-mix(in oklab, var(--color-brand) 20%, transparent)}}.border-danger\/25{border-color:var(--color-danger)}@supports (color:color-mix(in lab, red, red)){.border-danger\/25{border-color:color-mix(in oklab, var(--color-danger) 25%, transparent)}}.border-line{border-color:var(--color-line)}.border-line-strong{border-color:var(--color-line-strong)}.border-transparent{border-color:#0000}.border-warn\/25{border-color:var(--color-warn)}@supports (color:color-mix(in lab, red, red)){.border-warn\/25{border-color:color-mix(in oklab, var(--color-warn) 25%, transparent)}}.bg-brand{background-color:var(--color-brand)}.bg-brand-soft{background-color:var(--color-brand-soft)}.bg-brand-solid{background-color:var(--color-brand-solid)}.bg-danger{background-color:var(--color-danger)}.bg-danger-soft{background-color:var(--color-danger-soft)}.bg-line{background-color:var(--color-line)}.bg-line-strong{background-color:var(--color-line-strong)}.bg-ok-soft{background-color:var(--color-ok-soft)}.bg-raised{background-color:var(--color-raised)}.bg-sunken{background-color:var(--color-sunken)}.bg-surface{background-color:var(--color-surface)}.bg-transparent{background-color:#0000}.bg-warn{background-color:var(--color-warn)}.bg-warn-soft{background-color:var(--color-warn-soft)}.bg-white{background-color:var(--color-white)}.fill-brand-amber{fill:var(--color-brand-amber)}.p-0{padding:0}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.px-1{padding-inline:var(--spacing)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-3\.5{padding-inline:calc(var(--spacing) * 3.5)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-5{padding-inline:calc(var(--spacing) * 5)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0{padding-block:0}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-2\.5{padding-block:calc(var(--spacing) * 2.5)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-6{padding-block:calc(var(--spacing) * 6)}.py-10{padding-block:calc(var(--spacing) * 10)}.py-14{padding-block:calc(var(--spacing) * 14)}.pt-1{padding-top:var(--spacing)}.pt-2{padding-top:calc(var(--spacing) * 2)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pt-5{padding-top:calc(var(--spacing) * 5)}.pt-6{padding-top:calc(var(--spacing) * 6)}.pr-0\.5{padding-right:calc(var(--spacing) * .5)}.pr-1{padding-right:var(--spacing)}.pr-3{padding-right:calc(var(--spacing) * 3)}.pr-4{padding-right:calc(var(--spacing) * 4)}.pr-9{padding-right:calc(var(--spacing) * 9)}.pb-1{padding-bottom:var(--spacing)}.pb-2{padding-bottom:calc(var(--spacing) * 2)}.pb-5{padding-bottom:calc(var(--spacing) * 5)}.pb-24{padding-bottom:calc(var(--spacing) * 24)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-3\.5{padding-left:calc(var(--spacing) * 3.5)}.pl-10{padding-left:calc(var(--spacing) * 10)}.text-center{text-align:center}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.leading-4{--tw-leading:calc(var(--spacing) * 4);line-height:calc(var(--spacing) * 4)}.leading-5{--tw-leading:calc(var(--spacing) * 5);line-height:calc(var(--spacing) * 5)}.leading-6{--tw-leading:calc(var(--spacing) * 6);line-height:calc(var(--spacing) * 6)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-normal{--tw-tracking:var(--tracking-normal);letter-spacing:var(--tracking-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.break-all{word-break:break-all}.whitespace-pre-wrap{white-space:pre-wrap}.text-body{color:var(--color-body)}.text-brand{color:var(--color-brand)}.text-danger{color:var(--color-danger)}.text-faint,.text-faint\/70{color:var(--color-faint)}@supports (color:color-mix(in lab, red, red)){.text-faint\/70{color:color-mix(in oklab, var(--color-faint) 70%, transparent)}}.text-ink{color:var(--color-ink)}.text-muted{color:var(--color-muted)}.text-ok{color:var(--color-ok)}.text-warn{color:var(--color-warn)}.text-white{color:var(--color-white)}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.accent-brand-solid{accent-color:var(--color-brand-solid)}.opacity-50{opacity:.5}.opacity-80{opacity:.8}.shadow-card{--tw-shadow:var(--af-shadow);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-pop{--tw-shadow:var(--af-shadow-pop);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-black\/5{--tw-ring-color:#0000000d}@supports (color:color-mix(in lab, red, red)){.ring-black\/5{--tw-ring-color:color-mix(in oklab, var(--color-black) 5%, transparent)}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.transition-\[border-color\,box-shadow\]{transition-property:border-color,box-shadow;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}.group-open\:rotate-90:is(:where(.group):is([open],:popover-open,:open) *){rotate:90deg}@media (hover:hover){.group-hover\:text-brand:is(:where(.group):hover *){color:var(--color-brand)}}.placeholder\:text-faint::placeholder{color:var(--color-faint)}.backdrop\:bg-black\/45::backdrop{background-color:#00000073}@supports (color:color-mix(in lab, red, red)){.backdrop\:bg-black\/45::backdrop{background-color:color-mix(in oklab, var(--color-black) 45%, transparent)}}.before\:absolute:before{content:var(--tw-content);position:absolute}.before\:-inset-2\.5:before{content:var(--tw-content);inset:calc(var(--spacing) * -2.5)}.before\:top-6:before{content:var(--tw-content);top:calc(var(--spacing) * 6)}.before\:bottom-2:before{content:var(--tw-content);bottom:calc(var(--spacing) * 2)}.before\:-left-2\.5:before{content:var(--tw-content);left:calc(var(--spacing) * -2.5)}.before\:w-\[3px\]:before{content:var(--tw-content);width:3px}.before\:rounded:before{content:var(--tw-content);border-radius:.25rem}.before\:bg-brand:before{content:var(--tw-content);background-color:var(--color-brand)}.before\:content-\[\'\'\]:before{--tw-content:"";content:var(--tw-content)}.last\:border-b:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.focus-within\:border-brand:focus-within{border-color:var(--color-brand)}.focus-within\:ring-4:focus-within{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(4px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.focus-within\:ring-brand-ring:focus-within{--tw-ring-color:var(--color-brand-ring)}@media (hover:hover){.hover\:border-brand:hover{border-color:var(--color-brand)}.hover\:border-line-strong:hover{border-color:var(--color-line-strong)}.hover\:bg-hover:hover{background-color:var(--color-hover)}.hover\:bg-raised:hover{background-color:var(--color-raised)}.hover\:text-brand:hover{color:var(--color-brand)}.hover\:text-danger:hover{color:var(--color-danger)}.hover\:text-ink:hover{color:var(--color-ink)}.hover\:underline:hover{text-decoration-line:underline}}.focus\:not-sr-only:focus{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.focus\:fixed:focus{position:fixed}.focus\:top-3:focus{top:calc(var(--spacing) * 3)}.focus\:left-3:focus{left:calc(var(--spacing) * 3)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}@media (prefers-reduced-motion:reduce){.motion-reduce\:transition-none{transition-property:none}}@media not all and (min-width:64rem){.max-lg\:sticky{position:sticky}.max-lg\:top-14{top:calc(var(--spacing) * 14)}.max-lg\:z-20{z-index:20}.max-lg\:mx-4{margin-inline:calc(var(--spacing) * 4)}.max-lg\:mt-3{margin-top:calc(var(--spacing) * 3)}.max-lg\:contents{display:contents}.max-lg\:flex{display:flex}.max-lg\:hidden{display:none}.max-lg\:min-h-11{min-height:calc(var(--spacing) * 11)}.max-lg\:shrink-0{flex-shrink:0}.max-lg\:gap-0\.5{gap:calc(var(--spacing) * .5)}.max-lg\:overflow-x-auto{overflow-x:auto}.max-lg\:border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.max-lg\:border-line{border-color:var(--color-line)}.max-lg\:bg-surface{background-color:var(--color-surface)}.max-lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.max-lg\:py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.max-lg\:whitespace-nowrap{white-space:nowrap}}@media (min-width:40rem){.sm\:left-auto{left:auto}.sm\:flex{display:flex}.sm\:inline{display:inline}.sm\:size-5{width:calc(var(--spacing) * 5);height:calc(var(--spacing) * 5)}.sm\:h-6{height:calc(var(--spacing) * 6)}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-\[minmax\(0\,1fr\)_minmax\(0\,1\.1fr\)\]{grid-template-columns:minmax(0,1fr) minmax(0,1.1fr)}.sm\:grid-cols-\[minmax\(0\,260px\)_minmax\(0\,1fr\)\]{grid-template-columns:minmax(0,260px) minmax(0,1fr)}.sm\:gap-3{gap:calc(var(--spacing) * 3)}.sm\:gap-8{gap:calc(var(--spacing) * 8)}.sm\:px-4{padding-inline:calc(var(--spacing) * 4)}.sm\:px-8{padding-inline:calc(var(--spacing) * 8)}.sm\:py-0\.5{padding-block:calc(var(--spacing) * .5)}.sm\:py-1{padding-block:var(--spacing)}.sm\:pt-0\.5{padding-top:calc(var(--spacing) * .5)}.sm\:pt-8{padding-top:calc(var(--spacing) * 8)}.sm\:text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.sm\:before\:-left-3\.5:before{content:var(--tw-content);left:calc(var(--spacing) * -3.5)}}@media (min-width:48rem){.md\:block{display:block}.md\:inline{display:inline}}@media (min-width:64rem){.lg\:sticky{position:sticky}.lg\:top-14{top:calc(var(--spacing) * 14)}.lg\:block{display:block}.lg\:grid{display:grid}.lg\:inline-flex{display:inline-flex}.lg\:h-\[calc\(100dvh-3\.5rem\)\]{height:calc(100dvh - 3.5rem)}.lg\:-translate-x-6{--tw-translate-x:calc(var(--spacing) * -6);translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:translate-x-0{--tw-translate-x:0px;translate:var(--tw-translate-x) var(--tw-translate-y)}.lg\:grid-cols-\[260px_minmax\(0\,1fr\)\]{grid-template-columns:260px minmax(0,1fr)}.lg\:items-start{align-items:flex-start}.lg\:overflow-y-auto{overflow-y:auto}.lg\:border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.lg\:border-line{border-color:var(--color-line)}.lg\:bg-surface{background-color:var(--color-surface)}.lg\:px-3{padding-inline:calc(var(--spacing) * 3)}.lg\:pt-4{padding-top:calc(var(--spacing) * 4)}.lg\:pb-16{padding-bottom:calc(var(--spacing) * 16)}.lg\:opacity-0{opacity:0}.lg\:opacity-100{opacity:1}.lg\:transition-\[transform\,opacity\]{transition-property:transform,opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.lg\:duration-200{--tw-duration:.2s;transition-duration:.2s}.lg\:ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}}.\[\&\:\:-webkit-details-marker\]\:hidden::-webkit-details-marker{display:none}}:root{color-scheme:light;--af-canvas:#f5f7fb;--af-surface:#fff;--af-raised:#f1f5f9;--af-hover:#e8eef6;--af-sunken:#fff;--af-line:#0f172a17;--af-line-strong:#0f172a2b;--af-ink:#0b1220;--af-body:#1e293b;--af-muted:#475569;--af-faint:#64748b;--af-brand:#2563eb;--af-brand-solid:#2563eb;--af-brand-solid-hover:#1d4ed8;--af-brand-soft:#2563eb14;--af-brand-ring:#2563eb38;--af-brand-amber:#c8692f;--af-ok:#047857;--af-ok-soft:#10b9811a;--af-warn:#b45309;--af-warn-soft:#f59e0b1f;--af-danger:#dc2626;--af-danger-soft:#ef444417;--af-shadow:0 1px 2px #0f172a0a, 0 1px 3px #0f172a0a;--af-shadow-pop:0 12px 32px #0f172a24;--af-scrollbar:#0f172a33;--af-scrollbar-hover:#0f172a59;--af-json-key:#1d4ed8;--af-json-string:#047857;--af-json-number:#b45309;--af-json-literal:#7c3aed}@media (prefers-color-scheme:dark){:root{color-scheme:dark;--af-canvas:#08111f;--af-surface:#0c1727;--af-raised:#131e33;--af-hover:#1a2741;--af-sunken:#07101d;--af-line:#94a3b824;--af-line-strong:#94a3b842;--af-ink:#f8fafc;--af-body:#e2e8f0;--af-muted:#a3b1c6;--af-faint:#8391a7;--af-brand:#60a5fa;--af-brand-solid:#2563eb;--af-brand-solid-hover:#3b82f6;--af-brand-soft:#3b82f624;--af-brand-ring:#3b82f659;--af-brand-amber:#e9a57c;--af-ok:#34d399;--af-ok-soft:#10b98124;--af-warn:#fbbf24;--af-warn-soft:#f59e0b21;--af-danger:#f87171;--af-danger-soft:#ef444421;--af-shadow:0 1px 2px #02061766;--af-shadow-pop:0 16px 40px #0206178c;--af-scrollbar:#94a3b838;--af-scrollbar-hover:#94a3b866;--af-json-key:#93c5fd;--af-json-string:#6ee7b7;--af-json-number:#fcd34d;--af-json-literal:#c4b5fd}}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:0s!important;animation-duration:0s!important}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-content{syntax:"*";inherits:false;initial-value:""}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}} \ No newline at end of file diff --git a/tenxgraph_api/cli/config_editor/static/app.js b/tenxgraph_api/cli/config_editor/static/app.js index a73cf247..eda825d0 100644 --- a/tenxgraph_api/cli/config_editor/static/app.js +++ b/tenxgraph_api/cli/config_editor/static/app.js @@ -1,3 +1,3 @@ -var Ie,v,Nt,Zn,J,At,Lt,Dt,et,Se,ue,Ht,ot,tt,nt,eo,Ee={},Oe=[],to=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Ae=Array.isArray;function F(e,t){for(var o in t)e[o]=t[o];return e}function rt(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function no(e,t,o){var r,i,s,a={};for(s in t)s=="key"?r=t[s]:s=="ref"?i=t[s]:a[s]=t[s];if(arguments.length>2&&(a.children=arguments.length>3?Ie.call(arguments,2):o),typeof e=="function"&&e.defaultProps!=null)for(s in e.defaultProps)a[s]===void 0&&(a[s]=e.defaultProps[s]);return $e(e,a,r,i,null)}function $e(e,t,o,r,i){var s={type:e,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i??++Nt,__i:-1,__u:0};return i==null&&v.vnode!=null&&v.vnode(s),s}function x(e){return e.children}function Te(e,t){this.props=e,this.context=t}function Q(e,t){if(t==null)return e.__?Q(e.__,e.__i+1):null;for(var o;tt&&J.sort(Dt),e=J.shift(),t=J.length,oo(e)}finally{J.length=Pe.__r=0}}function Ft(e,t,o,r,i,s,a,u,m,l,p){var h,c,f,y,$,w,E=r&&r.__k||Oe,k=t.length;for(m=ro(o,t,E,m,k),h=0;h0?a=e.__k[s]=$e(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[s]=a,m=s+c,a.__=e,a.__b=e.__b+1,u=null,(l=a.__i=so(a,o,m,h))!=-1&&(h--,(u=o[l])&&(u.__u|=2)),u==null||u.__v==null?(l==-1&&(i>p?c--:im?c--:c++,a.__u|=4))):e.__k[s]=null;if(h)for(s=0;s(p?1:0)){for(i=o-1,s=o+1;i>=0||s=0?i--:s++])!=null&&(2&l.__u)==0&&u==l.key&&m==l.type)return a}return-1}function jt(e,t,o){t[0]=="-"?e.setProperty(t,o??""):e[t]=o==null?"":typeof o!="number"||to.test(t)?o:o+"px"}function Ce(e,t,o,r,i){var s,a;e:if(t=="style")if(typeof o=="string")e.style.cssText=o;else{if(typeof r=="string"&&(e.style.cssText=r=""),r)for(t in r)o&&t in o||jt(e.style,t,"");if(o)for(t in o)r&&o[t]==r[t]||jt(e.style,t,o[t])}else if(t[0]=="o"&&t[1]=="n")s=t!=(t=t.replace(Ht,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+s]=o,o?r?o[ue]=r[ue]:(o[ue]=ot,e.addEventListener(t,s?nt:tt,s)):e.removeEventListener(t,s?nt:tt,s);else{if(i=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=o??"";break e}catch{}typeof o=="function"||(o==null||o===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&o==1?"":o))}}function Mt(e){return function(t){if(this.l){var o=this.l[t.type+e];if(t[Se]==null)t[Se]=ot++;else if(t[Se]0?e:Ae(e)?e.map(Gt):e.constructor!==void 0?null:F({},e)}function io(e,t,o,r,i,s,a,u,m){var l,p,h,c,f,y,$,w=o.props||Ee,E=t.props,k=t.type;if(k=="svg"?i="http://www.w3.org/2000/svg":k=="math"?i="http://www.w3.org/1998/Math/MathML":i||(i="http://www.w3.org/1999/xhtml"),s!=null){for(l=0;l=o.__.length&&o.__.push({}),o.__[e]}function K(e){return je=1,dt(rn,e)}function dt(e,t,o){var r=ct(pe++,2);if(r.t=e,!r.__c&&(r.__=[o?o(t):rn(void 0,t),function(u){var m=r.__N?r.__N[0]:r.__[0],l=r.t(m,u);m!==l&&(r.__N=[l,r.__[1]],r.__c.setState({}))}],r.__c=C,!C.__f)){var i=function(u,m,l){if(!r.__c.__H)return!0;var p=!1,h=r.__c.props!==u;if(r.__c.__H.__.some(function(f){if(f.__N){p=!0;var y=f.__[0];f.__=f.__N,f.__N=void 0,y!==f.__[0]&&(h=!0)}}),s){var c=s.call(this,u,m,l);return p?c||h:c}return!p||h};C.__f=!0;var s=C.shouldComponentUpdate,a=C.componentWillUpdate;C.componentWillUpdate=function(u,m,l){if(this.__e){var p=s;s=void 0,i(u,m,l),s=p}a&&a.call(this,u,m,l)},C.shouldComponentUpdate=i}return r.__N||r.__}function U(e,t){var o=ct(pe++,3);!T.__s&&on(o.__H,t)&&(o.__=e,o.u=t,C.__H.__h.push(o))}function Me(e){return je=5,lo(function(){return{current:e}},[])}function lo(e,t){var o=ct(pe++,7);return on(o.__H,t)&&(o.__=e(),o.__H=t,o.__h=e),o.__}function co(){for(var e;e=nn.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Re),t.__h.some(lt),t.__h=[]}catch(o){t.__h=[],T.__e(o,e.__v)}}}T.__b=function(e){C=null,Kt&&Kt(e)},T.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),en&&en(e,t)},T.__r=function(e){Yt&&Yt(e),pe=0;var t=(C=e.__c).__H;t&&(at===C?(t.__h=[],C.__h=[],t.__.some(function(o){o.__N&&(o.__=o.__N),o.u=o.__N=void 0})):(t.__h.some(Re),t.__h.some(lt),t.__h=[],pe=0)),at=C},T.diffed=function(e){Xt&&Xt(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(nn.push(t)!==1&&Jt===T.requestAnimationFrame||((Jt=T.requestAnimationFrame)||uo)(co)),t.__H.__.some(function(o){o.u&&(o.__H=o.u,o.u=void 0)})),at=C=null},T.__c=function(e,t){t.some(function(o){try{o.__h.some(Re),o.__h=o.__h.filter(function(r){return!r.__||lt(r)})}catch(r){t.some(function(i){i.__h&&(i.__h=[])}),t=[],T.__e(r,o.__v)}}),Qt&&Qt(e,t)},T.unmount=function(e){Zt&&Zt(e);var t,o=e.__c;o&&o.__H&&(o.__H.__.some(function(r){try{Re(r)}catch(i){t=i}}),o.__H=void 0,t&&T.__e(t,o.__v))};var tn=typeof requestAnimationFrame=="function";function uo(e){var t,o=function(){clearTimeout(r),tn&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(o,35);tn&&(t=requestAnimationFrame(o))}function Re(e){var t=C,o=e.__c;typeof o=="function"&&(e.__c=void 0,o()),C=t}function lt(e){var t=C;e.__c=e.__(),C=t}function on(e,t){return!e||e.length!==t.length||t.some(function(o,r){return o!==e[r]})}function rn(e,t){return typeof t=="function"?t(e):t}var Ne=[{label:null,items:["get-running"]},{label:"Before production",items:["auth","cache_store","observability","metrics"]},{label:"Runs and streaming",items:["runs","websocket","ag_ui","remote_tools","media"]},{label:"Project wiring",items:["checkpointer","id_generator","thread_name_generator","store","injectq"]},{label:"Developer tools",items:["test","evaluation"]},{label:"Reference",items:["environment"]}],q={"get-running":{title:"Get running",icon:"flag",description:"Only the agent graph is required. Everything else is optional."},environment:{title:"Server environment (.env)",icon:"terminal",description:"Variables the server reads from .env. The editor never reads that file, so values are not shown."}},ut={"auth.path":"auth-path"},sn="tg-config-token",an=!1;function po(){let e=new URLSearchParams(location.hash.slice(1)).get("token")||"";try{e&&sessionStorage.setItem(sn,e);let t=sessionStorage.getItem(sn)||"";return an=t!==""&&t===(e||t),e||t}catch{return e}}var me=po(),pt=e=>an||!me?`#${e}`:`#token=${encodeURIComponent(me)}&page=${e}`;function ln(){let e=location.hash.slice(1);if(e.includes("=")){let t=new URLSearchParams(e);return t.get("page")||t.get("section")||""}try{return decodeURIComponent(e)}catch{return e}}var d={schema:null,sections:[],nav:[],fields:new Map,config:{},version:null,exists:!1,path:"",parseError:null,saved:"",validated:null,issues:[],page:"get-running",query:"",sideOpen:!0,sideRender:!0,sideShown:!0,drawerOpen:!1,tab:"json",copied:!1,busy:null,toolParams:{},toasts:[],fatal:null},cn=()=>{},dn=e=>{cn=e};function b(e){e&&e(d),cn()}var un=e=>JSON.parse(JSON.stringify(e)),N=()=>JSON.stringify(d.config),g=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),L=(e,t)=>t.reduce((o,r)=>g(o)?o[r]:void 0,e),fe=(e,t)=>`${e} ${t}${e===1?"":"s"}`;function oe(e,t,o){let r=e;for(let i of t.slice(0,-1))g(r[i])||(r[i]={}),r=r[i];r[t[t.length-1]]=o}var mo=new Set(["cache_store.rate_limit","cache_store.token_revocation"]);function he(e,t){let o=[],r=e;for(let i of t.slice(0,-1)){if(!g(r))return;o.push([r,i]),r=r[i]}g(r)&&delete r[t[t.length-1]];for(let i=o.length-1;i>=1;i--){let[s,a]=o[i];if(mo.has(t.slice(0,i+1).join(".")))break;if(g(s[a])&&Object.keys(s[a]).length===0)delete s[a];else break}}function mt(e){let t=L(d.config,e);return t!==void 0?t:d.fields.get(e.join("."))?.default}var fo=(e,t)=>JSON.stringify(e)===JSON.stringify(t),P=e=>d.sections.find(t=>t.id===e),W=()=>N()!==d.saved,Le=e=>!e.optional||e.key in d.config&&d.config[e.key]!==null,re=e=>d.issues.filter(t=>t.section===e),S=e=>d.issues.find(t=>t.path===e);function _e(e=d.issues){let t=e.filter(o=>o.level==="error").length;return{errors:t,warnings:e.length-t}}var De=e=>ut[e]||`f-${e.replaceAll(".","-")}`,ft=e=>{let t=L(d.config,e.path);return t!==void 0&&!fo(t,e.default)},pn=e=>e.id==="core"?!1:e.id==="auth"?d.config.auth!=null:e.id==="metrics"?d.config.metrics?.enabled===!0:e.optional?Le(e):e.fields.some(ft);function mn(e){let t=re(e);return t.find(o=>o.level==="error")||t[0]||null}var fn=e=>!!e.show_if&&mt(e.show_if.path)!==e.show_if.equals;function hn(e){let o=d.fields.get(e.show_if.path.join("."))?.label||e.show_if.path.join("."),r=e.show_if.equals;return`Used when ${o} is ${r===!0?"on":r===!1?"off":String(r)}.`}var ge=e=>{let t=P(e);return q[e]?{id:e,...q[e]}:t?{id:e,title:t.title,subtitle:t.subtitle,description:t.description,icon:null}:null},_n=()=>/^[\w.]+:[\w.]+$/.test(String(d.config.agent||""));async function _t(e,t,o){let r={"X-10xGraph-Token":me};o!==void 0&&(r["Content-Type"]="application/json");let i=await fetch(t,{method:e,headers:r,body:o===void 0?void 0:JSON.stringify(o)}),s={};try{s=await i.json()}catch{}return{ok:i.ok,status:i.status,data:s}}var ht=(e,t)=>b(o=>{o.fatal={title:e,text:t}});async function xn(){if(!me)return ht("Open the link printed by 10xgraph config","This page needs the session token included in that link.");let e;try{e=await _t("GET","/api/state")}catch{return ht("Cannot reach the config server","Check that 10xgraph config is still running in your terminal, then reload.")}if(!e.ok)return ht("This link has expired",e.data.error||"Restart 10xgraph config and open the new link it prints.");let t=e.data;b(o=>{o.schema=t.schema,o.sections=t.schema.sections;let r=new Set(Ne.flatMap(a=>a.items)),i=o.sections.map(a=>a.id).filter(a=>a!=="core"&&!r.has(a));o.nav=Ne.map((a,u)=>({label:a.label,items:uq[m]||P(m)):[...i,...a.items]}));for(let a of o.sections)for(let u of a.fields)o.fields.set(u.path.join("."),u);o.config=t.config,o.version=t.version,o.exists=t.exists,o.path=t.path,o.parseError=t.parse_error,o.issues=t.issues;let s=t.exists&&!t.parse_error;o.saved=s?N():"",o.validated=s?N():null}),ze({focus:!1}),history.replaceState(null,"",pt(d.page))}var gn=null;async function vn(){let e=N();if(!(!d.schema||d.validated===e||gn===e)&&!Object.values(d.toolParams).some(t=>t.error)){gn=e;try{let t=await _t("POST","/api/validate",{config:JSON.parse(e)});if(!t.ok)return;b(o=>{o.issues=t.data.issues,o.validated=e})}catch{}}}async function He(){if(!(d.busy||!d.schema)){if(d.exists&&!W()&&!d.parseError)return G("success","No changes to save.");if(ho()){b(e=>{e.busy="save"});try{let e=await _t("POST","/api/save",{config:d.config,version:d.version});if(e.data.issues&&(d.issues=e.data.issues,d.validated=N()),e.ok){d.version=e.data.version,d.config=e.data.config,d.toolParams={},d.validated=N(),d.saved=N(),d.exists=!0,d.parseError=null;let t=e.data.path.split(/[\\/]/).pop();G("success",e.data.backup?`Saved ${t}. The previous version is in ${t}.bak.`:`Created ${t}.`)}else if(e.status===409)G("error",e.data.error,{label:"Reload",run:()=>location.reload()});else if(e.status===422){let{errors:t}=_e();G("error",`Not saved: ${fe(t,"error")} to fix first.`,{label:"Show",run:_o})}else G("error",e.data.error||"Save failed.")}catch{G("error","Cannot reach the config server.")}finally{b(e=>{e.busy=null})}}}}function ho(){let e=Object.values(d.toolParams).some(t=>t.error);return e&&(D("remote_tools"),G("error","A remote tool has invalid parameters JSON.")),!e}function yn(e,t){b(o=>{t?o.config[e.key]=un(e.default):delete o.config[e.key],e.key==="remote_tools"&&(o.toolParams={})})}function _o(){let e=d.issues.find(t=>t.level==="error")||d.issues[0];e&&D(e.section,De(e.path))}var go=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches?"auto":"smooth";function bo(e){return e==="core"?"get-running":q[e]||P(e)?e:"get-running"}function ze({focus:e=!0}={}){b(t=>{t.page=bo(ln()),t.query="",t.drawerOpen=!1}),e&&requestAnimationFrame(()=>{window.scrollTo(0,0),document.getElementById("page-title")?.focus({preventScroll:!0})})}function D(e,t){let o=pt(e);location.hash!==o&&history.pushState(null,"",o),ze(),t&&requestAnimationFrame(()=>requestAnimationFrame(()=>{let r=document.getElementById(t);r&&(r.scrollIntoView({block:"center",behavior:go()}),r.focus({preventScroll:!0}))}))}function Fe(e){b(t=>{t.query=e})}var xo=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches,bn;function se(e,{instant:t=!1}={}){try{localStorage.setItem("tg-sidebar",e?"shown":"hidden")}catch{}clearTimeout(bn);let o=t||xo();b(r=>{r.sideOpen=e,e?(r.sideRender=!0,r.sideShown=o):r.sideShown=!1,!e&&o&&(r.sideRender=!1)}),e&&!o&&requestAnimationFrame(()=>requestAnimationFrame(()=>b(r=>{r.sideShown=!0}))),!e&&!o&&(bn=setTimeout(()=>b(r=>{r.sideOpen||(r.sideRender=!1)}),200))}function gt(){d.sideOpen||se(!0,{instant:!0}),requestAnimationFrame(()=>{let e=document.getElementById("q");e?.focus(),e?.select()})}function kn(e){b(t=>{e&&(t.tab=e),t.drawerOpen=!0})}var Ue=()=>b(e=>{e.drawerOpen=!1});async function wn(){let e="10xgraph dev";try{await navigator.clipboard.writeText(e)}catch{try{let t=document.createElement("textarea");t.value=e,document.body.append(t),t.select(),document.execCommand("copy"),t.remove()}catch{}}b(t=>{t.copied=!0}),setTimeout(()=>b(t=>{t.copied=!1}),2e3)}async function Cn(){try{await navigator.clipboard.writeText(JSON.stringify(d.config,null,2)+` -`),G("success","Copied 10xgraph.json to the clipboard.")}catch{G("error","The clipboard is not available here.")}}var vo=0;function G(e,t,o){let r=++vo;b(i=>{i.toasts=[...i.toasts,{id:r,kind:e,message:t,action:o}]}),setTimeout(()=>qe(r),o?8e3:4e3)}function qe(e){b(t=>{t.toasts=t.toasts.filter(o=>o.id!==e)})}var Sn={box:'',key:'',shield:'',gauge:'',arrows:'',activity:'',layers:'',wrench:'',flask:'',target:'',checkCircle:'',xCircle:'',alert:'',info:'',file:'',copy:'',x:'',plus:'',trash:'',panel:'',chevronDown:'',expand:'',collapse:'',power:'',spinner:'',book:'',flag:'',lock:'',play:'',bolt:'',terminal:'',search:'',chevronRight:'',code:'',circle:'',external:''},Z={"get-running":"flag",environment:"terminal",core:"flag",auth:"lock",cache_store:"gauge",observability:"activity",metrics:"gauge",runs:"play",websocket:"bolt",ag_ui:"external",remote_tools:"wrench",media:"file",checkpointer:"layers",id_generator:"key",thread_name_generator:"file",store:"box",injectq:"arrows",test:"flask",evaluation:"target"};var yo=0;function n(e,t,o,r,i,s){t||(t={});var a,u,m=t;if("ref"in m)for(u in m={},t)u=="ref"?a=t[u]:m[u]=t[u];var l={type:e,props:m,key:o,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--yo,__i:-1,__u:0,__source:i,__self:s};if(typeof e=="function"&&(a=e.defaultProps))for(u in a)m[u]===void 0&&(m[u]=a[u]);return v.vnode&&v.vnode(l),l}function _({name:e,class:t="size-4"}){return/(^|\s)size-/.test(t)||(t=`size-4 ${t}`),n("span",{class:`inline-flex shrink-0 ${t}`,"aria-hidden":"true",children:n("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round",class:"size-full",dangerouslySetInnerHTML:{__html:Sn[e]}})})}function ie({class:e,children:t}){return n("span",{class:`inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-semibold ${e}`,children:t})}function be({id:e,on:t,label:o,onChange:r}){return n("button",{id:e,type:"button",role:"switch","aria-checked":t?"true":"false","aria-label":o,class:`focus-ring relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200 before:absolute before:-inset-2.5 before:content-[''] ${t?"bg-brand-solid":"bg-line-strong"}`,onClick:()=>r(!t),children:n("span",{class:`pointer-events-none inline-block size-5 rounded-full bg-white shadow-sm ring-1 ring-black/5 transition-transform duration-200 ease-out ${t?"translate-x-[18px]":"translate-x-0.5"}`})})}function ee({id:e,label:t,options:o,value:r,onPick:i}){return n("div",{id:e,role:"radiogroup","aria-label":t,class:"inline-flex max-w-full flex-wrap gap-0.5 rounded-[10px] border border-line-strong bg-raised p-0.5",children:o.map(([s,a])=>n("button",{type:"button",role:"radio","aria-checked":s===r?"true":"false",class:`focus-ring min-h-10 cursor-pointer rounded-lg px-3.5 text-sm font-medium transition-colors duration-150 ${s===r?"bg-surface text-ink shadow-card":"text-muted hover:text-ink"}`,onClick:()=>{s!==r&&i(s)},children:a},s))})}function A({id:e,options:t,value:o,invalid:r,onChange:i,plain:s}){return n("div",{class:"relative",children:[n("select",{id:e,value:o,class:`input cursor-pointer appearance-none pr-9 ${s?"":"font-mono"} ${r?"input-invalid":""}`,onChange:a=>i(a.currentTarget.value),children:t.map(([a,u])=>n("option",{value:a,children:u},a))}),n(_,{name:"chevronDown",class:"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-faint"})]})}function H({id:e,values:t,placeholder:o,suggestions:r,onChange:i}){let s=Me(null),a=m=>{let l=m.trim();s.current&&(s.current.value=""),l&&!t.includes(l)&&i([...t,l])},u=r?`${e}-options`:void 0;return n("div",{class:"flex min-h-11 cursor-text flex-wrap items-center gap-1.5 rounded-lg border border-line-strong bg-sunken px-1.5 py-1.5 transition-[border-color,box-shadow] duration-150 focus-within:border-brand focus-within:ring-4 focus-within:ring-brand-ring sm:py-1",onClick:m=>{m.target===m.currentTarget&&s.current?.focus()},children:[t.map((m,l)=>n("span",{class:"inline-flex h-7 items-center gap-0.5 rounded-md border border-line bg-raised pr-0.5 pl-2 font-mono text-xs text-body sm:h-6",children:[m,n("button",{type:"button","aria-label":`Remove ${m}`,class:"inline-flex size-6 cursor-pointer items-center justify-center rounded text-faint transition-colors hover:bg-hover hover:text-ink sm:size-5",onClick:()=>i(t.filter((p,h)=>h!==l)),children:n(_,{name:"x",class:"size-3"})})]},m)),n("input",{ref:s,id:e,type:"text",list:u,placeholder:t.length?"Add...":o,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:"h-7 min-w-[7rem] flex-1 bg-transparent px-1 font-mono text-base text-ink outline-none placeholder:text-faint sm:text-sm",onKeyDown:m=>{let l=m.currentTarget;(m.key==="Enter"||m.key===",")&&l.value.trim()?(m.preventDefault(),a(l.value)):m.key==="Backspace"&&!l.value&&t.length&&i(t.slice(0,-1))},onChange:m=>{r?.includes(m.currentTarget.value.trim())&&a(m.currentTarget.value)},onBlur:m=>a(m.currentTarget.value)}),r?n("datalist",{id:u,children:r.filter(m=>!t.includes(m)).map(m=>n("option",{value:m},m))}):null]})}function xe({label:e,path:t,help:o,id:r,required:i}){return n("div",{class:"min-w-0",children:[n("label",{for:r,class:"block text-sm font-medium text-ink",children:[e,i?n("span",{class:"text-danger",children:" *"}):null]}),n("div",{class:"mt-0.5 font-mono text-xs text-faint",children:t}),o?n("p",{class:"mt-1.5 text-[13px] leading-5 text-muted",children:o}):null]})}function O({issue:e}){if(!e)return null;let t=e.level==="error";return n("p",{class:`mt-2 flex gap-1.5 text-[13px] leading-5 ${t?"text-danger":"text-warn"}`,role:t?"alert":void 0,children:[n(_,{name:t?"xCircle":"alert",class:"mt-0.5 size-3.5"}),n("span",{children:e.message})]})}function bt({children:e}){return n("div",{class:"grid gap-3 px-5 py-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] sm:gap-8",children:e})}function Y({id:e,label:t,path:o,value:r,placeholder:i,onInput:s,issue:a,help:u}){return n(bt,{children:[n(xe,{label:t,path:o,help:u,id:e}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n("input",{id:e,type:"text",value:r??"",placeholder:i,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${a?.level==="error"?"input-invalid":""}`,onInput:m=>s(m.currentTarget.value)}),n(O,{issue:a})]})]})}function xt({name:e,label:t,options:o,value:r,onPick:i}){return n("div",{role:"radiogroup","aria-label":t,class:"grid gap-3 sm:grid-cols-2",children:o.map(([s,a,u])=>{let m=s===r;return n("button",{type:"button",role:"radio","aria-checked":m?"true":"false",id:`${e}-${s}`,class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 ${m?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{m||i(s)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${m?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:m?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:a}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:u})]})]},s)})})}function R({title:e,children:t}){return n("section",{children:[e?n("h4",{class:"eyebrow mb-2.5",children:e}):null,t]})}var ko={info:["info","border-brand/20 bg-brand-soft","text-brand"],warn:["alert","border-warn/25 bg-warn-soft","text-warn"],error:["xCircle","border-danger/25 bg-danger-soft","text-danger"]};function ve({kind:e,title:t,lines:o}){let[r,i,s]=ko[e];return n("div",{class:`flex gap-3 rounded-xl border px-4 py-3 ${i}`,role:e==="error"?"alert":void 0,children:[n(_,{name:r,class:`mt-0.5 size-4 ${s}`}),n("div",{class:"min-w-0 space-y-1 text-[13px] leading-5 text-body",children:[t?n("p",{class:"font-medium text-ink",children:t}):null,o.map((a,u)=>n("p",{children:a},u))]})]})}function vt({issue:e}){return n(ve,{kind:e.level==="error"?"error":"warn",lines:[n("span",{children:[e.path?n("code",{class:"mr-1.5 font-mono text-xs text-muted",children:e.path}):null,e.message]})]})}function $n({icon:e,tint:t,title:o,text:r,children:i}){return n("div",{class:"px-6 py-14 text-center",children:[n("div",{class:`mx-auto flex size-11 items-center justify-center rounded-full bg-raised ${t}`,children:n(_,{name:e,class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:o}),n("p",{class:"mx-auto mt-1 max-w-xs text-sm leading-6 text-muted",children:r}),i]})}function We({title:e,open:t,class:o="",children:r}){return n("details",{class:`group ${o}`,open:t,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform duration-150 group-open:rotate-90"}),e]}),r]})}function Tn(){let e=Me(null);U(()=>{let a=e.current;a&&(d.drawerOpen&&!a.open?a.showModal():!d.drawerOpen&&a.open&&a.close())});let t=d.issues.length>0,o=t&&d.tab==="problems"?"problems":"json",{errors:r,warnings:i}=_e(),s=(a,u,m)=>n("button",{type:"button",role:"tab",id:`tab-${a}`,"aria-selected":o===a?"true":"false",class:`focus-ring inline-flex h-10 cursor-pointer items-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors duration-150 ${o===a?"bg-raised text-ink":"text-muted hover:text-ink"}`,onClick:()=>b(l=>{l.tab=a}),children:[u,m]});return n("dialog",{ref:e,"aria-label":"10xgraph.json preview",class:"m-0 ml-auto h-dvh max-h-none w-[min(540px,100vw)] border-0 border-l border-line-strong bg-surface p-0 text-body shadow-pop backdrop:bg-black/45",onClose:()=>{d.drawerOpen&&Ue()},onClick:a=>{a.target===a.currentTarget&&Ue()},children:n("div",{class:"flex h-full flex-col",children:[n("div",{class:"flex h-14 shrink-0 items-center gap-1 border-b border-line px-3",children:[n("div",{role:"tablist",class:"flex flex-1 gap-1",children:[s("json","10xgraph.json",null),t?s("problems","Problems",n("span",{class:`inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[11px] font-semibold ${r?"bg-danger-soft text-danger":"bg-warn-soft text-warn"}`,children:r+i})):null]}),n("button",{type:"button",class:"icon-btn","aria-label":"Close preview",onClick:Ue,children:n(_,{name:"x",class:"size-5"})})]}),n("div",{class:"min-h-0 flex-1 overflow-y-auto overscroll-contain",children:o==="problems"?n(wo,{}):n(So,{})})]})})}function wo(){let{errors:e,warnings:t}=_e(),o=d.sections.map(r=>[r,re(r.id)]).filter(([,r])=>r.length);return n("div",{children:[n("div",{class:"border-b border-line px-4 py-3",children:n("p",{class:"text-sm text-muted",children:[n("span",{class:e?"font-medium text-danger":"",children:fe(e,"error")})," \xB7 ",n("span",{class:t?"font-medium text-warn":"",children:fe(t,"warning")})]})}),n("div",{class:"space-y-5 px-3 py-4",children:o.map(([r,i])=>n("div",{children:[n("p",{class:"eyebrow mb-1.5 flex items-center gap-2 px-2",children:[n(_,{name:Z[r.id]||"box",class:"size-3.5"}),r.title]}),n("ul",{class:"space-y-0.5",children:i.map((s,a)=>{let u=s.level==="error";return n("li",{children:n("button",{type:"button",onClick:()=>D(r.id,De(s.path)),class:"focus-ring flex min-h-11 w-full cursor-pointer gap-2.5 rounded-lg px-2 py-2 text-left transition-colors duration-150 hover:bg-raised",children:[n(_,{name:u?"xCircle":"alert",class:`mt-0.5 size-4 ${u?"text-danger":"text-warn"}`}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-[13px] leading-5 text-body",children:s.message}),s.path?n("span",{class:"mt-0.5 block truncate font-mono text-xs text-faint",children:s.path}):null]})]})},a)})})]},r.id))})]})}function Co(e){return e.replace(/&/g,"&").replace(//g,">").replace(/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,o=>{let r="var(--af-json-number)";return o.startsWith('"')?r=o.endsWith(":")?"var(--af-json-key)":"var(--af-json-string)":(o==="true"||o==="false"||o==="null")&&(r="var(--af-json-literal)"),`${o}`})}function So(){let e=JSON.stringify(d.config,null,2).split(` -`);return n("div",{children:[n("div",{class:"sticky top-0 flex items-center justify-between gap-3 border-b border-line bg-surface px-4 py-2.5",children:[n("p",{class:"text-xs text-faint",children:W()||!d.exists?"Preview of what Save will write":"Matches the file on disk"}),n("button",{type:"button",class:"btn btn-ghost h-8 px-2.5 text-xs",onClick:Cn,children:[n(_,{name:"copy",class:"size-3.5"}),"Copy"]})]}),n("div",{class:"py-3 pr-4 font-mono text-[12.5px] leading-5",children:e.map((t,o)=>n("div",{class:"flex",children:[n("span",{class:"w-10 shrink-0 pr-3 text-right text-faint/70 select-none",children:o+1}),n("span",{class:"min-w-0 flex-1 break-all whitespace-pre-wrap",dangerouslySetInnerHTML:{__html:Co(t)}})]},o))})]})}function kt(e){let t=[];for(let o of e){let r=o.group||"",i=t.find(s=>s.name===r);i||t.push(i={name:r,fields:[]}),i.fields.push(o)}return t}var B=(e,t)=>b(o=>{if(t===void 0){he(o.config,e);return}let r=e.length>1?o.sections.find(i=>i.optional&&i.key===e[0]):null;r&&(o.config[r.key]===void 0||o.config[r.key]===null)&&(o.config[r.key]=JSON.parse(JSON.stringify(r.default))),oe(o.config,e,t)}),$o=e=>`f-${e.path.join("-")}`;function To({id:e,value:t,field:o,invalid:r,disabled:i}){let[s,a]=K(String(t??""));return U(()=>{(s===""?t!==void 0:Number(s)!==t)&&a(String(t??""))},[t]),n("input",{id:e,type:"number",inputmode:"decimal",value:s,disabled:i,min:o.min,step:o.step??"any",placeholder:o.placeholder??o.default??"",class:`input max-w-40 font-mono tabular-nums ${r?"input-invalid":""}`,onInput:u=>{let m=u.currentTarget.value;a(m),m===""?B(o.path,void 0):Number.isNaN(Number(m))||B(o.path,Number(m))}})}function yt({field:e,value:t}){return e.exclusive_min===void 0||typeof t!="number"||t>e.exclusive_min?null:n("p",{class:"mt-1.5 text-[13px] text-warn",children:["Must be greater than ",e.exclusive_min,"."]})}function Eo({id:e,field:t,value:o,invalid:r}){let i=t.unlimited_value!==void 0&&o===t.unlimited_value,s=n(To,{id:e,value:i?void 0:o,field:t,invalid:r,disabled:i});if(t.null_label!==void 0){let a=o===null?"n":o===void 0?"d":"s",u=typeof t.default=="number"?t.default:300;return n("div",{children:[n(ee,{id:e,label:t.label,value:a,options:[["d","Default"],["s","Seconds"],["n",t.null_label]],onPick:m=>B(t.path,m==="d"?void 0:m==="n"?null:u)}),a==="s"?n("div",{class:"mt-2.5",children:[s,n(yt,{field:t,value:o})]}):null]})}return t.unlimited_value!==void 0?n("div",{children:[n("div",{class:"flex flex-wrap items-center gap-x-4",children:[s,n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm text-body",children:[n("input",{type:"checkbox",class:"size-[18px] accent-brand-solid",checked:i,onChange:a=>B(t.path,a.currentTarget.checked?t.unlimited_value:void 0)}),"Unlimited"]})]}),n(yt,{field:t,value:o})]}):n("div",{children:[s,n(yt,{field:t,value:o})]})}function Oo({id:e,field:t,value:o}){if(t.unset_label!==void 0){let r=[["d","Default"]];return t.allow_true!==!1&&r.push(["t","On"]),r.push(["f","Off"]),n("div",{children:[n(ee,{id:e,label:t.label,value:o===void 0?"d":o?"t":"f",options:r,onPick:i=>B(t.path,i==="d"?void 0:i==="t")}),n("p",{class:"mt-1.5 text-xs text-faint",children:["Default: ",t.unset_label]})]})}return n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-semibold text-ink",children:[n("input",{id:e,type:"checkbox",class:"size-[18px] accent-brand-solid",checked:(o??t.default??!1)===!0,onChange:r=>B(t.path,r.currentTarget.checked)}),t.label]})}function Po({id:e,field:t,value:o,invalid:r}){if(t.type==="bool")return n(Oo,{id:e,field:t,value:o});if(t.type==="number")return n(Eo,{id:e,field:t,value:o,invalid:r});if(t.type==="list")return n(H,{id:e,values:Array.isArray(o)?o:[],placeholder:t.placeholder,onChange:i=>B(t.path,i.length?i:void 0)});if(t.type==="select"){let i=t.options.map(s=>Array.isArray(s)?s:[s,s]);return t.unset_label!==void 0&&i.unshift(["",t.unset_label]),n(A,{id:e,options:i,invalid:r,value:o??(t.unset_label!==void 0?"":t.default??(Array.isArray(t.options[0])?t.options[0][0]:t.options[0])),onChange:s=>B(t.path,s===""?void 0:s)})}return n("input",{id:e,type:"text",value:o??"",placeholder:t.placeholder,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${r?"input-invalid":""}`,onInput:i=>B(t.path,i.currentTarget.value===""?void 0:i.currentTarget.value)})}function I({field:e}){let t=e.path.join("."),o=$o(e),r=L(d.config,e.path),i=S(t),s=ft(e),a=fn(e),u=e.type==="bool"&&e.unset_label===void 0,m=e.default!==void 0&&!s,l=Array.isArray(e.default)?e.default.join(", "):String(e.default??"");return n("div",{id:`row-${o}`,class:`relative pt-5 pb-1 ${s?"before:absolute before:top-6 before:-left-2.5 before:bottom-2 before:w-[3px] before:rounded before:bg-brand before:content-[''] sm:before:-left-3.5":""}`,children:[n("div",{class:"flex min-h-6 items-center gap-2.5",children:[u?null:n("label",{for:o,class:`text-sm font-semibold text-ink ${a?"opacity-50":""}`,children:[e.label,e.required?n("span",{class:"text-danger",children:" *"}):null]}),u?null:n("code",{class:"hidden font-mono text-[11px] text-faint sm:inline",children:t}),m&&!u?n("span",{class:"rounded border border-line px-1.5 text-[11px] leading-4 font-medium text-faint",children:"default"}):null,n("span",{class:"flex-1"}),s?n("button",{type:"button",class:"focus-ring min-h-8 cursor-pointer rounded px-1 text-[13px] font-medium text-brand hover:underline",title:`Reset to default${e.default!==void 0?` (${l})`:""}`,"aria-label":`Reset ${e.label} to default`,onClick:()=>b(p=>he(p.config,e.path)),children:"Reset"}):null]}),e.help?n("p",{class:`mt-0.5 max-w-prose text-[13px] leading-5 text-muted ${a?"opacity-50":""}`,children:e.help}):null,n("fieldset",{disabled:a,class:`m-0 mt-2.5 min-w-0 max-w-[520px] border-0 p-0 ${a?"opacity-50":""}`,children:n(Po,{id:o,field:e,value:r,invalid:i?.level==="error"})}),a?n("p",{class:"mt-1.5 text-xs text-faint",children:hn(e)}):null,n(O,{issue:i})]})}var Io=[["","Choose reach"],["own","own"],["tenant","tenant"],["all","all"]],Pn=e=>Array.isArray(e)?e.length>0:g(e)&&Object.keys(e).length>0;function In(){let e=d.config.auth;return e==="jwt"?{method:"jwt"}:g(e)?e:{method:"none"}}function z(e){b(t=>{let o={...In()};e(o),t.config.auth=o.method==="none"?null:o})}function Ve(e,t,o){o==null||o===""||typeof o!="string"&&!Pn(o)?delete e[t]:e[t]=o}function Ao(e){return e.roles_claim!==void 0||Pn(e.roles)?"roles":e.permissions_claim!==void 0?"permissions":"default"}var Ro=e=>d.issues.filter(t=>t.path===e||t.path.startsWith(`${e}.`));function wt({prefix:e}){return Ro(e).map((t,o)=>n(O,{issue:t},o))}function An({id:e,label:t,name:o,onRename:r}){let[i,s]=K(o);return U(()=>s(o),[o]),n("div",{children:[n("label",{for:e,class:"sr-only",children:t}),n("input",{id:e,type:"text",value:i,class:"input font-mono",autocomplete:"off",spellcheck:!1,onInput:a=>s(a.currentTarget.value),onChange:a=>{r(a.currentTarget.value.trim())||s(o)}})]})}var Be=({children:e})=>n("span",{class:"mb-1.5 block text-xs font-medium text-faint",children:e});function Rn(e,t,o){return!o||o!==t&&o in e?null:Object.fromEntries(Object.entries(e).map(([r,i])=>[r===t?o:r,i]))}function jn({sec:e}){let t=In(),o=t.method??"none",r=Ao(t);return n("div",{class:"space-y-6",children:[n(R,{title:"Method",children:n(xt,{name:"auth-method",label:"Authentication method",value:o,options:[["none","None","Every request is accepted. For local development."],["jwt","JWT","Verify bearer tokens signed with JWT_SECRET_KEY."],["custom","Custom","Your own BaseAuth subclass decides who is calling."]],onPick:i=>z(s=>{s.method=i,i==="custom"?s.path=s.path??"":delete s.path})})}),o==="custom"?n(R,{title:"Class",children:n("div",{class:"panel",children:n(Y,{id:"auth-path",label:"Auth class",path:"auth.path",value:t.path,placeholder:"auth.custom:MyAuth",help:"Import path of a BaseAuth subclass, as module:attribute.",issue:S("auth.path"),onInput:i=>z(s=>{s.path=i})})})}):null,o!=="none"?n(x,{children:[n(R,{title:"Permission source",children:n(xt,{name:"auth-source",label:"Permission source",value:r,options:[["default","Default","Every signed-in user, own data only."],["roles","Roles from a claim","Map the roles in the token to permissions."],["permissions","Permissions from a claim","The token already lists the permissions."]],onPick:i=>z(s=>{delete s.roles_claim,delete s.permissions_claim,delete s.roles,delete s.default_role,i==="roles"&&(s.roles_claim="roles",s.roles={member:{permissions:[],reach:"own"}}),i==="permissions"&&(s.permissions_claim="permissions")})})}),r==="roles"?n(Mo,{auth:t,registry:e.registry}):null,r==="permissions"?n(R,{title:"Claim",children:n("div",{class:"panel",children:n(Y,{id:"auth-permissions-claim",label:"Permissions claim",path:"auth.permissions_claim",value:t.permissions_claim,placeholder:"permissions",issue:S("auth.permissions_claim"),help:"Token claim holding a list of permissions, such as graph:invoke.",onInput:i=>z(s=>{s.permissions_claim=i})})})}):null,n(R,{title:"Tenants",children:n("div",{class:"panel divide-y divide-line",children:[n("div",{class:"px-5 py-4",children:[n(xe,{label:"Tenant claims",path:"auth.tenant_claims",id:"auth-tenant-claims",help:"Token claims that name the tenant, such as org_id. Rows are separated by them."}),n("div",{class:"mt-3",children:n(H,{id:"auth-tenant-claims",values:t.tenant_claims??[],placeholder:"org_id",onChange:i=>z(s=>Ve(s,"tenant_claims",i))})}),n(wt,{prefix:"auth.tenant_claims"})]}),r==="roles"?n(jo,{auth:t}):null]})}),n(No,{auth:t})]}):null]})}function jo({auth:e}){let t=Object.keys(g(e.roles)?e.roles:{});return n(bt,{children:[n(xe,{label:"Default role",path:"auth.default_role",id:"auth-default-role",help:"Given to a signed-in user whose token carries no role."}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n(A,{id:"auth-default-role",value:e.default_role??"",options:[["","None"],...t.map(o=>[o,o])],onChange:o=>z(r=>Ve(r,"default_role",o))}),n(O,{issue:S("auth.default_role")})]})]})}function Mo({auth:e,registry:t}){let o=g(e.roles)?e.roles:{},r=Object.entries(o),i=Object.keys(o),s=["*",...Object.entries(t||{}).flatMap(([p,h])=>[`${p}:*`,...h.map(c=>`${p}:${c}`)])],a=p=>z(h=>{h.roles=p}),u=(p,h)=>a({...o,[p]:{...o[p],...h}}),m=()=>{let p=r.length+1;for(;`role${p}`in o;)p++;a({...o,[`role${p}`]:{permissions:[],reach:"own"}})},l=(p,h)=>{let c=Rn(o,p,h);return c?(z(f=>{f.roles=Object.fromEntries(Object.entries(c).map(([y,$])=>[y,$.inherits?{...$,inherits:$.inherits.map(w=>w===p?h:w)}:$])),f.default_role===p&&(f.default_role=h),g(f.system_identities)&&(f.system_identities=Object.fromEntries(Object.entries(f.system_identities).map(([y,$])=>[y,{...$,roles:($.roles??[]).map(w=>w===p?h:w)}])))}),!0):!1};return n(R,{title:"Roles",children:n("div",{class:"panel",children:[n("div",{class:"border-b border-line",children:n(Y,{id:"auth-roles-claim",label:"Roles claim",path:"auth.roles_claim",value:e.roles_claim,placeholder:"roles",issue:S("auth.roles_claim"),help:"Token claim holding the user's list of role names.",onInput:p=>z(h=>{h.roles_claim=p})})}),r.length?n("div",{class:"divide-y divide-line",children:r.map(([p,h],c)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_8rem_auto] items-start gap-3",children:[n(An,{id:`role-name-${c}`,label:"Role name",name:p,onRename:f=>l(p,f)}),n(A,{id:`role-reach-${c}`,value:h.reach??"",options:Io,invalid:S(`auth.roles.${p}.reach`)?.level==="error",onChange:f=>u(p,{reach:f})}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove role ${p}`,onClick:()=>a(Object.fromEntries(r.filter(([f])=>f!==p))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Be,{children:"Permissions"}),n(H,{id:`role-permissions-${c}`,values:h.permissions??[],placeholder:"graph:invoke, threads:*, *",suggestions:s,onChange:f=>u(p,{permissions:f})})]}),n("div",{children:[n(Be,{children:"Inherits"}),n(H,{id:`role-inherits-${c}`,values:h.inherits??[],placeholder:"another role",suggestions:i.filter(f=>f!==p),onChange:f=>{let y={...h};Ve(y,"inherits",f),a({...o,[p]:y})}})]}),n(wt,{prefix:`auth.roles.${p}`})]},c))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"No roles yet. Add one to use roles."}),n("div",{class:"flex items-center justify-between gap-4 border-t border-line px-5 py-3",children:[n("p",{class:"text-[13px] text-muted",children:["Reach: ",n("code",{class:"font-mono",children:"own"})," the user's rows, ",n("code",{class:"font-mono",children:"tenant"})," every row of their tenant, ",n("code",{class:"font-mono",children:"all"})," every row."]}),n("button",{type:"button",class:"btn btn-ghost",onClick:m,children:[n(_,{name:"plus"}),"Add role"]})]})]})})}function No({auth:e}){let t=g(e.system_identities)?e.system_identities:{},o=Object.entries(t),r=Object.keys(g(e.roles)?e.roles:{}),i=l=>z(p=>Ve(p,"system_identities",l)),s=(l,p)=>i({...t,[l]:{...t[l],...p}}),a=()=>{let l=o.length+1;for(;`job${l}`in t;)l++;i({...t,[`job${l}`]:{roles:[],tenant:{}}})},u=l=>Object.entries(l??{}).map(([p,h])=>`${p}=${h}`),m=l=>Object.fromEntries(l.filter(p=>p.includes("=")).map(p=>[p.slice(0,p.indexOf("=")).trim(),p.slice(p.indexOf("=")+1).trim()]));return n(R,{title:"System identities",children:n("div",{class:"panel",children:[o.length?n("div",{class:"divide-y divide-line",children:o.map(([l,p],h)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3",children:[n(An,{id:`system-name-${h}`,label:"Identity name",name:l,onRename:c=>{let f=Rn(t,l,c);return f&&i(f),!!f}}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${l}`,onClick:()=>i(Object.fromEntries(o.filter(([c])=>c!==l))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Be,{children:"Roles"}),n(H,{id:`system-roles-${h}`,values:p.roles??[],placeholder:"member",suggestions:r,onChange:c=>s(l,{roles:c})})]}),n("div",{children:[n(Be,{children:"Tenant, as claim=value"}),n(H,{id:`system-tenant-${h}`,values:u(p.tenant),placeholder:"org_id=acme",onChange:c=>s(l,{tenant:m(c)})})]}),n(wt,{prefix:`auth.system_identities.${l}`})]},h))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"None. Scheduled jobs and other non-user callers can run as one."}),n("div",{class:"flex justify-end border-t border-line px-5 py-3",children:n("button",{type:"button",class:"btn btn-ghost",onClick:a,children:[n(_,{name:"plus"}),"Add identity"]})})]})})}var Ge=[["memory","Memory","This process only"],["redis","Redis","Shared. Redis, Valkey, Dragonfly, KeyDB"],["custom","Custom","Your BaseCacheStore class"]],Lo={requests:100,window:60,by:"ip"},Je=()=>g(d.config.cache_store)?d.config.cache_store:{};function Ct(e){b(t=>{let o=g(t.config.cache_store)?t.config.cache_store:{};e(o),Object.keys(o).length?t.config.cache_store=o:delete t.config.cache_store})}var Do=()=>{let e=Je().rate_limit;return g(e)&&e.enabled!==!1},Ho=()=>{let e=Je().token_revocation;return e===!0||g(e)&&e.enabled!==!1};function zo(e){Ct(t=>{delete t.url,delete t.path,e==="memory"?delete t.type:t.type=e})}function Fo(e){Ct(t=>{e?g(t.rate_limit)?delete t.rate_limit.enabled:t.rate_limit={...Lo}:delete t.rate_limit})}function Uo(e){Ct(t=>{e?g(t.token_revocation)?delete t.token_revocation.enabled:t.token_revocation=!0:delete t.token_revocation})}var En=(e,t)=>b(o=>{oe(o.config,e,t)});function qo({value:e,onPick:t}){let o=(r,i)=>{let s=r.key==="ArrowRight"||r.key==="ArrowDown"?1:r.key==="ArrowLeft"||r.key==="ArrowUp"?-1:0;if(!s)return;r.preventDefault();let a=Ge[(i+s+Ge.length)%Ge.length][0];t(a),requestAnimationFrame(()=>document.getElementById(`cache-type-${a}`)?.focus())};return n("div",{role:"radiogroup","aria-label":"Cache store type",class:"grid gap-3 sm:grid-cols-3",children:Ge.map(([r,i,s],a)=>{let u=r===e;return n("button",{type:"button",role:"radio",id:`cache-type-${r}`,"aria-checked":u?"true":"false",tabIndex:u?0:-1,onKeyDown:m=>o(m,a),class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 motion-reduce:transition-none ${u?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{u||t(r)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${u?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:u?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:i}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:s})]})]},r)})})}function ye({warn:e,children:t}){return n("p",{class:`mt-3 flex items-start gap-1.5 text-[13px] leading-5 ${e?"text-warn":"text-muted"}`,children:[e?n(_,{name:"alert",class:"mt-0.5 size-3.5"}):null,n("span",{children:t})]})}function On({id:e,title:t,text:o,on:r,onChange:i}){return n("div",{class:"flex items-center gap-4",children:[n("div",{class:"min-w-0 flex-1",children:[n("h3",{class:"text-base font-semibold text-ink",children:t}),n("p",{class:"mt-0.5 text-[13px] leading-5 text-muted",children:o})]}),n(be,{id:e,on:r,label:t,onChange:i})]})}var Wo=[["ip","IP"],["user","User"],["global","Global"]];function Go({field:e,memory:t}){let[o,r]=K(!1),i=Je().rate_limit,s=g(i)&&i.by||"ip",a=!(g(i)&&i.fail_open===!1),u=["trusted_proxy_headers","trusted_proxy_hops","trusted_proxies","exclude_paths"];return n(x,{children:[t?n(ye,{warn:!0,children:"Counts per worker with the Memory store."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:e("cache_store.rate_limit.requests")}),n(I,{field:e("cache_store.rate_limit.window")}),n("div",{class:"pt-5",children:[n("span",{id:"cache-by-label",class:"block text-sm font-semibold text-ink",children:"Limit by"}),n("div",{class:"mt-2.5",children:n(ee,{id:"f-cache_store-rate_limit-by",label:"Limit by",options:Wo,value:s,onPick:m=>En(["cache_store","rate_limit","by"],m)})}),n(O,{issue:S("cache_store.rate_limit.by")})]}),n("div",{class:"pt-5",children:[n("span",{id:"cache-fail-open-label",class:"block text-sm font-semibold text-ink",children:"Fail open"}),n("div",{class:"mt-2.5 flex items-center gap-3",children:[n(be,{id:"f-cache_store-rate_limit-fail_open",on:a,label:"Fail open",onChange:m=>En(["cache_store","rate_limit","fail_open"],m)}),n("p",{class:"m-0 text-[13px] leading-5 text-muted",children:a?"Allow requests if the store errors.":"Deny with 503 if the store errors."})]})]})]}),n("div",{class:"mt-5 border-t border-line pt-1",children:[n("button",{type:"button","aria-expanded":o?"true":"false","aria-controls":"cache-advanced",class:"focus-ring flex min-h-11 cursor-pointer items-center gap-2 rounded font-medium text-muted hover:text-ink",onClick:()=>r(!o),children:[n(_,{name:"chevronRight",class:`size-4 transition-transform duration-150 motion-reduce:transition-none ${o?"rotate-90":""}`}),"Advanced"]}),o?n("div",{id:"cache-advanced",children:u.map(m=>n(I,{field:e(`cache_store.rate_limit.${m}`)},m))}):null]})]})}var ae=[["rate","Rate limiting"],["revoke","Token revocation"]];function Mn({sec:e}){let[t,o]=K("rate"),r=Je().type||"memory",i=l=>e.fields.find(p=>p.path.join(".")===l),s={rate:Do(),revoke:Ho()},a=r==="memory",u={rate:!!S("cache_store.rate_limit"),revoke:!!S("cache_store.token_revocation")},m=(l,p)=>{let h=l.key==="ArrowRight"?1:l.key==="ArrowLeft"?-1:0,c=l.key==="Home"?0:l.key==="End"?ae.length-1:h?(p+h+ae.length)%ae.length:-1;c<0||(l.preventDefault(),o(ae[c][0]),document.getElementById(`cache-tab-${ae[c][0]}`)?.focus())};return n("div",{children:[n(R,{title:"Store type",children:[n(qo,{value:r,onPick:zo}),r==="redis"?n(I,{field:i("cache_store.url")}):null,r==="custom"?n(I,{field:i("cache_store.path")}):null,a?n("p",{class:"mt-3 mb-0 text-[13px] leading-5 text-muted",children:"Not shared between workers: each worker keeps its own locks, limits and revocations."}):null]}),n("div",{role:"tablist","aria-label":"Cache store features",class:"mt-8 flex gap-1 border-b border-line",children:ae.map(([l,p],h)=>{let c=t===l;return n("button",{type:"button",role:"tab",id:`cache-tab-${l}`,"aria-selected":c?"true":"false","aria-controls":`cache-panel-${l}`,tabIndex:c?0:-1,onKeyDown:f=>m(f,h),class:`focus-ring -mb-px flex min-h-11 cursor-pointer items-center gap-2 border-b-2 px-3 text-sm font-medium transition-colors duration-150 motion-reduce:transition-none ${c?"border-brand-solid text-ink":"border-transparent text-muted hover:text-ink"}`,onClick:()=>o(l),children:[p,n(ie,{class:s[l]?"bg-brand-soft text-brand":"bg-raised text-muted",children:s[l]?"On":"Off"})]},l)})}),n("div",{role:"tabpanel",id:`cache-panel-${t}`,"aria-labelledby":`cache-tab-${t}`,class:"pt-5",children:t==="rate"?n(x,{children:[n(On,{id:"cache-rate-limit",title:"Rate limiting",on:s.rate,onChange:Fo,text:"Cap how many requests one caller can make."}),s.rate?n(Go,{field:i,memory:a}):u.rate?n(ye,{warn:!0,children:"Off: one caller can exhaust the service."}):null]}):n(x,{children:[n(On,{id:"cache-token-revocation",title:"Token revocation",on:s.revoke,onChange:Uo,text:"Reject revoked tokens before they expire (10xgraph revoke)."}),s.revoke?n(x,{children:[a?n(ye,{warn:!0,children:"10xgraph revoke cannot reach an in-memory store; pick Redis or Custom."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:i("cache_store.token_revocation.token_lifetime")}),n(I,{field:i("cache_store.token_revocation.user_ttl")})]}),n(ye,{children:"Fails closed: while the cache store is unreachable, authenticated requests get 503 and the server will not start."})]}):u.revoke?n(ye,{warn:!0,children:"Off: a leaked token works until it expires."}):null]})})]})}function Nn({sec:e}){let t=e.fields[0].path[0],o=e.choices.map(p=>p[0]),r=d.config[t],i=t==="thread_name_generator"?"random":"",s=r===void 0?i:typeof r=="string"&&o.includes(r)?r:"custom",a=[...t==="id_generator"?[["","Not set (the graph's own generator)"]]:[],...e.choices.map(([p,h])=>[p,t==="thread_name_generator"&&p==="random"?`${h} (default)`:h]),["custom","Custom (module:attr)"]],u=s===""?"The graph keeps the generator it was compiled with.":s==="custom"?"Your own class, as module:attribute.":e.choices.find(p=>p[0]===s)?.[2],m=p=>b(h=>{p===i?delete h.config[t]:h.config[t]=p==="custom"?"":p}),l=`f-${t}`;return n("div",{children:[n(R,{title:"Choice",children:n("div",{class:"max-w-[520px]",children:[n("label",{for:`${l}-select`,class:"sr-only",children:e.title}),n(A,{id:`${l}-select`,options:a,value:s,onChange:m}),n("p",{class:"mt-1.5 text-[13px] text-muted",children:u})]})}),s==="custom"?n(R,{title:"Class",children:n("div",{class:"panel",children:n(Y,{id:l,label:e.fields[0].label,path:t,value:typeof r=="string"?r:"",placeholder:e.fields[0].placeholder,help:e.fields[0].help,issue:S(t),onInput:p=>b(h=>{h.config[t]=p})})})}):null]})}function Ln(){let e=Array.isArray(d.config.remote_tools)?d.config.remote_tools:[],t=()=>{b(o=>{e.push({node:"",name:"",description:"",parameters:{type:"object",properties:{},required:[]}}),o.config.remote_tools=e}),requestAnimationFrame(()=>document.getElementById(`tool-${e.length-1}-name`)?.focus())};return e.length?n("div",{class:"space-y-4",children:[e.map((o,r)=>n(Bo,{tools:e,tool:o,index:r},r)),n("button",{type:"button",onClick:t,class:"focus-ring flex h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-dashed border-line-strong text-sm font-medium text-muted transition-colors duration-150 hover:border-brand hover:text-brand",children:[n(_,{name:"plus"}),"Add remote tool"]})]}):n("div",{class:"rounded-xl border border-dashed border-line-strong bg-surface px-6 py-10 text-center",children:[n("div",{class:"mx-auto flex size-11 items-center justify-center rounded-full bg-raised text-faint",children:n(_,{name:"wrench",class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:"No remote tools yet"}),n("p",{class:"mx-auto mt-1 max-w-sm text-sm leading-6 text-muted",children:"Declare a tool here and the client that calls the API runs it, for example to read the user's location."}),n("button",{type:"button",class:"btn btn-outline mt-5",onClick:t,children:[n(_,{name:"plus"}),"Add remote tool"]})]})}function Bo({tools:e,tool:t,index:o}){let r=d.toolParams[o]??{text:JSON.stringify(t.parameters??{},null,2),error:null},i=a=>b(()=>{e[o]={...e[o],...a}}),s=a=>`remote_tools[${o}].${a}`;return n("section",{class:"panel",children:[n("div",{class:"flex items-center gap-3 border-b border-line px-5 py-3",children:[n(_,{name:"wrench",class:"size-4 text-faint"}),n("span",{class:"min-w-0 flex-1 truncate font-mono text-sm font-medium text-ink",children:t.name||`tool_${o+1}`}),t.node?n("span",{class:"rounded-md border border-line bg-raised px-1.5 py-0.5 font-mono text-xs text-muted",children:t.node}):null,n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${t.name||"tool"}`,onClick:()=>b(a=>{e.splice(o,1),a.toolParams={}}),children:n(_,{name:"trash"})})]}),n("div",{class:"divide-y divide-line",children:[n(Y,{id:`tool-${o}-name`,label:"Tool name",path:s("name"),value:t.name,placeholder:"get_location",onInput:a=>i({name:a})}),n(Y,{id:`tool-${o}-node`,label:"Graph node",path:s("node"),value:t.node,placeholder:"MAIN",help:"The node whose model may call this tool.",onInput:a=>i({node:a})}),n(Y,{id:`tool-${o}-description`,label:"Description",path:s("description"),value:t.description,placeholder:"Returns the user's current city",help:"What the model reads when deciding to call it.",onInput:a=>i({description:a})}),n("div",{class:"px-5 py-4",children:[n(xe,{label:"Parameters",path:s("parameters"),help:"JSON schema of the arguments. type must be object.",id:`tool-${o}-params`}),n("textarea",{id:`tool-${o}-params`,rows:7,spellcheck:!1,value:r.text,class:`input mt-3 h-auto py-2.5 font-mono text-[13px] leading-5 ${r.error?"input-invalid":""}`,onInput:a=>{let u=a.currentTarget.value;b(m=>{try{e[o]={...e[o],parameters:JSON.parse(u)},m.toolParams[o]={text:u,error:null}}catch(l){m.toolParams[o]={text:u,error:l.message}}})}}),n(O,{issue:r.error?{level:"error",message:r.error}:null})]})]})]})}var Tt="focus-ring min-h-8 cursor-pointer rounded px-1.5 text-[13px] font-medium text-brand transition-colors duration-150 hover:underline",te="mt-2 max-w-prose text-[13px] leading-5 text-muted",V=(e,t)=>b(o=>{t===void 0||t===""?he(o.config,e):oe(o.config,e,t)});function Et({id:e,label:t,path:o,help:r,children:i}){return n("div",{class:"pt-4",children:[n("div",{class:"flex items-baseline gap-2.5",children:[n("label",{for:e,class:"text-sm font-semibold text-ink",children:t}),o?n("code",{class:"hidden font-mono text-[11px] text-faint sm:inline",children:o}):null]}),n("div",{class:"mt-2 max-w-[520px]",children:i}),r?n("p",{class:te,children:r}):null,n(O,{issue:S(o)})]})}function le({path:e,label:t,help:o,placeholder:r}){let i=e.join("."),s=`f-${e.join("-")}`,a=L(d.config,e);return n(Et,{id:s,label:t,path:i,help:o,children:n("input",{id:s,type:"text",value:a??"",placeholder:r,autocomplete:"off",spellcheck:!1,class:`input font-mono ${S(i)?.level==="error"?"input-invalid":""}`,onInput:u=>V(e,u.currentTarget.value)})})}function Vo({path:e,label:t,help:o}){let r=e.join("."),i=`f-${e.join("-")}`,s=L(d.config,e);return n(Et,{id:i,label:t,path:r,help:o,children:n("input",{id:i,type:"number",inputmode:"numeric",min:"1",step:"1",value:s??"",class:`input max-w-40 font-mono tabular-nums ${S(r)?.level==="error"?"input-invalid":""}`,onInput:a=>{let u=a.currentTarget.value;V(e,u===""?void 0:Number(u))}})})}function St({path:e,label:t,help:o,options:r,value:i,onChange:s}){let a=`f-${e.join("-")}`;return n(Et,{id:a,label:t,path:e.join("."),help:o,children:n(A,{id:a,plain:!0,options:r,value:i,onChange:s})})}function Jo({path:e,label:t,checked:o}){let r=`f-${e.join("-")}`;return n("label",{class:"mt-3 inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-semibold text-ink",children:[n("input",{id:r,type:"checkbox",class:"size-[18px] accent-brand-solid",checked:o,onChange:i=>V(e,i.currentTarget.checked)}),t]})}var Ye=[{key:"logfire",name:"Logfire",label:"Logfire",text:"Pydantic Logfire.",secret:n(x,{children:["Token: set ",n("code",{class:"font-mono text-xs",children:"LOGFIRE_TOKEN"})," in .env."]})},{key:"langsmith",name:"LangSmith",label:"LangSmith",text:"LangChain's tracing service.",secret:n(x,{children:["API key: set ",n("code",{class:"font-mono text-xs",children:"LANGSMITH_API_KEY"})," in .env."]})},{key:"phoenix",name:"Phoenix",label:"Phoenix",text:"Arize Phoenix, local or hosted.",secret:n(x,{children:["API key: set ",n("code",{class:"font-mono text-xs",children:"PHOENIX_API_KEY"})," in .env. A local Phoenix does not need one."]})},{key:"otlp",name:"OTLP endpoint",label:"OTLP endpoint",text:"Any OpenTelemetry endpoint: a collector, Tempo, Jaeger.",secret:n(x,{children:["Auth headers: set ",n("code",{class:"font-mono text-xs",children:"OTEL_EXPORTER_OTLP_HEADERS"})," in .env."]})}],Ko=[["standard","Standard"],["spans","Spans only"],["full","Full"]],Yo={standard:"Adds model name, token counts and request parameters.",spans:"Timing and structure only. No inputs or outputs.",full:"Records prompts and tool I/O. May contain PII."},Xo=[["raw","Raw"],["hash","Hashed"],["mask","Masked"]],Qo={raw:"The user id is sent as is.",hash:"A SHA-256 of the user id, keyed when TENXGRAPH_TRACE_USER_ID_KEY is set.",mask:"The middle of the user id is hidden."},$t=()=>g(d.config.observability)?d.config.observability:{},Xe=e=>g($t()[e])&&$t()[e].enabled===!0;function Dn(e){b(t=>{g(t.config.observability)||(t.config.observability={level:"standard",user_id_mode:"raw"}),t.config.observability[e]={enabled:!0}})}function Zo(e){b(t=>{delete t.config.observability[e],Ye.some(o=>Xe(o.key))||delete t.config.observability})}function er(){let e=d.config.metrics,t=g(e)&&g(e.otlp)?e.otlp.endpoint:null;return!g(e)||e.enabled!==!0||e.export!=="otlp"||!t?null:t.replace(/\/+$/,"").replace(/\/v1\/metrics$/,"")}function tr(){let e=["observability","otlp","use_metrics_endpoint"],t=L(d.config,e)===!0,o=er(),r="f-observability-otlp-use_metrics_endpoint";return n(x,{children:[o?n("div",{class:"pt-4",children:[n("div",{class:"flex items-center gap-3",children:[n(be,{id:r,on:t,label:"Use the metrics endpoint",onChange:i=>V(e,i?!0:void 0)}),n("label",{for:r,class:"cursor-pointer text-sm font-semibold text-ink",children:"Use the metrics endpoint"})]}),n("p",{class:te,children:t?n(x,{children:["Traces go to ",n("code",{class:"font-mono text-xs text-body",children:[o,"/v1/traces"]})]}):"Send traces to the same endpoint as the metrics."}),n(O,{issue:S(e.join("."))})]}):null,t&&o?null:n(le,{path:["observability","otlp","endpoint"],label:"Endpoint",placeholder:"http://collector:4318",help:"Base URL of an OTLP HTTP receiver. /v1/traces is appended."}),o?null:n("p",{class:te,children:["Sharing the metrics endpoint needs Metrics set to OTLP push with an endpoint.",t?n(x,{children:[" It is on in the file. ",n("button",{type:"button",class:Tt,onClick:()=>V(e,void 0),children:"Stop sharing"})]}):null]}),n(O,{issue:t&&!o?S(e.join(".")):null}),n("p",{class:te,children:"Prometheus stores metrics only. To send traces and metrics to one place, use a collector (OTel Collector, Grafana Alloy, Grafana Cloud)."})]})}function nr({dest:e}){let t=r=>["observability",e.key,r];if(e.key==="logfire"){let r=t("send_to_logfire"),i=t("console");return n(x,{children:[n(le,{path:t("service_name"),label:"Service name",placeholder:"my-agent"}),n(Jo,{path:r,label:"Send to Logfire",checked:L(d.config,r)!==!1}),n(St,{path:i,label:"Print spans to console",help:"Logfire also takes console options that JSON cannot express, so only its own behavior or Off is offered.",options:[["","Logfire's own behavior"],["off","Off"]],value:L(d.config,i)===!1?"off":"",onChange:s=>V(i,s==="off"?!1:void 0)})]})}if(e.key==="otlp")return n(tr,{});let o=e.key==="phoenix";return n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(le,{path:t("project"),label:"Project",placeholder:"my-agent"}),n(le,{path:t("endpoint"),label:"Endpoint",placeholder:o?"http://localhost:6006":"https://api.smith.langchain.com/otel",help:o?"Base collector URL. /v1/traces is appended.":void 0})]})}function or({dest:e}){return n("div",{id:`dest-${e.key}`,class:"mt-4 rounded-xl border border-line bg-surface p-5",children:[n("div",{class:"flex items-start gap-3",children:[n("div",{class:"min-w-0 flex-1",children:[n("h3",{class:"text-sm font-semibold text-ink",children:e.name}),n("p",{class:"mt-0.5 text-[13px] text-muted",children:e.text})]}),n("button",{type:"button",class:Tt,"aria-label":`Remove ${e.name}`,onClick:()=>Zo(e.key),children:"Remove"})]}),n(nr,{dest:e}),n("p",{class:"mt-5 border-t border-line pt-3 text-[13px] text-muted",children:e.secret})]})}function Hn(){let[e,t]=K(!1),o=Ye.filter(s=>Xe(s.key)),r=Ye.filter(s=>!Xe(s.key)),i=$t();return n("div",{children:o.length===0?n("div",{class:"max-w-[520px]",children:[n("label",{for:"trace-dest",class:"mb-2 block text-sm font-semibold text-ink",children:"Send AI traces to"}),n(A,{id:"trace-dest",value:"",plain:!0,options:[["","None"],...r.map(s=>[s.key,s.label])],onChange:s=>{s&&Dn(s)}}),n("p",{class:te,children:"Graph runs are not traced."})]}):n(x,{children:[n("h2",{class:"eyebrow",children:"Send AI traces to"}),o.map(s=>n(or,{dest:s},s.key)),r.length?e?n("div",{class:"mt-4 max-w-[320px]",children:[n("label",{for:"trace-add",class:"sr-only",children:"Add another destination"}),n(A,{id:"trace-add",value:"",plain:!0,options:[["","Choose a destination"],...r.map(s=>[s.key,s.label])],onChange:s=>{t(!1),s&&Dn(s)}})]}):n("button",{type:"button",class:`${Tt} mt-3 inline-flex min-h-10 items-center gap-1.5`,onClick:()=>t(!0),children:[n(_,{name:"plus",class:"size-4"}),"Add another destination"]}):null,n("h2",{class:"eyebrow mt-8",children:"Trace settings"}),n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(St,{path:["observability","level"],label:"Level",options:Ko,value:i.level??"standard",onChange:s=>V(["observability","level"],s),help:Yo[i.level??"standard"]}),n(St,{path:["observability","user_id_mode"],label:"User id in traces",options:Xo,value:i.user_id_mode??"raw",onChange:s=>V(["observability","user_id_mode"],s),help:Qo[i.user_id_mode??"raw"]})]})]})})}var rr=[["","None"],["otlp","OTLP push"],["prometheus","Prometheus scrape"]],sr={"":"Metrics are not exported.",otlp:"Recommended for production. Works with several worker processes.",prometheus:"One process only. The API serves the scrape path, the worker its own port."},ir=[50,100,250,500,1e3,2500,5e3,1e4,3e4,6e4,12e4],Ke=()=>g(d.config.metrics)?d.config.metrics:{},zn=()=>Ke().enabled===!0&&["otlp","prometheus"].includes(Ke().export)?Ke().export:"";function ar(e){b(t=>{if(!e){delete t.config.metrics;return}let o=g(t.config.metrics)?t.config.metrics:{};o.enabled=!0,o.export=e,e==="otlp"&&!g(o.otlp)&&(o.otlp={endpoint:null}),e==="prometheus"&&!g(o.prometheus)&&(o.prometheus={path:"/metrics",worker_port:9464}),t.config.metrics=o})}function Fn(){let e=zn(),t=Ke().latency_buckets_ms,o=S("metrics.latency_buckets_ms");return n("div",{children:[n("div",{class:"max-w-[520px]",children:[n("label",{for:"f-metrics-export",class:"mb-2 block text-sm font-semibold text-ink",children:"Export metrics"}),n(A,{id:"f-metrics-export",plain:!0,options:rr,value:e,invalid:S("metrics.export")?.level==="error",onChange:ar}),n("p",{class:te,children:sr[e]}),n(O,{issue:S("metrics.export")})]}),e===""?null:n("div",{class:"mt-5 rounded-xl border border-line bg-surface px-5 pt-1 pb-5",children:[e==="otlp"?n(le,{path:["metrics","otlp","endpoint"],label:"Endpoint",placeholder:"http://collector:4318",help:n(x,{children:["/v1/metrics is appended.",n("br",{}),"Prometheus: ",n("code",{class:"font-mono text-xs",children:"http://prometheus:9090/api/v1/otlp"})," (start it with ",n("code",{class:"font-mono text-xs",children:"--web.enable-otlp-receiver"}),")."]})}):n(x,{children:n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(le,{path:["metrics","prometheus","path"],label:"Scrape path",placeholder:"/metrics",help:"Route on the API server that Prometheus scrapes."}),n(Vo,{path:["metrics","prometheus","worker_port"],label:"Worker port",help:"Each worker serves its own metrics on this port."})]})}),e==="otlp"?n("p",{class:"mt-5 border-t border-line pt-3 text-[13px] text-muted",children:["Auth headers: set ",n("code",{class:"font-mono text-xs",children:"OTEL_EXPORTER_OTLP_HEADERS"})," in .env."]}):null]}),e!==""?n(We,{title:"Advanced",class:"mt-4",open:!!o||t!==void 0,children:n("div",{class:"pt-4",children:[n("label",{for:"f-metrics-latency_buckets_ms",class:"text-sm font-semibold text-ink",children:"Latency buckets (ms)"}),n("div",{class:"mt-2 max-w-[520px]",children:n(H,{id:"f-metrics-latency_buckets_ms",values:(t??ir).map(String),placeholder:"250",onChange:r=>V(["metrics","latency_buckets_ms"],r.length?r.map(Number).filter(Number.isFinite):void 0)})}),n("p",{class:te,children:"Upper bounds of the latency histogram in milliseconds, ascending."}),n(O,{issue:o})]})}):null]})}var lr=e=>e.length>1?`${e.slice(0,-1).join(", ")} and ${e[e.length-1]}`:e[0];function Un(e){let t=Ye.filter(o=>Xe(o.key)).map(o=>o.name);return t.length?{text:`Level ${mt(["observability","level"])}, sending to ${lr(t)}.`}:{text:e("observability")}}function qn(e){let t={otlp:"OTLP push",prometheus:"Prometheus scrape"}[zn()];return t?{text:`${t}.`}:{text:e("metrics")}}var ke=({children:e,code:t})=>n("h1",{id:"page-title",tabindex:-1,class:"flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-semibold tracking-tight text-ink focus:outline-none",children:[e,t?n("code",{class:"rounded border border-line bg-raised px-1.5 font-mono text-xs font-normal text-faint",children:t}):null]}),Pt=({children:e})=>n("p",{class:"mt-1.5 max-w-[62ch] text-[15px] leading-6 text-muted",children:e}),Wn=({children:e})=>n("h2",{class:"mt-10 mb-2 eyebrow",children:e});function Gn({id:e,text:t,warn:o,compact:r}){let i=ge(e);return i?n("a",{href:`#${e}`,onClick:s=>{s.preventDefault(),D(e)},class:`focus-ring group flex items-center gap-3.5 border-t border-line py-2 last:border-b ${r?"min-h-12":"min-h-14"}`,children:[n(_,{name:Z[e]||"box",class:"size-[18px] text-faint"}),n("span",{class:`min-w-0 flex-1 ${r?"flex flex-wrap items-baseline gap-x-4":""}`,children:[n("span",{class:`block text-sm text-ink group-hover:text-brand ${r?"min-w-40 font-medium":"font-semibold"}`,children:i.title}),n("span",{class:`block text-[13px] leading-5 ${o?"text-warn":"text-muted"}`,children:t??i.description})]}),n(_,{name:"chevronRight",class:"size-4 text-faint"})]}):null}function Ot(e){return`Not set. ${P(e)?.off_summary||""}`.trim()}function cr(){let e=d.config.auth,t=e==="jwt"?"jwt":g(e)?e.method??"none":"none";if(t==="none")return{text:Ot("auth")};let o=g(e)&&g(e.roles)?Object.keys(e.roles).length:0;return{text:`${t==="jwt"?"JWT bearer tokens":`Custom backend${e.path?`: ${e.path}`:""}`}${o?`, ${o} ${o===1?"role":"roles"}`:""}.`}}function dr(){let e=g(d.config.cache_store)?d.config.cache_store:{},t=[`${e.type||"memory"} store`],o=e.rate_limit;if(g(o)&&o.enabled!==!1){let i={ip:"per IP",user:"per user",global:"across all clients"}[o.by??"ip"]||o.by;t.push(`${o.requests??100} requests per ${o.window??60} s, ${i}`)}let r=e.token_revocation;return(r===!0||g(r)&&r.enabled!==!1)&&t.push("token revocation on"),{text:`${t.join("; ")}.`}}function Bn(){let e=P("core"),t=_n(),o=e?re("core").filter(s=>!e.fields.some(a=>a.path.join(".")===s.path)):[],r=[["auth",cr()],["cache_store",dr()],["observability",Un(Ot)],["metrics",qn(Ot)]],i=d.nav.flatMap(s=>s.items).filter(s=>s!=="get-running");return n(x,{children:[n(ke,{children:t?"You're ready to run":"One step to run your agent"}),n(Pt,{children:q["get-running"].description}),n("div",{class:`mt-4 flex items-center gap-2 font-medium ${t?"text-ok":"text-muted"}`,children:[n(_,{name:t?"checkCircle":"circle",class:"size-[18px]"}),"Required: ",t?"1":"0"," of 1 done"]}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((s,a)=>n(vt,{issue:s},a))}):null,n("div",{class:"mt-2",children:e?kt(e.fields).flatMap(s=>s.fields).map(s=>n(I,{field:s},s.path.join("."))):null}),t?n("div",{class:"mt-7 border-t border-line pt-5",children:[n("div",{class:"flex items-center gap-2.5 font-medium text-ok",children:[n(_,{name:"checkCircle",class:"size-[18px]"}),"Ready for 10xgraph dev"]}),n("div",{class:"mt-3 flex max-w-[420px] items-center gap-2 rounded-lg border border-line-strong bg-sunken py-0 pr-1 pl-3.5",children:[n("code",{class:"flex-1 font-mono text-[13px] font-medium text-ink",children:"10xgraph dev"}),d.copied?n("span",{class:"text-xs text-ok",children:"Copied"}):null,n("button",{type:"button",class:"icon-btn",onClick:wn,"aria-label":d.copied?"Copied 10xgraph dev":"Copy command 10xgraph dev",children:n(_,{name:"copy",class:"size-[18px]"})})]}),n("p",{class:"mt-3 text-muted",children:"Everything else already has working defaults."})]}):n("p",{class:"mt-4 text-xs text-faint",children:"Add the agent graph above, then run 10xgraph dev."}),n(Wn,{children:"Before production (optional)"}),n("div",{children:r.map(([s,a])=>n(Gn,{id:s,text:a.text,warn:a.warn},s))}),n(Wn,{children:"What you can configure"}),n("div",{children:i.map(s=>n(Gn,{id:s,compact:!0},s))})]})}function Qe({vars:e}){return n("dl",{class:"mb-3 grid gap-x-4 gap-y-1 sm:grid-cols-[minmax(0,260px)_minmax(0,1fr)]",children:e.map(t=>[n("dt",{class:"py-0.5 font-mono text-xs font-medium break-all text-ink",children:t.name},`${t.name}-n`),n("dd",{class:"m-0 pb-2 text-[13px] text-muted sm:py-0.5",children:t.purpose},`${t.name}-p`)])})}function ur({vars:e,open:t}){return e?.length?n(We,{title:"Set in .env",open:t,class:"mt-9 border-t border-line pt-1",children:[n("p",{class:"mb-2 text-xs text-faint",children:"Read only. The editor never reads .env, so only names are shown."}),n(Qe,{vars:e})]}):null}function Vn({sec:e}){return e.widget==="auth"?n(jn,{sec:e}):e.widget==="remote_tools"?n(Ln,{}):e.widget==="observability"?n(Hn,{}):e.widget==="metrics"?n(Fn,{}):e.widget==="cache_store"?n(Mn,{sec:e}):e.widget==="id_generator"||e.widget==="thread_name_generator"?n(Nn,{sec:e}):null}function pr(e){let t=new Set([...e.fields.map(i=>i.path.join(".")),...Object.keys(ut)]),o=new Set(["cache_store.rate_limit","cache_store.token_revocation"]),r=i=>i.startsWith("auth.roles.")||i.startsWith("auth.system_identities.");return re(e.id).filter(i=>!t.has(i.path)&&!(e.id==="cache_store"&&o.has(i.path))&&!(e.id==="auth"&&r(i.path)))}function mr({sec:e}){let t=Le(e),o=e.widget&&(t||!e.optional||e.self_managed);return n(x,{children:[o?n("div",{class:"mt-6",children:n(Vn,{sec:e})}):null,n("div",{class:e.optional&&!t?"opacity-80":"",children:kt(e.fields.filter(r=>!r.managed)).map((r,i)=>n("div",{children:[r.name?n("h3",{class:`eyebrow ${i===0?"mt-6":"mt-8 border-t border-line pt-4"}`,children:r.name}):null,r.fields.map(s=>n(I,{field:s},s.path.join(".")))]},r.name))})]})}function fr({sec:e}){let t=Le(e),o=pr(e);return n(x,{children:[n(ke,{code:e.key,children:e.title}),n(Pt,{children:e.description}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((r,i)=>n(vt,{issue:r},i))}):null,e.optional&&!e.self_managed?n("div",{class:"mt-5 flex flex-wrap items-center gap-4",children:[n(ee,{label:`Settings for ${e.title}`,value:t?"c":"d",options:[["d","Use defaults"],["c","Customize"]],onPick:r=>yn(e,r==="c")}),n("p",{class:"m-0 min-w-56 flex-1 text-[13px] text-muted",children:t?`Saved in 10xgraph.json under ${e.key}.`:e.off_summary})]}):null,e.notes?.length?n("div",{class:"mt-5",children:n(ve,{kind:"info",lines:e.notes})}):null,n(mr,{sec:e}),n(ur,{vars:e.env})]})}function hr(){let e=d.schema.environment||[];return n(x,{children:[n(ke,{children:q.environment.title}),n(Pt,{children:"These variables are set in .env or your deployment environment. The editor never reads that file, so only names and purposes are shown."}),e.map(t=>n("section",{children:[n("h2",{class:"mt-8 mb-2 text-sm font-semibold text-ink",children:t.title}),t.note?n("p",{class:"mb-2 text-xs text-faint",children:t.note}):null,n(Qe,{vars:t.vars})]},t.title))]})}var Jn=e=>String(e??"").toLowerCase();function _r(e){let t=i=>e.every(s=>Jn(i).includes(s)),o=[];for(let i of d.sections){let s=t(`${i.title} ${i.description} ${i.key||""}`),a=p=>s||t(`${p.label} ${p.help||""} ${p.path.join(".")}`),u=i.fields.filter(p=>!p.managed&&a(p)),m=(i.env||[]).filter(p=>t(`${p.name} ${p.purpose}`)),l=!!i.widget&&(s||i.fields.some(p=>p.managed&&a(p)));(u.length||m.length||l)&&o.push({id:i.id,sec:i,fields:u,env:m,widget:l})}let r=(d.schema.environment||[]).map(i=>({title:i.title,vars:i.vars.filter(s=>t(`${s.name} ${s.purpose} ${i.title}`))})).filter(i=>i.vars.length);return{results:o,envGroups:r}}function gr(){let e=Jn(d.query).split(/\s+/).filter(Boolean),{results:t,envGroups:o}=_r(e),r=t.reduce((i,s)=>i+s.fields.length+s.env.length+(s.widget?1:0),0)+o.reduce((i,s)=>i+s.vars.length,0);return r?n(x,{children:[n(ke,{code:d.query,children:[r," ",r===1?"setting matches":"settings match"]}),t.map(({id:i,sec:s,fields:a,env:u,widget:m})=>n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:Z[i]||"box",class:"size-4"}),s.title,n("a",{href:`#${i}`,onClick:l=>{l.preventDefault(),D(i)},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),m?n("div",{class:"mt-4",children:n(Vn,{sec:s})}):null,a.map(l=>n(I,{field:l},l.path.join("."))),u.length?n("details",{class:"group mt-5",open:!0,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform group-open:rotate-90"}),"Set in .env"]}),n(Qe,{vars:u})]}):null]},i)),o.length?n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:"terminal",class:"size-4"}),q.environment.title,n("a",{href:"#environment",onClick:i=>{i.preventDefault(),D("environment")},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),o.map(i=>n("div",{children:[n("h3",{class:"mt-4 mb-1.5 text-sm font-semibold text-ink",children:i.title}),i.note?n("p",{class:"mb-1.5 text-xs text-faint",children:i.note}):null,n(Qe,{vars:i.vars})]},i.title))]}):null]}):n(x,{children:[n(ke,{children:"No matches"}),n("p",{class:"mt-8 text-muted",children:['Nothing matches "',d.query,'". Try a shorter word, or press Esc to go back.']})]})}function Kn(){if(d.query.trim())return n(gr,{});if(d.page==="get-running")return n(Bn,{});if(d.page==="environment")return n(hr,{});let e=P(d.page);return e?n(fr,{sec:e}):n(Bn,{})}function br(){let e=d.path.split(/[\\/]/).pop()||"10xgraph.json",t=d.path.slice(0,d.path.length-e.length),o=null;return d.schema?d.exists?W()?o=n(ie,{class:"bg-warn-soft text-warn",children:"Unsaved"}):o=n(ie,{class:"bg-ok-soft text-ok",children:"Saved"}):o=n(ie,{class:"bg-brand-soft text-brand",children:"New"}):o=null,n("div",{class:"flex min-w-0 flex-1 items-center gap-2",title:d.path||void 0,children:[n(_,{name:"file",class:"size-4 text-faint"}),n("span",{class:"truncate font-mono text-[13px] font-medium text-ink",children:e}),o,t?n("span",{class:"hidden min-w-0 truncate font-mono text-xs text-faint lg:block",dir:"rtl",children:n("bdi",{children:t})}):null]})}function Yn(){let e=!!d.schema&&!d.fatal,t=d.sideOpen?"Hide sidebar":"Show sidebar";return n("header",{class:"sticky top-0 z-40 flex h-14 items-center gap-2 border-b border-line bg-surface px-3 sm:gap-3 sm:px-4",children:[n("button",{type:"button",class:"icon-btn","aria-controls":"side","aria-expanded":d.sideOpen?"true":"false","aria-label":t,title:`${t} (Ctrl/Cmd+B)`,onClick:()=>se(!d.sideOpen),children:n(_,{name:"panel",class:"size-[18px]"})}),n("div",{class:"flex shrink-0 items-center gap-2.5",children:[n("svg",{viewBox:"0 0 48 48",class:"size-7 shrink-0 text-ink","aria-hidden":"true",children:[n("rect",{x:"4",y:"4",width:"40",height:"40",rx:"11",fill:"none",stroke:"currentColor","stroke-width":"4.4"}),n("path",{d:"M13 33L21 23L34 15L34 31L21 23",fill:"none",stroke:"currentColor","stroke-width":"3","stroke-linecap":"round","stroke-linejoin":"round"}),n("g",{fill:"currentColor",children:[n("circle",{cx:"21",cy:"23",r:"4"}),n("circle",{cx:"34",cy:"15",r:"4"}),n("circle",{cx:"34",cy:"31",r:"4"})]}),n("circle",{cx:"13",cy:"33",r:"4.6",class:"fill-brand-amber"})]}),n("div",{class:"hidden items-baseline gap-1.5 sm:flex",children:[n("span",{class:"text-[15px] font-semibold tracking-tight text-ink",children:"10xGraph"}),n("span",{class:"text-[15px] text-faint",children:"Config"})]})]}),n("span",{class:"hidden h-6 w-px shrink-0 bg-line md:block","aria-hidden":"true"}),n(br,{}),n("button",{type:"button",class:"btn btn-ghost","aria-label":"View 10xgraph.json","aria-haspopup":"dialog",disabled:!e,onClick:()=>kn(),children:[n(_,{name:"code",class:"size-[18px]"}),n("span",{class:"hidden md:inline",children:"View 10xgraph.json"})]}),n("button",{type:"button",class:"btn btn-primary",onClick:He,disabled:!e||!!d.busy||d.exists&&!W()&&!d.parseError,children:[d.busy==="save"?n(_,{name:"spinner",class:"size-4 animate-spin"}):null,d.exists?"Save":"Create file",n("span",{class:"kbd hidden lg:inline-flex",children:"Ctrl S"})]})]})}function Xn(){return d.fatal?n("div",{class:"panel mt-10",children:n($n,{icon:"xCircle",tint:"text-danger",title:d.fatal.title,text:d.fatal.text})}):d.schema?n(x,{children:[d.parseError?n("div",{class:"mb-6",children:n(ve,{kind:"warn",title:"The existing file is not valid JSON",lines:[`${d.parseError}. The form starts from defaults; saving replaces the file and keeps the old one as 10xgraph.json.bak.`]})}):null,n(Kn,{})]}):n("div",{class:"animate-pulse space-y-4 pt-2","aria-label":"Loading",children:[n("div",{class:"h-7 w-48 rounded-md bg-raised"}),n("div",{class:"h-4 w-80 max-w-full rounded-md bg-raised"}),[0,1,2].map(e=>n("div",{class:"h-16 rounded-xl bg-raised"},e))]})}function xr({id:e}){let t=!!P(e)&&pn(P(e)),o=mn(e);return!t&&!o?null:n("span",{class:"flex shrink-0 gap-1",children:[t?n("span",{class:"size-2 rounded-full bg-brand",title:P(e).self_managed?"Configured":"Changed from default"}):null,o?n("span",{class:`size-2 rounded-full ${o.level==="error"?"bg-danger":"bg-warn"}`,title:o.level==="error"?"Needs attention":"Has a warning"}):null]})}function vr({id:e}){let t=ge(e);if(!t)return null;let o=!d.query&&d.page===e;return n("a",{href:`#${e}`,"aria-current":o?"page":void 0,onClick:r=>{r.preventDefault(),D(e)},ref:r=>{r&&o&&window.innerWidth<1024&&r.scrollIntoView({block:"nearest",inline:"nearest"})},class:`focus-ring flex min-h-10 items-center gap-2.5 rounded-lg px-3 py-1.5 font-medium transition-colors duration-150 max-lg:min-h-11 max-lg:shrink-0 max-lg:whitespace-nowrap ${o?"bg-brand-soft text-ink":"text-muted hover:bg-raised hover:text-ink"}`,children:[n(_,{name:Z[e]||"box",class:`size-[18px] ${o?"text-brand":""}`}),n("span",{class:"min-w-0 flex-1",children:[n("span",{class:"block truncate",children:t.title}),t.subtitle?n("span",{class:"block truncate text-xs font-normal text-faint max-lg:hidden",children:t.subtitle}):null]}),n(xr,{id:e})]})}function Qn(){return d.sideRender?n("aside",{id:"side","aria-label":"Settings navigation",class:`contents lg:sticky lg:top-14 lg:block lg:h-[calc(100dvh-3.5rem)] lg:overflow-y-auto lg:border-r lg:border-line lg:bg-surface lg:px-3 lg:pt-4 lg:pb-16 lg:transition-[transform,opacity] lg:duration-200 lg:ease-out ${d.sideShown?"lg:translate-x-0 lg:opacity-100":"lg:-translate-x-6 lg:opacity-0"}`,inert:!d.sideOpen,children:[n("div",{role:"search",class:"relative mx-1 mb-3 max-lg:mx-4 max-lg:mt-3",children:[n(_,{name:"search",class:"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-faint"}),n("label",{for:"q",class:"sr-only",children:"Search settings"}),n("input",{id:"q",type:"search",value:d.query,autocomplete:"off",spellcheck:!1,placeholder:"Search settings ( / )",class:"input pl-10",onInput:e=>Fe(e.currentTarget.value)})]}),n("nav",{"aria-label":"Pages",class:"max-lg:sticky max-lg:top-14 max-lg:z-20 max-lg:mt-3 max-lg:flex max-lg:gap-0.5 max-lg:overflow-x-auto max-lg:border-b max-lg:border-line max-lg:bg-surface max-lg:px-3 max-lg:py-1.5",children:d.nav.map(e=>n("div",{class:"max-lg:contents",children:[e.label?n("div",{class:"mt-4 mb-1 px-3 text-[11px] font-semibold tracking-wider text-faint uppercase max-lg:hidden",children:e.label}):null,e.items.map(t=>n(vr,{id:t},t))]},e.label||"top"))})]}):null}var yr={success:["checkCircle","text-ok"],warning:["alert","text-warn"],error:["xCircle","text-danger"]};function kr(){return n("div",{class:"pointer-events-none fixed right-4 bottom-4 left-4 z-50 flex flex-col items-end gap-2 sm:left-auto","aria-live":"polite",children:d.toasts.map(e=>{let[t,o]=yr[e.kind];return n("div",{role:e.kind==="error"?"alert":"status",class:"pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-xl border border-line bg-surface px-4 py-3 text-sm shadow-pop",children:[n(_,{name:t,class:`mt-0.5 size-4 ${o}`}),n("p",{class:"min-w-0 flex-1 leading-5 text-body",children:e.message}),e.action?n("button",{type:"button",class:"shrink-0 font-medium text-brand hover:underline",onClick:()=>{qe(e.id),e.action.run()},children:e.action.label}):null,n("button",{type:"button",class:"-mr-1 shrink-0 cursor-pointer rounded text-faint hover:text-ink","aria-label":"Dismiss",onClick:()=>qe(e.id),children:n(_,{name:"x"})})]},e.id)})})}var wr=()=>/^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName)||document.activeElement?.isContentEditable;function Cr(){let[,e]=dt(o=>o+1,0);U(()=>{dn(e);try{localStorage.getItem("tg-sidebar")==="hidden"&&se(!1,{instant:!0})}catch{}let o=s=>{let a=s.ctrlKey||s.metaKey,u=s.key.toLowerCase();a&&u==="s"?(s.preventDefault(),He()):a&&u==="b"?(s.preventDefault(),se(!d.sideOpen)):a&&u==="k"?(s.preventDefault(),gt()):s.key==="/"&&!a&&!s.altKey&&!wr()&&!d.drawerOpen?(s.preventDefault(),gt()):s.key==="Escape"&&d.query&&!d.drawerOpen&&Fe("")},r=s=>{d.schema&&W()&&s.preventDefault()},i=()=>ze();return document.addEventListener("keydown",o),window.addEventListener("beforeunload",r),window.addEventListener("hashchange",i),xn(),()=>{document.removeEventListener("keydown",o),window.removeEventListener("beforeunload",r),window.removeEventListener("hashchange",i)}},[]),U(()=>{if(!d.schema||d.validated===N())return;let o=setTimeout(vn,400);return()=>clearTimeout(o)});let t=d.sideRender;return n(x,{children:[n("a",{href:"#main",class:"sr-only z-50 rounded-md bg-brand-solid px-3 py-2 text-white focus:not-sr-only focus:fixed focus:top-3 focus:left-3",children:"Skip to content"}),n(Yn,{}),n("div",{class:t?"lg:grid lg:grid-cols-[260px_minmax(0,1fr)] lg:items-start":"",children:[n(Qn,{}),n("main",{id:"main",tabindex:-1,class:"min-w-0 px-4 pt-6 pb-24 focus:outline-none sm:px-8 sm:pt-8",children:n("div",{class:"mx-auto w-full max-w-[760px]",children:n(Xn,{})})})]}),n(Tn,{}),n("div",{class:"sr-only",role:"status","aria-live":"polite",children:d.copied?"Copied":""}),n(kr,{})]})}Vt(n(Cr,{}),document.getElementById("app")); +var Ie,v,Lt,eo,J,At,Nt,Dt,et,Se,ue,Ht,ot,tt,nt,to,Ee={},Pe=[],no=/acit|ex(?:s|g|n|p|$)|rph|grid|ows|mnc|ntw|ine[ch]|zoo|^ord|itera/i,Ae=Array.isArray;function F(e,t){for(var o in t)e[o]=t[o];return e}function rt(e){e&&e.parentNode&&e.parentNode.removeChild(e)}function oo(e,t,o){var r,i,s,a={};for(s in t)s=="key"?r=t[s]:s=="ref"?i=t[s]:a[s]=t[s];if(arguments.length>2&&(a.children=arguments.length>3?Ie.call(arguments,2):o),typeof e=="function"&&e.defaultProps!=null)for(s in e.defaultProps)a[s]===void 0&&(a[s]=e.defaultProps[s]);return Te(e,a,r,i,null)}function Te(e,t,o,r,i){var s={type:e,props:t,key:o,ref:r,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:i??++Lt,__i:-1,__u:0};return i==null&&v.vnode!=null&&v.vnode(s),s}function x(e){return e.children}function $e(e,t){this.props=e,this.context=t}function Z(e,t){if(t==null)return e.__?Z(e.__,e.__i+1):null;for(var o;tt&&J.sort(Dt),e=J.shift(),t=J.length,ro(e)}finally{J.length=Oe.__r=0}}function Ft(e,t,o,r,i,s,a,u,m,l,p){var h,c,f,y,T,w,E=r&&r.__k||Pe,k=t.length;for(m=so(o,t,E,m,k),h=0;h0?a=e.__k[s]=Te(a.type,a.props,a.key,a.ref?a.ref:null,a.__v):e.__k[s]=a,m=s+c,a.__=e,a.__b=e.__b+1,u=null,(l=a.__i=io(a,o,m,h))!=-1&&(h--,(u=o[l])&&(u.__u|=2)),u==null||u.__v==null?(l==-1&&(i>p?c--:im?c--:c++,a.__u|=4))):e.__k[s]=null;if(h)for(s=0;s(p?1:0)){for(i=o-1,s=o+1;i>=0||s=0?i--:s++])!=null&&(2&l.__u)==0&&u==l.key&&m==l.type)return a}return-1}function jt(e,t,o){t[0]=="-"?e.setProperty(t,o??""):e[t]=o==null?"":typeof o!="number"||no.test(t)?o:o+"px"}function Ce(e,t,o,r,i){var s,a;e:if(t=="style")if(typeof o=="string")e.style.cssText=o;else{if(typeof r=="string"&&(e.style.cssText=r=""),r)for(t in r)o&&t in o||jt(e.style,t,"");if(o)for(t in o)r&&o[t]==r[t]||jt(e.style,t,o[t])}else if(t[0]=="o"&&t[1]=="n")s=t!=(t=t.replace(Ht,"$1")),a=t.toLowerCase(),t=a in e||t=="onFocusOut"||t=="onFocusIn"?a.slice(2):t.slice(2),e.l||(e.l={}),e.l[t+s]=o,o?r?o[ue]=r[ue]:(o[ue]=ot,e.addEventListener(t,s?nt:tt,s)):e.removeEventListener(t,s?nt:tt,s);else{if(i=="http://www.w3.org/2000/svg")t=t.replace(/xlink(H|:h)/,"h").replace(/sName$/,"s");else if(t!="width"&&t!="height"&&t!="href"&&t!="list"&&t!="form"&&t!="tabIndex"&&t!="download"&&t!="rowSpan"&&t!="colSpan"&&t!="role"&&t!="popover"&&t in e)try{e[t]=o??"";break e}catch{}typeof o=="function"||(o==null||o===!1&&t[4]!="-"?e.removeAttribute(t):e.setAttribute(t,t=="popover"&&o==1?"":o))}}function Mt(e){return function(t){if(this.l){var o=this.l[t.type+e];if(t[Se]==null)t[Se]=ot++;else if(t[Se]0?e:Ae(e)?e.map(Gt):e.constructor!==void 0?null:F({},e)}function ao(e,t,o,r,i,s,a,u,m){var l,p,h,c,f,y,T,w=o.props||Ee,E=t.props,k=t.type;if(k=="svg"?i="http://www.w3.org/2000/svg":k=="math"?i="http://www.w3.org/1998/Math/MathML":i||(i="http://www.w3.org/1999/xhtml"),s!=null){for(l=0;l=o.__.length&&o.__.push({}),o.__[e]}function K(e){return je=1,dt(rn,e)}function dt(e,t,o){var r=ct(pe++,2);if(r.t=e,!r.__c&&(r.__=[o?o(t):rn(void 0,t),function(u){var m=r.__N?r.__N[0]:r.__[0],l=r.t(m,u);m!==l&&(r.__N=[l,r.__[1]],r.__c.setState({}))}],r.__c=C,!C.__f)){var i=function(u,m,l){if(!r.__c.__H)return!0;var p=!1,h=r.__c.props!==u;if(r.__c.__H.__.some(function(f){if(f.__N){p=!0;var y=f.__[0];f.__=f.__N,f.__N=void 0,y!==f.__[0]&&(h=!0)}}),s){var c=s.call(this,u,m,l);return p?c||h:c}return!p||h};C.__f=!0;var s=C.shouldComponentUpdate,a=C.componentWillUpdate;C.componentWillUpdate=function(u,m,l){if(this.__e){var p=s;s=void 0,i(u,m,l),s=p}a&&a.call(this,u,m,l)},C.shouldComponentUpdate=i}return r.__N||r.__}function U(e,t){var o=ct(pe++,3);!$.__s&&on(o.__H,t)&&(o.__=e,o.u=t,C.__H.__h.push(o))}function Me(e){return je=5,co(function(){return{current:e}},[])}function co(e,t){var o=ct(pe++,7);return on(o.__H,t)&&(o.__=e(),o.__H=t,o.__h=e),o.__}function uo(){for(var e;e=nn.shift();){var t=e.__H;if(e.__P&&t)try{t.__h.some(Re),t.__h.some(lt),t.__h=[]}catch(o){t.__h=[],$.__e(o,e.__v)}}}$.__b=function(e){C=null,Kt&&Kt(e)},$.__=function(e,t){e&&t.__k&&t.__k.__m&&(e.__m=t.__k.__m),en&&en(e,t)},$.__r=function(e){Yt&&Yt(e),pe=0;var t=(C=e.__c).__H;t&&(at===C?(t.__h=[],C.__h=[],t.__.some(function(o){o.__N&&(o.__=o.__N),o.u=o.__N=void 0})):(t.__h.some(Re),t.__h.some(lt),t.__h=[],pe=0)),at=C},$.diffed=function(e){Xt&&Xt(e);var t=e.__c;t&&t.__H&&(t.__H.__h.length&&(nn.push(t)!==1&&Jt===$.requestAnimationFrame||((Jt=$.requestAnimationFrame)||po)(uo)),t.__H.__.some(function(o){o.u&&(o.__H=o.u,o.u=void 0)})),at=C=null},$.__c=function(e,t){t.some(function(o){try{o.__h.some(Re),o.__h=o.__h.filter(function(r){return!r.__||lt(r)})}catch(r){t.some(function(i){i.__h&&(i.__h=[])}),t=[],$.__e(r,o.__v)}}),Qt&&Qt(e,t)},$.unmount=function(e){Zt&&Zt(e);var t,o=e.__c;o&&o.__H&&(o.__H.__.some(function(r){try{Re(r)}catch(i){t=i}}),o.__H=void 0,t&&$.__e(t,o.__v))};var tn=typeof requestAnimationFrame=="function";function po(e){var t,o=function(){clearTimeout(r),tn&&cancelAnimationFrame(t),setTimeout(e)},r=setTimeout(o,35);tn&&(t=requestAnimationFrame(o))}function Re(e){var t=C,o=e.__c;typeof o=="function"&&(e.__c=void 0,o()),C=t}function lt(e){var t=C;e.__c=e.__(),C=t}function on(e,t){return!e||e.length!==t.length||t.some(function(o,r){return o!==e[r]})}function rn(e,t){return typeof t=="function"?t(e):t}var Le=[{label:null,items:["get-running"]},{label:"Before production",items:["auth","cache_store","observability","metrics"]},{label:"Runs and streaming",items:["runs","websocket","ag_ui","remote_tools","media"]},{label:"Project wiring",items:["checkpointer","id_generator","thread_name_generator","store","injectq"]},{label:"Developer tools",items:["test","evaluation"]},{label:"Reference",items:["environment"]}],q={"get-running":{title:"Get running",icon:"flag",description:"Only the agent graph is required. Everything else is optional."},environment:{title:"Server environment (.env)",icon:"terminal",description:"Variables the server reads from .env. The editor never reads that file, so values are not shown."}},ut={"auth.path":"auth-path"},sn="tg-config-token",an=!1;function mo(){let e=new URLSearchParams(location.hash.slice(1)).get("token")||"";try{e&&sessionStorage.setItem(sn,e);let t=sessionStorage.getItem(sn)||"";return an=t!==""&&t===(e||t),e||t}catch{return e}}var me=mo(),pt=e=>an||!me?`#${e}`:`#token=${encodeURIComponent(me)}&page=${e}`;function ln(){let e=location.hash.slice(1);if(e.includes("=")){let t=new URLSearchParams(e);return t.get("page")||t.get("section")||""}try{return decodeURIComponent(e)}catch{return e}}var d={schema:null,sections:[],nav:[],fields:new Map,config:{},version:null,exists:!1,path:"",parseError:null,saved:"",validated:null,issues:[],page:"get-running",query:"",sideOpen:!0,sideRender:!0,sideShown:!0,drawerOpen:!1,tab:"json",copied:!1,busy:null,toolParams:{},toasts:[],fatal:null},cn=()=>{},dn=e=>{cn=e};function b(e){e&&e(d),cn()}var un=e=>JSON.parse(JSON.stringify(e)),L=()=>JSON.stringify(d.config),g=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),N=(e,t)=>t.reduce((o,r)=>g(o)?o[r]:void 0,e),fe=(e,t)=>`${e} ${t}${e===1?"":"s"}`;function oe(e,t,o){let r=e;for(let i of t.slice(0,-1))g(r[i])||(r[i]={}),r=r[i];r[t[t.length-1]]=o}var fo=new Set(["cache_store.rate_limit","cache_store.token_revocation"]);function he(e,t){let o=[],r=e;for(let i of t.slice(0,-1)){if(!g(r))return;o.push([r,i]),r=r[i]}g(r)&&delete r[t[t.length-1]];for(let i=o.length-1;i>=1;i--){let[s,a]=o[i];if(fo.has(t.slice(0,i+1).join(".")))break;if(g(s[a])&&Object.keys(s[a]).length===0)delete s[a];else break}}function mt(e){let t=N(d.config,e);return t!==void 0?t:d.fields.get(e.join("."))?.default}var ho=(e,t)=>JSON.stringify(e)===JSON.stringify(t),O=e=>d.sections.find(t=>t.id===e),W=()=>L()!==d.saved,Ne=e=>!e.optional||e.key in d.config&&d.config[e.key]!==null,re=e=>d.issues.filter(t=>t.section===e),S=e=>d.issues.find(t=>t.path===e);function _e(e=d.issues){let t=e.filter(o=>o.level==="error").length;return{errors:t,warnings:e.length-t}}var De=e=>ut[e]||`f-${e.replaceAll(".","-")}`,ft=e=>{let t=N(d.config,e.path);return t!==void 0&&!ho(t,e.default)},pn=e=>e.id==="core"?!1:e.id==="auth"?d.config.auth!=null:e.id==="metrics"?d.config.metrics?.enabled===!0:e.optional?Ne(e):e.fields.some(ft);function mn(e){let t=re(e);return t.find(o=>o.level==="error")||t[0]||null}var fn=e=>!!e.show_if&&mt(e.show_if.path)!==e.show_if.equals;function hn(e){let o=d.fields.get(e.show_if.path.join("."))?.label||e.show_if.path.join("."),r=e.show_if.equals;return`Used when ${o} is ${r===!0?"on":r===!1?"off":String(r)}.`}var ge=e=>{let t=O(e);return q[e]?{id:e,...q[e]}:t?{id:e,title:t.title,subtitle:t.subtitle,description:t.description,icon:null}:null},_n=()=>/^[\w.]+:[\w.]+$/.test(String(d.config.agent||""));async function _t(e,t,o){let r={"X-10xGraph-Token":me};o!==void 0&&(r["Content-Type"]="application/json");let i=await fetch(t,{method:e,headers:r,body:o===void 0?void 0:JSON.stringify(o)}),s={};try{s=await i.json()}catch{}return{ok:i.ok,status:i.status,data:s}}var ht=(e,t)=>b(o=>{o.fatal={title:e,text:t}});async function xn(){if(!me)return ht("Open the link printed by 10xgraph config","This page needs the session token included in that link.");let e;try{e=await _t("GET","/api/state")}catch{return ht("Cannot reach the config server","Check that 10xgraph config is still running in your terminal, then reload.")}if(!e.ok)return ht("This link has expired",e.data.error||"Restart 10xgraph config and open the new link it prints.");let t=e.data;b(o=>{o.schema=t.schema,o.sections=t.schema.sections;let r=new Set(Le.flatMap(a=>a.items)),i=o.sections.map(a=>a.id).filter(a=>a!=="core"&&!r.has(a));o.nav=Le.map((a,u)=>({label:a.label,items:uq[m]||O(m)):[...i,...a.items]}));for(let a of o.sections)for(let u of a.fields)o.fields.set(u.path.join("."),u);o.config=t.config,o.version=t.version,o.exists=t.exists,o.path=t.path,o.parseError=t.parse_error,o.issues=t.issues;let s=t.exists&&!t.parse_error;o.saved=s?L():"",o.validated=s?L():null}),ze({focus:!1}),history.replaceState(null,"",pt(d.page))}var gn=null;async function vn(){let e=L();if(!(!d.schema||d.validated===e||gn===e)&&!Object.values(d.toolParams).some(t=>t.error)){gn=e;try{let t=await _t("POST","/api/validate",{config:JSON.parse(e)});if(!t.ok)return;b(o=>{o.issues=t.data.issues,o.validated=e})}catch{}}}async function He(){if(!(d.busy||!d.schema)){if(d.exists&&!W()&&!d.parseError)return G("success","No changes to save.");if(_o()){b(e=>{e.busy="save"});try{let e=await _t("POST","/api/save",{config:d.config,version:d.version});if(e.data.issues&&(d.issues=e.data.issues,d.validated=L()),e.ok){d.version=e.data.version,d.config=e.data.config,d.toolParams={},d.validated=L(),d.saved=L(),d.exists=!0,d.parseError=null;let t=e.data.path.split(/[\\/]/).pop();G("success",e.data.backup?`Saved ${t}. The previous version is in ${t}.bak.`:`Created ${t}.`)}else if(e.status===409)G("error",e.data.error,{label:"Reload",run:()=>location.reload()});else if(e.status===422){let{errors:t}=_e();G("error",`Not saved: ${fe(t,"error")} to fix first.`,{label:"Show",run:go})}else G("error",e.data.error||"Save failed.")}catch{G("error","Cannot reach the config server.")}finally{b(e=>{e.busy=null})}}}}function _o(){let e=Object.values(d.toolParams).some(t=>t.error);return e&&(D("remote_tools"),G("error","A remote tool has invalid parameters JSON.")),!e}function yn(e,t){b(o=>{t?o.config[e.key]=un(e.default):delete o.config[e.key],e.key==="remote_tools"&&(o.toolParams={})})}function go(){let e=d.issues.find(t=>t.level==="error")||d.issues[0];e&&D(e.section,De(e.path))}var bo=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches?"auto":"smooth";function xo(e){return e==="core"?"get-running":q[e]||O(e)?e:"get-running"}function ze({focus:e=!0}={}){b(t=>{t.page=xo(ln()),t.query="",t.drawerOpen=!1}),e&&requestAnimationFrame(()=>{window.scrollTo(0,0),document.getElementById("page-title")?.focus({preventScroll:!0})})}function D(e,t){let o=pt(e);location.hash!==o&&history.pushState(null,"",o),ze(),t&&requestAnimationFrame(()=>requestAnimationFrame(()=>{let r=document.getElementById(t);r&&(r.scrollIntoView({block:"center",behavior:bo()}),r.focus({preventScroll:!0}))}))}function Fe(e){b(t=>{t.query=e})}var vo=()=>window.matchMedia("(prefers-reduced-motion: reduce)").matches,bn;function se(e,{instant:t=!1}={}){try{localStorage.setItem("tg-sidebar",e?"shown":"hidden")}catch{}clearTimeout(bn);let o=t||vo();b(r=>{r.sideOpen=e,e?(r.sideRender=!0,r.sideShown=o):r.sideShown=!1,!e&&o&&(r.sideRender=!1)}),e&&!o&&requestAnimationFrame(()=>requestAnimationFrame(()=>b(r=>{r.sideShown=!0}))),!e&&!o&&(bn=setTimeout(()=>b(r=>{r.sideOpen||(r.sideRender=!1)}),200))}function gt(){d.sideOpen||se(!0,{instant:!0}),requestAnimationFrame(()=>{let e=document.getElementById("q");e?.focus(),e?.select()})}function kn(e){b(t=>{e&&(t.tab=e),t.drawerOpen=!0})}var Ue=()=>b(e=>{e.drawerOpen=!1});async function wn(){let e="10xgraph dev";try{await navigator.clipboard.writeText(e)}catch{try{let t=document.createElement("textarea");t.value=e,document.body.append(t),t.select(),document.execCommand("copy"),t.remove()}catch{}}b(t=>{t.copied=!0}),setTimeout(()=>b(t=>{t.copied=!1}),2e3)}async function Cn(){try{await navigator.clipboard.writeText(JSON.stringify(d.config,null,2)+` +`),G("success","Copied 10xgraph.json to the clipboard.")}catch{G("error","The clipboard is not available here.")}}var yo=0;function G(e,t,o){let r=++yo;b(i=>{i.toasts=[...i.toasts,{id:r,kind:e,message:t,action:o}]}),setTimeout(()=>qe(r),o?8e3:4e3)}function qe(e){b(t=>{t.toasts=t.toasts.filter(o=>o.id!==e)})}var Sn={box:'',key:'',shield:'',gauge:'',arrows:'',activity:'',layers:'',wrench:'',flask:'',target:'',checkCircle:'',xCircle:'',alert:'',info:'',file:'',copy:'',x:'',plus:'',trash:'',panel:'',chevronDown:'',expand:'',collapse:'',power:'',spinner:'',book:'',flag:'',lock:'',play:'',bolt:'',terminal:'',search:'',chevronRight:'',code:'',circle:'',external:''},ee={"get-running":"flag",environment:"terminal",core:"flag",auth:"lock",cache_store:"gauge",observability:"activity",metrics:"gauge",runs:"play",websocket:"bolt",ag_ui:"external",remote_tools:"wrench",media:"file",checkpointer:"layers",id_generator:"key",thread_name_generator:"file",store:"box",injectq:"arrows",test:"flask",evaluation:"target"};var ko=0;function n(e,t,o,r,i,s){t||(t={});var a,u,m=t;if("ref"in m)for(u in m={},t)u=="ref"?a=t[u]:m[u]=t[u];var l={type:e,props:m,key:o,ref:a,__k:null,__:null,__b:0,__e:null,__c:null,constructor:void 0,__v:--ko,__i:-1,__u:0,__source:i,__self:s};if(typeof e=="function"&&(a=e.defaultProps))for(u in a)m[u]===void 0&&(m[u]=a[u]);return v.vnode&&v.vnode(l),l}function _({name:e,class:t="size-4"}){return/(^|\s)size-/.test(t)||(t=`size-4 ${t}`),n("span",{class:`inline-flex shrink-0 ${t}`,"aria-hidden":"true",children:n("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"1.75","stroke-linecap":"round","stroke-linejoin":"round",class:"size-full",dangerouslySetInnerHTML:{__html:Sn[e]}})})}function ie({class:e,children:t}){return n("span",{class:`inline-flex h-5 shrink-0 items-center rounded-full px-2 text-[11px] font-semibold ${e}`,children:t})}function be({id:e,on:t,label:o,onChange:r}){return n("button",{id:e,type:"button",role:"switch","aria-checked":t?"true":"false","aria-label":o,class:`focus-ring relative inline-flex h-6 w-10 shrink-0 cursor-pointer items-center rounded-full transition-colors duration-200 before:absolute before:-inset-2.5 before:content-[''] ${t?"bg-brand-solid":"bg-line-strong"}`,onClick:()=>r(!t),children:n("span",{class:`pointer-events-none inline-block size-5 rounded-full bg-white shadow-sm ring-1 ring-black/5 transition-transform duration-200 ease-out ${t?"translate-x-[18px]":"translate-x-0.5"}`})})}function te({id:e,label:t,options:o,value:r,onPick:i}){return n("div",{id:e,role:"radiogroup","aria-label":t,class:"inline-flex max-w-full flex-wrap gap-0.5 rounded-[10px] border border-line-strong bg-raised p-0.5",children:o.map(([s,a])=>n("button",{type:"button",role:"radio","aria-checked":s===r?"true":"false",class:`focus-ring min-h-10 cursor-pointer rounded-lg px-3.5 text-sm font-medium transition-colors duration-150 ${s===r?"bg-surface text-ink shadow-card":"text-muted hover:text-ink"}`,onClick:()=>{s!==r&&i(s)},children:a},s))})}function A({id:e,options:t,value:o,invalid:r,onChange:i,plain:s}){return n("div",{class:"relative",children:[n("select",{id:e,value:o,class:`input cursor-pointer appearance-none pr-9 ${s?"":"font-mono"} ${r?"input-invalid":""}`,onChange:a=>i(a.currentTarget.value),children:t.map(([a,u])=>n("option",{value:a,children:u},a))}),n(_,{name:"chevronDown",class:"pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-faint"})]})}function H({id:e,values:t,placeholder:o,suggestions:r,onChange:i}){let s=Me(null),a=m=>{let l=m.trim();s.current&&(s.current.value=""),l&&!t.includes(l)&&i([...t,l])},u=r?`${e}-options`:void 0;return n("div",{class:"flex min-h-11 cursor-text flex-wrap items-center gap-1.5 rounded-lg border border-line-strong bg-sunken px-1.5 py-1.5 transition-[border-color,box-shadow] duration-150 focus-within:border-brand focus-within:ring-4 focus-within:ring-brand-ring sm:py-1",onClick:m=>{m.target===m.currentTarget&&s.current?.focus()},children:[t.map((m,l)=>n("span",{class:"inline-flex h-7 items-center gap-0.5 rounded-md border border-line bg-raised pr-0.5 pl-2 font-mono text-xs text-body sm:h-6",children:[m,n("button",{type:"button","aria-label":`Remove ${m}`,class:"inline-flex size-6 cursor-pointer items-center justify-center rounded text-faint transition-colors hover:bg-hover hover:text-ink sm:size-5",onClick:()=>i(t.filter((p,h)=>h!==l)),children:n(_,{name:"x",class:"size-3"})})]},m)),n("input",{ref:s,id:e,type:"text",list:u,placeholder:t.length?"Add...":o,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:"h-7 min-w-[7rem] flex-1 bg-transparent px-1 font-mono text-base text-ink outline-none placeholder:text-faint sm:text-sm",onKeyDown:m=>{let l=m.currentTarget;(m.key==="Enter"||m.key===",")&&l.value.trim()?(m.preventDefault(),a(l.value)):m.key==="Backspace"&&!l.value&&t.length&&i(t.slice(0,-1))},onChange:m=>{r?.includes(m.currentTarget.value.trim())&&a(m.currentTarget.value)},onBlur:m=>a(m.currentTarget.value)}),r?n("datalist",{id:u,children:r.filter(m=>!t.includes(m)).map(m=>n("option",{value:m},m))}):null]})}function xe({label:e,path:t,help:o,id:r,required:i}){return n("div",{class:"min-w-0",children:[n("label",{for:r,class:"block text-sm font-medium text-ink",children:[e,i?n("span",{class:"text-danger",children:" *"}):null]}),n("div",{class:"mt-0.5 font-mono text-xs text-faint",children:t}),o?n("p",{class:"mt-1.5 text-[13px] leading-5 text-muted",children:o}):null]})}function P({issue:e}){if(!e)return null;let t=e.level==="error";return n("p",{class:`mt-2 flex gap-1.5 text-[13px] leading-5 ${t?"text-danger":"text-warn"}`,role:t?"alert":void 0,children:[n(_,{name:t?"xCircle":"alert",class:"mt-0.5 size-3.5"}),n("span",{children:e.message})]})}function bt({children:e}){return n("div",{class:"grid gap-3 px-5 py-4 sm:grid-cols-[minmax(0,1fr)_minmax(0,1.1fr)] sm:gap-8",children:e})}function Y({id:e,label:t,path:o,value:r,placeholder:i,onInput:s,issue:a,help:u}){return n(bt,{children:[n(xe,{label:t,path:o,help:u,id:e}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n("input",{id:e,type:"text",value:r??"",placeholder:i,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${a?.level==="error"?"input-invalid":""}`,onInput:m=>s(m.currentTarget.value)}),n(P,{issue:a})]})]})}function xt({name:e,label:t,options:o,value:r,onPick:i}){return n("div",{role:"radiogroup","aria-label":t,class:"grid gap-3 sm:grid-cols-2",children:o.map(([s,a,u])=>{let m=s===r;return n("button",{type:"button",role:"radio","aria-checked":m?"true":"false",id:`${e}-${s}`,class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 ${m?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{m||i(s)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${m?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:m?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:a}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:u})]})]},s)})})}function R({title:e,children:t}){return n("section",{children:[e?n("h4",{class:"eyebrow mb-2.5",children:e}):null,t]})}var wo={info:["info","border-brand/20 bg-brand-soft","text-brand"],warn:["alert","border-warn/25 bg-warn-soft","text-warn"],error:["xCircle","border-danger/25 bg-danger-soft","text-danger"]};function ve({kind:e,title:t,lines:o}){let[r,i,s]=wo[e];return n("div",{class:`flex gap-3 rounded-xl border px-4 py-3 ${i}`,role:e==="error"?"alert":void 0,children:[n(_,{name:r,class:`mt-0.5 size-4 ${s}`}),n("div",{class:"min-w-0 space-y-1 text-[13px] leading-5 text-body",children:[t?n("p",{class:"font-medium text-ink",children:t}):null,o.map((a,u)=>n("p",{children:a},u))]})]})}function vt({issue:e}){return n(ve,{kind:e.level==="error"?"error":"warn",lines:[n("span",{children:[e.path?n("code",{class:"mr-1.5 font-mono text-xs text-muted",children:e.path}):null,e.message]})]})}function Tn({icon:e,tint:t,title:o,text:r,children:i}){return n("div",{class:"px-6 py-14 text-center",children:[n("div",{class:`mx-auto flex size-11 items-center justify-center rounded-full bg-raised ${t}`,children:n(_,{name:e,class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:o}),n("p",{class:"mx-auto mt-1 max-w-xs text-sm leading-6 text-muted",children:r}),i]})}function We({title:e,open:t,class:o="",children:r}){return n("details",{class:`group ${o}`,open:t,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform duration-150 group-open:rotate-90"}),e]}),r]})}function $n(){let e=Me(null);U(()=>{let a=e.current;a&&(d.drawerOpen&&!a.open?a.showModal():!d.drawerOpen&&a.open&&a.close())});let t=d.issues.length>0,o=t&&d.tab==="problems"?"problems":"json",{errors:r,warnings:i}=_e(),s=(a,u,m)=>n("button",{type:"button",role:"tab",id:`tab-${a}`,"aria-selected":o===a?"true":"false",class:`focus-ring inline-flex h-10 cursor-pointer items-center gap-2 rounded-lg px-3 text-sm font-medium transition-colors duration-150 ${o===a?"bg-raised text-ink":"text-muted hover:text-ink"}`,onClick:()=>b(l=>{l.tab=a}),children:[u,m]});return n("dialog",{ref:e,"aria-label":"10xgraph.json preview",class:"m-0 ml-auto h-dvh max-h-none w-[min(540px,100vw)] border-0 border-l border-line-strong bg-surface p-0 text-body shadow-pop backdrop:bg-black/45",onClose:()=>{d.drawerOpen&&Ue()},onClick:a=>{a.target===a.currentTarget&&Ue()},children:n("div",{class:"flex h-full flex-col",children:[n("div",{class:"flex h-14 shrink-0 items-center gap-1 border-b border-line px-3",children:[n("div",{role:"tablist",class:"flex flex-1 gap-1",children:[s("json","10xgraph.json",null),t?s("problems","Problems",n("span",{class:`inline-flex h-5 min-w-5 items-center justify-center rounded-full px-1.5 text-[11px] font-semibold ${r?"bg-danger-soft text-danger":"bg-warn-soft text-warn"}`,children:r+i})):null]}),n("button",{type:"button",class:"icon-btn","aria-label":"Close preview",onClick:Ue,children:n(_,{name:"x",class:"size-5"})})]}),n("div",{class:"min-h-0 flex-1 overflow-y-auto overscroll-contain",children:o==="problems"?n(Co,{}):n(To,{})})]})})}function Co(){let{errors:e,warnings:t}=_e(),o=d.sections.map(r=>[r,re(r.id)]).filter(([,r])=>r.length);return n("div",{children:[n("div",{class:"border-b border-line px-4 py-3",children:n("p",{class:"text-sm text-muted",children:[n("span",{class:e?"font-medium text-danger":"",children:fe(e,"error")})," \xB7 ",n("span",{class:t?"font-medium text-warn":"",children:fe(t,"warning")})]})}),n("div",{class:"space-y-5 px-3 py-4",children:o.map(([r,i])=>n("div",{children:[n("p",{class:"eyebrow mb-1.5 flex items-center gap-2 px-2",children:[n(_,{name:ee[r.id]||"box",class:"size-3.5"}),r.title]}),n("ul",{class:"space-y-0.5",children:i.map((s,a)=>{let u=s.level==="error";return n("li",{children:n("button",{type:"button",onClick:()=>D(r.id,De(s.path)),class:"focus-ring flex min-h-11 w-full cursor-pointer gap-2.5 rounded-lg px-2 py-2 text-left transition-colors duration-150 hover:bg-raised",children:[n(_,{name:u?"xCircle":"alert",class:`mt-0.5 size-4 ${u?"text-danger":"text-warn"}`}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-[13px] leading-5 text-body",children:s.message}),s.path?n("span",{class:"mt-0.5 block truncate font-mono text-xs text-faint",children:s.path}):null]})]})},a)})})]},r.id))})]})}function So(e){return e.replace(/&/g,"&").replace(//g,">").replace(/("(\\u[a-fA-F0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+-]?\d+)?)/g,o=>{let r="var(--af-json-number)";return o.startsWith('"')?r=o.endsWith(":")?"var(--af-json-key)":"var(--af-json-string)":(o==="true"||o==="false"||o==="null")&&(r="var(--af-json-literal)"),`${o}`})}function To(){let e=JSON.stringify(d.config,null,2).split(` +`);return n("div",{children:[n("div",{class:"sticky top-0 flex items-center justify-between gap-3 border-b border-line bg-surface px-4 py-2.5",children:[n("p",{class:"text-xs text-faint",children:W()||!d.exists?"Preview of what Save will write":"Matches the file on disk"}),n("button",{type:"button",class:"btn btn-ghost h-8 px-2.5 text-xs",onClick:Cn,children:[n(_,{name:"copy",class:"size-3.5"}),"Copy"]})]}),n("div",{class:"py-3 pr-4 font-mono text-[12.5px] leading-5",children:e.map((t,o)=>n("div",{class:"flex",children:[n("span",{class:"w-10 shrink-0 pr-3 text-right text-faint/70 select-none",children:o+1}),n("span",{class:"min-w-0 flex-1 break-all whitespace-pre-wrap",dangerouslySetInnerHTML:{__html:So(t)}})]},o))})]})}function kt(e){let t=[];for(let o of e){let r=o.group||"",i=t.find(s=>s.name===r);i||t.push(i={name:r,fields:[]}),i.fields.push(o)}return t}var B=(e,t)=>b(o=>{if(t===void 0){he(o.config,e);return}let r=e.length>1?o.sections.find(i=>i.optional&&i.key===e[0]):null;r&&(o.config[r.key]===void 0||o.config[r.key]===null)&&(o.config[r.key]=JSON.parse(JSON.stringify(r.default))),oe(o.config,e,t)}),$o=e=>`f-${e.path.join("-")}`;function Eo({id:e,value:t,field:o,invalid:r,disabled:i}){let[s,a]=K(String(t??""));return U(()=>{(s===""?t!==void 0:Number(s)!==t)&&a(String(t??""))},[t]),n("input",{id:e,type:"number",inputmode:"decimal",value:s,disabled:i,min:o.min,step:o.step??"any",placeholder:o.placeholder??o.default??"",class:`input max-w-40 font-mono tabular-nums ${r?"input-invalid":""}`,onInput:u=>{let m=u.currentTarget.value;a(m),m===""?B(o.path,void 0):Number.isNaN(Number(m))||B(o.path,Number(m))}})}function yt({field:e,value:t}){return e.exclusive_min===void 0||typeof t!="number"||t>e.exclusive_min?null:n("p",{class:"mt-1.5 text-[13px] text-warn",children:["Must be greater than ",e.exclusive_min,"."]})}function Po({id:e,field:t,value:o,invalid:r}){let i=t.unlimited_value!==void 0&&o===t.unlimited_value,s=n(Eo,{id:e,value:i?void 0:o,field:t,invalid:r,disabled:i});if(t.null_label!==void 0){let a=o===null?"n":o===void 0?"d":"s",u=typeof t.default=="number"?t.default:300;return n("div",{children:[n(te,{id:e,label:t.label,value:a,options:[["d","Default"],["s","Seconds"],["n",t.null_label]],onPick:m=>B(t.path,m==="d"?void 0:m==="n"?null:u)}),a==="s"?n("div",{class:"mt-2.5",children:[s,n(yt,{field:t,value:o})]}):null]})}return t.unlimited_value!==void 0?n("div",{children:[n("div",{class:"flex flex-wrap items-center gap-x-4",children:[s,n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm text-body",children:[n("input",{type:"checkbox",class:"size-[18px] accent-brand-solid",checked:i,onChange:a=>B(t.path,a.currentTarget.checked?t.unlimited_value:void 0)}),"Unlimited"]})]}),n(yt,{field:t,value:o})]}):n("div",{children:[s,n(yt,{field:t,value:o})]})}function Oo({id:e,field:t,value:o}){if(t.unset_label!==void 0){let r=[["d","Default"]];return t.allow_true!==!1&&r.push(["t","On"]),r.push(["f","Off"]),n("div",{children:[n(te,{id:e,label:t.label,value:o===void 0?"d":o?"t":"f",options:r,onPick:i=>B(t.path,i==="d"?void 0:i==="t")}),n("p",{class:"mt-1.5 text-xs text-faint",children:["Default: ",t.unset_label]})]})}return n("label",{class:"inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-semibold text-ink",children:[n("input",{id:e,type:"checkbox",class:"size-[18px] accent-brand-solid",checked:(o??t.default??!1)===!0,onChange:r=>B(t.path,r.currentTarget.checked)}),t.label]})}function Io({id:e,field:t,value:o,invalid:r}){if(t.type==="bool")return n(Oo,{id:e,field:t,value:o});if(t.type==="number")return n(Po,{id:e,field:t,value:o,invalid:r});if(t.type==="list")return n(H,{id:e,values:Array.isArray(o)?o:[],placeholder:t.placeholder,onChange:i=>B(t.path,i.length?i:void 0)});if(t.type==="select"){let i=t.options.map(s=>Array.isArray(s)?s:[s,s]);return t.unset_label!==void 0&&i.unshift(["",t.unset_label]),n(A,{id:e,options:i,invalid:r,value:o??(t.unset_label!==void 0?"":t.default??(Array.isArray(t.options[0])?t.options[0][0]:t.options[0])),onChange:s=>B(t.path,s===""?void 0:s)})}return n("input",{id:e,type:"text",value:o??"",placeholder:t.placeholder,autocomplete:"off",spellcheck:!1,autocapitalize:"off",class:`input font-mono ${r?"input-invalid":""}`,onInput:i=>B(t.path,i.currentTarget.value===""?void 0:i.currentTarget.value)})}function I({field:e}){let t=e.path.join("."),o=$o(e),r=N(d.config,e.path),i=S(t),s=ft(e),a=fn(e),u=e.type==="bool"&&e.unset_label===void 0,m=e.default!==void 0&&!s,l=Array.isArray(e.default)?e.default.join(", "):String(e.default??"");return n("div",{id:`row-${o}`,class:`relative pt-5 pb-1 ${s?"before:absolute before:top-6 before:-left-2.5 before:bottom-2 before:w-[3px] before:rounded before:bg-brand before:content-[''] sm:before:-left-3.5":""}`,children:[n("div",{class:"flex min-h-6 items-center gap-2.5",children:[u?null:n("label",{for:o,class:`text-sm font-semibold text-ink ${a?"opacity-50":""}`,children:[e.label,e.required?n("span",{class:"text-danger",children:" *"}):null]}),u?null:n("code",{class:"hidden font-mono text-[11px] text-faint sm:inline",children:t}),m&&!u?n("span",{class:"rounded border border-line px-1.5 text-[11px] leading-4 font-medium text-faint",children:"default"}):null,n("span",{class:"flex-1"}),s?n("button",{type:"button",class:"focus-ring min-h-8 cursor-pointer rounded px-1 text-[13px] font-medium text-brand hover:underline",title:`Reset to default${e.default!==void 0?` (${l})`:""}`,"aria-label":`Reset ${e.label} to default`,onClick:()=>b(p=>he(p.config,e.path)),children:"Reset"}):null]}),e.help?n("p",{class:`mt-0.5 max-w-prose text-[13px] leading-5 text-muted ${a?"opacity-50":""}`,children:e.help}):null,n("fieldset",{disabled:a,class:`m-0 mt-2.5 min-w-0 max-w-[520px] border-0 p-0 ${a?"opacity-50":""}`,children:n(Io,{id:o,field:e,value:r,invalid:i?.level==="error"})}),a?n("p",{class:"mt-1.5 text-xs text-faint",children:hn(e)}):null,n(P,{issue:i})]})}var Ao=[["","Choose reach"],["own","own"],["tenant","tenant"],["all","all"]],On=e=>Array.isArray(e)?e.length>0:g(e)&&Object.keys(e).length>0;function In(){let e=d.config.auth;return e==="jwt"?{method:"jwt"}:g(e)?e:{method:"none"}}function z(e){b(t=>{let o={...In()};e(o),t.config.auth=o.method==="none"?null:o})}function Ve(e,t,o){o==null||o===""||typeof o!="string"&&!On(o)?delete e[t]:e[t]=o}function Ro(e){return e.roles_claim!==void 0||On(e.roles)?"roles":e.permissions_claim!==void 0?"permissions":"default"}var jo=e=>d.issues.filter(t=>t.path===e||t.path.startsWith(`${e}.`));function wt({prefix:e}){return jo(e).map((t,o)=>n(P,{issue:t},o))}function An({id:e,label:t,name:o,onRename:r}){let[i,s]=K(o);return U(()=>s(o),[o]),n("div",{children:[n("label",{for:e,class:"sr-only",children:t}),n("input",{id:e,type:"text",value:i,class:"input font-mono",autocomplete:"off",spellcheck:!1,onInput:a=>s(a.currentTarget.value),onChange:a=>{r(a.currentTarget.value.trim())||s(o)}})]})}var Be=({children:e})=>n("span",{class:"mb-1.5 block text-xs font-medium text-faint",children:e});function Rn(e,t,o){return!o||o!==t&&o in e?null:Object.fromEntries(Object.entries(e).map(([r,i])=>[r===t?o:r,i]))}function jn({sec:e}){let t=In(),o=t.method??"none",r=Ro(t);return n("div",{class:"space-y-6",children:[n(R,{title:"Method",children:n(xt,{name:"auth-method",label:"Authentication method",value:o,options:[["none","None","Every request is accepted. For local development."],["jwt","JWT","Verify bearer tokens signed with JWT_SECRET_KEY."],["custom","Custom","Your own BaseAuth subclass decides who is calling."]],onPick:i=>z(s=>{s.method=i,i==="custom"?s.path=s.path??"":delete s.path})})}),o==="custom"?n(R,{title:"Class",children:n("div",{class:"panel",children:n(Y,{id:"auth-path",label:"Auth class",path:"auth.path",value:t.path,placeholder:"auth.custom:MyAuth",help:"Import path of a BaseAuth subclass, as module:attribute.",issue:S("auth.path"),onInput:i=>z(s=>{s.path=i})})})}):null,o!=="none"?n(x,{children:[n(R,{title:"Permission source",children:n(xt,{name:"auth-source",label:"Permission source",value:r,options:[["default","Default","Every signed-in user, own data only."],["roles","Roles from a claim","Map the roles in the token to permissions."],["permissions","Permissions from a claim","The token already lists the permissions."]],onPick:i=>z(s=>{delete s.roles_claim,delete s.permissions_claim,delete s.roles,delete s.default_role,i==="roles"&&(s.roles_claim="roles",s.roles={member:{permissions:[],reach:"own"}}),i==="permissions"&&(s.permissions_claim="permissions")})})}),r==="roles"?n(Lo,{auth:t,registry:e.registry}):null,r==="permissions"?n(R,{title:"Claim",children:n("div",{class:"panel",children:n(Y,{id:"auth-permissions-claim",label:"Permissions claim",path:"auth.permissions_claim",value:t.permissions_claim,placeholder:"permissions",issue:S("auth.permissions_claim"),help:"Token claim holding a list of permissions, such as graph:invoke.",onInput:i=>z(s=>{s.permissions_claim=i})})})}):null,n(R,{title:"Tenants",children:n("div",{class:"panel divide-y divide-line",children:[n("div",{class:"px-5 py-4",children:[n(xe,{label:"Tenant claims",path:"auth.tenant_claims",id:"auth-tenant-claims",help:"Token claims that name the tenant, such as org_id. Rows are separated by them."}),n("div",{class:"mt-3",children:n(H,{id:"auth-tenant-claims",values:t.tenant_claims??[],placeholder:"org_id",onChange:i=>z(s=>Ve(s,"tenant_claims",i))})}),n(wt,{prefix:"auth.tenant_claims"})]}),r==="roles"?n(Mo,{auth:t}):null]})}),n(No,{auth:t})]}):null]})}function Mo({auth:e}){let t=Object.keys(g(e.roles)?e.roles:{});return n(bt,{children:[n(xe,{label:"Default role",path:"auth.default_role",id:"auth-default-role",help:"Given to a signed-in user whose token carries no role."}),n("div",{class:"min-w-0 sm:pt-0.5",children:[n(A,{id:"auth-default-role",value:e.default_role??"",options:[["","None"],...t.map(o=>[o,o])],onChange:o=>z(r=>Ve(r,"default_role",o))}),n(P,{issue:S("auth.default_role")})]})]})}function Lo({auth:e,registry:t}){let o=g(e.roles)?e.roles:{},r=Object.entries(o),i=Object.keys(o),s=["*",...Object.entries(t||{}).flatMap(([p,h])=>[`${p}:*`,...h.map(c=>`${p}:${c}`)])],a=p=>z(h=>{h.roles=p}),u=(p,h)=>a({...o,[p]:{...o[p],...h}}),m=()=>{let p=r.length+1;for(;`role${p}`in o;)p++;a({...o,[`role${p}`]:{permissions:[],reach:"own"}})},l=(p,h)=>{let c=Rn(o,p,h);return c?(z(f=>{f.roles=Object.fromEntries(Object.entries(c).map(([y,T])=>[y,T.inherits?{...T,inherits:T.inherits.map(w=>w===p?h:w)}:T])),f.default_role===p&&(f.default_role=h),g(f.system_identities)&&(f.system_identities=Object.fromEntries(Object.entries(f.system_identities).map(([y,T])=>[y,{...T,roles:(T.roles??[]).map(w=>w===p?h:w)}])))}),!0):!1};return n(R,{title:"Roles",children:n("div",{class:"panel",children:[n("div",{class:"border-b border-line",children:n(Y,{id:"auth-roles-claim",label:"Roles claim",path:"auth.roles_claim",value:e.roles_claim,placeholder:"roles",issue:S("auth.roles_claim"),help:"Token claim holding the user's list of role names.",onInput:p=>z(h=>{h.roles_claim=p})})}),r.length?n("div",{class:"divide-y divide-line",children:r.map(([p,h],c)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_8rem_auto] items-start gap-3",children:[n(An,{id:`role-name-${c}`,label:"Role name",name:p,onRename:f=>l(p,f)}),n(A,{id:`role-reach-${c}`,value:h.reach??"",options:Ao,invalid:S(`auth.roles.${p}.reach`)?.level==="error",onChange:f=>u(p,{reach:f})}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove role ${p}`,onClick:()=>a(Object.fromEntries(r.filter(([f])=>f!==p))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Be,{children:"Permissions"}),n(H,{id:`role-permissions-${c}`,values:h.permissions??[],placeholder:"graph:invoke, threads:*, *",suggestions:s,onChange:f=>u(p,{permissions:f})})]}),n("div",{children:[n(Be,{children:"Inherits"}),n(H,{id:`role-inherits-${c}`,values:h.inherits??[],placeholder:"another role",suggestions:i.filter(f=>f!==p),onChange:f=>{let y={...h};Ve(y,"inherits",f),a({...o,[p]:y})}})]}),n(wt,{prefix:`auth.roles.${p}`})]},c))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"No roles yet. Add one to use roles."}),n("div",{class:"flex items-center justify-between gap-4 border-t border-line px-5 py-3",children:[n("p",{class:"text-[13px] text-muted",children:["Reach: ",n("code",{class:"font-mono",children:"own"})," the user's rows, ",n("code",{class:"font-mono",children:"tenant"})," every row of their tenant, ",n("code",{class:"font-mono",children:"all"})," every row."]}),n("button",{type:"button",class:"btn btn-ghost",onClick:m,children:[n(_,{name:"plus"}),"Add role"]})]})]})})}function No({auth:e}){let t=g(e.system_identities)?e.system_identities:{},o=Object.entries(t),r=Object.keys(g(e.roles)?e.roles:{}),i=l=>z(p=>Ve(p,"system_identities",l)),s=(l,p)=>i({...t,[l]:{...t[l],...p}}),a=()=>{let l=o.length+1;for(;`job${l}`in t;)l++;i({...t,[`job${l}`]:{roles:[],tenant:{}}})},u=l=>Object.entries(l??{}).map(([p,h])=>`${p}=${h}`),m=l=>Object.fromEntries(l.filter(p=>p.includes("=")).map(p=>[p.slice(0,p.indexOf("=")).trim(),p.slice(p.indexOf("=")+1).trim()]));return n(R,{title:"System identities",children:n("div",{class:"panel",children:[o.length?n("div",{class:"divide-y divide-line",children:o.map(([l,p],h)=>n("div",{class:"space-y-3 px-5 py-4",children:[n("div",{class:"grid grid-cols-[minmax(0,1fr)_auto] items-start gap-3",children:[n(An,{id:`system-name-${h}`,label:"Identity name",name:l,onRename:c=>{let f=Rn(t,l,c);return f&&i(f),!!f}}),n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${l}`,onClick:()=>i(Object.fromEntries(o.filter(([c])=>c!==l))),children:n(_,{name:"trash"})})]}),n("div",{children:[n(Be,{children:"Roles"}),n(H,{id:`system-roles-${h}`,values:p.roles??[],placeholder:"member",suggestions:r,onChange:c=>s(l,{roles:c})})]}),n("div",{children:[n(Be,{children:"Tenant, as claim=value"}),n(H,{id:`system-tenant-${h}`,values:u(p.tenant),placeholder:"org_id=acme",onChange:c=>s(l,{tenant:m(c)})})]}),n(wt,{prefix:`auth.system_identities.${l}`})]},h))}):n("p",{class:"px-5 py-6 text-center text-sm text-muted",children:"None. Scheduled jobs and other non-user callers can run as one."}),n("div",{class:"flex justify-end border-t border-line px-5 py-3",children:n("button",{type:"button",class:"btn btn-ghost",onClick:a,children:[n(_,{name:"plus"}),"Add identity"]})})]})})}var Ge=[["memory","Memory","This process only"],["redis","Redis","Shared. Redis, Valkey, Dragonfly, KeyDB"],["custom","Custom","Your BaseCacheStore class"]],Do={requests:100,window:60,by:"ip"},Je=()=>g(d.config.cache_store)?d.config.cache_store:{};function Ct(e){b(t=>{let o=g(t.config.cache_store)?t.config.cache_store:{};e(o),Object.keys(o).length?t.config.cache_store=o:delete t.config.cache_store})}var Ho=()=>{let e=Je().rate_limit;return g(e)&&e.enabled!==!1},zo=()=>{let e=Je().token_revocation;return e===!0||g(e)&&e.enabled!==!1};function Fo(e){Ct(t=>{delete t.url,delete t.path,e==="memory"?delete t.type:t.type=e})}function Uo(e){Ct(t=>{e?g(t.rate_limit)?delete t.rate_limit.enabled:t.rate_limit={...Do}:delete t.rate_limit})}function qo(e){Ct(t=>{e?g(t.token_revocation)?delete t.token_revocation.enabled:t.token_revocation=!0:delete t.token_revocation})}var En=(e,t)=>b(o=>{oe(o.config,e,t)});function Wo({value:e,onPick:t}){let o=(r,i)=>{let s=r.key==="ArrowRight"||r.key==="ArrowDown"?1:r.key==="ArrowLeft"||r.key==="ArrowUp"?-1:0;if(!s)return;r.preventDefault();let a=Ge[(i+s+Ge.length)%Ge.length][0];t(a),requestAnimationFrame(()=>document.getElementById(`cache-type-${a}`)?.focus())};return n("div",{role:"radiogroup","aria-label":"Cache store type",class:"grid gap-3 sm:grid-cols-3",children:Ge.map(([r,i,s],a)=>{let u=r===e;return n("button",{type:"button",role:"radio",id:`cache-type-${r}`,"aria-checked":u?"true":"false",tabIndex:u?0:-1,onKeyDown:m=>o(m,a),class:`focus-ring flex cursor-pointer items-start gap-3 rounded-xl border p-4 text-left transition-colors duration-150 motion-reduce:transition-none ${u?"border-brand bg-brand-soft":"border-line bg-surface shadow-card hover:border-line-strong"}`,onClick:()=>{u||t(r)},children:[n("span",{class:`mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-full border ${u?"border-brand-solid bg-brand-solid":"border-line-strong"}`,children:u?n("span",{class:"size-1.5 rounded-full bg-white"}):null}),n("span",{class:"min-w-0",children:[n("span",{class:"block text-sm font-medium text-ink",children:i}),n("span",{class:"mt-0.5 block text-[13px] leading-5 text-muted",children:s})]})]},r)})})}function ye({warn:e,children:t}){return n("p",{class:`mt-3 flex items-start gap-1.5 text-[13px] leading-5 ${e?"text-warn":"text-muted"}`,children:[e?n(_,{name:"alert",class:"mt-0.5 size-3.5"}):null,n("span",{children:t})]})}function Pn({id:e,title:t,text:o,on:r,onChange:i}){return n("div",{class:"flex items-center gap-4",children:[n("div",{class:"min-w-0 flex-1",children:[n("h3",{class:"text-base font-semibold text-ink",children:t}),n("p",{class:"mt-0.5 text-[13px] leading-5 text-muted",children:o})]}),n(be,{id:e,on:r,label:t,onChange:i})]})}var Go=[["ip","IP"],["user","User"],["global","Global"]];function Bo({field:e,memory:t}){let[o,r]=K(!1),i=Je().rate_limit,s=g(i)&&i.by||"ip",a=!(g(i)&&i.fail_open===!1),u=["trusted_proxy_headers","trusted_proxy_hops","trusted_proxies","exclude_paths"];return n(x,{children:[t?n(ye,{warn:!0,children:"Counts per worker with the Memory store."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:e("cache_store.rate_limit.requests")}),n(I,{field:e("cache_store.rate_limit.window")}),n("div",{class:"pt-5",children:[n("span",{id:"cache-by-label",class:"block text-sm font-semibold text-ink",children:"Limit by"}),n("div",{class:"mt-2.5",children:n(te,{id:"f-cache_store-rate_limit-by",label:"Limit by",options:Go,value:s,onPick:m=>En(["cache_store","rate_limit","by"],m)})}),n(P,{issue:S("cache_store.rate_limit.by")})]}),n("div",{class:"pt-5",children:[n("span",{id:"cache-fail-open-label",class:"block text-sm font-semibold text-ink",children:"Fail open"}),n("div",{class:"mt-2.5 flex items-center gap-3",children:[n(be,{id:"f-cache_store-rate_limit-fail_open",on:a,label:"Fail open",onChange:m=>En(["cache_store","rate_limit","fail_open"],m)}),n("p",{class:"m-0 text-[13px] leading-5 text-muted",children:a?"Allow requests if the store errors.":"Deny with 503 if the store errors."})]})]})]}),n("div",{class:"mt-5 border-t border-line pt-1",children:[n("button",{type:"button","aria-expanded":o?"true":"false","aria-controls":"cache-advanced",class:"focus-ring flex min-h-11 cursor-pointer items-center gap-2 rounded font-medium text-muted hover:text-ink",onClick:()=>r(!o),children:[n(_,{name:"chevronRight",class:`size-4 transition-transform duration-150 motion-reduce:transition-none ${o?"rotate-90":""}`}),"Advanced"]}),o?n("div",{id:"cache-advanced",children:u.map(m=>n(I,{field:e(`cache_store.rate_limit.${m}`)},m))}):null]})]})}var ae=[["rate","Rate limiting"],["revoke","Token revocation"]];function Mn({sec:e}){let[t,o]=K("rate"),r=Je().type||"memory",i=l=>e.fields.find(p=>p.path.join(".")===l),s={rate:Ho(),revoke:zo()},a=r==="memory",u={rate:!!S("cache_store.rate_limit"),revoke:!!S("cache_store.token_revocation")},m=(l,p)=>{let h=l.key==="ArrowRight"?1:l.key==="ArrowLeft"?-1:0,c=l.key==="Home"?0:l.key==="End"?ae.length-1:h?(p+h+ae.length)%ae.length:-1;c<0||(l.preventDefault(),o(ae[c][0]),document.getElementById(`cache-tab-${ae[c][0]}`)?.focus())};return n("div",{children:[n(R,{title:"Store type",children:[n(Wo,{value:r,onPick:Fo}),r==="redis"?n(I,{field:i("cache_store.url")}):null,r==="custom"?n(I,{field:i("cache_store.path")}):null,a?n("p",{class:"mt-3 mb-0 text-[13px] leading-5 text-muted",children:"Not shared between workers: each worker keeps its own locks, limits and revocations."}):null]}),n("div",{role:"tablist","aria-label":"Cache store features",class:"mt-8 flex gap-1 border-b border-line",children:ae.map(([l,p],h)=>{let c=t===l;return n("button",{type:"button",role:"tab",id:`cache-tab-${l}`,"aria-selected":c?"true":"false","aria-controls":`cache-panel-${l}`,tabIndex:c?0:-1,onKeyDown:f=>m(f,h),class:`focus-ring -mb-px flex min-h-11 cursor-pointer items-center gap-2 border-b-2 px-3 text-sm font-medium transition-colors duration-150 motion-reduce:transition-none ${c?"border-brand-solid text-ink":"border-transparent text-muted hover:text-ink"}`,onClick:()=>o(l),children:[p,n(ie,{class:s[l]?"bg-brand-soft text-brand":"bg-raised text-muted",children:s[l]?"On":"Off"})]},l)})}),n("div",{role:"tabpanel",id:`cache-panel-${t}`,"aria-labelledby":`cache-tab-${t}`,class:"pt-5",children:t==="rate"?n(x,{children:[n(Pn,{id:"cache-rate-limit",title:"Rate limiting",on:s.rate,onChange:Uo,text:"Cap how many requests one caller can make."}),s.rate?n(Bo,{field:i,memory:a}):u.rate?n(ye,{warn:!0,children:"Off: one caller can exhaust the service."}):null]}):n(x,{children:[n(Pn,{id:"cache-token-revocation",title:"Token revocation",on:s.revoke,onChange:qo,text:"Reject revoked tokens before they expire (10xgraph revoke)."}),s.revoke?n(x,{children:[a?n(ye,{warn:!0,children:"10xgraph revoke cannot reach an in-memory store; pick Redis or Custom."}):null,n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(I,{field:i("cache_store.token_revocation.token_lifetime")}),n(I,{field:i("cache_store.token_revocation.user_ttl")})]}),n(ye,{children:"Fails closed: while the cache store is unreachable, authenticated requests get 503 and the server will not start."})]}):u.revoke?n(ye,{warn:!0,children:"Off: a leaked token works until it expires."}):null]})})]})}function Ln({sec:e}){let t=e.fields[0].path[0],o=e.choices.map(p=>p[0]),r=d.config[t],i=t==="thread_name_generator"?"random":"",s=r===void 0?i:typeof r=="string"&&o.includes(r)?r:"custom",a=[...t==="id_generator"?[["","Not set (the graph's own generator)"]]:[],...e.choices.map(([p,h])=>[p,t==="thread_name_generator"&&p==="random"?`${h} (default)`:h]),["custom","Custom (module:attr)"]],u=s===""?"The graph keeps the generator it was compiled with.":s==="custom"?"Your own class, as module:attribute.":e.choices.find(p=>p[0]===s)?.[2],m=p=>b(h=>{p===i?delete h.config[t]:h.config[t]=p==="custom"?"":p}),l=`f-${t}`;return n("div",{children:[n(R,{title:"Choice",children:n("div",{class:"max-w-[520px]",children:[n("label",{for:`${l}-select`,class:"sr-only",children:e.title}),n(A,{id:`${l}-select`,options:a,value:s,onChange:m}),n("p",{class:"mt-1.5 text-[13px] text-muted",children:u})]})}),s==="custom"?n(R,{title:"Class",children:n("div",{class:"panel",children:n(Y,{id:l,label:e.fields[0].label,path:t,value:typeof r=="string"?r:"",placeholder:e.fields[0].placeholder,help:e.fields[0].help,issue:S(t),onInput:p=>b(h=>{h.config[t]=p})})})}):null]})}function Nn(){let e=Array.isArray(d.config.remote_tools)?d.config.remote_tools:[],t=()=>{b(o=>{e.push({node:"",name:"",description:"",parameters:{type:"object",properties:{},required:[]}}),o.config.remote_tools=e}),requestAnimationFrame(()=>document.getElementById(`tool-${e.length-1}-name`)?.focus())};return e.length?n("div",{class:"space-y-4",children:[e.map((o,r)=>n(Vo,{tools:e,tool:o,index:r},r)),n("button",{type:"button",onClick:t,class:"focus-ring flex h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-dashed border-line-strong text-sm font-medium text-muted transition-colors duration-150 hover:border-brand hover:text-brand",children:[n(_,{name:"plus"}),"Add remote tool"]})]}):n("div",{class:"rounded-xl border border-dashed border-line-strong bg-surface px-6 py-10 text-center",children:[n("div",{class:"mx-auto flex size-11 items-center justify-center rounded-full bg-raised text-faint",children:n(_,{name:"wrench",class:"size-5"})}),n("p",{class:"mt-4 text-sm font-semibold text-ink",children:"No remote tools yet"}),n("p",{class:"mx-auto mt-1 max-w-sm text-sm leading-6 text-muted",children:"Declare a tool here and the client that calls the API runs it, for example to read the user's location."}),n("button",{type:"button",class:"btn btn-outline mt-5",onClick:t,children:[n(_,{name:"plus"}),"Add remote tool"]})]})}function Vo({tools:e,tool:t,index:o}){let r=d.toolParams[o]??{text:JSON.stringify(t.parameters??{},null,2),error:null},i=a=>b(()=>{e[o]={...e[o],...a}}),s=a=>`remote_tools[${o}].${a}`;return n("section",{class:"panel",children:[n("div",{class:"flex items-center gap-3 border-b border-line px-5 py-3",children:[n(_,{name:"wrench",class:"size-4 text-faint"}),n("span",{class:"min-w-0 flex-1 truncate font-mono text-sm font-medium text-ink",children:t.name||`tool_${o+1}`}),t.node?n("span",{class:"rounded-md border border-line bg-raised px-1.5 py-0.5 font-mono text-xs text-muted",children:t.node}):null,n("button",{type:"button",class:"icon-btn hover:text-danger","aria-label":`Remove ${t.name||"tool"}`,onClick:()=>b(a=>{e.splice(o,1),a.toolParams={}}),children:n(_,{name:"trash"})})]}),n("div",{class:"divide-y divide-line",children:[n(Y,{id:`tool-${o}-name`,label:"Tool name",path:s("name"),value:t.name,placeholder:"get_location",onInput:a=>i({name:a})}),n(Y,{id:`tool-${o}-node`,label:"Graph node",path:s("node"),value:t.node,placeholder:"MAIN",help:"The node whose model may call this tool.",onInput:a=>i({node:a})}),n(Y,{id:`tool-${o}-description`,label:"Description",path:s("description"),value:t.description,placeholder:"Returns the user's current city",help:"What the model reads when deciding to call it.",onInput:a=>i({description:a})}),n("div",{class:"px-5 py-4",children:[n(xe,{label:"Parameters",path:s("parameters"),help:"JSON schema of the arguments. type must be object.",id:`tool-${o}-params`}),n("textarea",{id:`tool-${o}-params`,rows:7,spellcheck:!1,value:r.text,class:`input mt-3 h-auto py-2.5 font-mono text-[13px] leading-5 ${r.error?"input-invalid":""}`,onInput:a=>{let u=a.currentTarget.value;b(m=>{try{e[o]={...e[o],parameters:JSON.parse(u)},m.toolParams[o]={text:u,error:null}}catch(l){m.toolParams[o]={text:u,error:l.message}}})}}),n(P,{issue:r.error?{level:"error",message:r.error}:null})]})]})]})}var $t="focus-ring min-h-8 cursor-pointer rounded px-1.5 text-[13px] font-medium text-brand transition-colors duration-150 hover:underline",X="mt-2 max-w-prose text-[13px] leading-5 text-muted",V=(e,t)=>b(o=>{t===void 0||t===""?he(o.config,e):oe(o.config,e,t)});function Et({id:e,label:t,path:o,help:r,children:i}){return n("div",{class:"pt-4",children:[n("div",{class:"flex items-baseline gap-2.5",children:[n("label",{for:e,class:"text-sm font-semibold text-ink",children:t}),o?n("code",{class:"hidden font-mono text-[11px] text-faint sm:inline",children:o}):null]}),n("div",{class:"mt-2 max-w-[520px]",children:i}),r?n("p",{class:X,children:r}):null,n(P,{issue:S(o)})]})}function le({path:e,label:t,help:o,placeholder:r}){let i=e.join("."),s=`f-${e.join("-")}`,a=N(d.config,e);return n(Et,{id:s,label:t,path:i,help:o,children:n("input",{id:s,type:"text",value:a??"",placeholder:r,autocomplete:"off",spellcheck:!1,class:`input font-mono ${S(i)?.level==="error"?"input-invalid":""}`,onInput:u=>V(e,u.currentTarget.value)})})}function Jo({path:e,label:t,help:o}){let r=e.join("."),i=`f-${e.join("-")}`,s=N(d.config,e);return n(Et,{id:i,label:t,path:r,help:o,children:n("input",{id:i,type:"number",inputmode:"numeric",min:"1",step:"1",value:s??"",class:`input max-w-40 font-mono tabular-nums ${S(r)?.level==="error"?"input-invalid":""}`,onInput:a=>{let u=a.currentTarget.value;V(e,u===""?void 0:Number(u))}})})}function St({path:e,label:t,help:o,options:r,value:i,onChange:s}){let a=`f-${e.join("-")}`;return n(Et,{id:a,label:t,path:e.join("."),help:o,children:n(A,{id:a,plain:!0,options:r,value:i,onChange:s})})}function Ko({path:e,label:t,checked:o}){let r=`f-${e.join("-")}`;return n("label",{class:"mt-3 inline-flex min-h-11 cursor-pointer items-center gap-2.5 text-sm font-semibold text-ink",children:[n("input",{id:r,type:"checkbox",class:"size-[18px] accent-brand-solid",checked:o,onChange:i=>V(e,i.currentTarget.checked)}),t]})}var Ye=[{key:"logfire",name:"Logfire",label:"Logfire",text:"Pydantic Logfire.",secret:n(x,{children:["Token: set ",n("code",{class:"font-mono text-xs",children:"LOGFIRE_TOKEN"})," in .env."]})},{key:"langsmith",name:"LangSmith",label:"LangSmith",text:"LangChain's tracing service.",secret:n(x,{children:["API key: set ",n("code",{class:"font-mono text-xs",children:"LANGSMITH_API_KEY"})," in .env."]})},{key:"phoenix",name:"Phoenix",label:"Phoenix",text:"Arize Phoenix, local or hosted.",secret:n(x,{children:["API key: set ",n("code",{class:"font-mono text-xs",children:"PHOENIX_API_KEY"})," in .env. A local Phoenix does not need one."]})},{key:"otlp",name:"OTLP endpoint",label:"OTLP endpoint",text:"Any OpenTelemetry endpoint: a collector, Tempo, Jaeger.",secret:n(x,{children:["Auth headers: set ",n("code",{class:"font-mono text-xs",children:"OTEL_EXPORTER_OTLP_HEADERS"})," in .env."]})}],Yo=[["standard","Standard"],["spans","Spans only"],["full","Full"]],Xo={standard:"Adds model name, token counts and request parameters.",spans:"Timing and structure only. No inputs or outputs.",full:"Records prompts and tool I/O. May contain PII."},Qo=[["raw","Raw"],["hash","Hashed"],["mask","Masked"]],Zo={raw:"The user id is sent as is.",hash:"A SHA-256 of the user id, keyed when TENXGRAPH_TRACE_USER_ID_KEY is set.",mask:"The middle of the user id is hidden."},Tt=()=>g(d.config.observability)?d.config.observability:{},Xe=e=>g(Tt()[e])&&Tt()[e].enabled===!0;function Dn(e){b(t=>{g(t.config.observability)||(t.config.observability={level:"standard",user_id_mode:"raw"}),t.config.observability[e]={enabled:!0}})}function er(e){b(t=>{delete t.config.observability[e],Ye.some(o=>Xe(o.key))||delete t.config.observability})}function tr(){let e=d.config.metrics,t=g(e)&&g(e.otlp)?e.otlp.endpoint:null;return!g(e)||e.enabled!==!0||e.export!=="otlp"||!t?null:t.replace(/\/+$/,"").replace(/\/v1\/metrics$/,"")}function nr(){let e=["observability","otlp","use_metrics_endpoint"],t=N(d.config,e)===!0,o=tr(),r="f-observability-otlp-use_metrics_endpoint";return n(x,{children:[o?n("div",{class:"pt-4",children:[n("div",{class:"flex items-center gap-3",children:[n(be,{id:r,on:t,label:"Use the metrics endpoint",onChange:i=>V(e,i?!0:void 0)}),n("label",{for:r,class:"cursor-pointer text-sm font-semibold text-ink",children:"Use the metrics endpoint"})]}),n("p",{class:X,children:t?n(x,{children:["Traces go to ",n("code",{class:"font-mono text-xs text-body",children:[o,"/v1/traces"]})]}):"Send traces to the same endpoint as the metrics."}),n(P,{issue:S(e.join("."))})]}):null,t&&o?null:n(le,{path:["observability","otlp","endpoint"],label:"Endpoint",placeholder:"http://collector:4318",help:"Base URL of an OTLP HTTP receiver. /v1/traces is appended."}),o?null:n("p",{class:X,children:["Sharing the metrics endpoint needs Metrics set to OTLP push with an endpoint.",t?n(x,{children:[" It is on in the file. ",n("button",{type:"button",class:$t,onClick:()=>V(e,void 0),children:"Stop sharing"})]}):null]}),n(P,{issue:t&&!o?S(e.join(".")):null}),n("p",{class:X,children:"Prometheus stores metrics only. To send traces and metrics to one place, use a collector (OTel Collector, Grafana Alloy, Grafana Cloud)."})]})}function or({dest:e}){let t=r=>["observability",e.key,r];if(e.key==="logfire"){let r=t("send_to_logfire"),i=t("console");return n(x,{children:[n(le,{path:t("service_name"),label:"Service name",placeholder:"my-agent"}),n(Ko,{path:r,label:"Send to Logfire",checked:N(d.config,r)!==!1}),n(St,{path:i,label:"Print spans to console",help:"Logfire also takes console options that JSON cannot express, so only its own behavior or Off is offered.",options:[["","Logfire's own behavior"],["off","Off"]],value:N(d.config,i)===!1?"off":"",onChange:s=>V(i,s==="off"?!1:void 0)})]})}if(e.key==="otlp")return n(nr,{});let o=e.key==="phoenix";return n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(le,{path:t("project"),label:"Project",placeholder:"my-agent"}),n(le,{path:t("endpoint"),label:"Endpoint",placeholder:o?"http://localhost:6006":"https://api.smith.langchain.com/otel",help:o?"Base collector URL. /v1/traces is appended.":void 0})]})}function rr({dest:e}){return n("div",{id:`dest-${e.key}`,class:"mt-4 rounded-xl border border-line bg-surface p-5",children:[n("div",{class:"flex items-start gap-3",children:[n("div",{class:"min-w-0 flex-1",children:[n("h3",{class:"text-sm font-semibold text-ink",children:e.name}),n("p",{class:"mt-0.5 text-[13px] text-muted",children:e.text})]}),n("button",{type:"button",class:$t,"aria-label":`Remove ${e.name}`,onClick:()=>er(e.key),children:"Remove"})]}),n(or,{dest:e}),n("p",{class:"mt-5 border-t border-line pt-3 text-[13px] text-muted",children:e.secret})]})}function Hn(){return n("p",{class:X,children:"These are observability publishers. They get graph, node, LLM and tool spans, AI errors included. Event publishers (Kafka, Redis, RabbitMQ) are set in code and also get task events. API and worker errors go to Metrics and logs."})}function zn(){let[e,t]=K(!1),o=Ye.filter(s=>Xe(s.key)),r=Ye.filter(s=>!Xe(s.key)),i=Tt();return n("div",{children:o.length===0?n("div",{class:"max-w-[520px]",children:[n("label",{for:"trace-dest",class:"mb-2 block text-sm font-semibold text-ink",children:"Send AI traces to"}),n(A,{id:"trace-dest",value:"",plain:!0,options:[["","None"],...r.map(s=>[s.key,s.label])],onChange:s=>{s&&Dn(s)}}),n("p",{class:X,children:"Graph runs are not traced."}),n(Hn,{})]}):n(x,{children:[n("h2",{class:"eyebrow",children:"Send AI traces to"}),o.map(s=>n(rr,{dest:s},s.key)),r.length?e?n("div",{class:"mt-4 max-w-[320px]",children:[n("label",{for:"trace-add",class:"sr-only",children:"Add another destination"}),n(A,{id:"trace-add",value:"",plain:!0,options:[["","Choose a destination"],...r.map(s=>[s.key,s.label])],onChange:s=>{t(!1),s&&Dn(s)}})]}):n("button",{type:"button",class:`${$t} mt-3 inline-flex min-h-10 items-center gap-1.5`,onClick:()=>t(!0),children:[n(_,{name:"plus",class:"size-4"}),"Add another destination"]}):null,n("div",{class:"max-w-[520px]",children:n(Hn,{})}),n("h2",{class:"eyebrow mt-8",children:"Trace settings"}),n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(St,{path:["observability","level"],label:"Level",options:Yo,value:i.level??"standard",onChange:s=>V(["observability","level"],s),help:Xo[i.level??"standard"]}),n(St,{path:["observability","user_id_mode"],label:"User id in traces",options:Qo,value:i.user_id_mode??"raw",onChange:s=>V(["observability","user_id_mode"],s),help:Zo[i.user_id_mode??"raw"]})]})]})})}var sr=[["","None"],["otlp","OTLP push"],["prometheus","Prometheus scrape"]],ir={"":"Metrics are not exported.",otlp:"Recommended for production. Works with several worker processes.",prometheus:"One process only. The API serves the scrape path, the worker its own port."},ar=[50,100,250,500,1e3,2500,5e3,1e4,3e4,6e4,12e4],Ke=()=>g(d.config.metrics)?d.config.metrics:{},Fn=()=>Ke().enabled===!0&&["otlp","prometheus"].includes(Ke().export)?Ke().export:"";function lr(e){b(t=>{if(!e){delete t.config.metrics;return}let o=g(t.config.metrics)?t.config.metrics:{};o.enabled=!0,o.export=e,e==="otlp"&&!g(o.otlp)&&(o.otlp={endpoint:null}),e==="prometheus"&&!g(o.prometheus)&&(o.prometheus={path:"/metrics",worker_port:9464}),t.config.metrics=o})}function Un(){let e=Fn(),t=Ke().latency_buckets_ms,o=S("metrics.latency_buckets_ms");return n("div",{children:[n("div",{class:"max-w-[520px]",children:[n("label",{for:"f-metrics-export",class:"mb-2 block text-sm font-semibold text-ink",children:"Export metrics"}),n(A,{id:"f-metrics-export",plain:!0,options:sr,value:e,invalid:S("metrics.export")?.level==="error",onChange:lr}),n("p",{class:X,children:ir[e]}),n(P,{issue:S("metrics.export")})]}),e===""?null:n("div",{class:"mt-5 rounded-xl border border-line bg-surface px-5 pt-1 pb-5",children:[e==="otlp"?n(le,{path:["metrics","otlp","endpoint"],label:"Endpoint",placeholder:"http://collector:4318",help:n(x,{children:["/v1/metrics is appended.",n("br",{}),"Prometheus: ",n("code",{class:"font-mono text-xs",children:"http://prometheus:9090/api/v1/otlp"})," (start it with ",n("code",{class:"font-mono text-xs",children:"--web.enable-otlp-receiver"}),")."]})}):n(x,{children:n("div",{class:"grid gap-x-6 sm:grid-cols-2",children:[n(le,{path:["metrics","prometheus","path"],label:"Scrape path",placeholder:"/metrics",help:"Route on the API server that Prometheus scrapes."}),n(Jo,{path:["metrics","prometheus","worker_port"],label:"Worker port",help:"Each worker serves its own metrics on this port."})]})}),e==="otlp"?n("p",{class:"mt-5 border-t border-line pt-3 text-[13px] text-muted",children:["Auth headers: set ",n("code",{class:"font-mono text-xs",children:"OTEL_EXPORTER_OTLP_HEADERS"})," in .env."]}):null]}),e!==""?n(We,{title:"Advanced",class:"mt-4",open:!!o||t!==void 0,children:n("div",{class:"pt-4",children:[n("label",{for:"f-metrics-latency_buckets_ms",class:"text-sm font-semibold text-ink",children:"Latency buckets (ms)"}),n("div",{class:"mt-2 max-w-[520px]",children:n(H,{id:"f-metrics-latency_buckets_ms",values:(t??ar).map(String),placeholder:"250",onChange:r=>V(["metrics","latency_buckets_ms"],r.length?r.map(Number).filter(Number.isFinite):void 0)})}),n("p",{class:X,children:"Upper bounds of the latency histogram in milliseconds, ascending."}),n(P,{issue:o})]})}):null]})}var cr=e=>e.length>1?`${e.slice(0,-1).join(", ")} and ${e[e.length-1]}`:e[0];function qn(e){let t=Ye.filter(o=>Xe(o.key)).map(o=>o.name);return t.length?{text:`Level ${mt(["observability","level"])}, sending to ${cr(t)}.`}:{text:e("observability")}}function Wn(e){let t={otlp:"OTLP push",prometheus:"Prometheus scrape"}[Fn()];return t?{text:`${t}.`}:{text:e("metrics")}}var ke=({children:e,code:t})=>n("h1",{id:"page-title",tabindex:-1,class:"flex flex-wrap items-baseline gap-x-3 gap-y-1 text-2xl font-semibold tracking-tight text-ink focus:outline-none",children:[e,t?n("code",{class:"rounded border border-line bg-raised px-1.5 font-mono text-xs font-normal text-faint",children:t}):null]}),Ot=({children:e})=>n("p",{class:"mt-1.5 max-w-[62ch] text-[15px] leading-6 text-muted",children:e}),Gn=({children:e})=>n("h2",{class:"mt-10 mb-2 eyebrow",children:e});function Bn({id:e,text:t,warn:o,compact:r}){let i=ge(e);return i?n("a",{href:`#${e}`,onClick:s=>{s.preventDefault(),D(e)},class:`focus-ring group flex items-center gap-3.5 border-t border-line py-2 last:border-b ${r?"min-h-12":"min-h-14"}`,children:[n(_,{name:ee[e]||"box",class:"size-[18px] text-faint"}),n("span",{class:`min-w-0 flex-1 ${r?"flex flex-wrap items-baseline gap-x-4":""}`,children:[n("span",{class:`block text-sm text-ink group-hover:text-brand ${r?"min-w-40 font-medium":"font-semibold"}`,children:i.title}),n("span",{class:`block text-[13px] leading-5 ${o?"text-warn":"text-muted"}`,children:t??i.description})]}),n(_,{name:"chevronRight",class:"size-4 text-faint"})]}):null}function Pt(e){return`Not set. ${O(e)?.off_summary||""}`.trim()}function dr(){let e=d.config.auth,t=e==="jwt"?"jwt":g(e)?e.method??"none":"none";if(t==="none")return{text:Pt("auth")};let o=g(e)&&g(e.roles)?Object.keys(e.roles).length:0;return{text:`${t==="jwt"?"JWT bearer tokens":`Custom backend${e.path?`: ${e.path}`:""}`}${o?`, ${o} ${o===1?"role":"roles"}`:""}.`}}function ur(){let e=g(d.config.cache_store)?d.config.cache_store:{},t=[`${e.type||"memory"} store`],o=e.rate_limit;if(g(o)&&o.enabled!==!1){let i={ip:"per IP",user:"per user",global:"across all clients"}[o.by??"ip"]||o.by;t.push(`${o.requests??100} requests per ${o.window??60} s, ${i}`)}let r=e.token_revocation;return(r===!0||g(r)&&r.enabled!==!1)&&t.push("token revocation on"),{text:`${t.join("; ")}.`}}function Vn(){let e=O("core"),t=_n(),o=e?re("core").filter(s=>!e.fields.some(a=>a.path.join(".")===s.path)):[],r=[["auth",dr()],["cache_store",ur()],["observability",qn(Pt)],["metrics",Wn(Pt)]],i=d.nav.flatMap(s=>s.items).filter(s=>s!=="get-running");return n(x,{children:[n(ke,{children:t?"You're ready to run":"One step to run your agent"}),n(Ot,{children:q["get-running"].description}),n("div",{class:`mt-4 flex items-center gap-2 font-medium ${t?"text-ok":"text-muted"}`,children:[n(_,{name:t?"checkCircle":"circle",class:"size-[18px]"}),"Required: ",t?"1":"0"," of 1 done"]}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((s,a)=>n(vt,{issue:s},a))}):null,n("div",{class:"mt-2",children:e?kt(e.fields).flatMap(s=>s.fields).map(s=>n(I,{field:s},s.path.join("."))):null}),t?n("div",{class:"mt-7 border-t border-line pt-5",children:[n("div",{class:"flex items-center gap-2.5 font-medium text-ok",children:[n(_,{name:"checkCircle",class:"size-[18px]"}),"Ready for 10xgraph dev"]}),n("div",{class:"mt-3 flex max-w-[420px] items-center gap-2 rounded-lg border border-line-strong bg-sunken py-0 pr-1 pl-3.5",children:[n("code",{class:"flex-1 font-mono text-[13px] font-medium text-ink",children:"10xgraph dev"}),d.copied?n("span",{class:"text-xs text-ok",children:"Copied"}):null,n("button",{type:"button",class:"icon-btn",onClick:wn,"aria-label":d.copied?"Copied 10xgraph dev":"Copy command 10xgraph dev",children:n(_,{name:"copy",class:"size-[18px]"})})]}),n("p",{class:"mt-3 text-muted",children:"Everything else already has working defaults."})]}):n("p",{class:"mt-4 text-xs text-faint",children:"Add the agent graph above, then run 10xgraph dev."}),n(Gn,{children:"Before production (optional)"}),n("div",{children:r.map(([s,a])=>n(Bn,{id:s,text:a.text,warn:a.warn},s))}),n(Gn,{children:"What you can configure"}),n("div",{children:i.map(s=>n(Bn,{id:s,compact:!0},s))})]})}function Qe({vars:e}){return n("dl",{class:"mb-3 grid gap-x-4 gap-y-1 sm:grid-cols-[minmax(0,260px)_minmax(0,1fr)]",children:e.map(t=>[n("dt",{class:"py-0.5 font-mono text-xs font-medium break-all text-ink",children:t.name},`${t.name}-n`),n("dd",{class:"m-0 pb-2 text-[13px] text-muted sm:py-0.5",children:t.purpose},`${t.name}-p`)])})}function pr({vars:e,open:t}){return e?.length?n(We,{title:"Set in .env",open:t,class:"mt-9 border-t border-line pt-1",children:[n("p",{class:"mb-2 text-xs text-faint",children:"Read only. The editor never reads .env, so only names are shown."}),n(Qe,{vars:e})]}):null}function Jn({sec:e}){return e.widget==="auth"?n(jn,{sec:e}):e.widget==="remote_tools"?n(Nn,{}):e.widget==="observability"?n(zn,{}):e.widget==="metrics"?n(Un,{}):e.widget==="cache_store"?n(Mn,{sec:e}):e.widget==="id_generator"||e.widget==="thread_name_generator"?n(Ln,{sec:e}):null}function mr(e){let t=new Set([...e.fields.map(i=>i.path.join(".")),...Object.keys(ut)]),o=new Set(["cache_store.rate_limit","cache_store.token_revocation"]),r=i=>i.startsWith("auth.roles.")||i.startsWith("auth.system_identities.");return re(e.id).filter(i=>!t.has(i.path)&&!(e.id==="cache_store"&&o.has(i.path))&&!(e.id==="auth"&&r(i.path)))}function fr({sec:e}){let t=Ne(e),o=e.widget&&(t||!e.optional||e.self_managed);return n(x,{children:[o?n("div",{class:"mt-6",children:n(Jn,{sec:e})}):null,n("div",{class:e.optional&&!t?"opacity-80":"",children:kt(e.fields.filter(r=>!r.managed)).map((r,i)=>n("div",{children:[r.name?n("h3",{class:`eyebrow ${i===0?"mt-6":"mt-8 border-t border-line pt-4"}`,children:r.name}):null,r.fields.map(s=>n(I,{field:s},s.path.join(".")))]},r.name))})]})}function hr({sec:e}){let t=Ne(e),o=mr(e);return n(x,{children:[n(ke,{code:e.key,children:e.title}),n(Ot,{children:e.description}),o.length?n("div",{class:"mt-4 space-y-2",children:o.map((r,i)=>n(vt,{issue:r},i))}):null,e.optional&&!e.self_managed?n("div",{class:"mt-5 flex flex-wrap items-center gap-4",children:[n(te,{label:`Settings for ${e.title}`,value:t?"c":"d",options:[["d","Use defaults"],["c","Customize"]],onPick:r=>yn(e,r==="c")}),n("p",{class:"m-0 min-w-56 flex-1 text-[13px] text-muted",children:t?`Saved in 10xgraph.json under ${e.key}.`:e.off_summary})]}):null,e.notes?.length?n("div",{class:"mt-5",children:n(ve,{kind:"info",lines:e.notes})}):null,n(fr,{sec:e}),n(pr,{vars:e.env})]})}function _r(){let e=d.schema.environment||[];return n(x,{children:[n(ke,{children:q.environment.title}),n(Ot,{children:"These variables are set in .env or your deployment environment. The editor never reads that file, so only names and purposes are shown."}),e.map(t=>n("section",{children:[n("h2",{class:"mt-8 mb-2 text-sm font-semibold text-ink",children:t.title}),t.note?n("p",{class:"mb-2 text-xs text-faint",children:t.note}):null,n(Qe,{vars:t.vars})]},t.title))]})}var Kn=e=>String(e??"").toLowerCase();function gr(e){let t=i=>e.every(s=>Kn(i).includes(s)),o=[];for(let i of d.sections){let s=t(`${i.title} ${i.description} ${i.key||""}`),a=p=>s||t(`${p.label} ${p.help||""} ${p.path.join(".")}`),u=i.fields.filter(p=>!p.managed&&a(p)),m=(i.env||[]).filter(p=>t(`${p.name} ${p.purpose}`)),l=!!i.widget&&(s||i.fields.some(p=>p.managed&&a(p)));(u.length||m.length||l)&&o.push({id:i.id,sec:i,fields:u,env:m,widget:l})}let r=(d.schema.environment||[]).map(i=>({title:i.title,vars:i.vars.filter(s=>t(`${s.name} ${s.purpose} ${i.title}`))})).filter(i=>i.vars.length);return{results:o,envGroups:r}}function br(){let e=Kn(d.query).split(/\s+/).filter(Boolean),{results:t,envGroups:o}=gr(e),r=t.reduce((i,s)=>i+s.fields.length+s.env.length+(s.widget?1:0),0)+o.reduce((i,s)=>i+s.vars.length,0);return r?n(x,{children:[n(ke,{code:d.query,children:[r," ",r===1?"setting matches":"settings match"]}),t.map(({id:i,sec:s,fields:a,env:u,widget:m})=>n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:ee[i]||"box",class:"size-4"}),s.title,n("a",{href:`#${i}`,onClick:l=>{l.preventDefault(),D(i)},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),m?n("div",{class:"mt-4",children:n(Jn,{sec:s})}):null,a.map(l=>n(I,{field:l},l.path.join("."))),u.length?n("details",{class:"group mt-5",open:!0,children:[n("summary",{class:"focus-ring flex min-h-11 cursor-pointer list-none items-center gap-2 font-medium text-muted [&::-webkit-details-marker]:hidden",children:[n(_,{name:"chevronRight",class:"size-4 transition-transform group-open:rotate-90"}),"Set in .env"]}),n(Qe,{vars:u})]}):null]},i)),o.length?n("section",{class:"mt-8",children:[n("h2",{class:"eyebrow m-0 flex items-center gap-2",children:[n(_,{name:"terminal",class:"size-4"}),q.environment.title,n("a",{href:"#environment",onClick:i=>{i.preventDefault(),D("environment")},class:"focus-ring ml-auto text-xs font-medium tracking-normal text-brand normal-case hover:underline",children:"Open page"})]}),o.map(i=>n("div",{children:[n("h3",{class:"mt-4 mb-1.5 text-sm font-semibold text-ink",children:i.title}),i.note?n("p",{class:"mb-1.5 text-xs text-faint",children:i.note}):null,n(Qe,{vars:i.vars})]},i.title))]}):null]}):n(x,{children:[n(ke,{children:"No matches"}),n("p",{class:"mt-8 text-muted",children:['Nothing matches "',d.query,'". Try a shorter word, or press Esc to go back.']})]})}function Yn(){if(d.query.trim())return n(br,{});if(d.page==="get-running")return n(Vn,{});if(d.page==="environment")return n(_r,{});let e=O(d.page);return e?n(hr,{sec:e}):n(Vn,{})}function xr(){let e=d.path.split(/[\\/]/).pop()||"10xgraph.json",t=d.path.slice(0,d.path.length-e.length),o=null;return d.schema?d.exists?W()?o=n(ie,{class:"bg-warn-soft text-warn",children:"Unsaved"}):o=n(ie,{class:"bg-ok-soft text-ok",children:"Saved"}):o=n(ie,{class:"bg-brand-soft text-brand",children:"New"}):o=null,n("div",{class:"flex min-w-0 flex-1 items-center gap-2",title:d.path||void 0,children:[n(_,{name:"file",class:"size-4 text-faint"}),n("span",{class:"truncate font-mono text-[13px] font-medium text-ink",children:e}),o,t?n("span",{class:"hidden min-w-0 truncate font-mono text-xs text-faint lg:block",dir:"rtl",children:n("bdi",{children:t})}):null]})}function Xn(){let e=!!d.schema&&!d.fatal,t=d.sideOpen?"Hide sidebar":"Show sidebar";return n("header",{class:"sticky top-0 z-40 flex h-14 items-center gap-2 border-b border-line bg-surface px-3 sm:gap-3 sm:px-4",children:[n("button",{type:"button",class:"icon-btn","aria-controls":"side","aria-expanded":d.sideOpen?"true":"false","aria-label":t,title:`${t} (Ctrl/Cmd+B)`,onClick:()=>se(!d.sideOpen),children:n(_,{name:"panel",class:"size-[18px]"})}),n("div",{class:"flex shrink-0 items-center gap-2.5",children:[n("svg",{viewBox:"0 0 48 48",class:"size-7 shrink-0 text-ink","aria-hidden":"true",children:[n("rect",{x:"4",y:"4",width:"40",height:"40",rx:"11",fill:"none",stroke:"currentColor","stroke-width":"4.4"}),n("path",{d:"M13 33L21 23L34 15L34 31L21 23",fill:"none",stroke:"currentColor","stroke-width":"3","stroke-linecap":"round","stroke-linejoin":"round"}),n("g",{fill:"currentColor",children:[n("circle",{cx:"21",cy:"23",r:"4"}),n("circle",{cx:"34",cy:"15",r:"4"}),n("circle",{cx:"34",cy:"31",r:"4"})]}),n("circle",{cx:"13",cy:"33",r:"4.6",class:"fill-brand-amber"})]}),n("div",{class:"hidden items-baseline gap-1.5 sm:flex",children:[n("span",{class:"text-[15px] font-semibold tracking-tight text-ink",children:"10xGraph"}),n("span",{class:"text-[15px] text-faint",children:"Config"})]})]}),n("span",{class:"hidden h-6 w-px shrink-0 bg-line md:block","aria-hidden":"true"}),n(xr,{}),n("button",{type:"button",class:"btn btn-ghost","aria-label":"View 10xgraph.json","aria-haspopup":"dialog",disabled:!e,onClick:()=>kn(),children:[n(_,{name:"code",class:"size-[18px]"}),n("span",{class:"hidden md:inline",children:"View 10xgraph.json"})]}),n("button",{type:"button",class:"btn btn-primary",onClick:He,disabled:!e||!!d.busy||d.exists&&!W()&&!d.parseError,children:[d.busy==="save"?n(_,{name:"spinner",class:"size-4 animate-spin"}):null,d.exists?"Save":"Create file",n("span",{class:"kbd hidden lg:inline-flex",children:"Ctrl S"})]})]})}function Qn(){return d.fatal?n("div",{class:"panel mt-10",children:n(Tn,{icon:"xCircle",tint:"text-danger",title:d.fatal.title,text:d.fatal.text})}):d.schema?n(x,{children:[d.parseError?n("div",{class:"mb-6",children:n(ve,{kind:"warn",title:"The existing file is not valid JSON",lines:[`${d.parseError}. The form starts from defaults; saving replaces the file and keeps the old one as 10xgraph.json.bak.`]})}):null,n(Yn,{})]}):n("div",{class:"animate-pulse space-y-4 pt-2","aria-label":"Loading",children:[n("div",{class:"h-7 w-48 rounded-md bg-raised"}),n("div",{class:"h-4 w-80 max-w-full rounded-md bg-raised"}),[0,1,2].map(e=>n("div",{class:"h-16 rounded-xl bg-raised"},e))]})}function vr({id:e}){let t=!!O(e)&&pn(O(e)),o=mn(e);return!t&&!o?null:n("span",{class:"flex shrink-0 gap-1",children:[t?n("span",{class:"size-2 rounded-full bg-brand",title:O(e).self_managed?"Configured":"Changed from default"}):null,o?n("span",{class:`size-2 rounded-full ${o.level==="error"?"bg-danger":"bg-warn"}`,title:o.level==="error"?"Needs attention":"Has a warning"}):null]})}function yr({id:e}){let t=ge(e);if(!t)return null;let o=!d.query&&d.page===e;return n("a",{href:`#${e}`,"aria-current":o?"page":void 0,onClick:r=>{r.preventDefault(),D(e)},ref:r=>{r&&o&&window.innerWidth<1024&&r.scrollIntoView({block:"nearest",inline:"nearest"})},class:`focus-ring flex min-h-10 items-center gap-2.5 rounded-lg px-3 py-1.5 font-medium transition-colors duration-150 max-lg:min-h-11 max-lg:shrink-0 max-lg:whitespace-nowrap ${o?"bg-brand-soft text-ink":"text-muted hover:bg-raised hover:text-ink"}`,children:[n(_,{name:ee[e]||"box",class:`size-[18px] ${o?"text-brand":""}`}),n("span",{class:"min-w-0 flex-1",children:[n("span",{class:"block truncate",children:t.title}),t.subtitle?n("span",{class:"block truncate text-xs font-normal text-faint max-lg:hidden",children:t.subtitle}):null]}),n(vr,{id:e})]})}function Zn(){return d.sideRender?n("aside",{id:"side","aria-label":"Settings navigation",class:`contents lg:sticky lg:top-14 lg:block lg:h-[calc(100dvh-3.5rem)] lg:overflow-y-auto lg:border-r lg:border-line lg:bg-surface lg:px-3 lg:pt-4 lg:pb-16 lg:transition-[transform,opacity] lg:duration-200 lg:ease-out ${d.sideShown?"lg:translate-x-0 lg:opacity-100":"lg:-translate-x-6 lg:opacity-0"}`,inert:!d.sideOpen,children:[n("div",{role:"search",class:"relative mx-1 mb-3 max-lg:mx-4 max-lg:mt-3",children:[n(_,{name:"search",class:"pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2 text-faint"}),n("label",{for:"q",class:"sr-only",children:"Search settings"}),n("input",{id:"q",type:"search",value:d.query,autocomplete:"off",spellcheck:!1,placeholder:"Search settings ( / )",class:"input pl-10",onInput:e=>Fe(e.currentTarget.value)})]}),n("nav",{"aria-label":"Pages",class:"max-lg:sticky max-lg:top-14 max-lg:z-20 max-lg:mt-3 max-lg:flex max-lg:gap-0.5 max-lg:overflow-x-auto max-lg:border-b max-lg:border-line max-lg:bg-surface max-lg:px-3 max-lg:py-1.5",children:d.nav.map(e=>n("div",{class:"max-lg:contents",children:[e.label?n("div",{class:"mt-4 mb-1 px-3 text-[11px] font-semibold tracking-wider text-faint uppercase max-lg:hidden",children:e.label}):null,e.items.map(t=>n(yr,{id:t},t))]},e.label||"top"))})]}):null}var kr={success:["checkCircle","text-ok"],warning:["alert","text-warn"],error:["xCircle","text-danger"]};function wr(){return n("div",{class:"pointer-events-none fixed right-4 bottom-4 left-4 z-50 flex flex-col items-end gap-2 sm:left-auto","aria-live":"polite",children:d.toasts.map(e=>{let[t,o]=kr[e.kind];return n("div",{role:e.kind==="error"?"alert":"status",class:"pointer-events-auto flex w-full max-w-sm items-start gap-3 rounded-xl border border-line bg-surface px-4 py-3 text-sm shadow-pop",children:[n(_,{name:t,class:`mt-0.5 size-4 ${o}`}),n("p",{class:"min-w-0 flex-1 leading-5 text-body",children:e.message}),e.action?n("button",{type:"button",class:"shrink-0 font-medium text-brand hover:underline",onClick:()=>{qe(e.id),e.action.run()},children:e.action.label}):null,n("button",{type:"button",class:"-mr-1 shrink-0 cursor-pointer rounded text-faint hover:text-ink","aria-label":"Dismiss",onClick:()=>qe(e.id),children:n(_,{name:"x"})})]},e.id)})})}var Cr=()=>/^(INPUT|TEXTAREA|SELECT)$/.test(document.activeElement?.tagName)||document.activeElement?.isContentEditable;function Sr(){let[,e]=dt(o=>o+1,0);U(()=>{dn(e);try{localStorage.getItem("tg-sidebar")==="hidden"&&se(!1,{instant:!0})}catch{}let o=s=>{let a=s.ctrlKey||s.metaKey,u=s.key.toLowerCase();a&&u==="s"?(s.preventDefault(),He()):a&&u==="b"?(s.preventDefault(),se(!d.sideOpen)):a&&u==="k"?(s.preventDefault(),gt()):s.key==="/"&&!a&&!s.altKey&&!Cr()&&!d.drawerOpen?(s.preventDefault(),gt()):s.key==="Escape"&&d.query&&!d.drawerOpen&&Fe("")},r=s=>{d.schema&&W()&&s.preventDefault()},i=()=>ze();return document.addEventListener("keydown",o),window.addEventListener("beforeunload",r),window.addEventListener("hashchange",i),xn(),()=>{document.removeEventListener("keydown",o),window.removeEventListener("beforeunload",r),window.removeEventListener("hashchange",i)}},[]),U(()=>{if(!d.schema||d.validated===L())return;let o=setTimeout(vn,400);return()=>clearTimeout(o)});let t=d.sideRender;return n(x,{children:[n("a",{href:"#main",class:"sr-only z-50 rounded-md bg-brand-solid px-3 py-2 text-white focus:not-sr-only focus:fixed focus:top-3 focus:left-3",children:"Skip to content"}),n(Xn,{}),n("div",{class:t?"lg:grid lg:grid-cols-[260px_minmax(0,1fr)] lg:items-start":"",children:[n(Zn,{}),n("main",{id:"main",tabindex:-1,class:"min-w-0 px-4 pt-6 pb-24 focus:outline-none sm:px-8 sm:pt-8",children:n("div",{class:"mx-auto w-full max-w-[760px]",children:n(Qn,{})})})]}),n($n,{}),n("div",{class:"sr-only",role:"status","aria-live":"polite",children:d.copied?"Copied":""}),n(wr,{})]})}Vt(n(Sr,{}),document.getElementById("app")); diff --git a/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md b/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md index 898b5fa1..63d25f0c 100644 --- a/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +++ b/tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md @@ -28,6 +28,13 @@ Publishers receive structured events from graph/tool execution. Use them for tra to stdout by default; pass `ConsolePublisher({"use_logger": True})` to route events through the `tenxgraph.publisher` logger instead of stdout. +Publisher kinds: + +- `BasePublisher.kind` is `"events"` by default (type `PublisherKind = Literal["events", "observability"]`). `OtelPublisher` sets `"observability"`, so `LogfirePublisher`, `LangsmithPublisher` and `PhoenixPublisher` have it too. A custom tracing backend sets `kind = "observability"`. +- `publish_event(event, only="events")` sends to event publishers only; `only="observability"` to tracing publishers only; `None` (default) to all. It works inside a `CompositePublisher`, also nested. +- Event publishers (Kafka, Redis, RabbitMQ, console, custom) are bound in code; there is no `10xgraph.json` key. Tracing publishers come from the `observability` block. `metrics` never goes through publishers. +- Routing: graph, node, LLM and tool events (AI errors included) go to event publishers and tracing, plus logs and LLM metrics. Task lifecycle events (queued, started, finished, saved, cancelled) go to event publishers only (`only="events"`), plus task metrics and logs. A task failure is not published: ERROR log and `tenxgraph.task.runs{status="failure"}`. API HTTP errors are not published: HTTP metrics and logs. + Realtime adds an `Event.REALTIME` event source and a `ContentType.TRANSCRIPT` content type (see `realtime.md`). diff --git a/tenxgraph_api/jobs/monitoring.py b/tenxgraph_api/jobs/monitoring.py index ff54beec..b59ff7fb 100644 --- a/tenxgraph_api/jobs/monitoring.py +++ b/tenxgraph_api/jobs/monitoring.py @@ -1,6 +1,9 @@ -"""``MonitoringMiddleware``: log every TaskIQ hook and send the same as core publisher events -when a ``BasePublisher`` is bound in the container. It also records two metrics through core's -metrics facade (exported by the ``metrics`` block of 10xgraph.json, or the legacy OTEL path): +"""``MonitoringMiddleware``: log every TaskIQ hook and send the lifecycle steps as core publisher +events when a ``BasePublisher`` is bound in the container. Events go to event publishers only +(``only="events"``), never to AI tracing publishers, which would take a task for a graph run. +A task failure is not published as an event: it is logged at ERROR and counted. It also +records two metrics through core's metrics facade (exported by the ``metrics`` block of +10xgraph.json, or the legacy OTEL path): ``tenxgraph.task.runs`` (attributes ``task``, ``status`` = queued, success, failure, cancelled) and ``tenxgraph.task.duration`` (attribute ``task``). Never raises.""" @@ -17,22 +20,21 @@ logger = logging.getLogger("tenxgraph_api.jobs") -MESSAGE_LIMIT = 500 RUNS_METRIC = "tenxgraph.task.runs" DURATION_METRIC = "tenxgraph.task.duration" class MonitoringMiddleware(TaskiqMiddleware): - """Task lifecycle logs and events. Core has no task event kind yet, so events use - ``Event.GRAPH_EXECUTION`` with ``metadata["source"] == "task"`` and the hook name in - ``metadata["phase"]``. + """Task lifecycle logs and events (event publishers only). Core has no task event kind yet, + so events use ``Event.GRAPH_EXECUTION`` with ``metadata["source"] == "task"`` and the hook + name in ``metadata["phase"]``. | hook | event type | phase | |---|---|---| | pre_send | none | | | post_send | UPDATE | queued | | pre_execute | START | started | - | on_error | ERROR (INTERRUPTED for ``TaskCancelled``) | failed (cancelled) | + | on_error | none for a failure (log, metric); INTERRUPTED for ``TaskCancelled`` | cancelled | | post_execute | END (success only) | finished | | post_save | RESULT | saved | """ @@ -84,13 +86,6 @@ def on_error( exc_info=exception, ) self._count(message, "failure") - data = { - "error": type(exception).__name__, - "message": str(exception)[:MESSAGE_LIMIT], - "duration": took, - "attempt": attempt, - } - self._publish(message, "error", "failed", data, is_error=True) def post_execute(self, message: TaskiqMessage, result: TaskiqResult[Any]) -> None: self._duration(message, result.execution_time) @@ -137,7 +132,6 @@ def _publish( event_type: str, phase: str, data: dict[str, Any] | None = None, - is_error: bool = False, ) -> None: try: from tenxgraph.runtime.publisher.base_publisher import BasePublisher @@ -156,10 +150,9 @@ def _publish( node_name=message.task_name, run_id=message.task_id, user_id=state.parse_config(message.labels).get("user_id"), - is_error=is_error, data=data or {}, metadata={"source": "task", "task_name": message.task_name, "phase": phase}, ) - publish_event(event, publisher=publisher, task_manager=manager) + publish_event(event, publisher=publisher, task_manager=manager, only="events") except Exception: logger.debug("Task event not published", exc_info=True) diff --git a/tests/unit_tests/jobs/test_middleware.py b/tests/unit_tests/jobs/test_middleware.py index 1c6d206a..0f16167e 100644 --- a/tests/unit_tests/jobs/test_middleware.py +++ b/tests/unit_tests/jobs/test_middleware.py @@ -171,6 +171,22 @@ async def close(self): ... def sync_close(self): ... +@pytest.fixture(autouse=True) +def only_calls(monkeypatch): + """Record the ``only`` argument of each publish_event call. The installed core may predate + ``only``, so the stand-in drops it before calling the real function.""" + from tenxgraph.runtime.publisher import publish + + real, seen = publish.publish_event, [] + + def fake(event, publisher, task_manager, only=None): + seen.append(only) + real(event, publisher=publisher, task_manager=task_manager) + + monkeypatch.setattr(publish, "publish_event", fake) + return seen + + @pytest_asyncio.fixture async def monitored(): env = Env(InMemoryCacheStore(), MonitoringMiddleware(), TaskMiddleware()) @@ -181,7 +197,7 @@ async def monitored(): await env.broker.shutdown() -async def test_monitoring_logs_and_publishes(monitored, caplog): +async def test_monitoring_logs_and_publishes(monitored, caplog, only_calls): env, manager = monitored publisher = FakePublisher() env.container.bind_instance(BasePublisher, publisher) @@ -210,7 +226,6 @@ async def run(task, *args) -> list[tuple[str, str]]: ok_events = list(publisher.events) caplog.clear() failed = await run(boom) - failed_events = list(publisher.events) failed_records = list(caplog.records) caplog.clear() cancelled = await run(stopped) @@ -225,16 +240,9 @@ async def run(task, *args) -> list[tuple[str, str]]: ] assert ok_events[2].data["duration"] >= 0 assert "return_value" not in ok_events[3].data - assert failed == [ - ("queued", "update"), - ("started", "start"), - ("failed", "error"), - ("saved", "result"), - ] - error = failed_events[2] - assert error.is_error and error.data["error"] == "ValueError" - assert error.data["message"].startswith("bad x") and len(error.data["message"]) == 500 - assert error.data["attempt"] == 1 and "duration" in error.data + assert only_calls and set(only_calls) == {"events"} + # A failure is logged and counted but not published (no "failed" event). + assert failed == [("queued", "update"), ("started", "start"), ("saved", "result")] logged = [ r for r in failed_records if r.levelno == logging.ERROR and r.name == "tenxgraph_api.jobs" ] @@ -255,7 +263,7 @@ async def run(task, *args) -> list[tuple[str, str]]: assert any("cancelled" in r.getMessage() for r in cancelled_records) -async def test_monitoring_counts_retries(): +async def test_monitoring_counts_retries(caplog): env = Env( InMemoryCacheStore(), SimpleRetryMiddleware(default_retry_count=3), MonitoringMiddleware() ) @@ -269,11 +277,17 @@ async def test_monitoring_counts_retries(): async def flaky() -> str: raise RuntimeError("again") + caplog.set_level(logging.ERROR, logger="tenxgraph_api.jobs") handle = await kiq_with_id(flaky) await handle.wait_result(timeout=5) await asyncio.sleep(0.3) # retries are re-sent in the background await manager.wait_for_all(timeout=5) - attempts = [e.data["attempt"] for e in publisher.events if e.metadata["phase"] == "failed"] + assert not [e for e in publisher.events if e.is_error] + attempts = [ + int(r.getMessage().split("failed on attempt ")[1].split()[0]) + for r in caplog.records + if "failed on attempt" in r.getMessage() + ] assert attempts == [1, 2, 3] await env.broker.shutdown() From c5e11d3f278ff7d59ba1c96a1cddf8da88a4cc81 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Tue, 10 Nov 2026 00:48:19 +0600 Subject: [PATCH 5/9] feat(checkpointer): Update checkpointer setup to use InMemoryCheckpointer and InjectQ for binding --- tenxgraph_api/src/app/main.py | 6 +++++- tests/unit_tests/test_group3a_storage.py | 21 +++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/tenxgraph_api/src/app/main.py b/tenxgraph_api/src/app/main.py index e1992107..a74d0523 100644 --- a/tenxgraph_api/src/app/main.py +++ b/tenxgraph_api/src/app/main.py @@ -99,7 +99,11 @@ async def _prepare_checkpointer() -> None: missing permission shows in the boot log instead of on the first request. Best effort: the checkpointer also creates its schema on its first query, so a failure here only logs. """ - checkpointer = container.try_get("checkpointer") or container.try_get("BaseCheckpointer") + from tenxgraph_api.src.app.routers.ping.readiness import readiness_targets + + # The graph binds its checkpointer under the BaseCheckpointer class (never a string + # key); the same lookup the startup check and /health/ready use. + checkpointer = readiness_targets(container)[0] asetup = getattr(checkpointer, "asetup", None) if not callable(asetup): return diff --git a/tests/unit_tests/test_group3a_storage.py b/tests/unit_tests/test_group3a_storage.py index fd87def4..b7326a8c 100644 --- a/tests/unit_tests/test_group3a_storage.py +++ b/tests/unit_tests/test_group3a_storage.py @@ -262,10 +262,15 @@ async def test_message_media_still_in_state_through_another_message_is_kept(monk @pytest.mark.asyncio async def test_boot_runs_the_checkpointer_setup(monkeypatch): - checkpointer = MagicMock() + # A real container, bound the way StateGraph.compile() binds it (the class, not a string + # key): a mocked "checkpointer" key hid that boot never found the graph's checkpointer. + from injectq import InjectQ + from tenxgraph.storage.checkpointer import BaseCheckpointer, InMemoryCheckpointer + + checkpointer = InMemoryCheckpointer() checkpointer.asetup = AsyncMock(return_value=True) - container = MagicMock() - container.try_get.side_effect = lambda name: checkpointer if name == "checkpointer" else None + container = InjectQ() + container.bind_instance(BaseCheckpointer, checkpointer, allow_concrete=True) monkeypatch.setattr(app_main, "container", container) await app_main._prepare_checkpointer() checkpointer.asetup.assert_awaited_once() @@ -273,12 +278,16 @@ async def test_boot_runs_the_checkpointer_setup(monkeypatch): @pytest.mark.asyncio async def test_boot_survives_a_failing_setup(monkeypatch): - checkpointer = MagicMock() + from injectq import InjectQ + from tenxgraph.storage.checkpointer import BaseCheckpointer, InMemoryCheckpointer + + checkpointer = InMemoryCheckpointer() checkpointer.asetup = AsyncMock(side_effect=ConnectionError("postgres down")) - container = MagicMock() - container.try_get.return_value = checkpointer + container = InjectQ() + container.bind_instance(BaseCheckpointer, checkpointer, allow_concrete=True) monkeypatch.setattr(app_main, "container", container) await app_main._prepare_checkpointer() # logs, does not raise + checkpointer.asetup.assert_awaited_once() @pytest.mark.asyncio From 566cad93ec5730f1f437700013fdfe6e65052989 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Tue, 10 Nov 2026 01:04:11 +0600 Subject: [PATCH 6/9] feat(init): Add background worker support with configuration and prompts --- tenxgraph_api/cli/commands/init.py | 22 ++- tenxgraph_api/cli/commands/init_context.py | 47 ++++- tenxgraph_api/cli/commands/init_prompts.py | 14 ++ tenxgraph_api/cli/main.py | 6 + tenxgraph_api/cli/templates/prod/.env.example | 10 +- .../cli/templates/prod/worker/__init__.py | 0 .../cli/templates/prod/worker/tasks.py | 45 +++++ tests/cli/test_init_prod.py | 3 +- tests/cli/test_init_worker.py | 183 ++++++++++++++++++ 9 files changed, 325 insertions(+), 5 deletions(-) create mode 100644 tenxgraph_api/cli/templates/prod/worker/__init__.py create mode 100644 tenxgraph_api/cli/templates/prod/worker/tasks.py create mode 100644 tests/cli/test_init_worker.py diff --git a/tenxgraph_api/cli/commands/init.py b/tenxgraph_api/cli/commands/init.py index 8e653995..b7e1becb 100644 --- a/tenxgraph_api/cli/commands/init.py +++ b/tenxgraph_api/cli/commands/init.py @@ -11,6 +11,7 @@ DEFAULT_PROVIDER, PROVIDERS, STORAGE_PRESETS, + WORKERS, build_config, build_context, check_agent_name, @@ -34,6 +35,7 @@ _SKIP_DIRS = {"__pycache__", ".ruff_cache"} _AUTH_DIR = "auth" _OPTIONAL_DIRS = ("evals", "tests") +_WORKER_DIR = "worker" _CONFIG_HINT = "10xgraph config for more settings." @@ -79,6 +81,7 @@ def execute( # noqa: PLR0913 tests: bool | None = None, ag_ui: bool | None = None, otel: bool | None = None, + worker: str | None = None, yes: bool = False, non_interactive: bool = False, dry_run: bool = False, @@ -107,6 +110,7 @@ def execute( # noqa: PLR0913 tests=tests, ag_ui=ag_ui, otel=otel, + worker=worker, ) else: context = self._prompt_user() @@ -235,9 +239,17 @@ def _print_completion(self, context: dict, base_path: Path, is_prod: bool) -> No ] before_production: list[str] = [] else: - if storage.startswith("postgres"): - key_step += " (and DATABASE_URL, REDIS_URL)" + worker = context.get("worker", "none") + env_keys = ["DATABASE_URL"] if storage.startswith("postgres") else [] + if storage.startswith("postgres") or worker != "none": + env_keys.append("REDIS_URL") + if worker in ("rabbitmq", "kafka"): + env_keys.append("BROKER_URL") + if env_keys: + key_step += f" (and {', '.join(env_keys)})" steps = [f"cd {base_path}", "pip install -e .", f"{key_step}.", "10xgraph dev"] + if worker != "none": + steps += ["10xgraph worker", "10xgraph scheduler (run exactly one)"] secret = " and a strong JWT_SECRET_KEY" if context["auth"] == "jwt" else "" before_production = [ "10xgraph config to review auth, rate limit, CORS origins and the other " @@ -297,6 +309,10 @@ def _print_summary(self, context: dict) -> None: if auth != "none": revocation = "On" if context.get("token_revocation") else "Off" rows.append(("Token revocation", revocation)) + worker = context.get("worker", "none") + rows.append( + ("Background tasks", WORKERS[worker]["label"] if worker != "none" else "None") + ) rows.append(("Evaluations", "On" if context.get("evals") else "Off")) rows.append(("Tests", "On" if context.get("tests") else "Off")) for key, label in (("ag_ui", "AG-UI"), ("otel", "OpenTelemetry")): @@ -323,6 +339,8 @@ def _should_skip(self, src: Path, template_dir: Path, context: dict, is_prod: bo top = src.relative_to(template_dir).parts[0] if top == _AUTH_DIR: return context["auth"] != "custom" + if top == _WORKER_DIR: + return context.get("worker", "none") == "none" if top in _OPTIONAL_DIRS: return not context.get(top) return False diff --git a/tenxgraph_api/cli/commands/init_context.py b/tenxgraph_api/cli/commands/init_context.py index c41c699c..cf1744d9 100644 --- a/tenxgraph_api/cli/commands/init_context.py +++ b/tenxgraph_api/cli/commands/init_context.py @@ -45,6 +45,22 @@ "postgres-memory": {"label": "Postgres + Redis + pgvector memory", "cache_store": "redis"}, } AUTH_MODES = ("none", "jwt", "custom") +# Background worker choices: label, broker type in 10xgraph.json, broker URL, API extra. +WORKERS: dict[str, dict[str, str]] = { + "redis": {"label": "Redis", "type": "redis_stream", "url": "${REDIS_URL}", "extra": "jobs"}, + "rabbitmq": { + "label": "RabbitMQ", + "type": "rabbitmq", + "url": "${BROKER_URL}", + "extra": "jobs-rabbitmq", + }, + "kafka": { + "label": "Kafka", + "type": "kafka", + "url": "${BROKER_URL}", + "extra": "jobs-kafka", + }, +} _MODE_ALIASES = {"quick-start": "quick", "production": "full"} _MARKER = re.compile( @@ -107,6 +123,7 @@ def build_context( # noqa: PLR0913 tests: bool | None = None, ag_ui: bool | None = None, otel: bool | None = None, + worker: str | None = None, ) -> dict[str, Any]: """Resolve a complete, reproducible scaffold recipe without prompting.""" resolved_mode = _resolve_mode(mode, template) @@ -138,6 +155,7 @@ def build_context( # noqa: PLR0913 "--tests": tests, "--ag-ui": ag_ui, "--otel": otel, + "--worker": worker not in (None, "none"), } used = [flag for flag, value in full_only.items() if value not in (None, False)] if used: @@ -154,17 +172,25 @@ def build_context( # noqa: PLR0913 "tests": False, "ag_ui": False, "otel": False, + "worker": "none", } resolved_storage = _choice(storage, "memory", STORAGE_PRESETS, "storage", "storage") resolved_auth = _choice(auth, "jwt", AUTH_MODES, "auth", "auth mode") + resolved_worker = _choice(worker, "none", ("none", *WORKERS), "worker", "worker") resolved_cache = _choice( cache_store, - STORAGE_PRESETS[resolved_storage]["cache_store"], + "redis" if resolved_worker != "none" else STORAGE_PRESETS[resolved_storage]["cache_store"], CACHE_STORES, "cache_store", "cache store", ) + if resolved_worker != "none" and resolved_cache == "memory": + raise ValidationError( + "A background worker needs --cache-store redis: task status and cancel live in " + "the cache store, and a memory store is private to each process.", + field="cache_store", + ) auth_on = resolved_auth != "none" return { **base, @@ -184,6 +210,7 @@ def build_context( # noqa: PLR0913 "tests": tests is not False, "ag_ui": bool(ag_ui), "otel": bool(otel), + "worker": resolved_worker, } @@ -240,6 +267,13 @@ def build_config(context: dict, is_prod: bool) -> dict: cache_store["token_revocation"] = True config["cache_store"] = cache_store + worker = WORKERS.get(context.get("worker", "none")) + if worker: + config["worker"] = { + "broker": {"type": worker["type"], "url": worker["url"]}, + "tasks": ["worker.tasks"], + } + if context.get("tests"): config["test"] = {"path": "tests", "coverage": True, "coverage_threshold": 70} if context.get("evals"): @@ -261,6 +295,7 @@ def template_flags(context: dict[str, Any]) -> dict[str, bool]: provider = context.get("provider", DEFAULT_PROVIDER) postgres = storage.startswith("postgres") store = storage == "postgres-memory" + worker = context.get("worker", "none") return { "jwt": context.get("auth") == "jwt", "custom_auth": context.get("auth") == "custom", @@ -275,6 +310,8 @@ def template_flags(context: dict[str, Any]) -> dict[str, bool]: "otel": bool(context.get("otel")), "websocket": bool(context.get("websocket")), "ag_ui": bool(context.get("ag_ui")), + "worker": worker != "none", + "broker_url": worker in ("rabbitmq", "kafka"), } @@ -341,7 +378,15 @@ def template_vars(context: dict[str, Any]) -> dict[str, Any]: ) if wanted ] + if flags["worker"]: + api_extras.append(WORKERS[context["worker"]]["extra"]) + broker_example = ( + "amqp://guest:guest@localhost:5672/" + if context.get("worker") == "rabbitmq" + else "localhost:9092" + ) return { + "broker_url_example": broker_example, "llm_provider": context.get("provider", DEFAULT_PROVIDER), "llm_model": provider["model"], "llm_sim_model": provider["sim_model"], diff --git a/tenxgraph_api/cli/commands/init_prompts.py b/tenxgraph_api/cli/commands/init_prompts.py index 56927827..ae1ebaff 100644 --- a/tenxgraph_api/cli/commands/init_prompts.py +++ b/tenxgraph_api/cli/commands/init_prompts.py @@ -80,6 +80,19 @@ def ask_questions( # noqa: PLR0911 if auth is None: return None + worker = prompts.select( + "Background tasks?", + [ + Choice("none", "None", "No worker"), + Choice("redis", "Redis", "Redis Streams broker, results in Redis"), + Choice("rabbitmq", "RabbitMQ", "AMQP broker, results in Redis"), + Choice("kafka", "Kafka", "At-most-once delivery, results in Redis"), + ], + default="none", + ) + if worker is None: + return None + extras = prompts.checkbox("Which extras?", _EXTRAS) if extras is None: return None @@ -88,5 +101,6 @@ def ask_questions( # noqa: PLR0911 **answers, "storage": storage, "auth": "none" if auth == "open" else auth, + "worker": worker, **{choice.value: choice.value in extras for choice in _EXTRAS}, } diff --git a/tenxgraph_api/cli/main.py b/tenxgraph_api/cli/main.py index 8fa3b6fa..498be6eb 100644 --- a/tenxgraph_api/cli/main.py +++ b/tenxgraph_api/cli/main.py @@ -601,6 +601,11 @@ def init( # noqa: PLR0913 "--otel/--no-otel", help="Full mode: add OpenTelemetry (default off).", ), + worker: str | None = typer.Option( + None, + "--worker", + help="Full mode background worker: none (default), redis, rabbitmq, or kafka.", + ), yes: bool = typer.Option( False, "--yes", @@ -656,6 +661,7 @@ def init( # noqa: PLR0913 tests=tests, ag_ui=ag_ui, otel=otel, + worker=worker, yes=yes, non_interactive=non_interactive, dry_run=dry_run, diff --git a/tenxgraph_api/cli/templates/prod/.env.example b/tenxgraph_api/cli/templates/prod/.env.example index 394cca4d..a9cab4db 100644 --- a/tenxgraph_api/cli/templates/prod/.env.example +++ b/tenxgraph_api/cli/templates/prod/.env.example @@ -71,10 +71,18 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/agent" ###### Redis Config ############# ################################# # Hot thread cache for the Postgres checkpointer and the API's cache store (rate limiting, -# run locks and caps, token revocation). Local example value; replace it in production. +# run locks and caps, token revocation), plus background task status and results. Local example value; replace it in production. REDIS_URL="redis://localhost:6379/0" ##[/IF_REDIS]## +##[IF_BROKER_URL]## +################################# +###### Broker Config ############ +################################# +# Message broker for background tasks. Local example value; replace it in production. +BROKER_URL="$broker_url_example" +##[/IF_BROKER_URL]## + ##[IF_JWT]## ################################# ###### JWT Config ############### diff --git a/tenxgraph_api/cli/templates/prod/worker/__init__.py b/tenxgraph_api/cli/templates/prod/worker/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tenxgraph_api/cli/templates/prod/worker/tasks.py b/tenxgraph_api/cli/templates/prod/worker/tasks.py new file mode 100644 index 00000000..c5d56cdf --- /dev/null +++ b/tenxgraph_api/cli/templates/prod/worker/tasks.py @@ -0,0 +1,45 @@ +"""Background tasks for this project. + +Run a worker with `10xgraph worker`. Run `10xgraph scheduler` for the scheduled tasks, and run +exactly one scheduler or each schedule fires once per scheduler. + +Start a task over HTTP: POST /v1/tasks/summarize_thread with {"kwargs": {"thread_id": "..."}}. +Start it from Python: `await summarize_thread.kiq(thread_id="...")`. +""" + +import logging +from typing import Annotated + +from injectq.integrations.taskiq import InjectTaskiq +from taskiq import TaskiqDepends +from taskiq.depends.progress_tracker import ProgressTracker, TaskState +from tenxgraph.core.graph import CompiledGraph +from tenxgraph.core.state import Message +from tenxgraph_api.jobs import TaskContext, broker + + +logger = logging.getLogger(__name__) + + +@broker.task(task_name="summarize_thread", http=True) +async def summarize_thread( + thread_id: str, # set by the caller + config: dict, # filled by the server: user_id, user, task_id + ctx: Annotated[TaskContext, TaskiqDepends()], + tracker: Annotated[ProgressTracker, TaskiqDepends()], + graph: Annotated[CompiledGraph, InjectTaskiq(CompiledGraph)], +) -> dict: + if await ctx.cancelled(): # a running task stops only if it checks + return {"cancelled": True} + await tracker.set_progress(TaskState.STARTED, {"step": "summarizing"}) + await graph.ainvoke( + {"messages": [Message.text_message("Summarize this conversation in two sentences.")]}, + config={**config, "thread_id": thread_id}, + ) + return {"thread_id": thread_id, "task_id": ctx.task_id} + + +@broker.task(schedule=[{"cron": "0 2 * * *"}]) # every day at 02:00, sent by `10xgraph scheduler` +async def nightly_cleanup() -> None: + # Put cleanup work here, for example deleting old threads. + logger.info("nightly cleanup ran") diff --git a/tests/cli/test_init_prod.py b/tests/cli/test_init_prod.py index 7d6c2acb..1f1d285c 100644 --- a/tests/cli/test_init_prod.py +++ b/tests/cli/test_init_prod.py @@ -172,7 +172,7 @@ def test_interactive_quick_asks_three_questions(monkeypatch) -> None: assert "cache_store" not in build_config(context, False) -def test_interactive_full_asks_three_more_questions(monkeypatch) -> None: +def test_interactive_full_asks_four_more_questions(monkeypatch) -> None: context, scripted, _ = _run_prompts(monkeypatch, _FULL) assert scripted.asked == [ "Which setup?", @@ -180,6 +180,7 @@ def test_interactive_full_asks_three_more_questions(monkeypatch) -> None: "Which AI provider?", "Where should state be stored?", "How should the API be secured?", + "Background tasks?", "Which extras?", ] assert context is not None diff --git a/tests/cli/test_init_worker.py b/tests/cli/test_init_worker.py new file mode 100644 index 00000000..9eab5fe9 --- /dev/null +++ b/tests/cli/test_init_worker.py @@ -0,0 +1,183 @@ +"""`10xgraph init` Full mode background worker: question, flag, config, template files.""" + +from __future__ import annotations + +import io +import json +from pathlib import Path + +import pytest + +from tenxgraph_api.cli.commands.init import InitCommand +from tenxgraph_api.cli.commands.init_context import ( + build_config, + build_context, + template_flags, + template_vars, +) +from tenxgraph_api.cli.core.output import OutputFormatter +from tenxgraph_api.cli.exceptions import ValidationError +from tenxgraph_api.jobs.config import WorkerConfig + +from .test_init_prod import _FULL, _run_prompts + + +BROKERS = { + "redis": ("redis_stream", "${REDIS_URL}", "jobs"), + "rabbitmq": ("rabbitmq", "${BROKER_URL}", "jobs-rabbitmq"), + "kafka": ("kafka", "${BROKER_URL}", "jobs-kafka"), +} + + +def _full(**options) -> dict: + return build_context(path=".", agent_name="demo", mode="full", **options) + + +def _init(path: Path, **options) -> int: + return InitCommand(OutputFormatter(stream=io.StringIO())).execute( + path=str(path), agent_name="Demo", non_interactive=True, mode="full", **options + ) + + +# --- prompts ------------------------------------------------------------------------- + + +def test_full_prompt_asks_worker_after_auth_and_defaults_to_none(monkeypatch) -> None: + context, scripted, _ = _run_prompts(monkeypatch, _FULL) + assert scripted.asked[-3:] == [ + "How should the API be secured?", + "Background tasks?", + "Which extras?", + ] + assert context is not None and context["worker"] == "none" + + +def test_full_prompt_picks_a_worker(monkeypatch) -> None: + context, _, _ = _run_prompts(monkeypatch, {**_FULL, "Background tasks?": "rabbitmq"}) + assert context is not None + assert context["worker"] == "rabbitmq" and context["cache_store"] == "redis" + + +def test_cancel_at_the_worker_question_returns_none(monkeypatch) -> None: + context, _, _ = _run_prompts(monkeypatch, {**_FULL, "Background tasks?": None}) + assert context is None + + +def test_quick_prompt_does_not_ask_about_workers(monkeypatch) -> None: + context, scripted, _ = _run_prompts(monkeypatch) + assert "Background tasks?" not in scripted.asked + assert context is not None and context["worker"] == "none" + + +# --- flag ---------------------------------------------------------------------------- + + +def test_worker_flag_in_quick_mode_is_a_full_only_error() -> None: + with pytest.raises(ValidationError, match="--worker requires --mode full"): + build_context(path=".", agent_name="demo", mode="quick", worker="redis") + assert ( + build_context(path=".", agent_name="demo", mode="quick", worker="none")["worker"] == "none" + ) + + +def test_invalid_worker_is_rejected() -> None: + with pytest.raises(ValidationError, match="Invalid worker"): + _full(worker="sqs") + + +def test_worker_with_memory_cache_store_is_an_error() -> None: + with pytest.raises(ValidationError, match="cache-store redis"): + _full(worker="redis", cache_store="memory") + + +def test_worker_forces_the_redis_cache_store() -> None: + context = _full(worker="kafka", storage="memory") + assert context["cache_store"] == "redis" + assert template_flags(context)["redis"] + + +# --- config, flags, extras ----------------------------------------------------------- + + +@pytest.mark.parametrize("worker", list(BROKERS)) +def test_build_config_has_the_worker_section(worker: str) -> None: + broker_type, url, _ = BROKERS[worker] + config = build_config(_full(worker=worker), True) + assert config["worker"] == { + "broker": {"type": broker_type, "url": url}, + "tasks": ["worker.tasks"], + } + WorkerConfig.model_validate(config["worker"]) + + +def test_no_worker_section_for_none() -> None: + assert "worker" not in build_config(_full(), True) + assert "worker" not in build_config(_full(worker="none"), True) + + +@pytest.mark.parametrize("worker", list(BROKERS)) +def test_pyproject_extra_and_flags(worker: str) -> None: + context = _full(worker=worker) + flags = template_flags(context) + assert flags["worker"] and flags["broker_url"] is (worker != "redis") + assert f"{BROKERS[worker][2]}" in template_vars(context)["api_requirement"].split("[")[1] + + +def test_no_worker_flags_by_default() -> None: + flags = template_flags(_full()) + assert not flags["worker"] and not flags["broker_url"] + assert "jobs" not in template_vars(_full())["api_requirement"] + + +# --- scaffold ------------------------------------------------------------------------ + + +def test_worker_files_only_when_picked(tmp_path: Path) -> None: + assert _init(tmp_path / "off") == 0 + assert not (tmp_path / "off" / "worker").exists() + assert _init(tmp_path / "on", worker="redis") == 0 + assert (tmp_path / "on" / "worker" / "__init__.py").is_file() + assert (tmp_path / "on" / "worker" / "tasks.py").is_file() + + +def test_dry_run_lists_worker_files(tmp_path: Path) -> None: + stream = io.StringIO() + code = InitCommand(OutputFormatter(stream=stream)).execute( + path=str(tmp_path / "p"), + agent_name="Demo", + non_interactive=True, + mode="full", + worker="redis", + dry_run=True, + ) + assert code == 0 and "tasks.py" in stream.getvalue() + assert not (tmp_path / "p").exists() + + +@pytest.mark.parametrize( + ("worker", "expected"), + [ + ("redis", None), + ("rabbitmq", 'BROKER_URL="amqp://guest:guest@localhost:5672/"'), + ("kafka", 'BROKER_URL="localhost:9092"'), + ], +) +def test_env_example_per_broker(tmp_path: Path, worker: str, expected: str | None) -> None: + assert _init(tmp_path, worker=worker) == 0 + env = (tmp_path / ".env.example").read_text() + assert "REDIS_URL=" in env + if expected is None: + assert "BROKER_URL" not in env + else: + assert expected in env + + +def test_generated_project_files_are_valid(tmp_path: Path) -> None: + assert _init(tmp_path, worker="kafka") == 0 + tasks = (tmp_path / "worker" / "tasks.py").read_text() + compile(tasks, "tasks.py", "exec") + assert "$" not in tasks + config = json.loads((tmp_path / "10xgraph.json").read_text()) + worker = WorkerConfig.model_validate(config["worker"]) + assert worker.broker.type == "kafka" and worker.tasks == ["worker.tasks"] + assert "jobs-kafka" in (tmp_path / "pyproject.toml").read_text() From 31326857bf79981454cbada975fd790f4656bf97 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Tue, 10 Nov 2026 12:40:01 +0600 Subject: [PATCH 7/9] Update dependencies and add metrics support - Updated the git source for the "10xgraph" package to a new commit. - Added new optional dependencies under the "metrics" extra, including: - opentelemetry-api - opentelemetry-exporter-otlp-proto-http - opentelemetry-exporter-prometheus - opentelemetry-instrumentation-fastapi - opentelemetry-sdk - prometheus-client - Updated version numbers for several opentelemetry packages to 1.45.2 and 0.66b2. - Added "prometheus-client" as a new package dependency. - Updated the "provides-extras" list to include "metrics". --- uv.lock | 125 ++++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 81 insertions(+), 44 deletions(-) diff --git a/uv.lock b/uv.lock index 61c17471..8b645d39 100644 --- a/uv.lock +++ b/uv.lock @@ -11,7 +11,7 @@ resolution-markers = [ [[package]] name = "10xgraph" version = "1.0.0rc4" -source = { git = "https://github.com/10xGraph/10xGraph?branch=main#32d6bd45a02bef9ae0ab65e443e6a567716b9d7a" } +source = { git = "https://github.com/10xGraph/10xGraph?branch=main#0b65e3bd4a695f6a10960c82e19570220feb5c18" } dependencies = [ { name = "injectq" }, { name = "pydantic" }, @@ -76,6 +76,14 @@ jwt = [ media = [ { name = "textxtract", extra = ["docx", "html", "md", "pdf", "xml"] }, ] +metrics = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-exporter-otlp-proto-http" }, + { name = "opentelemetry-exporter-prometheus" }, + { name = "opentelemetry-instrumentation-fastapi" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, +] otel = [ { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp" }, @@ -131,10 +139,16 @@ requires-dist = [ { name = "firebase-admin", marker = "extra == 'firebase'", specifier = ">=6.5.0" }, { name = "google-cloud-logging", marker = "extra == 'gcloud'" }, { name = "oauth2client", marker = "extra == 'firebase'", specifier = ">=4.1.3" }, + { name = "opentelemetry-api", marker = "extra == 'metrics'", specifier = ">=1.45.1" }, { name = "opentelemetry-api", marker = "extra == 'otel'", specifier = ">=1.45.1" }, { name = "opentelemetry-exporter-otlp", marker = "extra == 'otel'", specifier = ">=1.20.0" }, + { name = "opentelemetry-exporter-otlp-proto-http", marker = "extra == 'metrics'", specifier = ">=1.45.2" }, + { name = "opentelemetry-exporter-prometheus", marker = "extra == 'metrics'", specifier = ">=0.66b2" }, + { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'metrics'", specifier = ">=0.66b2" }, { name = "opentelemetry-instrumentation-fastapi", marker = "extra == 'otel'", specifier = ">=0.40b0" }, + { name = "opentelemetry-sdk", marker = "extra == 'metrics'", specifier = ">=1.45.1" }, { name = "opentelemetry-sdk", marker = "extra == 'otel'", specifier = ">=1.45.1" }, + { name = "prometheus-client", marker = "extra == 'metrics'", specifier = ">=0.26.0" }, { name = "pydantic", specifier = ">=2.13,<3" }, { name = "pydantic-settings", specifier = ">=2.3,<3" }, { name = "pyjwt", marker = "extra == 'jwt'", specifier = ">=2.15.0,<3" }, @@ -159,7 +173,7 @@ requires-dist = [ { name = "uvicorn", specifier = ">=0.36,<1.0" }, { name = "websockets", marker = "extra == 'websocket'", specifier = ">=13.0,<17" }, ] -provides-extras = ["sentry", "firebase", "snowflakekit", "redis", "jobs", "jobs-rabbitmq", "jobs-kafka", "jwt", "media", "images", "gcloud", "otel", "ag-ui", "websocket"] +provides-extras = ["sentry", "firebase", "snowflakekit", "redis", "jobs", "jobs-rabbitmq", "jobs-kafka", "jwt", "media", "images", "gcloud", "otel", "metrics", "ag-ui", "websocket"] [package.metadata.requires-dev] dev = [ @@ -1908,26 +1922,26 @@ wheels = [ [[package]] name = "opentelemetry-api" -version = "1.45.1" +version = "1.45.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/2e/02/6e0ae9cc61bd3169d401077b507b3ebc344745171e1051ab430be012dcd9/opentelemetry_api-1.45.1.tar.gz", hash = "sha256:aa38ed19bcc084ba42782a73255b3582283eced7ad6dddbd6695189e69adfb75", size = 72804, upload-time = "2026-10-06T17:32:58.133Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/14/cf3a6b6448849656e1e5f4e2101f3988dc6789724050096aba1bc5ccf592/opentelemetry_api-1.45.2.tar.gz", hash = "sha256:d7b0008e21e5fd61cbe7a7f34ca3b6905a78ab677d9bc395e299359c75cda6b4", size = 73277, upload-time = "2026-10-29T17:29:37.888Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/41/f7dcf80b81ee8e71c1a2b59f14208bc723edbd89ed027a73b175abf6348e/opentelemetry_api-1.45.1-py3-none-any.whl", hash = "sha256:b31553efa588ae44bc306f863c785c5333a9ecc091248c6ee68b4b6c87fdedfb", size = 60256, upload-time = "2026-10-06T17:32:33.506Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/610db864d7513158cebb8f36cc88e67e73bd6c7aa1905ef18d89e1281c81/opentelemetry_api-1.45.2-py3-none-any.whl", hash = "sha256:2cbd737377e710ee740d261c94e93fa0f08e6d037c18f17513cc0a2d94479a34", size = 60335, upload-time = "2026-10-29T17:29:06.955Z" }, ] [[package]] name = "opentelemetry-exporter-http-transport" -version = "0.66b1" +version = "0.66b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/0c/e3ebdb4b507f66afcc905e6885a4946969bd75b45988492643356fbbdc63/opentelemetry_exporter_http_transport-0.66b1.tar.gz", hash = "sha256:443080203bf52586ce0b2ad901e8951c61833eab1aa539ae6f1f16fe9e8e7952", size = 11693, upload-time = "2026-10-06T17:32:59.65Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/e6/dbeb3e217418071656232a4028c791060d219f218cef1249b664049d12c0/opentelemetry_exporter_http_transport-0.66b2.tar.gz", hash = "sha256:66d6ed810a515432c8891da9a834464332fd70badeca45bdeeae81b4280c903b", size = 11693, upload-time = "2026-10-29T17:29:39.444Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/69/6af86ff66492b481c6a4c05dcfd68beb47ed8ba046440a26a2aac76b95c7/opentelemetry_exporter_http_transport-0.66b1-py3-none-any.whl", hash = "sha256:2f95404bdee7f9d2d529c7de56c7bd86d014d774d8fbf137810e0167f8a492bf", size = 12155, upload-time = "2026-10-06T17:32:35.454Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b2/c622a101f31c0e6670b82f4264c4e77c161bce415bc6fc158fda0b82e218/opentelemetry_exporter_http_transport-0.66b2-py3-none-any.whl", hash = "sha256:fd5f5b490c099acda1cd7b909d5e8981436bf18cc8c6098d302d114c271720e9", size = 12154, upload-time = "2026-10-29T17:29:10.244Z" }, ] [package.optional-dependencies] @@ -1937,44 +1951,44 @@ requests = [ [[package]] name = "opentelemetry-exporter-otlp" -version = "1.45.1" +version = "1.45.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-exporter-otlp-proto-grpc" }, { name = "opentelemetry-exporter-otlp-proto-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e3/6f/5a561048ea372894f22f58f70e8478e4cd97d9a4e2d6f014559584a29bc5/opentelemetry_exporter_otlp-1.45.1.tar.gz", hash = "sha256:d0ac35592e77663a9fabf2740b4818c57196b1b764e0c8449d0d7bb2c7b2bc67", size = 6094, upload-time = "2026-10-06T17:33:00.966Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1e/45/52e9ce585d825e14cc211871acc3bc46b6cd36cdb0c09255a7feea98322b/opentelemetry_exporter_otlp-1.45.2.tar.gz", hash = "sha256:879044ba65b8b1be7a0d9d904afb76700e96d2a4d36700918d9325c9ee3b8be4", size = 6093, upload-time = "2026-10-29T17:29:40.905Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/e1/68c28d7da4482ce882a640b2c61405a78284c0a3a17a4db125647189b0b7/opentelemetry_exporter_otlp-1.45.1-py3-none-any.whl", hash = "sha256:ef3910d32b36ccbaf62390189759bd43a8109885e2d50b738ed9c9b255bf5cb5", size = 6730, upload-time = "2026-10-06T17:32:37.236Z" }, + { url = "https://files.pythonhosted.org/packages/19/27/fcc3b725ddbfa274fbf6098c9583032557671d35aa8d5e08d7e7277fb126/opentelemetry_exporter_otlp-1.45.2-py3-none-any.whl", hash = "sha256:52c8b9f268289c92c08b3a97f48c63d6e5c037f25ca1f6f29ef162fb78974afc", size = 6732, upload-time = "2026-10-29T17:29:12.653Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-common" -version = "0.66b1" +version = "0.66b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-sdk" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/cb/19/41de712173f43057e4532d42ece7d0c6d4210d353e5752433cb14987643f/opentelemetry_exporter_otlp_common-0.66b1.tar.gz", hash = "sha256:6b1403487a2185ac1feb45fd5546fdf8630ce71c36bcefaadf51e2130e9e23f9", size = 14325, upload-time = "2026-10-06T17:33:01.725Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/3c/3f2cc8c42bde2620e65024d71e2eb025f7bd0821bb363b52bcc5a3260dbd/opentelemetry_exporter_otlp_common-0.66b2.tar.gz", hash = "sha256:06f5a9b88638e310efa1f1d206cdf6b75393e112aba4e174a963416192bd1028", size = 14325, upload-time = "2026-10-29T17:29:41.701Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fc/39/8c23d67665c762aa51840fa06f86e902e8f6f1693bc8d7e3d98cd6e2f753/opentelemetry_exporter_otlp_common-0.66b1-py3-none-any.whl", hash = "sha256:00ff8592c3a7cb729ff3fdc7ffa12372c243bdf2163e80c180994d0c7bd83ee9", size = 12385, upload-time = "2026-10-06T17:32:38.177Z" }, + { url = "https://files.pythonhosted.org/packages/a8/47/b3049e30b339c82e3c7ec1fc692185a5d5a3ba65a94c570bb39b2c0b8424/opentelemetry_exporter_otlp_common-0.66b2-py3-none-any.whl", hash = "sha256:ace86efb75489f925f1ef809856ae5f0c7db534e4afdc624530222228b1236a5", size = 12385, upload-time = "2026-10-29T17:29:13.667Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.45.1" +version = "1.45.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-proto" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c1/8e/65e85e5137991a3c493b11682151d198638a5bc1dd4b4c5f67e013c57d7c/opentelemetry_exporter_otlp_proto_common-1.45.1.tar.gz", hash = "sha256:2e4adcc3a67bcf57804fc49514f0ef64974ca7590aa3491da389852b4a0628f6", size = 18873, upload-time = "2026-10-06T17:33:04.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/58/239c98b36d9ea317098769957043c251703636a26189c884fba46c1dad84/opentelemetry_exporter_otlp_proto_common-1.45.2.tar.gz", hash = "sha256:f1f2806f3f28ce7938033823e50934fcd91595a39de38e69a28a51e19f40fc6d", size = 18873, upload-time = "2026-10-29T17:29:44.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/aa/92f225d353904e7f70b8b3e3c1b02db0cf56f744c2e83c581dc372e78873/opentelemetry_exporter_otlp_proto_common-1.45.1-py3-none-any.whl", hash = "sha256:2f446183ae7047b036226f1d846c41a834b0e8755ad13b51a51dd38952eb466c", size = 15393, upload-time = "2026-10-06T17:32:41.911Z" }, + { url = "https://files.pythonhosted.org/packages/e6/d6/0f30d38120a949aef70528f845bf539f80df62abb4b8e62fdf69c2b6df5a/opentelemetry_exporter_otlp_proto_common-1.45.2-py3-none-any.whl", hash = "sha256:af483b24217028892f017096731b794b196b785025d2e6808874720fd0b05a80", size = 15392, upload-time = "2026-10-29T17:29:17.913Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.45.1" +version = "1.45.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -1986,14 +2000,14 @@ dependencies = [ { name = "opentelemetry-sdk" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d6/00/a82af0be959dc58495740b169c6669a86e0811f6cd353a01eda34d255db3/opentelemetry_exporter_otlp_proto_grpc-1.45.1.tar.gz", hash = "sha256:3b3dcfbfdcb4e35149fcf309972282054b45228f5c10547d0095d6578510a9a0", size = 25997, upload-time = "2026-10-06T17:33:05.114Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/2c/996861b327ddde01d3c0971582ebcd3f01a376acc720f7b0cb5274df4b70/opentelemetry_exporter_otlp_proto_grpc-1.45.2.tar.gz", hash = "sha256:9e4a6490ca7711479fec7c519425bc09808abbc9a2a1775528edecf200d19a5a", size = 25995, upload-time = "2026-10-29T17:29:45.2Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/46/2d1da202f1e17c81aae7efcf702898d524b46709e4d3e2bf1f7f8ca8fbc6/opentelemetry_exporter_otlp_proto_grpc-1.45.1-py3-none-any.whl", hash = "sha256:e42ecb789d2fc5d8145e3dadc3e2991c9f18cd166d7c7514e234702540274b76", size = 19492, upload-time = "2026-10-06T17:32:42.838Z" }, + { url = "https://files.pythonhosted.org/packages/e8/48/14301cac3d106e795eef7e89e191489f086b4e5ad26e47c9635a198e95f4/opentelemetry_exporter_otlp_proto_grpc-1.45.2-py3-none-any.whl", hash = "sha256:8a48d60835e90536846bd82de487bfbd81bccb5a1b3d0fb82edeea17db07395c", size = 19490, upload-time = "2026-10-29T17:29:18.934Z" }, ] [[package]] name = "opentelemetry-exporter-otlp-proto-http" -version = "1.45.1" +version = "1.45.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "googleapis-common-protos" }, @@ -2006,14 +2020,28 @@ dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1b/17/26487707ea4caa97b17e6e4b5fa72133a53512ffa2f5cf7a49ef284b29cb/opentelemetry_exporter_otlp_proto_http-1.45.1.tar.gz", hash = "sha256:45c218405ce3fd879596924b1874bf9a8f6880206d61065c5a912c8e5c297fb7", size = 28839, upload-time = "2026-10-06T17:33:05.713Z" } +sdist = { url = "https://files.pythonhosted.org/packages/db/07/b7b01ecb69d02f540e679a9e36f7413d081f225bbbed4469d212ada10bb7/opentelemetry_exporter_otlp_proto_http-1.45.2.tar.gz", hash = "sha256:5984a3fb46bc4628e4e2b85d4a6e72637e03962aae6cf372791137768a4ad939", size = 28843, upload-time = "2026-10-29T17:29:46.15Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/1f/517eaa0187ba106a9da97160ce2add3a371812681dc440930b267f714e42/opentelemetry_exporter_otlp_proto_http-1.45.1-py3-none-any.whl", hash = "sha256:24a97cf3753c7fb52fad44a696e452ff371686339e2acf3309e2eda3d0230700", size = 22180, upload-time = "2026-10-06T17:32:43.946Z" }, + { url = "https://files.pythonhosted.org/packages/32/65/f64d7d2450217e2989714e1dee15e19bf79a77a313387abac0cac77bc10e/opentelemetry_exporter_otlp_proto_http-1.45.2-py3-none-any.whl", hash = "sha256:6e4d025138829d0ee57946930c2ccad6491ad4d92a958e9c7eea69278fe60cfc", size = 22180, upload-time = "2026-10-29T17:29:20.154Z" }, +] + +[[package]] +name = "opentelemetry-exporter-prometheus" +version = "0.66b2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-sdk" }, + { name = "prometheus-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e8/c5/d49a08ebd180fb1471ae44ccaa265abf56e95cb5164ed5208e1663584b06/opentelemetry_exporter_prometheus-0.66b2.tar.gz", hash = "sha256:ada6b2507a31e10405f686e6a8498cb463a6931b5791337b04cfe14288db497f", size = 17337, upload-time = "2026-10-29T17:29:46.903Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/ec/91617f3e9bbe50487fd899a9efee4d111eabc3c61b66c2ef3ecf0aaba68e/opentelemetry_exporter_prometheus-0.66b2-py3-none-any.whl", hash = "sha256:b73998788aa30bec3bfe20fae312650878adb59f6563ed8bf37c188cb450a189", size = 13371, upload-time = "2026-10-29T17:29:21.28Z" }, ] [[package]] name = "opentelemetry-instrumentation" -version = "0.66b1" +version = "0.66b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -2021,14 +2049,14 @@ dependencies = [ { name = "packaging" }, { name = "wrapt" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a5/03/89e47ff8d52a4f83b343e6eb9ef1698ff45357216e5b6b2b21e0da5c5c7d/opentelemetry_instrumentation-0.66b1.tar.gz", hash = "sha256:e79a510f7d87c72d95e964ddb42193a0d9a75668c027d980eab032ea1322a5ce", size = 43308, upload-time = "2026-10-06T17:36:10.703Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/b9/46a136199fa6928eecefec8418867a460d94f01c608a85d22bd2ed4e8d48/opentelemetry_instrumentation-0.66b2.tar.gz", hash = "sha256:02b094150e53b76f5906d75e628dbb3833bbc6e62fa109b5c7ba0c081865e923", size = 43305, upload-time = "2026-10-29T17:38:11.618Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/b2/d1413681ff43e13ac9860df27e1226d3199ab0b97b352ceea41abcc660a5/opentelemetry_instrumentation-0.66b1-py3-none-any.whl", hash = "sha256:4c4aa14dc9a24a02325a9d4c42c4d0208dbb1374c2b1b8fe6c9392d59f3e1008", size = 36904, upload-time = "2026-10-06T17:35:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/3b/41/014d40e2b08a5d9d9058ed4967ad992657c203ce0c292ded97364ee7b82a/opentelemetry_instrumentation-0.66b2-py3-none-any.whl", hash = "sha256:fa9ccfdab147a14af8107d19ef0a7b5015675273c5adced7a1fc38f28b42ec4c", size = 36902, upload-time = "2026-10-29T17:37:06.994Z" }, ] [[package]] name = "opentelemetry-instrumentation-asgi" -version = "0.66b1" +version = "0.66b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "asgiref" }, @@ -2037,14 +2065,14 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5a/d9/ff522f5c3e340e9007554923b1a4d2ac451676f8757bafb3d0057f68b5c3/opentelemetry_instrumentation_asgi-0.66b1.tar.gz", hash = "sha256:78cdc5e45e897e16a8dac9d282e8d5bdf9af2d58e1313fa0bdd4a134c6f9dafc", size = 25992, upload-time = "2026-10-06T17:36:14.593Z" } +sdist = { url = "https://files.pythonhosted.org/packages/54/96/0524b34042406e632d2ec625be2432db703c2911bb594cef3280666e7b9e/opentelemetry_instrumentation_asgi-0.66b2.tar.gz", hash = "sha256:da8a2df4589976ad483feaec150a78ec7d1947754e420b838a4942361e465bfb", size = 25991, upload-time = "2026-10-29T17:38:16.291Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/ea/10ba99110bf3c9fb736af39c96ca8f3668b988cabb6b59309e058c44461c/opentelemetry_instrumentation_asgi-0.66b1-py3-none-any.whl", hash = "sha256:78b3f9bdf0fa38c65935a2ab46d59e0f9de873a51e0c95b0329f106e2ccb5274", size = 15812, upload-time = "2026-10-06T17:35:17.638Z" }, + { url = "https://files.pythonhosted.org/packages/b8/6e/bbb647fc9c789365593b77aefc949beef5fa684015e2b81fb41f5da0aa9c/opentelemetry_instrumentation_asgi-0.66b2-py3-none-any.whl", hash = "sha256:9c5ec36e6b898f18e6b07b5077c2a3bafa9abf69de98e729797b9efa465f8aa3", size = 15810, upload-time = "2026-10-29T17:37:13.306Z" }, ] [[package]] name = "opentelemetry-instrumentation-fastapi" -version = "0.66b1" +version = "0.66b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, @@ -2053,57 +2081,57 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5d/2a/cd4125b7acbea2ed17f1d31b58c184cb0a79fcb5541ceb4de90ffc6d8c01/opentelemetry_instrumentation_fastapi-0.66b1.tar.gz", hash = "sha256:584cf9d2c4417ff8b2d6ff2bc606bfe13c8b3456018bf94f50f2cf658492505b", size = 26634, upload-time = "2026-10-06T17:36:25.157Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/71/8ccb9d84bbdbb4798682796dcf20892e404ef9548f58975c4b0dd3d372d6/opentelemetry_instrumentation_fastapi-0.66b2.tar.gz", hash = "sha256:bd226e0196990999eef77380a64527a26f35b621e681c3cad860984ecc3c23f5", size = 26631, upload-time = "2026-10-29T17:38:28.173Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/70/676928d537978acc7bff2ac8657bd0836ba608ffc8f65455238f1fa2bd0f/opentelemetry_instrumentation_fastapi-0.66b1-py3-none-any.whl", hash = "sha256:97f8ac8fd7537517f9e6988bd0aca04bfa5aad564bcd46c245530739e2be72d1", size = 14216, upload-time = "2026-10-06T17:35:32.827Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/10338083ccc116e65b0471796baa90fcb817daf96fed67e51eff9fb80d4e/opentelemetry_instrumentation_fastapi-0.66b2-py3-none-any.whl", hash = "sha256:401eb9fe58cc86015ac0a6682dcaf1b4bcea32c428969853d02171d6650d1774", size = 14217, upload-time = "2026-10-29T17:37:30.465Z" }, ] [[package]] name = "opentelemetry-proto" -version = "1.45.1" +version = "1.45.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "protobuf" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/4b/7f/15f014fb195da6c2dbb6c71399b8e76824878718e94de6454038488eed28/opentelemetry_proto-1.45.1.tar.gz", hash = "sha256:79e0fb95e4616691a469439238aa9224d75779b3e108e895d1aa125ab29ca77c", size = 46488, upload-time = "2026-10-06T17:33:11.49Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/9c/1aa4ce60e9bc72af20dc49c92cc0566825092d92b3d6a277439fb5fb7c01/opentelemetry_proto-1.45.2.tar.gz", hash = "sha256:bd8d23cf770605b0af9b7be2071d26a464fea289d35429abfd2a66226114ad64", size = 46486, upload-time = "2026-10-29T17:29:53.007Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/9a/42ec8180a769516ae757e893b69736826efceac7332553915b4528a91c6d/opentelemetry_proto-1.45.1-py3-none-any.whl", hash = "sha256:f38e2a8413053c180cd3d2637fbb279673ec2f6a6e09c995aafa2f452c52b46e", size = 72488, upload-time = "2026-10-06T17:32:53.057Z" }, + { url = "https://files.pythonhosted.org/packages/7e/0d/a0b8f2b9efe2ec1abdf639d3696db9b1563ebeb11d91aac08ba7341e9122/opentelemetry_proto-1.45.2-py3-none-any.whl", hash = "sha256:03a7ca1b43f89fd5b965d5975902bb2515c0c86e2428b456836e9a6149c6235a", size = 72484, upload-time = "2026-10-29T17:29:31.403Z" }, ] [[package]] name = "opentelemetry-sdk" -version = "1.45.1" +version = "1.45.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "opentelemetry-semantic-conventions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/79/7392e21a1c8f0c61d90b223e31c7e48cb9d452e91a6b820ad24cca5f23c4/opentelemetry_sdk-1.45.1.tar.gz", hash = "sha256:63d24a6ca645019a631e6a51999c73e93adcac1196ca640b8ae78a7cc4762bf3", size = 218324, upload-time = "2026-10-06T17:33:13.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/1a/74a21d4d6969fb8b432ac397e66112a8167a6deb267ea4559dbaf2fe91ef/opentelemetry_sdk-1.45.2.tar.gz", hash = "sha256:7eec4de13dd21ff3a1670dcb0e02079e692736f94d733dfdce5af461d884248d", size = 221705, upload-time = "2026-10-29T17:29:54.685Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/3c/87c42b4bd6dd297536f04cd9383d212ac557ecd49f2cbdcd46da1c9ef5c8/opentelemetry_sdk-1.45.1-py3-none-any.whl", hash = "sha256:c604c11dc429810812348989115fa44bd558772a3d7442afc43d024f2c250ca4", size = 140063, upload-time = "2026-10-06T17:32:55.04Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a5/9a3b793ef48245f2336a16beb2324c2397dbb3f987cd68942f77cca06156/opentelemetry_sdk-1.45.2-py3-none-any.whl", hash = "sha256:ff974ea70ee4f1d51e2ec152db37c2b626c7bf82bb3f7d86114fd8704e7199c6", size = 141756, upload-time = "2026-10-29T17:29:33.722Z" }, ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.66b1" +version = "0.66b2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "opentelemetry-api" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/46/e4/dbbfb2a010c4db2224a5114638acede6fe563d33cc20fb1752cebcbe6298/opentelemetry_semantic_conventions-0.66b1.tar.gz", hash = "sha256:497ca63bf383723411e8eaf60c8779e9877633c936bb641080adab59d0eb6ec8", size = 150250, upload-time = "2026-10-06T17:33:14.073Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/3f/50f95972b50e8e353f5b9342034c9cd17730f1956f009ce6a3fbbd89f9ce/opentelemetry_semantic_conventions-0.66b2.tar.gz", hash = "sha256:7da5f1a5dc4383eb66711d4d3d3e71824d9403ac1559fea021cc103c1a4c4976", size = 150246, upload-time = "2026-10-29T17:29:55.682Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/14/67f8aa798857f8cf686f515bf93d9bb877ce952ddc8efae0fa25b45ce0d6/opentelemetry_semantic_conventions-0.66b1-py3-none-any.whl", hash = "sha256:d4cddeb4315490b35213f55e2bdc9ac54bb1e4d318927475bed62b35545e581b", size = 206279, upload-time = "2026-10-06T17:32:56.103Z" }, + { url = "https://files.pythonhosted.org/packages/67/bd/6bf33ed31521c565931c037e9e81bac47c1acb000e850c6e24220e2a0c00/opentelemetry_semantic_conventions-0.66b2-py3-none-any.whl", hash = "sha256:4cbe61e5f9275d57ab31708446e7938ef4291ebbe6a7fca6f7a629c5acae87d8", size = 206280, upload-time = "2026-10-29T17:29:35.062Z" }, ] [[package]] name = "opentelemetry-util-http" -version = "0.66b1" +version = "0.66b2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7c/b5/df4b61da899f6ebdffdbdf0c8b0f3189ee57151694ccd5b7d50ee2906241/opentelemetry_util_http-0.66b1.tar.gz", hash = "sha256:047dea1a628031f857a5a32261dc0e955bc162d39993ed1cffb8f2cff5ba8a62", size = 11586, upload-time = "2026-10-06T17:36:46.572Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/57/33566966a7bced4d02fbc672f92029184e4a743f5b531e44b4c0dc4c999b/opentelemetry_util_http-0.66b2.tar.gz", hash = "sha256:7ba066ea609202e474679ddf1d0f753a1e6619bd206b36afaecba4bbb86e25d0", size = 11587, upload-time = "2026-10-29T17:38:56.043Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/eb/9b/c77ecaea79ba0de1a11e7f06a7f5eea7043ec23f1860dcf5f03536698e4c/opentelemetry_util_http-0.66b1-py3-none-any.whl", hash = "sha256:8f443d7abcaf29c4a07b373bbd31b5b39132c0ed3c27d015a59dc0323d5b1c58", size = 8387, upload-time = "2026-10-06T17:36:06.984Z" }, + { url = "https://files.pythonhosted.org/packages/18/8d/39885564eefa43f6181acfa3d698af2f6b5fe3e64c86dce3f1b955f6397f/opentelemetry_util_http-0.66b2-py3-none-any.whl", hash = "sha256:c4bb5d6744e60353bcaa3e325bc529a1515346b51cb4d9c8e5b730579337e06d", size = 8383, upload-time = "2026-10-29T17:38:07.462Z" }, ] [[package]] @@ -2243,6 +2271,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, ] +[[package]] +name = "prometheus-client" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/73/f1334c29c2af4cd9dba6c7817e61b611bd0215e2eb5565c6064a4de18802/prometheus_client-0.26.0.tar.gz", hash = "sha256:04a91bcf94e2cf74a44a1a874d651a2e853ed354b6e822f3b7487751465d5c2b", size = 92910, upload-time = "2026-07-24T19:36:41.893Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/a3/b69efbf4143b5b9859b977770bbbabcc2796b702fa69dc40271e45cd5a56/prometheus_client-0.26.0-py3-none-any.whl", hash = "sha256:fa93d06737aa02bacd05794768508bb97d2fbee28cb3bca04eaae92f0ca953d6", size = 64494, upload-time = "2026-07-24T19:36:40.854Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" From 530473206945f6c15e5c5fe2e99a75e3d602d4d8 Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Tue, 10 Nov 2026 12:44:19 +0600 Subject: [PATCH 8/9] fix(schema): Correct purpose description for OTEL_ENABLED configuration --- tenxgraph_api/cli/config_editor/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tenxgraph_api/cli/config_editor/schema.py b/tenxgraph_api/cli/config_editor/schema.py index a4506af5..e62b9065 100644 --- a/tenxgraph_api/cli/config_editor/schema.py +++ b/tenxgraph_api/cli/config_editor/schema.py @@ -793,7 +793,7 @@ }, { "name": "OTEL_ENABLED", - "purpose": "Export OpenTelemetry traces from the API server (off unless set to true).", + "purpose": "Export OpenTelemetry traces from the API server (off unless set true)", }, { "name": "OTEL_LEVEL", From b4f11d49a843df8280ed43aba4f1dfaabf44f1ae Mon Sep 17 00:00:00 2001 From: Shudipto Trafder Date: Tue, 10 Nov 2026 12:49:38 +0600 Subject: [PATCH 9/9] fix(metrics): Specify type for DEFAULT_BUCKETS_MS as list[float] --- tenxgraph_api/src/app/core/config/metrics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tenxgraph_api/src/app/core/config/metrics.py b/tenxgraph_api/src/app/core/config/metrics.py index f01ac69c..84c17b32 100644 --- a/tenxgraph_api/src/app/core/config/metrics.py +++ b/tenxgraph_api/src/app/core/config/metrics.py @@ -17,7 +17,7 @@ from .settings import get_settings, logger -DEFAULT_BUCKETS_MS = [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000, 120000] +DEFAULT_BUCKETS_MS: list[float] = [50, 100, 250, 500, 1000, 2500, 5000, 10000, 30000, 60000, 120000] METRICS_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_METRICS_ENDPOINT" OTLP_ENDPOINT_ENV = "OTEL_EXPORTER_OTLP_ENDPOINT" SEMCONV_ENV = "OTEL_SEMCONV_STABILITY_OPT_IN"