ummaya 0.2.2 → 0.2.4

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 (111) hide show
  1. package/README.md +2 -1
  2. package/npm-shrinkwrap.json +2 -2
  3. package/package.json +1 -1
  4. package/prompts/manifest.yaml +2 -2
  5. package/prompts/session_guidance_v1.md +3 -1
  6. package/prompts/system_v1.md +8 -7
  7. package/pyproject.toml +2 -7
  8. package/src/ummaya/context/builder.py +17 -11
  9. package/src/ummaya/engine/engine.py +27 -7
  10. package/src/ummaya/engine/query.py +20 -0
  11. package/src/ummaya/evidence/__init__.py +25 -0
  12. package/src/ummaya/evidence/__main__.py +7 -0
  13. package/src/ummaya/evidence/models.py +58 -0
  14. package/src/ummaya/evidence/runner.py +308 -0
  15. package/src/ummaya/evidence/task_registry.py +264 -0
  16. package/src/ummaya/ipc/frame_schema.py +47 -0
  17. package/src/ummaya/ipc/stdio.py +1349 -90
  18. package/src/ummaya/llm/client.py +132 -56
  19. package/src/ummaya/llm/reasoning.py +84 -0
  20. package/src/ummaya/tools/discovery_bridge.py +17 -1
  21. package/src/ummaya/tools/executor.py +32 -12
  22. package/src/ummaya/tools/geocoding/kakao_client.py +1 -2
  23. package/src/ummaya/tools/kma/apihub_catalog.py +984 -1
  24. package/src/ummaya/tools/kma/apihub_structured_adapter.py +86 -6
  25. package/src/ummaya/tools/kma/apihub_url_adapter.py +593 -0
  26. package/src/ummaya/tools/kma/apihub_url_catalog.py +296 -0
  27. package/src/ummaya/tools/location_adapters.py +8 -6
  28. package/src/ummaya/tools/manifest_metadata.py +16 -3
  29. package/src/ummaya/tools/mvp_surface.py +2 -2
  30. package/src/ummaya/tools/nmc/emergency_search.py +8 -6
  31. package/src/ummaya/tools/register_all.py +9 -0
  32. package/src/ummaya/tools/resolve_location.py +4 -4
  33. package/src/ummaya/tools/search.py +664 -18
  34. package/src/ummaya/tools/verified_data_go_kr/_manifest.py +115 -25
  35. package/src/ummaya/tools/verified_data_go_kr/airkorea_air_quality.py +109 -4
  36. package/src/ummaya/tools/verified_data_go_kr/nmc_aed_site.py +108 -2
  37. package/src/ummaya/tools/verified_data_go_kr/pps_bid_public_info.py +174 -9
  38. package/src/ummaya/tools/verified_data_go_kr/tago_bus_arrival.py +66 -3
  39. package/src/ummaya/tools/verified_data_go_kr/tago_bus_location.py +12 -2
  40. package/src/ummaya/tools/verified_data_go_kr/tago_bus_route.py +8 -2
  41. package/src/ummaya/tools/verified_data_go_kr/tago_bus_route_station.py +114 -0
  42. package/src/ummaya/tools/verified_data_go_kr/tago_bus_station.py +14 -3
  43. package/src/ummaya/tools/verify_canonical_map.py +21 -0
  44. package/tui/package.json +1 -2
  45. package/tui/src/QueryEngine.ts +4 -0
  46. package/tui/src/cli/handlers/auth.ts +1 -1
  47. package/tui/src/cli/handlers/mcp.tsx +3 -3
  48. package/tui/src/cli/print.ts +69 -18
  49. package/tui/src/cli/update.ts +13 -13
  50. package/tui/src/commands/copy/index.ts +1 -1
  51. package/tui/src/commands/cost/cost.ts +2 -2
  52. package/tui/src/commands/init-verifiers.ts +5 -5
  53. package/tui/src/commands/init.ts +30 -30
  54. package/tui/src/commands/insights.ts +43 -43
  55. package/tui/src/commands/install-github-app/install-github-app.tsx +2 -2
  56. package/tui/src/commands/install-github-app/setupGitHubActions.ts +3 -3
  57. package/tui/src/commands/install.tsx +5 -5
  58. package/tui/src/commands/mcp/addCommand.ts +5 -5
  59. package/tui/src/commands/mcp/xaaIdpCommand.ts +2 -2
  60. package/tui/src/commands/plugin/ManageMarketplaces.tsx +2 -2
  61. package/tui/src/commands/reasoning/index.ts +13 -0
  62. package/tui/src/commands/reasoning/reasoning.tsx +177 -0
  63. package/tui/src/commands/thinkback/thinkback.tsx +3 -3
  64. package/tui/src/commands.ts +2 -0
  65. package/tui/src/components/Messages.tsx +2 -1
  66. package/tui/src/components/Spinner.tsx +2 -2
  67. package/tui/src/components/design-system/LoadingState.tsx +2 -2
  68. package/tui/src/ipc/codec.ts +26 -0
  69. package/tui/src/ipc/frames.generated.ts +398 -303
  70. package/tui/src/ipc/llmClient.ts +130 -51
  71. package/tui/src/ipc/llmTypes.ts +16 -1
  72. package/tui/src/ipc/schema/frame.schema.json +1 -3475
  73. package/tui/src/main.tsx +3 -0
  74. package/tui/src/query.ts +467 -2
  75. package/tui/src/screens/REPL.tsx +3 -3
  76. package/tui/src/services/api/claude.ts +54 -25
  77. package/tui/src/services/api/client.ts +33 -12
  78. package/tui/src/services/api/ummaya.ts +70 -16
  79. package/tui/src/skills/bundled/stuck.ts +12 -12
  80. package/tui/src/state/AppStateStore.ts +7 -0
  81. package/tui/src/tools/AdapterTool/AdapterTool.ts +590 -7
  82. package/tui/src/tools/LookupPrimitive/LookupPrimitive.ts +43 -17
  83. package/tui/src/tools/LookupPrimitive/prompt.ts +7 -6
  84. package/tui/src/tools/ResolveLocationPrimitive/ResolveLocationPrimitive.ts +40 -19
  85. package/tui/src/tools/SubmitPrimitive/SubmitPrimitive.ts +25 -9
  86. package/tui/src/tools/VerifyPrimitive/VerifyPrimitive.ts +25 -9
  87. package/tui/src/tools/_shared/citizenUserText.ts +49 -0
  88. package/tui/src/tools/_shared/directPublicDataGuard.ts +362 -0
  89. package/tui/src/tools/_shared/kmaAnalysisGuard.ts +197 -0
  90. package/tui/src/tools/_shared/kmaAviationGuard.ts +70 -0
  91. package/tui/src/tools/_shared/locationInputRepair.ts +112 -0
  92. package/tui/src/tools/_shared/nmcAedGuard.ts +234 -0
  93. package/tui/src/tools/_shared/protectedCheckGuard.ts +207 -0
  94. package/tui/src/tools/_shared/rootPrimitiveInput.ts +67 -0
  95. package/tui/src/tools/_shared/textToolCallGuard.ts +91 -0
  96. package/tui/src/tools/_shared/toolChoiceRepair.ts +866 -0
  97. package/tui/src/utils/attachments.ts +1 -1
  98. package/tui/src/utils/kExaoneReasoning.ts +138 -0
  99. package/tui/src/utils/messages.ts +1 -0
  100. package/tui/src/utils/multiToolLayout.ts +13 -0
  101. package/tui/src/utils/processUserInput/processSlashCommand.tsx +2 -2
  102. package/tui/src/utils/processUserInput/processUserInput.ts +26 -0
  103. package/tui/src/utils/settings/applySettingsChange.ts +4 -0
  104. package/tui/src/utils/settings/types.ts +9 -3
  105. package/tui/src/utils/stats.ts +1 -1
  106. package/uv.lock +1 -15
  107. package/assets/copilot-gate-logo.svg +0 -58
  108. package/assets/govon-logo.svg +0 -40
  109. package/src/ummaya/eval/__init__.py +0 -5
  110. package/src/ummaya/eval/retrieval.py +0 -713
  111. package/tui/src/utils/messageStream.ts +0 -186
@@ -10,7 +10,7 @@ import logging
10
10
  import os
11
11
  import random
12
12
  import time
13
- from collections.abc import AsyncIterator
13
+ from collections.abc import AsyncIterator, Iterator
14
14
  from contextvars import Token
15
15
  from dataclasses import dataclass
16
16
  from typing import TYPE_CHECKING, cast
@@ -39,6 +39,7 @@ from ummaya.llm.models import (
39
39
  ToolCall,
40
40
  ToolDefinition,
41
41
  )
42
+ from ummaya.llm.reasoning import ReasoningMode, resolve_reasoning_policy
42
43
  from ummaya.llm.usage import UsageTracker
43
44
  from ummaya.observability.semconv import (
44
45
  ERROR_TYPE,
@@ -238,6 +239,7 @@ class LLMClient:
238
239
  presence_penalty: float = 0.0,
239
240
  max_tokens: int = 1024,
240
241
  stop: list[str] | None = None,
242
+ reasoning_mode: ReasoningMode | str | None = None,
241
243
  ) -> ChatCompletionResponse:
242
244
  """Send a non-streaming chat completion request.
243
245
 
@@ -271,6 +273,7 @@ class LLMClient:
271
273
  tools=tools,
272
274
  tool_choice=tool_choice,
273
275
  stream=False,
276
+ reasoning_mode=reasoning_mode,
274
277
  )
275
278
 
276
279
  logger.debug(
@@ -365,6 +368,7 @@ class LLMClient:
365
368
  presence_penalty: float = 0.0,
366
369
  max_tokens: int = 1024,
367
370
  stop: list[str] | None = None,
371
+ reasoning_mode: ReasoningMode | str | None = None,
368
372
  ) -> AsyncIterator[StreamEvent]:
369
373
  """Send a streaming chat completion request.
370
374
 
@@ -403,7 +407,9 @@ class LLMClient:
403
407
  tools=tools,
404
408
  tool_choice=tool_choice,
405
409
  stream=True,
410
+ reasoning_mode=reasoning_mode,
406
411
  )
412
+ allow_reasoning = payload.get("include_reasoning") is True
407
413
 
408
414
  logger.debug(
409
415
  "LLM stream request: model=%s messages=%d",
@@ -447,7 +453,11 @@ class LLMClient:
447
453
  _finalize: dict[str, object] = {}
448
454
 
449
455
  try:
450
- async for event in self._stream_with_retry(payload, _finalize):
456
+ async for event in self._stream_with_retry(
457
+ payload,
458
+ _finalize,
459
+ allow_reasoning=allow_reasoning,
460
+ ):
451
461
  active_span.detach()
452
462
  yield event
453
463
  active_span.attach()
@@ -490,6 +500,8 @@ class LLMClient:
490
500
  self,
491
501
  payload: dict[str, object],
492
502
  _finalize: dict[str, object],
503
+ *,
504
+ allow_reasoning: bool,
493
505
  ) -> AsyncIterator[StreamEvent]:
494
506
  """Execute stream() with Retry-After-first backoff loop (T015/T016).
495
507
 
@@ -607,7 +619,10 @@ class LLMClient:
607
619
  if chunk_info.get("model"):
608
620
  _response_model = chunk_info["model"]
609
621
 
610
- async for event in self._parse_sse_line(line):
622
+ async for event in self._parse_sse_line(
623
+ line,
624
+ allow_reasoning=allow_reasoning,
625
+ ):
611
626
  yield event
612
627
  if event.type == "done":
613
628
  _duration_ms = (time.monotonic() - _stream_start) * 1000
@@ -848,7 +863,12 @@ class LLMClient:
848
863
  if i + step < n:
849
864
  await asyncio.sleep(_LLM_STREAM_PACE_S)
850
865
 
851
- async def _parse_sse_line(self, line: str) -> AsyncIterator[StreamEvent]: # noqa: C901
866
+ async def _parse_sse_line(
867
+ self,
868
+ line: str,
869
+ *,
870
+ allow_reasoning: bool = False,
871
+ ) -> AsyncIterator[StreamEvent]:
852
872
  """Parse a single SSE line and yield corresponding StreamEvent(s)."""
853
873
  if not line or not line.startswith("data: "):
854
874
  return
@@ -859,14 +879,35 @@ class LLMClient:
859
879
  yield StreamEvent(type="done")
860
880
  return
861
881
 
882
+ chunk = self._decode_sse_payload(payload_text)
883
+ if chunk is None:
884
+ return
885
+
886
+ usage_event = self._usage_event_from_chunk(chunk)
887
+ if usage_event is not None:
888
+ yield usage_event
889
+
890
+ async for event in self._events_from_sse_choices(
891
+ chunk,
892
+ allow_reasoning=allow_reasoning,
893
+ ):
894
+ yield event
895
+
896
+ def _decode_sse_payload(self, payload_text: str) -> dict[str, object] | None:
897
+ """Decode a JSON SSE payload, returning None for malformed chunks."""
862
898
  try:
863
899
  chunk = json.loads(payload_text)
864
900
  except json.JSONDecodeError:
865
901
  logger.warning("Failed to parse SSE chunk: %r", payload_text)
866
- return
902
+ return None
903
+ return chunk if isinstance(chunk, dict) else None
867
904
 
905
+ def _usage_event_from_chunk(self, chunk: dict[str, object]) -> StreamEvent | None:
906
+ """Debit usage from a stream chunk and return the corresponding event."""
868
907
  if "usage" in chunk and chunk["usage"] is not None:
869
908
  raw_usage = chunk["usage"]
909
+ if not isinstance(raw_usage, dict):
910
+ return None
870
911
  usage = TokenUsage(
871
912
  input_tokens=raw_usage.get("prompt_tokens", 0),
872
913
  output_tokens=raw_usage.get("completion_tokens", 0),
@@ -877,66 +918,104 @@ class LLMClient:
877
918
  usage.output_tokens,
878
919
  )
879
920
  self._usage.debit(usage)
880
- yield StreamEvent(type="usage", usage=usage)
921
+ return StreamEvent(type="usage", usage=usage)
922
+ return None
881
923
 
924
+ async def _events_from_sse_choices(
925
+ self,
926
+ chunk: dict[str, object],
927
+ *,
928
+ allow_reasoning: bool,
929
+ ) -> AsyncIterator[StreamEvent]:
930
+ """Yield stream events for the first OpenAI-compatible choice delta."""
882
931
  choices = chunk.get("choices")
883
932
  if not choices:
884
933
  return
934
+ if not isinstance(choices, list):
935
+ return
885
936
 
886
937
  choice = choices[0]
938
+ if not isinstance(choice, dict):
939
+ return
887
940
  delta = choice.get("delta", {})
941
+ if not isinstance(delta, dict):
942
+ return
888
943
 
944
+ async for event in self._events_from_sse_delta(
945
+ delta,
946
+ allow_reasoning=allow_reasoning,
947
+ ):
948
+ yield event
949
+
950
+ async def _events_from_sse_delta(
951
+ self,
952
+ delta: dict[str, object],
953
+ *,
954
+ allow_reasoning: bool,
955
+ ) -> AsyncIterator[StreamEvent]:
956
+ """Yield content, reasoning, and tool-call events from a delta object."""
889
957
  if "content" in delta and delta["content"] is not None:
890
958
  # CC reference: services/api/claude.ts:2113 (text_delta content_block_delta).
891
- content = delta["content"]
959
+ content = str(delta["content"])
892
960
  async for sub in self._pace_text_chunk(content, "content"):
893
961
  yield sub
894
962
  elif "reasoning_content" in delta and delta["reasoning_content"] is not None:
895
- # CC reference: services/api/claude.ts:2148-2161 (thinking_delta
896
- # content_block_delta) — K-EXAONE emits chain-of-thought on a
897
- # separate ``delta.reasoning_content`` channel. Forwarding the
898
- # same StreamEvent shape on UMMAYA lets the TUI's
899
- # ``AssistantThinkingMessage`` component render the reasoning
900
- # inline instead of swallowing it. Log only the chunk length —
901
- # never the raw content (CoT may contain user PII or sensitive
902
- # reasoning about user input).
903
- reasoning_text = delta["reasoning_content"]
963
+ async for sub in self._events_from_reasoning_delta(
964
+ str(delta["reasoning_content"]),
965
+ allow_reasoning=allow_reasoning,
966
+ ):
967
+ yield sub
968
+
969
+ if "tool_calls" in delta and delta["tool_calls"]:
970
+ for event in self._events_from_tool_call_deltas(delta["tool_calls"]):
971
+ yield event
972
+
973
+ async def _events_from_reasoning_delta(
974
+ self,
975
+ reasoning_text: str,
976
+ *,
977
+ allow_reasoning: bool,
978
+ ) -> AsyncIterator[StreamEvent]:
979
+ """Yield reasoning text only when the request opted into reasoning parsing."""
980
+ if not allow_reasoning:
904
981
  logger.debug(
905
- "Forwarding reasoning_content as thinking_delta (len=%d)",
982
+ "Suppressed unexpected reasoning_content while include_reasoning=false (len=%d)",
906
983
  len(reasoning_text),
907
984
  )
908
- async for sub in self._pace_text_chunk(reasoning_text, "thinking"):
909
- yield sub
985
+ return
986
+ logger.debug(
987
+ "Forwarding reasoning_content as thinking_delta (len=%d)",
988
+ len(reasoning_text),
989
+ )
990
+ async for sub in self._pace_text_chunk(reasoning_text, "thinking"):
991
+ yield sub
910
992
 
911
- if "tool_calls" in delta and delta["tool_calls"]:
912
- # CC reference: services/api/claude.ts:1997 (tool_use content_block_start)
913
- # + services/api/claude.ts:2087 (input_json_delta content_block_delta).
914
- # FriendliAI's OpenAI-compatible streaming buffers tool_call argument
915
- # JSON across multiple deltas (matching OpenAI's incremental parser).
916
- # UMMAYA mirrors CC's pattern by emitting one StreamEvent per delta;
917
- # the IPC bridge (stdio.py) accumulates them into the final
918
- # ToolCallFrame.
919
- for tc_delta in delta["tool_calls"]:
920
- func = tc_delta.get("function", {})
921
- # Log only tool metadata (index/id/name + arg length).
922
- # Raw `arguments` often carries user-provided location strings
923
- # or other PII — never log them.
924
- _args_field = func.get("arguments")
925
- _args_len = len(_args_field) if isinstance(_args_field, str) else 0
926
- logger.debug(
927
- "tool_call_delta idx=%s id=%s name=%r args_len=%d",
928
- tc_delta.get("index"),
929
- tc_delta.get("id"),
930
- func.get("name"),
931
- _args_len,
932
- )
933
- yield StreamEvent(
934
- type="tool_call_delta",
935
- tool_call_index=tc_delta.get("index"),
936
- tool_call_id=tc_delta.get("id"),
937
- function_name=func.get("name"),
938
- function_args_delta=func.get("arguments"),
939
- )
993
+ def _events_from_tool_call_deltas(self, tool_calls: object) -> Iterator[StreamEvent]:
994
+ """Yield tool-call deltas without logging raw argument content."""
995
+ if not isinstance(tool_calls, list):
996
+ return
997
+ for tc_delta in tool_calls:
998
+ if not isinstance(tc_delta, dict):
999
+ continue
1000
+ func = tc_delta.get("function", {})
1001
+ if not isinstance(func, dict):
1002
+ func = {}
1003
+ _args_field = func.get("arguments")
1004
+ _args_len = len(_args_field) if isinstance(_args_field, str) else 0
1005
+ logger.debug(
1006
+ "tool_call_delta idx=%s id=%s name=%r args_len=%d",
1007
+ tc_delta.get("index"),
1008
+ tc_delta.get("id"),
1009
+ func.get("name"),
1010
+ _args_len,
1011
+ )
1012
+ yield StreamEvent(
1013
+ type="tool_call_delta",
1014
+ tool_call_index=tc_delta.get("index"),
1015
+ tool_call_id=tc_delta.get("id"),
1016
+ function_name=func.get("name"),
1017
+ function_args_delta=func.get("arguments"),
1018
+ )
940
1019
 
941
1020
  def _build_payload(
942
1021
  self,
@@ -950,6 +1029,7 @@ class LLMClient:
950
1029
  tools: list[ToolDefinition | dict[str, object]] | None = None,
951
1030
  tool_choice: str | dict[str, object] | None = None,
952
1031
  stream: bool,
1032
+ reasoning_mode: ReasoningMode | str | None = None,
953
1033
  ) -> dict[str, object]:
954
1034
  """Construct the JSON payload for a chat completions request.
955
1035
 
@@ -979,13 +1059,7 @@ class LLMClient:
979
1059
  is not treated as normal assistant text and is never required for the
980
1060
  default CLI/TUI path.
981
1061
  """
982
- import os # noqa: PLC0415 — local import keeps top-level imports thin
983
-
984
- enable_thinking = os.environ.get("UMMAYA_K_EXAONE_THINKING", "false").lower() in (
985
- "true",
986
- "1",
987
- "yes",
988
- )
1062
+ reasoning = resolve_reasoning_policy(reasoning_mode)
989
1063
 
990
1064
  payload: dict[str, object] = {
991
1065
  "model": self._config.model,
@@ -999,7 +1073,9 @@ class LLMClient:
999
1073
  # enable_thinking=False the model emits an answer directly
1000
1074
  # without the <think>...</think> trace, dropping first-token
1001
1075
  # latency from ~60-180s to <10s for typical citizen prompts.
1002
- "chat_template_kwargs": {"enable_thinking": enable_thinking},
1076
+ "chat_template_kwargs": {"enable_thinking": reasoning.enable_thinking},
1077
+ "parse_reasoning": reasoning.parse_reasoning,
1078
+ "include_reasoning": reasoning.include_reasoning,
1003
1079
  }
1004
1080
  if stop is not None:
1005
1081
  payload["stop"] = stop
@@ -0,0 +1,84 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """K-EXAONE/FriendliAI reasoning payload policy."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import os
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from typing import Literal
10
+
11
+ ReasoningMode = Literal["fast", "balanced", "deep", "diagnostic", "auto"]
12
+ ReasoningModeSource = Literal["env", "session", "legacy-env", "default"]
13
+
14
+ _MODES: set[str] = {"fast", "balanced", "deep", "diagnostic", "auto"}
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ResolvedReasoningPolicy:
19
+ """Provider-facing reasoning policy for one request."""
20
+
21
+ mode: ReasoningMode
22
+ source: ReasoningModeSource
23
+ enable_thinking: bool
24
+ parse_reasoning: bool
25
+ include_reasoning: bool
26
+ persist_thinking: bool = False
27
+
28
+
29
+ def parse_reasoning_mode(value: object) -> ReasoningMode | None:
30
+ """Return a valid reasoning mode from an untrusted value."""
31
+ if value is None:
32
+ return None
33
+ normalized = str(value).lower()
34
+ if normalized in _MODES:
35
+ return normalized # type: ignore[return-value]
36
+ return None
37
+
38
+
39
+ def resolve_reasoning_policy(
40
+ reasoning_mode: ReasoningMode | str | None = None,
41
+ *,
42
+ env: Mapping[str, str] | None = None,
43
+ ) -> ResolvedReasoningPolicy:
44
+ """Resolve request, settings, and env state into FriendliAI payload fields."""
45
+ effective_env = os.environ if env is None else env
46
+ env_mode = parse_reasoning_mode(effective_env.get("UMMAYA_K_EXAONE_REASONING_MODE"))
47
+ if env_mode is not None:
48
+ return _policy_for(env_mode, "env")
49
+
50
+ explicit_mode = parse_reasoning_mode(reasoning_mode)
51
+ if explicit_mode is not None:
52
+ return _policy_for(explicit_mode, "session")
53
+
54
+ legacy_mode = _legacy_thinking_mode(effective_env)
55
+ if legacy_mode is not None:
56
+ return _policy_for(legacy_mode, "legacy-env")
57
+
58
+ return _policy_for("balanced", "default")
59
+
60
+
61
+ def _legacy_thinking_mode(env: Mapping[str, str]) -> ReasoningMode | None:
62
+ raw = env.get("UMMAYA_K_EXAONE_THINKING")
63
+ if raw is None:
64
+ return None
65
+ normalized = raw.lower()
66
+ if normalized in {"1", "true", "yes"}:
67
+ return "deep"
68
+ if normalized in {"0", "false", "no"}:
69
+ return "fast"
70
+ return None
71
+
72
+
73
+ def _policy_for(
74
+ mode: ReasoningMode,
75
+ source: ReasoningModeSource,
76
+ ) -> ResolvedReasoningPolicy:
77
+ enable_thinking = mode in {"deep", "diagnostic"}
78
+ return ResolvedReasoningPolicy(
79
+ mode=mode,
80
+ source=source,
81
+ enable_thinking=enable_thinking,
82
+ parse_reasoning=True,
83
+ include_reasoning=enable_thinking,
84
+ )
@@ -138,7 +138,11 @@ _VERIFY_FAMILIES: list[dict[str, Any]] = [
138
138
  "네이버인증",
139
139
  "토스인증",
140
140
  "정부24",
141
+ "홈택스",
142
+ "국세청",
141
143
  "민원",
144
+ "증명원",
145
+ "소득금액증명원",
142
146
  "주민등록등본",
143
147
  "발급",
144
148
  ],
@@ -149,12 +153,20 @@ _VERIFY_FAMILIES: list[dict[str, Any]] = [
149
153
  "Naver",
150
154
  "Toss",
151
155
  "gov24",
156
+ "hometax",
157
+ "national tax service",
152
158
  "civil petition",
159
+ "income certificate",
153
160
  "resident registration certificate",
154
161
  "issuance",
155
162
  ],
156
163
  "endpoint": "https://api.gateway.ummaya.gov.kr/v1/verify/simple",
157
164
  "policy_authority": "https://www.kftc.or.kr/",
165
+ "scope_rules": (
166
+ "Scope rule: simple-auth module verification uses the canonical "
167
+ "check tool_id 'mock_verify_module_simple_auth'. Do not set tool_id "
168
+ "to 'simple_auth_module'; that string is only the internal family_hint."
169
+ ),
158
170
  },
159
171
  {
160
172
  "tool_id": "mock_verify_module_any_id_sso",
@@ -221,7 +233,9 @@ _VERIFY_FAMILIES: list[dict[str, Any]] = [
221
233
  "scope_rules": (
222
234
  "Scope rule: mobile ID identity verification uses exactly scope_list "
223
235
  "['check:mobile_id.identity']; do not invent find:identity.info "
224
- "or find:identity.verify scopes."
236
+ "or find:identity.verify scopes. Use the canonical check tool_id "
237
+ "'mock_verify_mobile_id'. Do not set tool_id to 'mobile_id'; that "
238
+ "string is only the internal family_hint."
225
239
  ),
226
240
  },
227
241
  {
@@ -304,6 +318,8 @@ def _verify_to_govapitool(entry: dict[str, Any]) -> GovAPITool:
304
318
  output_schema=_OpaqueOutput,
305
319
  llm_description=(
306
320
  "Use only through the core check primitive: "
321
+ f"check({{tool_id: '{entry['tool_id']}', params: {{...}}}}). "
322
+ "Canonical action-scope form: "
307
323
  f"check(tool_id='{entry['tool_id']}', params={{...}}). "
308
324
  "Do not call this adapter through find.\n\n"
309
325
  f"{scope_clause}"
@@ -30,6 +30,7 @@ from ummaya.tools.envelope import make_error_envelope, normalize
30
30
  from ummaya.tools.errors import (
31
31
  EnvelopeNormalizationError,
32
32
  LookupErrorReason,
33
+ ToolExecutionError,
33
34
  ToolNotFoundError,
34
35
  UmmayaToolError,
35
36
  )
@@ -79,6 +80,11 @@ def _classify_adapter_exception(exc: Exception) -> tuple[LookupErrorReason, bool
79
80
  if isinstance(exc, Layer3GateViolation):
80
81
  # Programming error: stub handler was reached despite auth-gate — never retry.
81
82
  return (LookupErrorReason.upstream_unavailable, False)
83
+ if isinstance(exc, ToolExecutionError) and isinstance(exc.cause, httpx.HTTPStatusError):
84
+ status_code = exc.cause.response.status_code
85
+ if status_code in {400, 401, 403, 404}:
86
+ return (LookupErrorReason.upstream_unavailable, False)
87
+ return (LookupErrorReason.upstream_unavailable, True)
82
88
  if isinstance(exc, LiveAdapterProxyConfigurationError):
83
89
  return (LookupErrorReason.upstream_unavailable, False)
84
90
  if isinstance(exc, (ValueError, TypeError, KMADomainError)):
@@ -118,6 +124,19 @@ def _adapter_validation_recovery_hint(tool_id: str) -> str:
118
124
  " prefer nmc_emergency_search when authenticated, or a location POI"
119
125
  " search when no authenticated NMC session is present."
120
126
  )
127
+ if tool_id == "nmc_aed_site_locate":
128
+ return (
129
+ " REGION FILTER ONLY: this AED adapter uses official NMC Q0/Q1"
130
+ " region filters, not ER-search mode. If you have"
131
+ " a place name, call kakao_keyword_search({query:'<장소명>'}), then"
132
+ " kakao_coord_to_region({lat:<lat>, lon:<lon>}). Re-invoke this"
133
+ " tool as nmc_aed_site_locate({q0:region.region_1depth_name,"
134
+ " q1:region.region_2depth_name, page_no:1, num_of_rows:10,"
135
+ " origin_lat:<original place lat>, origin_lon:<original place lon>})."
136
+ " origin_lat/origin_lon are optional client-side distance-sort fields;"
137
+ " copy them from the coordinate-producing locate result when available."
138
+ " Do NOT pass mode, lat/lon, or ER-only fields."
139
+ )
121
140
  if tool_id in {"kakao_coord_to_region", "sgis_adm_cd_lookup"}:
122
141
  return (
123
142
  " COPY EXACT COORDINATES: call this reverse-geocode adapter with"
@@ -311,8 +330,9 @@ class ToolExecutor:
311
330
  reason=reason,
312
331
  message=(
313
332
  f"Adapter '{tool_id}' raised {type(exc).__name__}: {str(exc)[:240]}. "
314
- "Do NOT fabricate a response from prior knowledge; use another "
315
- "appropriate adapter or explain that the lookup failed."
333
+ "Do NOT fabricate a response from prior knowledge; explain that "
334
+ "the lookup failed, cite the official agency channel, and ask "
335
+ "before trying a different adapter."
316
336
  ),
317
337
  request_id=request_id,
318
338
  elapsed_ms=_elapsed(),
@@ -480,20 +500,20 @@ class ToolExecutor:
480
500
  recovery_hint = _adapter_validation_recovery_hint(tool_id)
481
501
  if not recovery_hint and tool_id == "nmc_emergency_search":
482
502
  recovery_hint = (
483
- " LOCATE FIRST: call locate with a locate adapter from"
503
+ " LOCATE FIRST: call a concrete locate adapter from"
484
504
  " <available_adapters>. For a named place use"
485
- " locate({tool_id:'kakao_keyword_search', params:{query:'<지역명>'}}),"
486
- " then call locate({tool_id:'kakao_coord_to_region',"
487
- " params:{lat:<lat>, lon:<lon>}}). Re-invoke this tool with"
488
- " params {mode:'region', q0:region.region_1depth_name,"
505
+ " kakao_keyword_search({query:'<지역명>'}), then call"
506
+ " kakao_coord_to_region({lat:<lat>, lon:<lon>}). Re-invoke"
507
+ " this tool as nmc_emergency_search({mode:'region',"
508
+ " q0:region.region_1depth_name,"
489
509
  " q1:region.region_2depth_name, origin_lat:<lat>, origin_lon:<lon>,"
490
- " limit:<N>}. Copy decimal WGS-84 coordinates exactly from locate;"
510
+ " limit:<N>}). Copy decimal WGS-84 coordinates exactly from locate;"
491
511
  " do NOT round, guess coordinates, or set QN unless the citizen"
492
512
  " gave a specific institution name."
493
513
  )
494
514
  elif not recovery_hint and need_resolve:
495
515
  recovery_hint = (
496
- " LOCATE FIRST: call locate with the appropriate locate adapter"
516
+ " LOCATE FIRST: call the appropriate concrete locate adapter"
497
517
  " from <available_adapters> to obtain the missing coordinates /"
498
518
  " admin code, then re-invoke this tool with the returned values."
499
519
  " Do NOT guess coordinates or codes from prior knowledge."
@@ -566,8 +586,8 @@ class ToolExecutor:
566
586
  f"Adapter '{tool_id}' raised an exception during upstream call. "
567
587
  f"Detail: {_exc_summary}. "
568
588
  "Do NOT fabricate a response from prior knowledge — tell the citizen "
569
- "the lookup failed, cite the official agency channel, and offer to "
570
- "retry or try a different tool."
589
+ "the lookup failed, cite the official agency channel, and ask "
590
+ "before retrying or trying a different tool."
571
591
  ),
572
592
  request_id=request_id,
573
593
  elapsed_ms=_elapsed(),
@@ -637,7 +657,7 @@ class ToolExecutor:
637
657
  f"expected envelope schema. Detail: {_exc_detail}. "
638
658
  "Do NOT fabricate a response from prior knowledge — tell the citizen "
639
659
  "the data could not be parsed, cite the official agency channel, and "
640
- "offer to retry or try a different tool."
660
+ "ask before retrying or trying a different tool."
641
661
  ),
642
662
  request_id=request_id,
643
663
  elapsed_ms=_elapsed(),
@@ -19,8 +19,7 @@ business logic. Reference: PyKakao 1.x ``Local`` class
19
19
  endpoints as six methods on a single facade — the industry-standard
20
20
  Korean wrapper for this API. UMMAYA originally shipped only
21
21
  ``search_address`` (Spec 022); ``search_keyword`` is added here to close
22
- the POI-coverage gap captured in
23
- ``specs/integration-verification/donga-univ-poi-bug/``.
22
+ the POI-coverage gap captured during historical live-location debugging.
24
23
 
25
24
  Authentication: REST API key via ``Authorization: KakaoAK {key}`` header.
26
25
  Key source: ``UMMAYA_KAKAO_API_KEY`` environment variable.