ummaya 0.2.3 → 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 +1287 -54
  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 +48 -18
  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
@@ -3,9 +3,10 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ from datetime import datetime, timedelta
6
7
  from typing import Literal
7
8
 
8
- from pydantic import BaseModel, ConfigDict, Field
9
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
9
10
 
10
11
  from ummaya.tools.executor import ToolExecutor
11
12
  from ummaya.tools.models import GovAPITool
@@ -17,27 +18,113 @@ from ummaya.tools.verified_data_go_kr._factory import (
17
18
  )
18
19
  from ummaya.tools.verified_data_go_kr._manifest import require_spec
19
20
 
21
+ _PPS_DATETIME_FORMAT = "%Y%m%d%H%M"
22
+ _PPS_MAX_SEARCH_WINDOW = timedelta(days=31)
23
+
20
24
 
21
25
  class PpsBidPublicInfoInput(BaseModel):
22
26
  """Input for PPS bid public information."""
23
27
 
24
28
  model_config = ConfigDict(extra="forbid")
25
29
 
26
- inqry_div: Literal["2"] = Field(
27
- default="2",
30
+ inqry_div: Literal["1", "2"] = Field(
31
+ default="1",
32
+ description=(
33
+ "Official PPS inqryDiv. Use '1' for bid notice publication datetime "
34
+ "(pblancDate) searches such as 'this week posted notices'; use '2' "
35
+ "for bid opening datetime (opengDt) searches."
36
+ ),
37
+ )
38
+ inqry_bgn_dt: str = Field(
39
+ ...,
40
+ pattern=r"^\d{12}$",
28
41
  description=(
29
- "PPS inquiry division. This adapter wraps the verified "
30
- "inqryDiv=2 bid-notice-number lookup path."
42
+ "Official PPS inqryBgnDt search start datetime in YYYYMMDDHHMM. "
43
+ "Required when inqry_div is '1' or '2'. Keep each PPS request window "
44
+ "within 31 days."
31
45
  ),
32
46
  )
33
- bid_ntce_no: str = Field(
47
+ inqry_end_dt: str = Field(
34
48
  ...,
35
- min_length=1,
36
- description="Bid notice number required by the verified inqryDiv=2 lookup path.",
49
+ pattern=r"^\d{12}$",
50
+ description=(
51
+ "Official PPS inqryEndDt search end datetime in YYYYMMDDHHMM. "
52
+ "Required when inqry_div is '1' or '2'. Keep each PPS request window "
53
+ "within 31 days."
54
+ ),
55
+ )
56
+ bid_ntce_nm: str | None = Field(
57
+ default=None,
58
+ max_length=1000,
59
+ description=(
60
+ "Official PPS bidNtceNm notice-name keyword. Partial names are allowed; "
61
+ "use this for citizen keywords such as 전기공사."
62
+ ),
63
+ )
64
+ ntce_instt_nm: str | None = Field(
65
+ default=None,
66
+ max_length=400,
67
+ description=(
68
+ "Official PPS ntceInsttNm public notice agency name filter. "
69
+ "Partial agency names are allowed."
70
+ ),
71
+ )
72
+ dminstt_nm: str | None = Field(
73
+ default=None,
74
+ max_length=400,
75
+ description=(
76
+ "Official PPS dminsttNm demand agency name filter. Partial agency names are allowed."
77
+ ),
78
+ )
79
+ region_name: str | None = Field(
80
+ default=None,
81
+ max_length=100,
82
+ description=(
83
+ "UMMAYA client-side region relevance filter copied from citizen wording. "
84
+ "This is not sent to PPS upstream; after the official response, UMMAYA "
85
+ "keeps rows whose documented region/agency fields such as cnstrtsiteRgnNm, "
86
+ "prtcptLmtRgnNm, ntceInsttNm, or dminsttNm match this value."
87
+ ),
88
+ )
89
+ prtcpt_lmt_rgn_nm: str | None = Field(
90
+ default=None,
91
+ max_length=100,
92
+ description=(
93
+ "Official PPS prtcptLmtRgnNm participation-limit region name. "
94
+ "For Busan-region notices use 부산광역시 when the citizen says 부산시."
95
+ ),
96
+ )
97
+ indstryty_nm: str | None = Field(
98
+ default=None,
99
+ max_length=100,
100
+ description=(
101
+ "Official PPS indstrytyNm industry/license name. Use 전기공사업 "
102
+ "for electrical-construction qualification searches when requested."
103
+ ),
37
104
  )
38
105
  page_no: int = Field(default=1, ge=1, description="Page number.")
39
106
  num_of_rows: int = Field(default=10, ge=1, le=100, description="Rows per page.")
40
107
 
108
+ @model_validator(mode="after")
109
+ def validate_official_search_window(self) -> PpsBidPublicInfoInput:
110
+ """Keep PPS searches inside the observed upstream contract window."""
111
+
112
+ try:
113
+ start = datetime.strptime(self.inqry_bgn_dt, _PPS_DATETIME_FORMAT)
114
+ end = datetime.strptime(self.inqry_end_dt, _PPS_DATETIME_FORMAT)
115
+ except ValueError as exc:
116
+ raise ValueError(
117
+ "PPS inqry_bgn_dt and inqry_end_dt must be valid YYYYMMDDHHMM datetimes."
118
+ ) from exc
119
+ if end < start:
120
+ raise ValueError("PPS inqry_end_dt must be greater than or equal to inqry_bgn_dt.")
121
+ if end - start > _PPS_MAX_SEARCH_WINDOW:
122
+ raise ValueError(
123
+ "PPS Nara Market bid searches must be split into 31-day-or-smaller "
124
+ "inqry_bgn_dt/inqry_end_dt windows before calling the upstream API."
125
+ )
126
+ return self
127
+
41
128
 
42
129
  SPEC = require_spec("pps_bid_public_info")
43
130
  INPUT_SCHEMA = PpsBidPublicInfoInput
@@ -51,7 +138,85 @@ async def handle(
51
138
  ) -> dict[str, object]:
52
139
  """Fetch or replay PPS bid public information rows."""
53
140
 
54
- return await handle_verified_input(input_model, SPEC, fixture_body=fixture_body)
141
+ output = await handle_verified_input(input_model, SPEC, fixture_body=fixture_body)
142
+ return _filter_output_by_region_name(output, input_model.region_name)
143
+
144
+
145
+ _REGION_RELEVANCE_FIELDS: tuple[str, ...] = (
146
+ "cnstrtsiteRgnNm",
147
+ "prtcptLmtRgnNm",
148
+ "ntceInsttNm",
149
+ "dminsttNm",
150
+ "jntcontrctDutyRgnNm1",
151
+ "jntcontrctDutyRgnNm2",
152
+ "jntcontrctDutyRgnNm3",
153
+ )
154
+
155
+
156
+ def _filter_output_by_region_name(
157
+ output: dict[str, object],
158
+ region_name: str | None,
159
+ ) -> dict[str, object]:
160
+ """Filter PPS rows by documented region-bearing response fields."""
161
+
162
+ terms = _region_terms(region_name)
163
+ if not terms:
164
+ return output
165
+
166
+ raw_items = output.get("items")
167
+ if not isinstance(raw_items, list):
168
+ return output
169
+
170
+ filtered: list[dict[str, object]] = []
171
+ for raw_item in raw_items:
172
+ if not isinstance(raw_item, dict):
173
+ continue
174
+ if _item_matches_region(raw_item, terms):
175
+ filtered.append(raw_item)
176
+
177
+ next_output = dict(output)
178
+ next_output["items"] = filtered
179
+ next_output["total_count"] = len(filtered)
180
+ raw_meta = output.get("meta")
181
+ meta = dict(raw_meta) if isinstance(raw_meta, dict) else {}
182
+ meta["upstream_total_count"] = output.get("total_count")
183
+ meta["client_filter"] = {
184
+ "field": "region_name",
185
+ "value": region_name,
186
+ "matched_count": len(filtered),
187
+ }
188
+ next_output["meta"] = meta
189
+ return next_output
190
+
191
+
192
+ def _region_terms(region_name: str | None) -> tuple[str, ...]:
193
+ if region_name is None:
194
+ return ()
195
+ normalized = " ".join(region_name.split())
196
+ if not normalized:
197
+ return ()
198
+ terms = [normalized]
199
+ for suffix in ("특별자치시", "특별자치도", "특별시", "광역시", "도"):
200
+ if normalized.endswith(suffix):
201
+ short = normalized[: -len(suffix)]
202
+ if short:
203
+ terms.append(short)
204
+ break
205
+ return tuple(dict.fromkeys(terms))
206
+
207
+
208
+ def _item_matches_region(item: dict[str, object], terms: tuple[str, ...]) -> bool:
209
+ raw_record = item.get("record")
210
+ record = raw_record if isinstance(raw_record, dict) else item
211
+ for field_name in _REGION_RELEVANCE_FIELDS:
212
+ value = record.get(field_name)
213
+ if not isinstance(value, str):
214
+ continue
215
+ compact_value = value.replace(" ", "")
216
+ for term in terms:
217
+ if term in value or term.replace(" ", "") in compact_value:
218
+ return True
219
+ return False
55
220
 
56
221
 
57
222
  def register(registry: ToolRegistry, executor: ToolExecutor) -> None:
@@ -3,6 +3,8 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
+ from typing import cast
7
+
6
8
  from pydantic import BaseModel, ConfigDict, Field
7
9
 
8
10
  from ummaya.tools.executor import ToolExecutor
@@ -15,14 +17,41 @@ from ummaya.tools.verified_data_go_kr._factory import (
15
17
  )
16
18
  from ummaya.tools.verified_data_go_kr._manifest import require_spec
17
19
 
20
+ _TAGO_CITY_CODE_DESCRIPTION = (
21
+ "Official TAGO cityCode from the provider getCtyCodeList contract. "
22
+ "Common metropolitan examples: Busan=21, Daegu=22, Incheon=23, "
23
+ "Gwangju=24, Daejeon=25, Ulsan=26."
24
+ )
25
+
18
26
 
19
27
  class TagoBusArrivalInput(BaseModel):
20
28
  """Input for TAGO bus arrival search."""
21
29
 
22
30
  model_config = ConfigDict(extra="forbid")
23
31
 
24
- city_code: str = Field(..., min_length=1, description="TAGO city code.")
25
- node_id: str = Field(..., min_length=1, description="TAGO bus station ID.")
32
+ city_code: str = Field(..., min_length=1, description=_TAGO_CITY_CODE_DESCRIPTION)
33
+ node_id: str = Field(
34
+ ...,
35
+ min_length=1,
36
+ description="Official TAGO nodeId returned by tago_bus_station_search.",
37
+ )
38
+ route_no: str | None = Field(
39
+ default=None,
40
+ min_length=1,
41
+ description=(
42
+ "Optional client-side filter against the official TAGO response field routeno. "
43
+ "It is not sent upstream; use it when the citizen names a visible bus route "
44
+ "such as 1001."
45
+ ),
46
+ )
47
+ route_id: str | None = Field(
48
+ default=None,
49
+ min_length=1,
50
+ description=(
51
+ "Optional client-side filter against the official TAGO response field routeid. "
52
+ "Get it from tago_bus_route_search when route_no alone is ambiguous."
53
+ ),
54
+ )
26
55
  page_no: int = Field(default=1, ge=1, description="Page number.")
27
56
  num_of_rows: int = Field(default=10, ge=1, le=100, description="Rows per page.")
28
57
 
@@ -39,7 +68,41 @@ async def handle(
39
68
  ) -> dict[str, object]:
40
69
  """Fetch or replay TAGO bus arrival rows."""
41
70
 
42
- return await handle_verified_input(input_model, SPEC, fixture_body=fixture_body)
71
+ output = await handle_verified_input(input_model, SPEC, fixture_body=fixture_body)
72
+ if input_model.route_no is None and input_model.route_id is None:
73
+ return output
74
+ return _filter_arrivals(output, route_no=input_model.route_no, route_id=input_model.route_id)
75
+
76
+
77
+ def _filter_arrivals(
78
+ output: dict[str, object],
79
+ *,
80
+ route_no: str | None,
81
+ route_id: str | None,
82
+ ) -> dict[str, object]:
83
+ items = output.get("items")
84
+ if not isinstance(items, list):
85
+ return output
86
+
87
+ filtered: list[dict[str, object]] = []
88
+ for item in items:
89
+ if not isinstance(item, dict):
90
+ continue
91
+ record = item.get("record")
92
+ if not isinstance(record, dict):
93
+ continue
94
+ typed_record = cast(dict[str, object], record)
95
+ if route_no is not None and str(typed_record.get("routeno", "")) != route_no:
96
+ continue
97
+ if route_id is not None and str(typed_record.get("routeid", "")) != route_id:
98
+ continue
99
+ filtered.append(cast(dict[str, object], item))
100
+
101
+ filtered_output = dict(output)
102
+ filtered_output["items"] = filtered
103
+ filtered_output["total_count"] = len(filtered)
104
+ filtered_output["next_cursor"] = None
105
+ return filtered_output
43
106
 
44
107
 
45
108
  def register(registry: ToolRegistry, executor: ToolExecutor) -> None:
@@ -15,14 +15,24 @@ from ummaya.tools.verified_data_go_kr._factory import (
15
15
  )
16
16
  from ummaya.tools.verified_data_go_kr._manifest import require_spec
17
17
 
18
+ _TAGO_CITY_CODE_DESCRIPTION = (
19
+ "Official TAGO cityCode from the provider getCtyCodeList contract. "
20
+ "Common metropolitan examples: Busan=21, Daegu=22, Incheon=23, "
21
+ "Gwangju=24, Daejeon=25, Ulsan=26."
22
+ )
23
+
18
24
 
19
25
  class TagoBusLocationInput(BaseModel):
20
26
  """Input for TAGO bus location search."""
21
27
 
22
28
  model_config = ConfigDict(extra="forbid")
23
29
 
24
- city_code: str = Field(..., min_length=1, description="TAGO city code.")
25
- route_id: str = Field(..., min_length=1, description="TAGO route ID.")
30
+ city_code: str = Field(..., min_length=1, description=_TAGO_CITY_CODE_DESCRIPTION)
31
+ route_id: str = Field(
32
+ ...,
33
+ min_length=1,
34
+ description="Official TAGO routeId returned by tago_bus_route_search.",
35
+ )
26
36
  page_no: int = Field(default=1, ge=1, description="Page number.")
27
37
  num_of_rows: int = Field(default=10, ge=1, le=100, description="Rows per page.")
28
38
 
@@ -15,14 +15,20 @@ from ummaya.tools.verified_data_go_kr._factory import (
15
15
  )
16
16
  from ummaya.tools.verified_data_go_kr._manifest import require_spec
17
17
 
18
+ _TAGO_CITY_CODE_DESCRIPTION = (
19
+ "Official TAGO cityCode from the provider getCtyCodeList contract. "
20
+ "Common metropolitan examples: Busan=21, Daegu=22, Incheon=23, "
21
+ "Gwangju=24, Daejeon=25, Ulsan=26."
22
+ )
23
+
18
24
 
19
25
  class TagoBusRouteInput(BaseModel):
20
26
  """Input for TAGO bus route search."""
21
27
 
22
28
  model_config = ConfigDict(extra="forbid")
23
29
 
24
- city_code: str = Field(..., min_length=1, description="TAGO city code.")
25
- route_no: str = Field(..., min_length=1, description="Bus route number.")
30
+ city_code: str = Field(..., min_length=1, description=_TAGO_CITY_CODE_DESCRIPTION)
31
+ route_no: str = Field(..., min_length=1, description="Bus route number visible to citizens.")
26
32
  page_no: int = Field(default=1, ge=1, description="Page number.")
27
33
  num_of_rows: int = Field(default=10, ge=1, le=100, description="Rows per page.")
28
34
 
@@ -0,0 +1,114 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """TAGO bus route-station search adapter."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from typing import cast
7
+
8
+ from pydantic import BaseModel, ConfigDict, Field
9
+
10
+ from ummaya.tools.executor import ToolExecutor
11
+ from ummaya.tools.models import GovAPITool
12
+ from ummaya.tools.registry import ToolRegistry
13
+ from ummaya.tools.verified_data_go_kr._factory import (
14
+ build_tool,
15
+ handle_verified_input,
16
+ register_module,
17
+ )
18
+ from ummaya.tools.verified_data_go_kr._manifest import require_spec
19
+
20
+ _TAGO_CITY_CODE_DESCRIPTION = (
21
+ "Official TAGO cityCode from the provider getCtyCodeList contract. "
22
+ "Common metropolitan examples: Busan=21, Daegu=22, Incheon=23, "
23
+ "Gwangju=24, Daejeon=25, Ulsan=26."
24
+ )
25
+
26
+
27
+ class TagoBusRouteStationInput(BaseModel):
28
+ """Input for TAGO route-station search."""
29
+
30
+ model_config = ConfigDict(extra="forbid")
31
+
32
+ city_code: str = Field(..., min_length=1, description=_TAGO_CITY_CODE_DESCRIPTION)
33
+ route_id: str = Field(
34
+ ...,
35
+ min_length=1,
36
+ description="Official TAGO routeId returned by tago_bus_route_search.",
37
+ )
38
+ node_nm: str | None = Field(
39
+ default=None,
40
+ min_length=1,
41
+ description=(
42
+ "Optional client-side filter against the official TAGO response field nodenm. "
43
+ "Use it to narrow a route's passing stops to a citizen-named place such as 부산역."
44
+ ),
45
+ )
46
+ updown_cd: str | None = Field(
47
+ default=None,
48
+ min_length=1,
49
+ description=(
50
+ "Optional client-side filter against the official TAGO response field updowncd "
51
+ "when a direction is already known."
52
+ ),
53
+ )
54
+ page_no: int = Field(default=1, ge=1, description="Page number.")
55
+ num_of_rows: int = Field(default=100, ge=1, le=100, description="Rows per page.")
56
+
57
+
58
+ SPEC = require_spec("tago_bus_route_station_search")
59
+ INPUT_SCHEMA = TagoBusRouteStationInput
60
+ TOOL: GovAPITool = build_tool(SPEC, INPUT_SCHEMA)
61
+
62
+
63
+ async def handle(
64
+ input_model: TagoBusRouteStationInput,
65
+ *,
66
+ fixture_body: bytes | None = None,
67
+ ) -> dict[str, object]:
68
+ """Fetch or replay TAGO route-station rows."""
69
+
70
+ output = await handle_verified_input(input_model, SPEC, fixture_body=fixture_body)
71
+ if input_model.node_nm is None and input_model.updown_cd is None:
72
+ return output
73
+ return _filter_route_stations(
74
+ output,
75
+ node_nm=input_model.node_nm,
76
+ updown_cd=input_model.updown_cd,
77
+ )
78
+
79
+
80
+ def _filter_route_stations(
81
+ output: dict[str, object],
82
+ *,
83
+ node_nm: str | None,
84
+ updown_cd: str | None,
85
+ ) -> dict[str, object]:
86
+ items = output.get("items")
87
+ if not isinstance(items, list):
88
+ return output
89
+
90
+ filtered: list[dict[str, object]] = []
91
+ for item in items:
92
+ if not isinstance(item, dict):
93
+ continue
94
+ record = item.get("record")
95
+ if not isinstance(record, dict):
96
+ continue
97
+ typed_record = cast(dict[str, object], record)
98
+ if node_nm is not None and node_nm not in str(typed_record.get("nodenm", "")):
99
+ continue
100
+ if updown_cd is not None and str(typed_record.get("updowncd", "")) != updown_cd:
101
+ continue
102
+ filtered.append(cast(dict[str, object], item))
103
+
104
+ filtered_output = dict(output)
105
+ filtered_output["items"] = filtered
106
+ filtered_output["total_count"] = len(filtered)
107
+ filtered_output["next_cursor"] = None
108
+ return filtered_output
109
+
110
+
111
+ def register(registry: ToolRegistry, executor: ToolExecutor) -> None:
112
+ """Register this adapter."""
113
+
114
+ register_module(registry, executor, tool=TOOL, input_schema=INPUT_SCHEMA, handler=handle)
@@ -15,15 +15,26 @@ from ummaya.tools.verified_data_go_kr._factory import (
15
15
  )
16
16
  from ummaya.tools.verified_data_go_kr._manifest import require_spec
17
17
 
18
+ _TAGO_CITY_CODE_DESCRIPTION = (
19
+ "Official TAGO cityCode from the provider getCtyCodeList contract. "
20
+ "Common metropolitan examples: Busan=21, Daegu=22, Incheon=23, "
21
+ "Gwangju=24, Daejeon=25, Ulsan=26."
22
+ )
23
+
18
24
 
19
25
  class TagoBusStationInput(BaseModel):
20
26
  """Input for TAGO bus station search."""
21
27
 
22
28
  model_config = ConfigDict(extra="forbid")
23
29
 
24
- city_code: str = Field(..., min_length=1, description="TAGO city code.")
25
- node_nm: str | None = Field(default=None, description="Bus station name.")
26
- node_no: str | None = Field(default=None, description="Bus station number.")
30
+ city_code: str = Field(..., min_length=1, description=_TAGO_CITY_CODE_DESCRIPTION)
31
+ node_nm: str | None = Field(
32
+ default=None,
33
+ description=(
34
+ "Bus stop name fragment, for example 부산역 when the citizen asks near Busan Station."
35
+ ),
36
+ )
37
+ node_no: str | None = Field(default=None, description="Bus stop number printed at the stop.")
27
38
  page_no: int = Field(default=1, ge=1, description="Page number.")
28
39
  num_of_rows: int = Field(default=10, ge=1, le=100, description="Rows per page.")
29
40
 
@@ -11,6 +11,11 @@ Public API
11
11
  Return the ``family_hint`` string for the given check tool_id, or ``None``
12
12
  if the tool_id is not recognised.
13
13
 
14
+ ``resolve_tool_id(identifier)`` → ``str | None``
15
+ Return the canonical ``mock_verify_*`` tool_id when *identifier* is either a
16
+ canonical check tool_id or an internal family_hint alias such as
17
+ ``mobile_id`` / ``simple_auth_module``.
18
+
14
19
  ``get_canonical_map()`` → ``Mapping[str, str]``
15
20
  Return the full frozen ``{tool_id: family_hint}`` mapping.
16
21
 
@@ -99,6 +104,22 @@ def resolve_family(tool_id: str) -> str | None:
99
104
  return _load_map().get(tool_id)
100
105
 
101
106
 
107
+ def resolve_tool_id(identifier: str) -> str | None:
108
+ """Return the canonical check tool_id for *identifier*.
109
+
110
+ This is the runtime guard for model-facing alias drift. The manifest can
111
+ include internal verify-family entries for backward compatibility, but
112
+ adapter dispatch is owned by canonical ``mock_verify_*`` tool ids.
113
+ """
114
+ mapping = _load_map()
115
+ if identifier in mapping:
116
+ return identifier
117
+ for tool_id, family in mapping.items():
118
+ if family == identifier:
119
+ return tool_id
120
+ return None
121
+
122
+
102
123
  def get_canonical_map() -> Mapping[str, str]:
103
124
  """Return the full ``{tool_id: family_hint}`` frozen mapping (read-only)."""
104
125
  return _load_map()
package/tui/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ummaya",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "engines": {
@@ -11,7 +11,6 @@
11
11
  "gen:ipc": "bun run scripts/gen-ipc-types.ts",
12
12
  "gen:pipa-hash": "bun run scripts/gen-pipa-hash.ts",
13
13
  "diff:upstream": "bun run scripts/diff-upstream.ts",
14
- "tui:smoke": "bun run scripts/tui-smoke.ts",
15
14
  "test:soak": "bun test --timeout 600000 tests/soak",
16
15
  "test": "bun test --max-concurrency=1 tests/entrypoints tests/hooks tests/i18n tests/ink tests/ipc tests/memdir tests/permissions tests/primitive tests/store tests/theme tests/unit",
17
16
  "typecheck": "tsc --noEmit -p tsconfig.typecheck.json",
@@ -53,6 +53,10 @@ import type { AppState } from './state/AppState.js'
53
53
  import { type Tools, type ToolUseContext, toolMatchesName } from './Tool.js'
54
54
  import type { AgentDefinition } from './tools/AgentTool/loadAgentsDir.js'
55
55
  import { SYNTHETIC_OUTPUT_TOOL_NAME } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
56
+ import {
57
+ stripTextToolCallBlocks,
58
+ textContainsToolCall,
59
+ } from './tools/_shared/textToolCallGuard.js'
56
60
  import type { Message } from './types/message.js'
57
61
  import type { OrphanedPermission } from './types/textInputTypes.js'
58
62
  import { createAbortController } from './utils/abortController.js'
@@ -326,6 +326,6 @@ export async function authLogout(): Promise<void> {
326
326
  process.stderr.write('Failed to log out.\n')
327
327
  process.exit(1)
328
328
  }
329
- process.stdout.write('Successfully logged out from your Anthropic account.\n')
329
+ process.stdout.write('Successfully logged out from FriendliAI.\n')
330
330
  process.exit(0)
331
331
  }