@@ -1289,6 +1289,9 @@ class FrameworkAppConfig(BaseModel):
12891289 escalation_teams : list [str ] = Field (default_factory = list )
12901290 severity_aliases : dict [str , str ] = Field (default_factory = dict )
12911291 dedup_system_prompt : str = _DEFAULT_DEDUP_SYSTEM_PROMPT
1292+ # Intake runner knobs: forwarded into IntakeContext at graph-build time.
1293+ intake_top_k : int = 3
1294+ intake_similarity_threshold : float = 0.7
12921295
12931296
12941297def resolve_framework_app_config (
@@ -1928,14 +1931,18 @@ def _validate_supervisor(self) -> None:
19281931 f"skill { self .name !r} (kind=supervisor) requires a non-empty "
19291932 f"subordinates list"
19301933 )
1931- if self .runner is not None :
1932- # Resolve at skill-load time so a typo in YAML surfaces here,
1933- # not in the middle of a session. The resolver itself raises
1934- # ``ValueError`` with a helpful message — bubble that up.
1935- _resolve_dotted_callable (
1936- self .runner ,
1937- source = f"skill { self .name !r} runner" ,
1938- )
1934+ if self .runner is None :
1935+ # Default every supervisor to the framework intake runner
1936+ # (similarity retrieval + dedup gate). Apps override by
1937+ # setting ``runner:`` in YAML.
1938+ self .runner = "runtime.intake:default_intake_runner"
1939+ # Resolve at skill-load time so a typo in YAML surfaces here,
1940+ # not in the middle of a session. The resolver itself raises
1941+ # ``ValueError`` with a helpful message — bubble that up.
1942+ _resolve_dotted_callable (
1943+ self .runner ,
1944+ source = f"skill { self .name !r} runner" ,
1945+ )
19391946 if self .dispatch_strategy == "llm" and not self .dispatch_prompt :
19401947 raise ValueError (
19411948 f"skill { self .name !r} (kind=supervisor, strategy=llm) requires "
@@ -4428,8 +4435,13 @@ def _sqlite_path_from_url(url: str) -> str:
44284435 if path .startswith ("//" ):
44294436 # sqlite:////abs/path -> urlparse path "//abs/path" -> "/abs/path"
44304437 return path [1 :]
4431- # sqlite:///x -> urlparse path "/x". sqlite3 accepts both absolute
4432- # and relative; tests use absolute via tmp_path.
4438+ # sqlite:///<rel> -> urlparse path "/<rel>". SQLAlchemy treats this
4439+ # as a path *relative* to CWD; strip the leading slash so the helper
4440+ # agrees (otherwise mkdir tries the filesystem root). Tests pass
4441+ # absolute paths via tmp_path which composes to the four-slash form
4442+ # above, so this branch only matters for the relative case.
4443+ if path .startswith ("/" ):
4444+ return path [1 :]
44334445 return path
44344446
44354447
@@ -5854,6 +5866,7 @@ async def _stage2(
58545866
58555867
58565868
5869+
58575870from langgraph .types import Command
58585871
58595872
@@ -6125,6 +6138,20 @@ async def create(cls, cfg: AppConfig) -> "Orchestrator":
61256138 similarity_threshold = framework_cfg .similarity_threshold ,
61266139 distance_strategy = cfg .storage .vector .distance_strategy ,
61276140 )
6141+ # Attach intake_context onto framework_cfg so supervisor nodes can
6142+ # reach the live stores via app_cfg.intake_context. FrameworkAppConfig
6143+ # is a Pydantic model; use object.__setattr__ to set a runtime
6144+ # attribute without triggering Pydantic's frozen-model guard.
6145+ object .__setattr__ (
6146+ framework_cfg ,
6147+ "intake_context" ,
6148+ IntakeContext (
6149+ history_store = history ,
6150+ dedup_pipeline = None , # dedup_pipeline built below; patched after
6151+ top_k = framework_cfg .intake_top_k ,
6152+ similarity_threshold = framework_cfg .intake_similarity_threshold ,
6153+ ),
6154+ )
61286155 # Configure incident_management state via importlib so we hit the
61296156 # *same* module instance the MCP loader will import. In the
61306157 # single-file dist bundle a direct ``set_state`` call would
@@ -6202,6 +6229,11 @@ def _factory():
62026229 model_factory = _factory ,
62036230 framework_cfg = framework_cfg ,
62046231 )
6232+ # Backfill dedup_pipeline into the IntakeContext now that it is built.
6233+ # The IntakeContext was constructed with dedup_pipeline=None above
6234+ # because the pipeline is built after graph construction.
6235+ if dedup_pipeline is not None :
6236+ framework_cfg .intake_context .dedup_pipeline = dedup_pipeline
62056237 # P2-I: no resume graph anymore — resume runs through the
62066238 # main graph via ``Command(resume=...)`` against the same
62076239 # thread_id, with the checkpointer rehydrates paused state.
@@ -8026,7 +8058,7 @@ def hydrate_and_gate(
80268058# a ``runner`` extension point: a callable invoked at supervisor-node entry
80278059# that may mutate ``GraphState`` and/or short-circuit to ``"__end__"``. The
80288060# functions below adapt :func:`hydrate_and_gate` to that contract so the
8029- # asr_supervisor skill actually exercises the hydration + dup-gate logic
8061+ # intake skill actually exercises the hydration + dup-gate logic
80308062# inside the live graph, instead of leaving it to be called only from
80318063# unit tests.
80328064#
@@ -8114,31 +8146,61 @@ def _runner(state: Any, *, app_cfg: Any | None = None) -> dict[str, Any] | None:
81148146 return _runner
81158147
81168148
8117- def default_supervisor_runner (
8118- state : Any , * , app_cfg : Any | None = None ,
8119- ) -> dict [str , Any ] | None :
8120- """Module-level runner the YAML can wire in via dotted path.
81218149
8122- Anchored on the bundled seed directory (``seeds/kg``,
8123- ``seeds/releases``, ``seeds/playbooks``) so the asr_supervisor
8124- skill works out of the box. Real deployments should construct
8125- their own runner via :func:`make_hydrate_runner` and register it
8126- with their app's skill loader.
8150+ def make_default_supervisor_runner (
8151+ * ,
8152+ kg_store : KGStore ,
8153+ release_store : ReleaseStore ,
8154+ playbook_store : PlaybookStore ,
8155+ get_active_sessions : Callable [[], list [dict [str , Any ]]] | None = None ,
8156+ component_lookup : Callable [[str ], list [str ]] | None = None ,
8157+ ) -> Callable [..., dict [str , Any ] | None ]:
8158+ """Compose framework default_intake_runner + ASR memory hydration.
8159+
8160+ Framework default runs first (similarity retrieval + dedup gate).
8161+ If it short-circuits via ``next_route='__end__'`` (duplicate), the
8162+ ASR hydration is skipped — the duplicate session ends without
8163+ paying for KG/playbook lookups.
81278164 """
8128- runner = _BUILT_DEFAULT_RUNNER
8129- return runner (state , app_cfg = app_cfg )
8165+ asr_runner = make_hydrate_runner (
8166+ kg_store = kg_store ,
8167+ release_store = release_store ,
8168+ playbook_store = playbook_store ,
8169+ get_active_sessions = get_active_sessions ,
8170+ component_lookup = component_lookup ,
8171+ )
8172+ return compose_runners (default_intake_runner , asr_runner )
81308173
81318174
81328175# Build the default runner exactly once at import time so per-call
81338176# overhead is just a closure invocation. Constructor stays cheap:
81348177# the stores read seed JSON lazily on first access.
8135- _BUILT_DEFAULT_RUNNER = make_hydrate_runner (
8178+ _BUILT_DEFAULT_RUNNER = make_default_supervisor_runner (
81368179 kg_store = KGStore (_DEFAULT_SEEDS / "kg" ),
81378180 release_store = ReleaseStore (_DEFAULT_SEEDS / "releases" ),
81388181 playbook_store = PlaybookStore (_DEFAULT_SEEDS / "playbooks" ),
81398182 get_active_sessions = lambda : [],
81408183)
81418184
8185+
8186+ def default_supervisor_runner (
8187+ state : Any , * , app_cfg : Any | None = None ,
8188+ ) -> dict [str , Any ] | None :
8189+ """Module-level runner the YAML can wire in via dotted path.
8190+
8191+ Anchored on the bundled seed directory (``seeds/kg``,
8192+ ``seeds/releases``, ``seeds/playbooks``) so the intake
8193+ skill works out of the box. Real deployments should construct
8194+ their own runner via :func:`make_default_supervisor_runner` and
8195+ register it with their app's skill loader.
8196+
8197+ Composition: framework ``default_intake_runner`` (similarity
8198+ retrieval + dedup gate) runs first; ASR memory hydration follows.
8199+ If the framework short-circuits (``next_route='__end__'``), the
8200+ hydration step is skipped.
8201+ """
8202+ return _BUILT_DEFAULT_RUNNER (state , app_cfg = app_cfg )
8203+
81428204# ====== module: examples/incident_management/asr/hypothesis_loop.py ======
81438205
81448206MAX_ITERATIONS : int = 3
0 commit comments