-
Notifications
You must be signed in to change notification settings - Fork 1
Implementation Plan: Serve Surface Symmetry β SERVE_SURFACES __all__ Alphabetical Fix #4210
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f3b0276
8b25de5
3e67bd1
e698cb7
53e689c
d2720fa
e9e2c5d
70e2b56
1f5a656
cf13845
e91e350
77f58a7
e620e02
b7c532d
6eb5cd0
8465abc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,114 @@ | ||
| """Unified serve-pipeline helpers. | ||
|
|
||
| The only legal call site for load_and_validate in server/tools/. | ||
| All four serve surfaces (open_kitchen normal, open_kitchen deferred-recall, | ||
| load_recipe, get_recipe) must call serve_recipe() instead of calling | ||
| ctx.recipes.load_and_validate() directly. This structural invariant is | ||
| enforced by tests/arch/test_serve_surface_registry.py. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| if TYPE_CHECKING: | ||
| from autoskillit.pipeline.context import ToolContext | ||
|
|
||
|
|
||
| def _build_serve_override_stack( | ||
| ctx: ToolContext, | ||
| caller_overrides: dict[str, str] | None, | ||
| *, | ||
| config_default: dict[str, str], | ||
| session_overrides: dict[str, str], | ||
| config_layer: dict[str, str], | ||
| backend_overrides: dict[str, str] | None = None, | ||
| ) -> dict[str, str]: | ||
| """Single source of truth for serve override stack construction. | ||
|
|
||
| Priority (highest wins): config_layer > backend_overrides > caller_overrides | ||
| > snapshot_baseline > session_overrides > config_default | ||
| """ | ||
| snapshot_baseline: dict[str, str] = {} | ||
| if ctx.session_serve_overrides is not None: | ||
| snapshot_baseline = dict(ctx.session_serve_overrides) | ||
| return { | ||
| **config_default, | ||
| **session_overrides, | ||
| **snapshot_baseline, | ||
| **(caller_overrides or {}), | ||
| **(backend_overrides or {}), | ||
| **config_layer, | ||
| } | ||
|
|
||
|
|
||
| def _resolve_serve_defer_unresolved( | ||
| ctx: ToolContext, | ||
| caller_overrides: dict[str, str] | None, | ||
| ) -> bool: | ||
| """Resolve defer_unresolved from session snapshot or compute fresh.""" | ||
| if ctx.session_serve_overrides is not None: | ||
| return ctx.session_serve_defer_unresolved | ||
| return not bool(caller_overrides) | ||
|
|
||
|
|
||
| def reset_session_serve_overrides(ctx: ToolContext) -> None: | ||
| """Clear the session serve-overrides snapshot on ctx. | ||
|
|
||
| Called by the test _reset_mcp_tags autouse fixture to prevent snapshot | ||
| state from leaking between tests on the same xdist worker. In production, | ||
| _close_kitchen_handler performs the equivalent reset. | ||
| """ | ||
| ctx.session_serve_overrides = None | ||
| ctx.session_serve_defer_unresolved = False | ||
|
|
||
|
|
||
| def serve_recipe( | ||
| ctx: ToolContext, | ||
| name: str, | ||
| caller_overrides: dict[str, str] | None, | ||
| *, | ||
| config_default: dict[str, str], | ||
| session_overrides: dict[str, str], | ||
| config_layer: dict[str, str], | ||
| backend_overrides: dict[str, str] | None = None, | ||
| ingredients_only: bool = False, | ||
| resolved_defaults: dict[str, str] | None = None, | ||
| effective_backend_map: dict[str, str] | None = None, | ||
| suppressed: list[str] | None = None, | ||
| backend_name: str | None = None, | ||
| temp_dir: Path | str | None = None, | ||
| temp_dir_relpath: str | None = None, | ||
| ) -> dict[str, Any]: | ||
| """Unified recipe serve path. Only legal caller of load_and_validate in server/tools/.""" | ||
| ingredient_overrides = _build_serve_override_stack( | ||
| ctx, | ||
| caller_overrides, | ||
| config_default=config_default, | ||
| session_overrides=session_overrides, | ||
| config_layer=config_layer, | ||
| backend_overrides=backend_overrides, | ||
| ) | ||
| defer_unresolved = _resolve_serve_defer_unresolved(ctx, caller_overrides) | ||
| kwargs: dict[str, Any] = { | ||
| "ingredient_overrides": ingredient_overrides, | ||
| "defer_unresolved": defer_unresolved, | ||
| } | ||
| if resolved_defaults is not None: | ||
| kwargs["resolved_defaults"] = resolved_defaults | ||
| if effective_backend_map is not None: | ||
| kwargs["effective_backend_map"] = effective_backend_map | ||
| if suppressed is not None: | ||
| kwargs["suppressed"] = suppressed | ||
| if backend_name is not None: | ||
| kwargs["backend_name"] = backend_name | ||
| if ingredients_only: | ||
|
Trecek marked this conversation as resolved.
|
||
| kwargs["ingredients_only"] = ingredients_only | ||
|
Trecek marked this conversation as resolved.
|
||
| if temp_dir is not None: | ||
| kwargs["temp_dir"] = temp_dir | ||
| if temp_dir_relpath is not None: | ||
| kwargs["temp_dir_relpath"] = temp_dir_relpath | ||
| if ctx.recipes is None: | ||
| raise RuntimeError("serve_recipe() called with ctx.recipes=None") | ||
| return ctx.recipes.load_and_validate(name, ctx.project_dir, **kwargs) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -75,6 +75,7 @@ | |
| _check_dispatch_feasibility, | ||
| filter_steps_by_post_prune, | ||
| ) | ||
| from autoskillit.server.tools._serve_helpers import serve_recipe | ||
| from autoskillit.server.tools._types import _validate_result | ||
|
|
||
| logger = get_logger(__name__) | ||
|
|
@@ -402,6 +403,8 @@ def _close_kitchen_handler() -> None: | |
| ctx.active_recipe_features = None | ||
| ctx.active_recipe_steps = None | ||
| ctx.active_recipe_ingredients = None | ||
| ctx.session_serve_overrides = None | ||
| ctx.session_serve_defer_unresolved = False | ||
| ctx.recipe_name = "" | ||
| ctx.recipe_content_hash = "" | ||
| ctx.recipe_composite_hash = "" | ||
|
|
@@ -483,6 +486,10 @@ def get_recipe(name: str) -> str: | |
|
|
||
| _defaults = resolve_ingredient_defaults(ctx.project_dir) | ||
| _config_layer = build_config_authoritative_layer(_defaults) | ||
| _session_overrides: dict[str, str] = { | ||
| "kitchen_id": ctx.kitchen_id, | ||
| "diagnostics_log_dir": str(resolve_log_dir(ctx.config.linux_tracing.log_dir)), | ||
| } | ||
| try: | ||
| _raw_recipe = ctx.recipes.load(match.path) | ||
| _backend_overrides, _cap_detail = _provider_aware_capability_overrides( | ||
|
|
@@ -500,17 +507,17 @@ def get_recipe(name: str) -> str: | |
| skill_resolver=ctx.skill_resolver, | ||
| ) | ||
| _config_default = build_config_default_layer(_defaults) | ||
| result = ctx.recipes.load_and_validate( | ||
| result = serve_recipe( | ||
| ctx, | ||
| name, | ||
| ctx.project_dir, | ||
| caller_overrides=None, | ||
| config_default=_config_default, | ||
| session_overrides=_session_overrides, | ||
| config_layer=_config_layer, | ||
| backend_overrides=_backend_overrides, | ||
| resolved_defaults=_defaults, | ||
| ingredient_overrides={ | ||
| **_config_default, | ||
| **_backend_overrides, | ||
| **_config_layer, | ||
| }, | ||
| backend_name=ctx.backend.name if ctx.backend else None, | ||
| effective_backend_map=_effective_backend_map, | ||
| backend_name=ctx.backend.name if ctx.backend else None, | ||
| ) | ||
| except ProcessStaleError: | ||
| logger.warning("get_recipe_failure", recipe=name, stage="process_stale", exc_info=True) | ||
|
|
@@ -731,12 +738,6 @@ async def open_kitchen( | |
| _config_layer = build_config_authoritative_layer(_defaults) | ||
| _config_default = build_config_default_layer(_defaults) | ||
| _promote_capability_keys(_config_layer, _session_overrides) | ||
| _merged_overrides = { | ||
| **_config_default, | ||
| **_session_overrides, | ||
| **(overrides or {}), | ||
| **_config_layer, | ||
| } | ||
| _effective_backend_map = _compute_effective_backend_map( | ||
| _raw_recipe.steps if _raw_recipe is not None else None, | ||
| tool_ctx.backend.name if tool_ctx.backend else None, | ||
|
|
@@ -756,16 +757,17 @@ async def open_kitchen( | |
| ) | ||
| } | ||
| ) | ||
| _user_overrides_present = overrides is not None and len(overrides) > 0 | ||
| if _is_deferred_recall: | ||
| try: | ||
| result = tool_ctx.recipes.load_and_validate( | ||
| result = serve_recipe( | ||
| tool_ctx, | ||
| name, | ||
| tool_ctx.project_dir, | ||
| suppressed=suppressed, | ||
| caller_overrides=overrides, | ||
| config_default=_config_default, | ||
| session_overrides=_session_overrides, | ||
| config_layer=_config_layer, | ||
| resolved_defaults=_defaults, | ||
| ingredient_overrides=_merged_overrides, | ||
| defer_unresolved=False, | ||
| suppressed=suppressed, | ||
| backend_name=tool_ctx.backend.name if tool_ctx.backend else None, | ||
| effective_backend_map=_effective_backend_map, | ||
| ) | ||
|
|
@@ -859,15 +861,24 @@ async def open_kitchen( | |
| result["warnings"] = _override_warnings | ||
| if ingredients_only: | ||
| result = strip_ingredients_only_keys(result) | ||
| # When caller provides explicit overrides, update the snapshot so | ||
| # subsequent load_recipe/get_recipe calls see the new overrides. | ||
| # When overrides=None (replay previous context), leave the existing | ||
| # snapshot intact β the caller's intent is continuity, not reset. | ||
| if overrides is not None: | ||
| tool_ctx.session_serve_overrides = dict(overrides) | ||
| tool_ctx.session_serve_defer_unresolved = not bool(overrides) | ||
| return json.dumps(result) | ||
| try: | ||
| result = tool_ctx.recipes.load_and_validate( | ||
| result = serve_recipe( | ||
| tool_ctx, | ||
| name, | ||
| tool_ctx.project_dir, | ||
| suppressed=suppressed, | ||
| caller_overrides=overrides, | ||
| config_default=_config_default, | ||
| session_overrides=_session_overrides, | ||
| config_layer=_config_layer, | ||
| resolved_defaults=_defaults, | ||
| ingredient_overrides=_merged_overrides, | ||
| defer_unresolved=not _user_overrides_present, | ||
| suppressed=suppressed, | ||
| backend_name=tool_ctx.backend.name if tool_ctx.backend else None, | ||
| effective_backend_map=_effective_backend_map, | ||
| ) | ||
|
|
@@ -957,6 +968,12 @@ async def open_kitchen( | |
| await ctx.disable_components(tags={"kitchen"}) | ||
| return _preflight_err | ||
|
|
||
| # Snapshot the caller-supplied values ONLY β NOT _merged_overrides. | ||
| # Storing _merged_overrides would inject stale kitchen_id/diagnostics_log_dir | ||
| # into subsequent load_recipe merges, silently overwriting fresh infra values. | ||
| tool_ctx.session_serve_overrides = dict(overrides) if overrides else {} | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [warning] bugs: If a caller explicitly passes |
||
| tool_ctx.session_serve_defer_unresolved = not bool(overrides) | ||
|
|
||
| result["success"] = True | ||
| result["kitchen"] = "open" | ||
| result["version"] = __version__ | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.