Coverage for integrations / agent_engine / escalation_reasons.py: 100.0%

19 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-12 04:49 +0000

1"""Canonical taxonomy of *why* the draft-first dispatcher escalated a turn 

2to the expert background path. 

3 

4Design intent 

5============= 

6 

7`dispatch_draft_first` runs several short-circuit guards that promote a 

8turn from "draft answered → done" to "draft was a standby, expert will 

9take over": refusal pattern in the reply, low-confidence ``delegate=none``, 

10agent-bound prompt, classifier-surfaced actionable intent, or the draft's 

11own explicit ``delegate=local/hive``. 

12 

13Today each guard logs its decision and that's it — once we leave the 

14function, callers / observers / telemetry / WorldModelBridge can't tell 

15*why* a given speculation_id ended up in expert-pending state. That 

16matters for three downstream consumers: 

17 

18 1. **WorldModelBridge / continual learning** — distillation should weight 

19 refusal-overridden draft replies very differently from 

20 parse-failure-defaulted ones. Without the reason, the bridge has to 

21 re-derive the heuristic, which is exactly the parallel-path 

22 anti-pattern Gate 4 warns against. 

23 2. **Telemetry / admin diag** — calibration of the draft model's 

24 classifier needs the breakdown of *which* guard fires most often. 

25 Without persisted reasons, calibration has to be re-run against logs. 

26 3. **The collapsed expert path** (next commit) — when we route to the 

27 full langchain path, the system prompt needs to know whether to 

28 bind the full tool registry (actionable intent / agent-bound) or 

29 reason in-band (refusal override / low-confidence verifier). 

30 

31This module is the single source of truth. Everywhere downstream that 

32mentions an escalation reason imports from here — no string-literal 

33parallel paths. 

34""" 

35from __future__ import annotations 

36 

37from enum import Enum 

38from typing import Optional, Union 

39 

40 

41class EscalationReason(str, Enum): 

42 """Why dispatch_draft_first escalated this turn to expert. 

43 

44 Inherits ``str`` so the value round-trips cleanly through JSON 

45 serialisation (telemetry, WorldModelBridge, SSE) and ``==`` works 

46 against both the enum member and the raw string. Matches the 

47 pattern ``ModelTier`` already uses in this package. 

48 """ 

49 

50 #: The draft's own classifier returned ``delegate='local'`` or 

51 #: ``delegate='hive'`` — the model itself decided to hand off. This 

52 #: is the baseline / "expected" reason. All other reasons describe a 

53 #: *forced* escalation where the dispatcher overrode a 

54 #: ``delegate='none'`` decision. 

55 CLASSIFIER_DELEGATE = 'classifier_delegate' 

56 

57 #: The draft emitted a refusal pattern ("I can't…", "Sorry, I'm 

58 #: unable…") which violates the role contract. Standby reply 

59 #: replaces it and expert (with full tool registry) takes the turn. 

60 REFUSAL_OVERRIDE = 'refusal_override' 

61 

62 #: Draft said ``delegate='none'`` but its self-reported confidence 

63 #: fell below ``_DRAFT_CONFIDENCE_FLOOR``. Promote to a local 

64 #: verifier rather than ship an uncertain answer as final. 

65 LOW_CONFIDENCE = 'low_confidence' 

66 

67 #: The prompt_id binds this turn to a specific agent (recipe on 

68 #: disk, not request-id fallback). The user picked a specialist; 

69 #: even trivial replies must pass through that specialist's 

70 #: persona / system prompt / tool registry. 

71 AGENT_BOUND = 'agent_bound' 

72 

73 #: Classifier surfaced an actionable-intent flag (channel_connect, 

74 #: is_create_agent, language_change, invite_intent, join_room_intent, 

75 #: memory_query). Answering in-band would orphan the action — only 

76 #: the expert path has the matching tools bound. 

77 ACTIONABLE_INTENT = 'actionable_intent' 

78 

79 #: Draft envelope failed to parse as JSON. Defaults to a local 

80 #: escalation so we don't ship an unparseable draft as final. Often 

81 #: indicates the draft model needs re-prompting or replacement. 

82 PARSE_FAILURE = 'parse_failure' 

83 

84 

85def coerce(value: Union[None, str, EscalationReason]) -> Optional[EscalationReason]: 

86 """Coerce a possibly-string value to ``EscalationReason``. 

87 

88 Returns ``None`` when ``value`` is None / empty / unrecognised so 

89 callers can use this in legacy paths that may have stamped a raw 

90 string in a dict. Never raises — escalation_reason is observability 

91 metadata; a bad value must not break the chat path. 

92 """ 

93 if value is None or value == '': 

94 return None 

95 if isinstance(value, EscalationReason): 

96 return value 

97 try: 

98 return EscalationReason(str(value)) 

99 except ValueError: 

100 return None