claude-smart 0.2.46 → 0.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/README.md +19 -11
  3. package/bin/claude-smart.js +290 -68
  4. package/package.json +1 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.codex-plugin/plugin.json +1 -1
  7. package/plugin/README.md +11 -10
  8. package/plugin/dashboard/app/layout.tsx +20 -0
  9. package/plugin/opencode/dist/server.mjs +76 -2
  10. package/plugin/opencode/server.mts +79 -2
  11. package/plugin/pyproject.toml +6 -2
  12. package/plugin/scripts/smart-install.sh +7 -1
  13. package/plugin/src/claude_smart/cli.py +210 -22
  14. package/plugin/src/claude_smart/context_format.py +9 -9
  15. package/plugin/src/claude_smart/cs_cite.py +66 -19
  16. package/plugin/uv.lock +5 -5
  17. package/plugin/vendor/reflexio/README.md +3 -3
  18. package/plugin/vendor/reflexio/pyproject.toml +1 -1
  19. package/plugin/vendor/reflexio/reflexio/README.md +3 -1
  20. package/plugin/vendor/reflexio/reflexio/cli/bootstrap_config.py +1 -1
  21. package/plugin/vendor/reflexio/reflexio/cli/commands/setup_cmd.py +2 -2
  22. package/plugin/vendor/reflexio/reflexio/cli/utils.py +44 -1
  23. package/plugin/vendor/reflexio/reflexio/client/client.py +97 -0
  24. package/plugin/vendor/reflexio/reflexio/lib/_agent_playbook.py +8 -0
  25. package/plugin/vendor/reflexio/reflexio/lib/_base.py +15 -0
  26. package/plugin/vendor/reflexio/reflexio/lib/_generation.py +9 -8
  27. package/plugin/vendor/reflexio/reflexio/lib/_profiles.py +27 -16
  28. package/plugin/vendor/reflexio/reflexio/lib/_search.py +23 -5
  29. package/plugin/vendor/reflexio/reflexio/lib/_user_playbook.py +9 -0
  30. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/__init__.py +1 -0
  31. package/plugin/vendor/reflexio/reflexio/models/api_schema/domain/governance.py +117 -0
  32. package/plugin/vendor/reflexio/reflexio/models/api_schema/retriever_schema.py +45 -3
  33. package/plugin/vendor/reflexio/reflexio/models/config_schema.py +16 -0
  34. package/plugin/vendor/reflexio/reflexio/server/README.md +14 -2
  35. package/plugin/vendor/reflexio/reflexio/server/api.py +176 -29
  36. package/plugin/vendor/reflexio/reflexio/server/api_endpoints/request_context.py +1 -1
  37. package/plugin/vendor/reflexio/reflexio/server/{_auth.py → auth.py} +2 -0
  38. package/plugin/vendor/reflexio/reflexio/server/extensions.py +213 -0
  39. package/plugin/vendor/reflexio/reflexio/server/llm/model_defaults.py +4 -4
  40. package/plugin/vendor/reflexio/reflexio/server/llm/providers/claude_code_provider.py +1 -1
  41. package/plugin/vendor/reflexio/reflexio/server/llm/rerank/cross_encoder_reranker.py +12 -1
  42. package/plugin/vendor/reflexio/reflexio/server/prompt/prompt_bank/playbook_extraction_context/v4.4.0.prompt.md +14 -2
  43. package/plugin/vendor/reflexio/reflexio/server/services/README.md +3 -1
  44. package/plugin/vendor/reflexio/reflexio/server/services/extraction/resume_worker.py +8 -0
  45. package/plugin/vendor/reflexio/reflexio/server/services/governance/config.py +52 -0
  46. package/plugin/vendor/reflexio/reflexio/server/services/governance/service.py +378 -0
  47. package/plugin/vendor/reflexio/reflexio/server/services/governance/subject_refs.py +34 -0
  48. package/plugin/vendor/reflexio/reflexio/server/services/lineage/gc_scheduler.py +45 -19
  49. package/plugin/vendor/reflexio/reflexio/server/services/playbook/README.md +9 -1
  50. package/plugin/vendor/reflexio/reflexio/server/services/playbook/aggregation_prompt_processing.py +100 -0
  51. package/plugin/vendor/reflexio/reflexio/server/services/playbook/components/aggregator.py +129 -111
  52. package/plugin/vendor/reflexio/reflexio/server/services/playbook/service.py +9 -9
  53. package/plugin/vendor/reflexio/reflexio/server/services/profile/components/extractor.py +3 -2
  54. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/recency.py +211 -0
  55. package/plugin/vendor/reflexio/reflexio/server/services/retrieval/relevance_floor.py +29 -13
  56. package/plugin/vendor/reflexio/reflexio/server/services/storage/error.py +4 -0
  57. package/plugin/vendor/reflexio/reflexio/server/services/storage/governance_validation.py +681 -0
  58. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/__init__.py +14 -2
  59. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_base.py +2 -0
  60. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_extras.py +49 -19
  61. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_governance.py +1965 -0
  62. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_lineage.py +5 -3
  63. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_playbook.py +1 -2133
  64. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_profiles.py +262 -107
  65. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/_requests.py +73 -33
  66. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/__init__.py +13 -0
  67. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_agent.py +952 -0
  68. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_eval_results.py +189 -0
  69. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_optimization.py +247 -0
  70. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_source_linkage.py +145 -0
  71. package/plugin/vendor/reflexio/reflexio/server/services/storage/sqlite_storage/playbook/_user.py +838 -0
  72. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/__init__.py +19 -3
  73. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_extras.py +4 -4
  74. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_governance.py +148 -0
  75. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_playbook.py +0 -909
  76. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_profiles.py +13 -0
  77. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/_requests.py +2 -0
  78. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/__init__.py +13 -0
  79. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_agent.py +365 -0
  80. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_eval_results.py +124 -0
  81. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_optimization.py +85 -0
  82. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_source_linkage.py +47 -0
  83. package/plugin/vendor/reflexio/reflexio/server/services/storage/storage_base/playbook/_user.py +333 -0
  84. package/plugin/vendor/reflexio/reflexio/server/services/unified_search_service.py +153 -12
  85. package/plugin/vendor/reflexio/reflexio/server/services/playbook/user_detail_stripping.py +0 -84
@@ -6,7 +6,10 @@ import time
6
6
  from collections.abc import Callable
7
7
  from concurrent.futures import ThreadPoolExecutor
8
8
  from datetime import UTC, datetime
9
- from typing import Any
9
+ from typing import TYPE_CHECKING, Any
10
+
11
+ if TYPE_CHECKING:
12
+ from reflexio.server.extensions import AppContext, CapabilityRegistry
10
13
 
11
14
  from anyio.to_thread import current_default_thread_limiter
12
15
  from fastapi import (
@@ -162,12 +165,6 @@ from reflexio.models.config_schema import (
162
165
  SINGLETON_AGENT_SUCCESS_EVALUATION_NAME,
163
166
  Config,
164
167
  )
165
- from reflexio.server._auth import (
166
- DEFAULT_ORG_ID,
167
- default_billing_gate,
168
- default_get_caller_type,
169
- default_get_org_id,
170
- )
171
168
  from reflexio.server.api_endpoints import (
172
169
  account_api,
173
170
  health_api,
@@ -175,6 +172,12 @@ from reflexio.server.api_endpoints import (
175
172
  publisher_api,
176
173
  stall_state_api,
177
174
  )
175
+ from reflexio.server.auth import (
176
+ DEFAULT_ORG_ID,
177
+ default_billing_gate,
178
+ default_get_caller_type,
179
+ default_get_org_id,
180
+ )
178
181
  from reflexio.server.cache.reflexio_cache import (
179
182
  get_reflexio,
180
183
  invalidate_reflexio_cache,
@@ -1845,6 +1848,12 @@ def get_all_profiles(
1845
1848
  limit: int = 100,
1846
1849
  status_filter: str | None = None,
1847
1850
  profile_id: str | None = None,
1851
+ user_id: str | None = None,
1852
+ query: str | None = None,
1853
+ source: str | None = None,
1854
+ profile_time_to_live: str | None = None,
1855
+ start_time: int | None = None,
1856
+ end_time: int | None = None,
1848
1857
  include_tombstones: bool = False,
1849
1858
  org_id: str = Depends(default_get_org_id),
1850
1859
  ) -> GetProfilesViewResponse:
@@ -1854,6 +1863,12 @@ def get_all_profiles(
1854
1863
  limit (int, optional): Maximum number of profiles to return. Defaults to 100.
1855
1864
  status_filter (str, optional): Filter by profile status. Can be "current", "pending", or "archived".
1856
1865
  profile_id (str, optional): Exact profile ID to retrieve.
1866
+ user_id (str, optional): Exact user ID to filter by.
1867
+ query (str, optional): Case-insensitive text filter across visible fields.
1868
+ source (str, optional): Exact profile source to filter by.
1869
+ profile_time_to_live (str, optional): Exact TTL value to filter by.
1870
+ start_time (int, optional): Minimum last-modified epoch seconds.
1871
+ end_time (int, optional): Maximum last-modified epoch seconds.
1857
1872
  include_tombstones (bool, optional): Include merged/superseded rows when
1858
1873
  looking up a specific profile_id.
1859
1874
  org_id (str): Organization ID
@@ -1863,7 +1878,7 @@ def get_all_profiles(
1863
1878
  """
1864
1879
  reflexio = get_reflexio(org_id=org_id)
1865
1880
 
1866
- if profile_id:
1881
+ if profile_id and include_tombstones:
1867
1882
  storage = reflexio.request_context.storage
1868
1883
  if storage is None:
1869
1884
  return GetProfilesViewResponse(
@@ -1890,7 +1905,17 @@ def get_all_profiles(
1890
1905
  elif status_filter == "archived":
1891
1906
  status_filter_list = [Status.ARCHIVED]
1892
1907
 
1893
- response = reflexio.get_all_profiles(limit=limit, status_filter=status_filter_list) # type: ignore[reportArgumentType]
1908
+ response = reflexio.get_all_profiles(
1909
+ limit=limit,
1910
+ status_filter=status_filter_list, # type: ignore[reportArgumentType]
1911
+ user_id=user_id,
1912
+ profile_id=profile_id,
1913
+ query=query,
1914
+ source=source,
1915
+ profile_time_to_live=profile_time_to_live,
1916
+ start_time=start_time,
1917
+ end_time=end_time,
1918
+ )
1894
1919
  return GetProfilesViewResponse(
1895
1920
  success=response.success,
1896
1921
  user_profiles=[to_profile_view(p) for p in response.user_profiles],
@@ -3302,6 +3327,97 @@ def _add_openapi_security(app: FastAPI) -> None:
3302
3327
  app.openapi = custom_openapi # type: ignore[method-assign]
3303
3328
 
3304
3329
 
3330
+ def _resolve_lifespan_org_id(get_org_id: Callable[..., str] | None) -> str:
3331
+ """Resolve the bootstrap org ID for lifespan schedulers without a request context.
3332
+
3333
+ Args:
3334
+ get_org_id (Callable[..., str] | None): Custom org-ID dependency, or None for
3335
+ the default single-tenant mode.
3336
+
3337
+ Returns:
3338
+ str: The resolved org ID string.
3339
+ """
3340
+ from reflexio.server.auth import default_get_org_id
3341
+
3342
+ if get_org_id is None:
3343
+ return default_get_org_id()
3344
+ try:
3345
+ signature = inspect.signature(get_org_id)
3346
+ except (TypeError, ValueError):
3347
+ return default_get_org_id()
3348
+ if signature.parameters:
3349
+ return default_get_org_id()
3350
+ try:
3351
+ return str(get_org_id())
3352
+ except Exception:
3353
+ logger.exception("Failed to resolve lifespan org_id; using default org")
3354
+ return default_get_org_id()
3355
+
3356
+
3357
+ def _wire_capabilities(
3358
+ app: FastAPI,
3359
+ capabilities: "CapabilityRegistry | None",
3360
+ mount_data_plane: bool,
3361
+ additional_routers: list[APIRouter] | None = None,
3362
+ ) -> None:
3363
+ """Wire capability routers, services, and hooks into the app at construction time.
3364
+
3365
+ No-op when ``capabilities`` is None.
3366
+
3367
+ The deployment role passed to each capability's ``routers(role)`` is
3368
+ sourced from ``capabilities.role`` when set (threaded in by the enterprise
3369
+ composition root via ``build_registry(deployment_role())``). When
3370
+ ``capabilities.role`` is ``None`` the role falls back to the
3371
+ ``mount_data_plane`` derivation (``"all"`` when True, ``"control-plane"``
3372
+ when False), preserving the existing OSS-only behaviour.
3373
+
3374
+ Args:
3375
+ app (FastAPI): The application instance to wire into.
3376
+ capabilities (CapabilityRegistry | None): Capabilities to install, or None.
3377
+ mount_data_plane (bool): Whether the data-plane role is active.
3378
+ additional_routers (list[APIRouter] | None): Routers already mounted via
3379
+ ``additional_routers``; used to detect and reject double-mounts at boot.
3380
+
3381
+ Raises:
3382
+ ValueError: If a capability's router object is also present in
3383
+ ``additional_routers`` — each router must be mounted exactly once.
3384
+ """
3385
+ if capabilities is None:
3386
+ return
3387
+ from reflexio.server.auth import default_billing_gate
3388
+ from reflexio.server.extensions import HookRegistry
3389
+
3390
+ if capabilities.role is not None:
3391
+ role = capabilities.role
3392
+ else:
3393
+ role = "all" if mount_data_plane else "control-plane"
3394
+ if capabilities.configurator_class is not None:
3395
+ from reflexio.server.services.configurator.configurator import (
3396
+ set_configurator_class,
3397
+ )
3398
+
3399
+ set_configurator_class(capabilities.configurator_class)
3400
+ if capabilities.billing_gate is not None:
3401
+ for line in ("application", "learnings_generated"):
3402
+ app.dependency_overrides[default_billing_gate(line)] = (
3403
+ capabilities.billing_gate(line)
3404
+ )
3405
+ # HookRegistry is a stateless facade: its methods configure process-global
3406
+ # tracer/usage-recorder/retrieval-capture singletons; the object needn't be stored.
3407
+ hooks = HookRegistry()
3408
+ additional = additional_routers or []
3409
+ for cap in capabilities.capabilities:
3410
+ cap.install_services()
3411
+ cap.install_hooks(hooks)
3412
+ for r in cap.routers(role):
3413
+ if any(r is a for a in additional):
3414
+ raise ValueError(
3415
+ f"router for capability {cap.name!r} is mounted both via the "
3416
+ f"registry and additional_routers; mount it exactly once"
3417
+ )
3418
+ app.include_router(r)
3419
+
3420
+
3305
3421
  def create_app(
3306
3422
  get_org_id: Callable[..., str] | None = None,
3307
3423
  additional_routers: list[APIRouter] | None = None,
@@ -3310,6 +3426,8 @@ def create_app(
3310
3426
  get_caller_type: Callable[..., str] | None = None,
3311
3427
  get_billing_gate: Callable[[str], Callable[..., None]] | None = None,
3312
3428
  mount_data_plane: bool = True,
3429
+ capabilities: "CapabilityRegistry | None" = None,
3430
+ app_context_factory: "Callable[[], AppContext] | None" = None,
3313
3431
  ) -> FastAPI:
3314
3432
  """Factory to create a FastAPI app.
3315
3433
 
@@ -3337,19 +3455,40 @@ def create_app(
3337
3455
  scheduler, while keeping all other scaffolding (middleware, CORS,
3338
3456
  auth overrides, OpenAPI security, health, ``/meta/version``,
3339
3457
  ``additional_routers``).
3458
+ capabilities: Optional registry of enterprise capabilities. When provided,
3459
+ each capability's routers, services, hooks, and lifecycle methods are
3460
+ wired into the app at construction time. Behavior is unchanged when
3461
+ ``None``.
3462
+ app_context_factory: Optional callable returning the AppContext passed to
3463
+ each capability's on_startup. When None, an empty AppContext() is used
3464
+ (local OSS / tests). Enterprise binds this to supply self_host_org_id /
3465
+ activated computed during its own startup.
3340
3466
 
3341
3467
  Returns:
3342
3468
  Configured FastAPI application.
3343
- """
3469
+
3470
+ Raises:
3471
+ ValueError: If both ``get_billing_gate`` and ``capabilities.billing_gate``
3472
+ are provided — pass billing_gate through exactly one path.
3473
+ """
3474
+ if (
3475
+ get_billing_gate is not None
3476
+ and capabilities is not None
3477
+ and capabilities.billing_gate is not None
3478
+ ):
3479
+ raise ValueError(
3480
+ "pass billing_gate via either get_billing_gate or capabilities, not both"
3481
+ )
3344
3482
  from collections.abc import AsyncIterator
3345
3483
  from contextlib import asynccontextmanager
3346
3484
 
3347
- from reflexio.server._auth import (
3485
+ from reflexio.server.api_endpoints.request_context import RequestContext
3486
+ from reflexio.server.auth import (
3348
3487
  default_billing_gate,
3349
3488
  default_get_caller_type,
3350
3489
  default_get_org_id,
3351
3490
  )
3352
- from reflexio.server.api_endpoints.request_context import RequestContext
3491
+ from reflexio.server.extensions import AppContext
3353
3492
  from reflexio.server.llm.model_defaults import validate_llm_availability
3354
3493
  from reflexio.server.services.extraction.resume_scheduler import (
3355
3494
  maybe_start_resume_scheduler,
@@ -3358,25 +3497,11 @@ def create_app(
3358
3497
  maybe_start_lineage_gc,
3359
3498
  )
3360
3499
 
3361
- def _lifespan_org_id() -> str:
3362
- if get_org_id is None:
3363
- return default_get_org_id()
3364
- try:
3365
- signature = inspect.signature(get_org_id)
3366
- except (TypeError, ValueError):
3367
- return default_get_org_id()
3368
- if signature.parameters:
3369
- return default_get_org_id()
3370
- try:
3371
- return str(get_org_id())
3372
- except Exception:
3373
- logger.exception("Failed to resolve lifespan org_id; using default org")
3374
- return default_get_org_id()
3375
-
3376
3500
  @asynccontextmanager
3377
3501
  async def lifespan(app: FastAPI) -> AsyncIterator[None]: # noqa: ARG001
3378
3502
  scheduler = None
3379
3503
  gc_scheduler = None
3504
+ started_caps: list = []
3380
3505
  if mount_data_plane:
3381
3506
  log_publish_hardware_capacity()
3382
3507
  validate_llm_availability()
@@ -3387,17 +3512,36 @@ def create_app(
3387
3512
  # drives a per-org worker with org-scoped claims, so it is not limited
3388
3513
  # to the bootstrap org. The bootstrap org is only used to read config
3389
3514
  # and to seed cross-org discovery.
3515
+ bootstrap_org_id = _resolve_lifespan_org_id(get_org_id)
3390
3516
  scheduler = maybe_start_resume_scheduler(
3391
3517
  lambda org_id: RequestContext(org_id=org_id),
3392
- bootstrap_org_id=_lifespan_org_id(),
3518
+ bootstrap_org_id=bootstrap_org_id,
3393
3519
  )
3394
3520
  gc_scheduler = maybe_start_lineage_gc(
3395
3521
  lambda org_id: RequestContext(org_id=org_id),
3396
- bootstrap_org_id=_lifespan_org_id(),
3522
+ bootstrap_org_id=bootstrap_org_id,
3397
3523
  )
3398
3524
  try:
3525
+ if capabilities is not None:
3526
+ ctx = (
3527
+ app_context_factory()
3528
+ if app_context_factory is not None
3529
+ else AppContext()
3530
+ )
3531
+ for cap in capabilities.capabilities:
3532
+ await cap.on_startup(ctx)
3533
+ started_caps.append(cap)
3399
3534
  yield
3400
3535
  finally:
3536
+ for cap in reversed(started_caps):
3537
+ try:
3538
+ await cap.on_shutdown()
3539
+ except Exception:
3540
+ logger.warning(
3541
+ "capability %r on_shutdown raised; continuing cleanup",
3542
+ cap,
3543
+ exc_info=True,
3544
+ )
3401
3545
  if scheduler is not None:
3402
3546
  scheduler.stop()
3403
3547
  if gc_scheduler is not None:
@@ -3507,6 +3651,9 @@ def create_app(
3507
3651
  for router in additional_routers or []:
3508
3652
  app.include_router(router)
3509
3653
 
3654
+ # Wire capability routers, services, and hooks (composition-root only)
3655
+ _wire_capabilities(app, capabilities, mount_data_plane, additional_routers)
3656
+
3510
3657
  # Health/observability endpoint (per-worker metrics for recycling)
3511
3658
  health_api.install(app)
3512
3659
 
@@ -1,6 +1,6 @@
1
1
  from fastapi import Depends
2
2
 
3
- from reflexio.server._auth import default_get_org_id
3
+ from reflexio.server.auth import default_get_org_id
4
4
  from reflexio.server.prompt.prompt_manager import PromptManager
5
5
  from reflexio.server.services.configurator.base_configurator import BaseConfigurator
6
6
  from reflexio.server.services.configurator.configurator import get_configurator_class
@@ -5,6 +5,8 @@ Lives in its own module to avoid an import cycle: api endpoints reference
5
5
  :func:`default_get_org_id` (e.g. via FastAPI ``Depends``) and are themselves
6
6
  imported by :mod:`reflexio.server.api`. Putting the dependency here keeps the
7
7
  import graph acyclic.
8
+
9
+ Public module path: :mod:`reflexio.server.auth`.
8
10
  """
9
11
 
10
12
  from __future__ import annotations
@@ -0,0 +1,213 @@
1
+ """OSS extension primitives: the process-global capability/service registry.
2
+
3
+ OSS defines these seams; enterprise registers implementations at its composition
4
+ root. OSS never imports reflexio_ext. The service registry is process-global (a
5
+ module dict), mirroring set_configurator_class/get_configurator_class, so it is
6
+ reachable from every RequestContext construction site — HTTP, library
7
+ (lib/_base.py), and background workers — not just app-scoped ones.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from abc import ABC
13
+ from collections.abc import Callable
14
+ from dataclasses import dataclass
15
+ from typing import ClassVar
16
+
17
+ from fastapi import APIRouter
18
+
19
+ from reflexio.server.services.unified_search_service import (
20
+ RetrievalCaptureHook,
21
+ configure_retrieval_capture_hook,
22
+ )
23
+ from reflexio.server.tracing import Tracer, configure_tracer
24
+ from reflexio.server.usage_metrics import (
25
+ UsageEventRecorder,
26
+ configure_usage_event_recorder,
27
+ )
28
+
29
+
30
+ class ServiceKey[T]:
31
+ """Typed handle into the runtime-service registry.
32
+
33
+ Args:
34
+ name (str): Stable identifier, used in error messages and as the dict key.
35
+ """
36
+
37
+ def __init__(self, name: str) -> None:
38
+ self.name = name
39
+
40
+ def __repr__(self) -> str:
41
+ return f"ServiceKey({self.name!r})"
42
+
43
+
44
+ _services: dict[str, object] = {}
45
+
46
+
47
+ def register_service[T](
48
+ key: ServiceKey[T], value: T, *, override: bool = False
49
+ ) -> None:
50
+ """Register a runtime-service provider under ``key`` (composition root only).
51
+
52
+ Args:
53
+ key (ServiceKey[T]): The typed key.
54
+ value (T): The provider/value.
55
+ override (bool): Allow replacing an existing registration. Default False.
56
+
57
+ Raises:
58
+ RuntimeError: If ``key`` is already registered and ``override`` is False.
59
+ """
60
+ if not override and key.name in _services:
61
+ raise RuntimeError(f"service already registered for key {key.name!r}")
62
+ _services[key.name] = value
63
+
64
+
65
+ def get_service[T](key: ServiceKey[T]) -> T | None:
66
+ """Return the provider for ``key``, or None if unset (optional providers)."""
67
+ return _services.get(key.name) # type: ignore[return-value]
68
+
69
+
70
+ def require_service[T](key: ServiceKey[T]) -> T:
71
+ """Return the provider for ``key``, raising if unset (required providers).
72
+
73
+ Args:
74
+ key (ServiceKey[T]): The typed key whose provider to retrieve.
75
+
76
+ Returns:
77
+ T: The registered provider value.
78
+
79
+ Raises:
80
+ LookupError: If no provider is registered for ``key``.
81
+ """
82
+ if key.name not in _services:
83
+ raise LookupError(f"required service not registered for key {key.name!r}")
84
+ return _services[key.name] # type: ignore[return-value]
85
+
86
+
87
+ def reset_services() -> None:
88
+ """Clear all registered services. Test-only."""
89
+ _services.clear()
90
+
91
+
92
+ # ---------------------------------------------------------------------------
93
+ # Hook registry, app context, and capability lifecycle primitives
94
+ # ---------------------------------------------------------------------------
95
+
96
+
97
+ class HookRegistry:
98
+ """Facade over OSS process-global hook setters; capabilities install through it."""
99
+
100
+ def set_tracer(self, tracer: Tracer | None) -> None:
101
+ """Install (or clear) the process-global request tracer.
102
+
103
+ Args:
104
+ tracer (Tracer | None): Tracer implementation, or None to disable.
105
+ """
106
+ configure_tracer(tracer)
107
+
108
+ def set_usage_recorder(self, recorder: UsageEventRecorder | None) -> None:
109
+ """Install (or clear) the process-global usage-event recorder.
110
+
111
+ Args:
112
+ recorder (UsageEventRecorder | None): Recorder callable, or None to disable.
113
+ """
114
+ configure_usage_event_recorder(recorder)
115
+
116
+ def set_retrieval_capture(self, hook: RetrievalCaptureHook | None) -> None:
117
+ """Install (or clear) the process-global retrieval-capture hook.
118
+
119
+ Args:
120
+ hook (RetrievalCaptureHook | None): Hook callable, or None to disable.
121
+ """
122
+ configure_retrieval_capture_hook(hook)
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class AppContext:
127
+ """Inter-step startup data passed to Capability.on_startup.
128
+
129
+ Mirrors the values that thread through reflexio_ext lifespan locals
130
+ (prepare_enterprise_startup output).
131
+
132
+ Attributes:
133
+ self_host_org_id (str | None): Org ID for self-hosted deployments, or None for platform.
134
+ activated (bool): Whether the enterprise activation check passed.
135
+ """
136
+
137
+ self_host_org_id: str | None = None
138
+ activated: bool = False
139
+
140
+
141
+ class Capability(ABC):
142
+ """An enterprise feature plugged into the app at its composition root.
143
+
144
+ All methods are no-op defaults so the registry can drive every capability
145
+ through the same phases without hasattr checks. The provider owns this
146
+ lifecycle shape (ABC); runtime-feature *contracts* are Protocols defined by OSS.
147
+ """
148
+
149
+ name: ClassVar[str]
150
+
151
+ def install_services(self) -> None: # noqa: B027
152
+ """Register runtime-service providers (process-global). Default: none."""
153
+
154
+ def install_hooks(self, hooks: HookRegistry) -> None: # noqa: B027
155
+ """Install pipeline hooks (tracer/usage-recorder/retrieval-capture). Default: none.
156
+
157
+ Args:
158
+ hooks (HookRegistry): Registry to install hooks through.
159
+ """
160
+
161
+ def routers(self, role: str) -> list[APIRouter]: # noqa: ARG002
162
+ """Return routers to mount for this deployment role. Default: none.
163
+
164
+ Args:
165
+ role (str): Deployment role (e.g. "all", "data-plane").
166
+
167
+ Returns:
168
+ list[APIRouter]: Routers to mount; empty list by default.
169
+ """
170
+ return []
171
+
172
+ async def on_startup(self, ctx: AppContext) -> None: # noqa: B027
173
+ """Start daemons/schedulers. Raising aborts boot (fail-loud). Default: none.
174
+
175
+ Args:
176
+ ctx (AppContext): Startup context produced by prepare_enterprise_startup.
177
+ """
178
+
179
+ async def on_shutdown(self) -> None: # noqa: B027
180
+ """Stop what on_startup started. Default: none."""
181
+
182
+
183
+ class CapabilityRegistry:
184
+ """Holds additive capabilities plus the two singular construction-time concerns.
185
+
186
+ Args:
187
+ capabilities (list[Capability]): Capability instances; names must be unique.
188
+ configurator_class (type | None): Single configurator class, or None.
189
+ billing_gate (Callable | None): Billing gate factory, or None.
190
+ role (str | None): Deployment role to thread into ``_wire_capabilities``
191
+ (e.g. ``"all"``, ``"data-plane"``). When ``None``, ``create_app``
192
+ derives the role from ``mount_data_plane``.
193
+
194
+ Raises:
195
+ RuntimeError: If two capabilities share the same name.
196
+ """
197
+
198
+ def __init__(
199
+ self,
200
+ capabilities: list[Capability],
201
+ *,
202
+ configurator_class: type | None = None,
203
+ billing_gate: Callable[[str], Callable[..., None]] | None = None,
204
+ role: str | None = None,
205
+ ) -> None:
206
+ names = [c.name for c in capabilities]
207
+ dupes = {n for n in names if names.count(n) > 1}
208
+ if dupes:
209
+ raise RuntimeError(f"duplicate capability names: {sorted(dupes)}")
210
+ self.capabilities = capabilities
211
+ self.configurator_class = configurator_class
212
+ self.billing_gate = billing_gate
213
+ self.role = role
@@ -195,12 +195,12 @@ _PROVIDER_DEFAULTS: dict[str, ProviderDefaults] = {
195
195
  extraction_agent="gpt-5.5",
196
196
  ),
197
197
  "anthropic": ProviderDefaults(
198
- generation="claude-sonnet-4-6",
199
- evaluation="claude-sonnet-4-6",
198
+ generation="claude-sonnet-5",
199
+ evaluation="claude-sonnet-5",
200
200
  should_run="claude-haiku-4-5-20251001",
201
201
  pre_retrieval="claude-haiku-4-5-20251001",
202
202
  embedding=None,
203
- extraction_agent="claude-sonnet-4-6",
203
+ extraction_agent="claude-sonnet-5",
204
204
  ),
205
205
  "gemini": ProviderDefaults(
206
206
  generation="gemini/gemini-3-flash-preview",
@@ -229,7 +229,7 @@ _PROVIDER_DEFAULTS: dict[str, ProviderDefaults] = {
229
229
  should_run="minimax/MiniMax-M3",
230
230
  pre_retrieval="minimax/MiniMax-M3",
231
231
  embedding=None,
232
- # Same M2.7 model handles resumable extraction. Surfaced by an
232
+ # Same M3 model handles resumable extraction. Surfaced by an
233
233
  # e2e run on a MiniMax-only VPS where publish printed
234
234
  # "No provider in ['minimax'] supports role=extraction_agent"
235
235
  # warnings and silently skipped profile creation. Without this,
@@ -69,7 +69,7 @@ _CODEX_COMPAT_SCRIPT_NAMES = (
69
69
  )
70
70
  _CODEX_COMPAT_SCRIPT_NAME_SET = set(_CODEX_COMPAT_SCRIPT_NAMES)
71
71
  _DEFAULT_TIMEOUT_SECONDS = 120
72
- _DEFAULT_CLI_MODEL = "claude-sonnet-4-6"
72
+ _DEFAULT_CLI_MODEL = "claude-sonnet-5"
73
73
 
74
74
  _TRUTHY_ENV_VALUES = {"1", "true", "yes"}
75
75
  _UNSUPPORTED_PARAMS_WARNED: set[str] = set()
@@ -150,7 +150,18 @@ def score_pairs(query: str, docs: list[str]) -> list[float]:
150
150
  return []
151
151
  model = _get_model()
152
152
  pairs = [(query, doc) for doc in docs]
153
- raw_scores = model.predict(pairs, show_progress_bar=False)
153
+ try:
154
+ from torch import nn
155
+ except ImportError as e:
156
+ raise CrossEncoderUnavailableError(
157
+ "torch is not installed; cannot use the cross-encoder reranker"
158
+ ) from e
159
+
160
+ raw_scores = model.predict(
161
+ pairs,
162
+ show_progress_bar=False,
163
+ activation_fn=nn.Identity(),
164
+ )
154
165
  # ``predict`` returns a numpy array; convert to plain Python floats so
155
166
  # the caller can serialise the result without numpy as a dependency.
156
167
  return [float(s) for s in raw_scores]
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  active: true
3
3
  description: "Context setting prompt for resumable playbook extraction. Every claim must be grounded in the conversation and generalized to a reusable task context; both requirements apply jointly to Correction SOPs and Success Path Recipes."
4
- changelog: "v4.4.0: deliver the result as a native structured response — output the {{\"playbooks\": [...]}} JSON directly as the final assistant message instead of a finish_extraction tool call; ask_human/attach_pending_info_request remain real intermediate tools, and a plain (no-tool) final turn carrying the JSON is now the valid completion. No output schema change. v4.3.0: expands Correction SOP signals to include avoidable follow-ups, partial delivery, stalled conditional authorization, unverified claims, and incomplete coverage; requires Correction SOPs whenever correction signals are present; adds antecedent-not-reaction trigger guidance. (in-place 2026-06-18: make triggers future-user-query-like retrieval keys.) (in-place 2026-06-15: adds concise retrieve-general/action-specific guidance.) v4.2.3: strengthens strict tool-call discipline so plain-text/no-tool responses are explicitly invalid, including empty extraction outcomes. v4.2.2: replaces output examples with compact tool-call and JSON-shape guidance, while preserving the resumable extraction contract. v4.2.1: negative/avoid guidance cannot contradict the final verified implementation or final evaluation. v4.2.0: adds emergent skill-convention guidance for structuring multi-aspect playbook content as a small set of grouped do/avoid rules (no schema change). v4.1.7: adds resumable extraction guidance, names legacy fallback and malformed-input branches in boundary-change recipes, and avoids a polarity output field. (in-place 2026-05-30: tool-oriented finish_extraction output framing; deprecated and removed blocking_issue.) (in-place 2026-05-30: clarify in the Output Format section that finish_extraction may be preceded by ask_human/attach_pending_info_request; restructure examples into one no-tool example plus a worked ask_human and attach_pending_info_request example; stress that ask_human is rare and reserved for critical missing org-level facts — finish_extraction alone is the norm.) (in-place 2026-05-30: condense the resumable guidance — state finish-required/optional once, replace the duplicated Output Format block with a one-line pointer, and trim example prose.) (in-place 2026-06-03: consolidate and shorten the ask_human condition: ask only for missing shared context needed to know what to do, while still finishing extraction.) (in-place 2026-06-03: frame finish_extraction as the final completion tool and pending-info tools as intermediate.) (in-place 2026-06-03: clarify that empty finish_extraction alone must not replace ask_human when the ask_human condition applies.) (in-place 2026-06-04: clarify ask_human and attach_pending_info_request are alternatives for the same gap, not a sequence.)"
4
+ changelog: "v4.4.0: deliver the result as a native structured response — output the {{\"playbooks\": [...]}} JSON directly as the final assistant message instead of a finish_extraction tool call; ask_human/attach_pending_info_request remain real intermediate tools, and a plain (no-tool) final turn carrying the JSON is now the valid completion. No output schema change. (in-place 2026-06-28: strengthen the ask_human decision boundary for cases where a correction proves a durable shared procedure exists but only avoidance is grounded.) v4.3.0: expands Correction SOP signals to include avoidable follow-ups, partial delivery, stalled conditional authorization, unverified claims, and incomplete coverage; requires Correction SOPs whenever correction signals are present; adds antecedent-not-reaction trigger guidance. (in-place 2026-06-18: make triggers future-user-query-like retrieval keys.) (in-place 2026-06-15: adds concise retrieve-general/action-specific guidance.) v4.2.3: strengthens strict tool-call discipline so plain-text/no-tool responses are explicitly invalid, including empty extraction outcomes. v4.2.2: replaces output examples with compact tool-call and JSON-shape guidance, while preserving the resumable extraction contract. v4.2.1: negative/avoid guidance cannot contradict the final verified implementation or final evaluation. v4.2.0: adds emergent skill-convention guidance for structuring multi-aspect playbook content as a small set of grouped do/avoid rules (no schema change). v4.1.7: adds resumable extraction guidance, names legacy fallback and malformed-input branches in boundary-change recipes, and avoids a polarity output field. (in-place 2026-05-30: tool-oriented finish_extraction output framing; deprecated and removed blocking_issue.) (in-place 2026-05-30: clarify in the Output Format section that finish_extraction may be preceded by ask_human/attach_pending_info_request; restructure examples into one no-tool example plus a worked ask_human and attach_pending_info_request example; stress that ask_human is rare and reserved for critical missing org-level facts — finish_extraction alone is the norm.) (in-place 2026-05-30: condense the resumable guidance — state finish-required/optional once, replace the duplicated Output Format block with a one-line pointer, and trim example prose.) (in-place 2026-06-03: consolidate and shorten the ask_human condition: ask only for missing shared context needed to know what to do, while still finishing extraction.) (in-place 2026-06-03: frame finish_extraction as the final completion tool and pending-info tools as intermediate.) (in-place 2026-06-03: clarify that empty finish_extraction alone must not replace ask_human when the ask_human condition applies.) (in-place 2026-06-04: clarify ask_human and attach_pending_info_request are alternatives for the same gap, not a sequence.)"
5
5
  variables:
6
6
  - agent_context_prompt
7
7
  - extraction_definition_prompt
@@ -26,6 +26,16 @@ Choose exactly one intermediate path per missing fact: use `attach_pending_info_
26
26
 
27
27
  Do not ask for user-scoped/private facts, context derivable from the trajectory, agent context, tool list, or Prior Knowledge, complete avoidance-only rules, or merely nice-to-have details.
28
28
 
29
+ ### Ask-human decision boundary
30
+
31
+ Before finalizing any Correction SOP from a negative correction, run this check:
32
+
33
+ 1. Did the correction merely establish a complete avoidance rule, with no evidence that a shared positive procedure exists? If yes, emit the grounded avoidance rule and do not ask.
34
+ 2. Did the correction state or strongly imply that a shared policy, process, approval path, escalation route, canonical target, standard wording, or other durable positive procedure exists, while leaving the actual target/procedure unknown? If yes, the future playbook is incomplete: call `ask_human` (or `attach_pending_info_request` if Prior Knowledge already has the same pending question).
35
+ 3. Did the missing detail concern a single user, customer, client, assignment, record, or current factual lookup rather than a reusable agent/org rule? If yes, do not ask; emit only grounded guidance or no playbook.
36
+
37
+ Phrases like "specific process", "required workflow", "approval path", "special route", "canonical path/target", "standard policy", "standard wording/script", or "do not know the exact name/text" are strong evidence of a shared positive procedure gap when the conversation does not provide the missing procedure. Avoidance-only playbooks are acceptable interim guidance, but they are not a substitute for `ask_human` when the conversation proves that future agents need a missing shared positive action to handle the class of task correctly. The question should ask for the smallest durable missing fact, such as the approved route, required procedure, canonical target, policy threshold, or standard wording.
38
+
29
39
  If an intermediate tool is needed, call exactly one of the two intermediate tools before delivering your final result. Then finalize the current run with any independently valid playbooks now, including avoidance rules; if none are valid without the answer, finalize with `{{"playbooks": []}}`.
30
40
 
31
41
  When the `ask_human` condition applies, an `ask_human` tool call is required before finalization. Do not replace the question with an empty `{{"playbooks": []}}` final response alone.
@@ -161,6 +171,8 @@ Grounding constrains the *source* of a claim (it must come from the conversation
161
171
 
162
172
  **When the agent doesn't know what to do:** Describe what to AVOID (the observed mistake) and state the limitation honestly. It is much better to say "do not fabricate order status — admit you cannot look it up" than to invent a specific alternative the agent may not actually have. If the missing "what to do" detail satisfies the `ask_human` condition above, use the tool; otherwise emit only the grounded avoidance rule or no playbook.
163
173
 
174
+ If a user correction says there is a specific process, approval path, route, standard, or policy but does not provide it, the extractor does not know enough to produce the durable positive SOP. Do not silently downgrade that case to avoidance-only unless avoidance is the complete reusable lesson; ask for the missing shared procedure and still finalize any grounded interim playbooks.
175
+
164
176
  **Rule of thumb (both checks must pass):**
165
177
  1. *Grounded* — if you remove the conversation and only read the `content`, could someone verify every claim by re-reading the conversation? If not, you've hallucinated.
166
178
  2. *Generalized* — could a future agent in a similar task context, with no access to this conversation, apply the entry as written? If not, you've overfit — restate the pattern at the level of situation, artifact role, action sequence, verification, and detour.
@@ -175,7 +187,7 @@ For **Correction SOPs**:
175
187
  3. Identify the violated implicit expectation
176
188
  4. Draft the `trigger` (the problem or situation)
177
189
  5. Tautology Check (see above)
178
- 6. Draft `content`: reason through what the agent did wrong and what the user's feedback tells us the agent should do differently. Ground every statement in evidence from the conversation. If the user told the agent what to do, capture that. If the user only told the agent what NOT to do, capture the avoidance. Do not guess what the right action is if the conversation doesn't tell you; use `ask_human` only when the condition above applies.
190
+ 6. Draft `content`: reason through what the agent did wrong and what the user's feedback tells us the agent should do differently. Ground every statement in evidence from the conversation. If the user told the agent what to do, capture that. If the user only told the agent what NOT to do, capture the avoidance. If the user indicated that a shared process/policy/route/approval/standard/canonical target exists but did not provide it, call `ask_human` or attach to matching Prior Knowledge before finalizing; the avoidance rule alone does not answer what a future agent should do. Do not guess what the right action is if the conversation doesn't tell you; use `ask_human` only when the condition above applies.
179
191
 
180
192
  For **Success Path Recipes**:
181
193
  1. Identify whether the agent completed the task successfully