ummaya 0.2.4 → 0.2.6

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 (482) hide show
  1. package/README.md +15 -2
  2. package/bin/ummaya +10 -1
  3. package/bun.lock +180 -244
  4. package/npm-shrinkwrap.json +760 -1760
  5. package/package.json +39 -22
  6. package/prompts/manifest.yaml +1 -1
  7. package/prompts/system_v1.md +1 -0
  8. package/pyproject.toml +27 -2
  9. package/specs/2803-document-production-hardening/contracts/document-tools.schema.json +1043 -0
  10. package/src/ummaya/_canonical/__init__.py +2 -0
  11. package/src/ummaya/_canonical/baselines.yaml +113 -0
  12. package/src/ummaya/engine/engine.py +29 -132
  13. package/src/ummaya/evidence/__init__.py +21 -2
  14. package/src/ummaya/evidence/dataset_contract.py +193 -0
  15. package/src/ummaya/evidence/document_authoring_cases.py +33 -0
  16. package/src/ummaya/evidence/document_harness.py +313 -0
  17. package/src/ummaya/evidence/document_viewer_ux.py +391 -0
  18. package/src/ummaya/evidence/gates.py +70 -0
  19. package/src/ummaya/evidence/json_types.py +20 -0
  20. package/src/ummaya/evidence/models.py +88 -1
  21. package/src/ummaya/evidence/output_payload.py +89 -0
  22. package/src/ummaya/evidence/payload_documents.py +233 -0
  23. package/src/ummaya/evidence/route_contracts.py +224 -0
  24. package/src/ummaya/evidence/route_helpers.py +150 -0
  25. package/src/ummaya/evidence/runner.py +81 -212
  26. package/src/ummaya/evidence/source_provenance.py +246 -0
  27. package/src/ummaya/evidence/source_provenance_redaction.py +176 -0
  28. package/src/ummaya/evidence/tool_layer.py +39 -0
  29. package/src/ummaya/evidence/tool_layer_models.py +151 -0
  30. package/src/ummaya/ipc/adapter_manifest_emitter.py +26 -10
  31. package/src/ummaya/ipc/document_intent_normalization.py +185 -0
  32. package/src/ummaya/ipc/frame_schema.py +5 -5
  33. package/src/ummaya/ipc/route_diagnostics.py +73 -0
  34. package/src/ummaya/ipc/stdio.py +1109 -477
  35. package/src/ummaya/llm/client.py +102 -3
  36. package/src/ummaya/llm/config.py +8 -3
  37. package/src/ummaya/primitives/__init__.py +6 -2
  38. package/src/ummaya/primitives/delegation.py +1 -1
  39. package/src/ummaya/primitives/document.py +28 -0
  40. package/src/ummaya/settings.py +0 -3
  41. package/src/ummaya/tools/discovery_bridge.py +17 -1
  42. package/src/ummaya/tools/documents/__init__.py +297 -0
  43. package/src/ummaya/tools/documents/adapter_registry.py +487 -0
  44. package/src/ummaya/tools/documents/archive_container_probe.py +167 -0
  45. package/src/ummaya/tools/documents/artifact_store.py +454 -0
  46. package/src/ummaya/tools/documents/authoring.py +283 -0
  47. package/src/ummaya/tools/documents/baselines.py +132 -0
  48. package/src/ummaya/tools/documents/capability.py +331 -0
  49. package/src/ummaya/tools/documents/contracts.py +112 -0
  50. package/src/ummaya/tools/documents/conversion.py +521 -0
  51. package/src/ummaya/tools/documents/diff.py +275 -0
  52. package/src/ummaya/tools/documents/engines.py +163 -0
  53. package/src/ummaya/tools/documents/evaluation.py +291 -0
  54. package/src/ummaya/tools/documents/explicit_values.py +108 -0
  55. package/src/ummaya/tools/documents/fixtures.py +174 -0
  56. package/src/ummaya/tools/documents/format_completion_audit.py +471 -0
  57. package/src/ummaya/tools/documents/formats/__init__.py +2 -0
  58. package/src/ummaya/tools/documents/formats/archive.py +528 -0
  59. package/src/ummaya/tools/documents/formats/base.py +41 -0
  60. package/src/ummaya/tools/documents/formats/code_file.py +211 -0
  61. package/src/ummaya/tools/documents/formats/data_file.py +272 -0
  62. package/src/ummaya/tools/documents/formats/hwp.py +284 -0
  63. package/src/ummaya/tools/documents/formats/hwpx.py +1837 -0
  64. package/src/ummaya/tools/documents/formats/odf.py +435 -0
  65. package/src/ummaya/tools/documents/formats/ooxml.py +1030 -0
  66. package/src/ummaya/tools/documents/formats/passive.py +766 -0
  67. package/src/ummaya/tools/documents/formats/pdf.py +702 -0
  68. package/src/ummaya/tools/documents/formats/text_web.py +268 -0
  69. package/src/ummaya/tools/documents/hwp_conversion_probe.py +178 -0
  70. package/src/ummaya/tools/documents/hwp_direct_candidate.py +141 -0
  71. package/src/ummaya/tools/documents/inspection.py +289 -0
  72. package/src/ummaya/tools/documents/intake.py +1079 -0
  73. package/src/ummaya/tools/documents/legacy_office_promotion_probe.py +366 -0
  74. package/src/ummaya/tools/documents/models.py +1598 -0
  75. package/src/ummaya/tools/documents/odf_promotion_probe.py +167 -0
  76. package/src/ummaya/tools/documents/orchestrator.py +96 -0
  77. package/src/ummaya/tools/documents/passive_capability_probe.py +251 -0
  78. package/src/ummaya/tools/documents/patch.py +170 -0
  79. package/src/ummaya/tools/documents/pdfa_conformance.py +284 -0
  80. package/src/ummaya/tools/documents/pdfa_promotion_probe.py +198 -0
  81. package/src/ummaya/tools/documents/permissions.py +110 -0
  82. package/src/ummaya/tools/documents/planner.py +616 -0
  83. package/src/ummaya/tools/documents/registry.py +2733 -0
  84. package/src/ummaya/tools/documents/render.py +978 -0
  85. package/src/ummaya/tools/documents/render_comparison.py +113 -0
  86. package/src/ummaya/tools/documents/render_comparison_models.py +74 -0
  87. package/src/ummaya/tools/documents/render_comparison_regions.py +73 -0
  88. package/src/ummaya/tools/documents/render_comparison_style.py +161 -0
  89. package/src/ummaya/tools/documents/reread.py +157 -0
  90. package/src/ummaya/tools/documents/runtime_authoring.py +244 -0
  91. package/src/ummaya/tools/documents/runtime_authoring_bundle.py +76 -0
  92. package/src/ummaya/tools/documents/scorecard.py +184 -0
  93. package/src/ummaya/tools/documents/socratic_planner.py +193 -0
  94. package/src/ummaya/tools/documents/style.py +48 -0
  95. package/src/ummaya/tools/documents/tool_defs.py +523 -0
  96. package/src/ummaya/tools/documents/validate.py +347 -0
  97. package/src/ummaya/tools/executor.py +29 -0
  98. package/src/ummaya/tools/live_proxy.py +0 -3
  99. package/src/ummaya/tools/models.py +5 -1
  100. package/src/ummaya/tools/register_all.py +8 -0
  101. package/src/ummaya/tools/registry.py +10 -1
  102. package/src/ummaya/tools/routing/__init__.py +59 -0
  103. package/src/ummaya/tools/routing/builder.py +105 -0
  104. package/src/ummaya/tools/routing/cards.py +29 -0
  105. package/src/ummaya/tools/routing/decision_service.py +534 -0
  106. package/src/ummaya/tools/routing/decision_types.py +74 -0
  107. package/src/ummaya/tools/routing/feasibility.py +122 -0
  108. package/src/ummaya/tools/routing/intent.py +17 -0
  109. package/src/ummaya/tools/routing/intent_extractor.py +207 -0
  110. package/src/ummaya/tools/routing/intent_patterns.py +160 -0
  111. package/src/ummaya/tools/routing/intent_public_data.py +150 -0
  112. package/src/ummaya/tools/routing/intent_types.py +48 -0
  113. package/src/ummaya/tools/routing/lint.py +78 -0
  114. package/src/ummaya/tools/routing/metadata.py +174 -0
  115. package/src/ummaya/tools/routing/projection.py +340 -0
  116. package/src/ummaya/tools/routing/retrieval_policy.py +629 -0
  117. package/src/ummaya/tools/routing/schema.py +81 -0
  118. package/src/ummaya/tools/routing/types.py +96 -0
  119. package/src/ummaya/tools/routing_index.py +2 -2
  120. package/src/ummaya/tools/search.py +34 -746
  121. package/tests/fixtures/documents/public_forms/baselines.yaml +113 -0
  122. package/tui/bun.lock +126 -305
  123. package/tui/package.json +35 -22
  124. package/tui/src/.cc-byte-identical-whitelist.yaml +266 -0
  125. package/tui/src/QueryEngine.ts +12 -8
  126. package/tui/src/bridge/inboundAttachments.ts +3 -3
  127. package/tui/src/cli/handlers/auth.ts +3 -12
  128. package/tui/src/cli/handlers/mcp.tsx +0 -1
  129. package/tui/src/cli/print.ts +8 -9
  130. package/tui/src/commands/insights.ts +1 -1
  131. package/tui/src/commands/install-github-app/types.ts +8 -30
  132. package/tui/src/commands/plugin/types.ts +6 -28
  133. package/tui/src/commands/plugin/unifiedTypes.ts +4 -26
  134. package/tui/src/commands/rename/generateSessionName.ts +1 -1
  135. package/tui/src/components/Feedback.tsx +1 -1
  136. package/tui/src/components/LogoV2/EmergencyTip.tsx +11 -2
  137. package/tui/src/components/LogoV2/WelcomeV2.tsx +1 -3
  138. package/tui/src/components/ScrollKeybindingHandler.tsx +6 -6
  139. package/tui/src/components/Spinner/types.ts +6 -28
  140. package/tui/src/components/agents/generateAgent.ts +1 -1
  141. package/tui/src/components/agents/new-agent-creation/types.ts +4 -26
  142. package/tui/src/components/config/EnvSecretIsolatedEditor.tsx +1 -1
  143. package/tui/src/components/mcp/types.ts +16 -38
  144. package/tui/src/components/messages/AssistantToolUseMessage.tsx +3 -2
  145. package/tui/src/components/messages/UserCrossSessionMessage.ts +16 -4
  146. package/tui/src/components/messages/UserForkBoilerplateMessage.ts +16 -4
  147. package/tui/src/components/messages/UserGitHubWebhookMessage.ts +16 -4
  148. package/tui/src/components/messages/UserToolResultMessage/utils.tsx +3 -2
  149. package/tui/src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.ts +9 -4
  150. package/tui/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.ts +9 -4
  151. package/tui/src/components/primitive/DocumentSocraticReviewBlock.tsx +129 -0
  152. package/tui/src/components/primitive/DocumentToolResultCard.tsx +224 -0
  153. package/tui/src/components/primitive/documentSocraticReview.ts +215 -0
  154. package/tui/src/components/primitive/index.tsx +43 -1
  155. package/tui/src/components/primitive/types.ts +137 -0
  156. package/tui/src/components/ui/option.ts +4 -26
  157. package/tui/src/constants/common.ts +0 -2
  158. package/tui/src/constants/prompts.ts +4 -3
  159. package/tui/src/constants/querySource.ts +4 -26
  160. package/tui/src/entrypoints/sdk/controlTypes.ts +26 -48
  161. package/tui/src/entrypoints/sdk/coreTypes.generated.ts +3 -25
  162. package/tui/src/entrypoints/sdk/runtimeTypes.ts +38 -60
  163. package/tui/src/entrypoints/sdk/sdkUtilityTypes.ts +4 -26
  164. package/tui/src/entrypoints/sdk/settingsTypes.generated.ts +3 -25
  165. package/tui/src/entrypoints/sdk/toolTypes.ts +3 -25
  166. package/tui/src/hooks/toolPermission/handlers/interactiveHandler.ts +10 -0
  167. package/tui/src/hooks/useApiKeyVerification.ts +1 -1
  168. package/tui/src/hooks/useVirtualScroll.ts +1 -1
  169. package/tui/src/ink/ink.tsx +33 -14
  170. package/tui/src/ink/reconciler.ts +2 -3
  171. package/tui/src/ink/render-to-screen.ts +30 -10
  172. package/tui/src/ipc/bridge.ts +62 -15
  173. package/tui/src/ipc/bridgeSingleton.ts +5 -1
  174. package/tui/src/ipc/codec.ts +3 -3
  175. package/tui/src/ipc/frames.generated.ts +12 -12
  176. package/tui/src/ipc/llmClient.ts +151 -27
  177. package/tui/src/ipc/schema/frame.schema.json +1 -1
  178. package/tui/src/keybindings/defaultBindings.ts +4 -0
  179. package/tui/src/main.tsx +32 -15
  180. package/tui/src/native-ts/file-index/index.ts +33 -3
  181. package/tui/src/observability/surface.ts +2 -2
  182. package/tui/src/probes/toolRegistryProbe.tsx +3 -1
  183. package/tui/src/projectOnboardingState.ts +7 -6
  184. package/tui/src/query/chatMessageTypes.ts +18 -0
  185. package/tui/src/query/chatMessagesBuilder.ts +1 -1
  186. package/tui/src/query/deps.ts +1 -1
  187. package/tui/src/query/messageGuards.ts +106 -0
  188. package/tui/src/query/publicDataTerminalRepair.ts +384 -0
  189. package/tui/src/query/run.ts +1075 -0
  190. package/tui/src/query/supportBoundary.ts +168 -0
  191. package/tui/src/query/toolResultErrors.ts +103 -0
  192. package/tui/src/query/toolRunner.ts +687 -0
  193. package/tui/src/query/unavailableToolRepair.ts +118 -0
  194. package/tui/src/query.ts +9 -2186
  195. package/tui/src/screens/REPL.tsx +40 -29
  196. package/tui/src/services/api/adapterManifest.ts +4 -0
  197. package/tui/src/services/api/backendChat/events.ts +117 -0
  198. package/tui/src/services/api/backendChat/finalMessage.ts +40 -0
  199. package/tui/src/services/api/backendChat/frame.ts +9 -0
  200. package/tui/src/services/api/backendChat/streaming.ts +430 -0
  201. package/tui/src/services/api/backendChat/types.ts +62 -0
  202. package/tui/src/services/api/backendChat.ts +1 -0
  203. package/tui/src/services/api/client.ts +65 -2
  204. package/tui/src/services/api/errorUtils.ts +5 -5
  205. package/tui/src/services/api/errors.ts +1 -1
  206. package/tui/src/services/api/logging.ts +1 -1
  207. package/tui/src/services/api/ummaya/evidence.ts +194 -0
  208. package/tui/src/services/api/ummaya/messages.ts +255 -0
  209. package/tui/src/services/api/ummaya/nonStreaming.ts +66 -0
  210. package/tui/src/services/api/ummaya/provider.ts +200 -0
  211. package/tui/src/services/api/ummaya/reasoning.ts +24 -0
  212. package/tui/src/services/api/ummaya/request.ts +200 -0
  213. package/tui/src/services/api/ummaya/selectionContext.ts +240 -0
  214. package/tui/src/services/api/ummaya/streaming.ts +365 -0
  215. package/tui/src/services/api/ummaya/streamingPayload.ts +129 -0
  216. package/tui/src/services/api/ummaya/streamingReader.ts +40 -0
  217. package/tui/src/services/api/ummaya/toolSelection.ts +217 -0
  218. package/tui/src/services/api/ummaya/types.ts +110 -0
  219. package/tui/src/services/api/ummaya/usage.ts +30 -0
  220. package/tui/src/services/api/ummaya.ts +26 -418
  221. package/tui/src/services/api/withRetry.ts +1 -1
  222. package/tui/src/services/awaySummary.ts +2 -2
  223. package/tui/src/services/claudeAiLimits.ts +1 -1
  224. package/tui/src/services/compact/autoCompact.ts +1 -1
  225. package/tui/src/services/compact/compact.ts +1 -1
  226. package/tui/src/services/lsp/types.ts +8 -30
  227. package/tui/src/services/tips/types.ts +6 -28
  228. package/tui/src/services/tokenEstimation.ts +1 -1
  229. package/tui/src/services/toolRegistry/bootGuard.ts +5 -5
  230. package/tui/src/services/toolUseSummary/toolUseSummaryGenerator.ts +1 -1
  231. package/tui/src/services/tools/toolExecution.ts +94 -1
  232. package/tui/src/store/pendingPermissionSlot.ts +1 -1
  233. package/tui/src/store/session-store.ts +10 -36
  234. package/tui/src/stubs/any-stub.ts +15 -10
  235. package/tui/src/stubs/color-diff-napi.ts +37 -23
  236. package/tui/src/stubs/globals.d.ts +3 -3
  237. package/tui/src/stubs/macro-preload.ts +23 -12
  238. package/tui/src/tools/AdapterTool/AdapterTool.ts +1207 -714
  239. package/tui/src/tools/AdapterTool/routeDiagnostics.ts +75 -0
  240. package/tui/src/tools/AgentTool/AgentTool.tsx +84 -1371
  241. package/tui/src/tools/AgentTool/agentToolHandoff.ts +114 -0
  242. package/tui/src/tools/AgentTool/agentToolPartialResult.ts +16 -0
  243. package/tui/src/tools/AgentTool/agentToolProgress.ts +32 -0
  244. package/tui/src/tools/AgentTool/agentToolResolver.ts +161 -0
  245. package/tui/src/tools/AgentTool/agentToolResult.ts +163 -0
  246. package/tui/src/tools/AgentTool/agentToolUtils.ts +14 -686
  247. package/tui/src/tools/AgentTool/asyncAgentLifecycle.ts +208 -0
  248. package/tui/src/tools/AgentTool/asyncLifecycle.ts +153 -0
  249. package/tui/src/tools/AgentTool/backgroundedCompletion.ts +126 -0
  250. package/tui/src/tools/AgentTool/backgroundedLifecycle.ts +174 -0
  251. package/tui/src/tools/AgentTool/foregroundBackground.ts +83 -0
  252. package/tui/src/tools/AgentTool/foregroundDrain.tsx +133 -0
  253. package/tui/src/tools/AgentTool/foregroundFinalize.ts +98 -0
  254. package/tui/src/tools/AgentTool/foregroundLifecycle.tsx +237 -0
  255. package/tui/src/tools/AgentTool/foregroundProgress.tsx +169 -0
  256. package/tui/src/tools/AgentTool/foregroundTask.ts +89 -0
  257. package/tui/src/tools/AgentTool/forkSubagent.ts +1 -12
  258. package/tui/src/tools/AgentTool/forkSubagentGate.ts +34 -0
  259. package/tui/src/tools/AgentTool/launchRouting.ts +203 -0
  260. package/tui/src/tools/AgentTool/lifecycle.ts +244 -0
  261. package/tui/src/tools/AgentTool/mcpRouting.ts +73 -0
  262. package/tui/src/tools/AgentTool/orchestrationSupport.ts +70 -0
  263. package/tui/src/tools/AgentTool/permissions.ts +39 -0
  264. package/tui/src/tools/AgentTool/promptSetup.ts +181 -0
  265. package/tui/src/tools/AgentTool/remoteRouting.ts +62 -0
  266. package/tui/src/tools/AgentTool/resultMapping.ts +116 -0
  267. package/tui/src/tools/AgentTool/resumeAgent.ts +39 -107
  268. package/tui/src/tools/AgentTool/resumeAgentHelpers.ts +140 -0
  269. package/tui/src/tools/AgentTool/runAgent.ts +1 -1
  270. package/tui/src/tools/AgentTool/runtimeConfig.ts +57 -0
  271. package/tui/src/tools/AgentTool/schemas.ts +196 -0
  272. package/tui/src/tools/AgentTool/sourceVerificationPropagation.ts +263 -0
  273. package/tui/src/tools/AgentTool/worktreeLifecycle.ts +105 -0
  274. package/tui/src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx +174 -202
  275. package/tui/src/tools/BashTool/BashTool.tsx +71 -1072
  276. package/tui/src/tools/BashTool/bashCommandHelpers.ts +12 -12
  277. package/tui/src/tools/BashTool/bashPermissions/astPreflight.ts +173 -0
  278. package/tui/src/tools/BashTool/bashPermissions/classifierChecks.ts +199 -0
  279. package/tui/src/tools/BashTool/bashPermissions/compoundGuards.ts +53 -0
  280. package/tui/src/tools/BashTool/bashPermissions/constants.ts +99 -0
  281. package/tui/src/tools/BashTool/bashPermissions/index.ts +38 -0
  282. package/tui/src/tools/BashTool/bashPermissions/legacyMisparsing.ts +62 -0
  283. package/tui/src/tools/BashTool/bashPermissions/main.ts +135 -0
  284. package/tui/src/tools/BashTool/bashPermissions/normalizedCommands.ts +33 -0
  285. package/tui/src/tools/BashTool/bashPermissions/operatorFlow.ts +98 -0
  286. package/tui/src/tools/BashTool/bashPermissions/permissionChecks.ts +200 -0
  287. package/tui/src/tools/BashTool/bashPermissions/prefixSuggestions.ts +88 -0
  288. package/tui/src/tools/BashTool/bashPermissions/promptClassifierRules.ts +125 -0
  289. package/tui/src/tools/BashTool/bashPermissions/ruleDelegates.ts +19 -0
  290. package/tui/src/tools/BashTool/bashPermissions/ruleMatching.ts +145 -0
  291. package/tui/src/tools/BashTool/bashPermissions/sandboxAutoAllow.ts +75 -0
  292. package/tui/src/tools/BashTool/bashPermissions/subcommandFlow.ts +205 -0
  293. package/tui/src/tools/BashTool/bashPermissions/subcommandGuards.ts +73 -0
  294. package/tui/src/tools/BashTool/bashPermissions/subcommandResultHelpers.ts +116 -0
  295. package/tui/src/tools/BashTool/bashPermissions/types.ts +26 -0
  296. package/tui/src/tools/BashTool/bashPermissions/wrapperStripping.ts +139 -0
  297. package/tui/src/tools/BashTool/bashPermissions.ts +26 -2621
  298. package/tui/src/tools/BashTool/call.ts +202 -0
  299. package/tui/src/tools/BashTool/callLoader.ts +35 -0
  300. package/tui/src/tools/BashTool/commandClassification.ts +151 -0
  301. package/tui/src/tools/BashTool/commandClassificationLoader.ts +40 -0
  302. package/tui/src/tools/BashTool/cwdReset.ts +33 -0
  303. package/tui/src/tools/BashTool/lineTruncation.ts +11 -0
  304. package/tui/src/tools/BashTool/modeValidation.ts +13 -1
  305. package/tui/src/tools/BashTool/outputPersistence.ts +42 -0
  306. package/tui/src/tools/BashTool/permissionClassification.ts +66 -0
  307. package/tui/src/tools/BashTool/permissionLoader.ts +44 -0
  308. package/tui/src/tools/BashTool/resultLoader.ts +29 -0
  309. package/tui/src/tools/BashTool/resultMapping.ts +83 -0
  310. package/tui/src/tools/BashTool/sandboxPolicy.ts +79 -0
  311. package/tui/src/tools/BashTool/schemas.ts +65 -0
  312. package/tui/src/tools/BashTool/sedEditExecution.ts +59 -0
  313. package/tui/src/tools/BashTool/shellExecution.tsx +245 -0
  314. package/tui/src/tools/BashTool/shellOutputUtils.ts +85 -0
  315. package/tui/src/tools/BashTool/shellPermissionGauntlet.ts +97 -0
  316. package/tui/src/tools/BashTool/uiLoader.ts +37 -0
  317. package/tui/src/tools/BriefTool/upload.ts +1 -1
  318. package/tui/src/tools/CalculatorTool/parser.ts +2 -2
  319. package/tui/src/tools/DocumentPrimitive/DocumentPrimitive.ts +262 -0
  320. package/tui/src/tools/DocumentPrimitive/dispatchNormalization.ts +270 -0
  321. package/tui/src/tools/DocumentPrimitive/documentDestinationPath.ts +18 -0
  322. package/tui/src/tools/DocumentPrimitive/documentMutationGuard.ts +22 -0
  323. package/tui/src/tools/DocumentPrimitive/documentPatchNormalization.ts +248 -0
  324. package/tui/src/tools/DocumentPrimitive/documentSourceVerification.ts +245 -0
  325. package/tui/src/tools/DocumentPrimitive/documentSourceVerificationFields.ts +103 -0
  326. package/tui/src/tools/DocumentPrimitive/modelVisibleOutput.ts +40 -0
  327. package/tui/src/tools/DocumentPrimitive/prompt.ts +35 -0
  328. package/tui/src/tools/FileEditTool/FileEditTool.ts +9 -507
  329. package/tui/src/tools/FileEditTool/call.ts +228 -0
  330. package/tui/src/tools/FileEditTool/validateInput.ts +196 -0
  331. package/tui/src/tools/FileReadTool/imageProcessor.ts +13 -0
  332. package/tui/src/tools/FileWriteTool/FileWriteTool.ts +7 -300
  333. package/tui/src/tools/FileWriteTool/call.ts +223 -0
  334. package/tui/src/tools/FileWriteTool/validateInput.ts +80 -0
  335. package/tui/src/tools/ListMcpResourcesTool/ListMcpResourcesTool.ts +19 -3
  336. package/tui/src/tools/LookupPrimitive/LookupPrimitive.ts +25 -32
  337. package/tui/src/tools/LookupPrimitive/prompt.ts +0 -2
  338. package/tui/src/tools/MCPTool/trustPolicy.ts +118 -0
  339. package/tui/src/tools/McpAuthTool/McpAuthTool.ts +21 -3
  340. package/tui/src/tools/NotebookEditTool/NotebookEditTool.ts +7 -326
  341. package/tui/src/tools/NotebookEditTool/call.ts +254 -0
  342. package/tui/src/tools/NotebookEditTool/notebookModel.ts +51 -0
  343. package/tui/src/tools/NotebookEditTool/validateInput.ts +142 -0
  344. package/tui/src/tools/PowerShellTool/PowerShellTool.tsx +46 -937
  345. package/tui/src/tools/PowerShellTool/acceptEditsCommandValidation.ts +162 -0
  346. package/tui/src/tools/PowerShellTool/call.ts +179 -0
  347. package/tui/src/tools/PowerShellTool/callLoader.ts +37 -0
  348. package/tui/src/tools/PowerShellTool/commandClassification.ts +86 -0
  349. package/tui/src/tools/PowerShellTool/modeValidation.ts +25 -332
  350. package/tui/src/tools/PowerShellTool/outputPersistence.ts +42 -0
  351. package/tui/src/tools/PowerShellTool/permissionClassification.ts +28 -0
  352. package/tui/src/tools/PowerShellTool/resultLoader.ts +31 -0
  353. package/tui/src/tools/PowerShellTool/resultMapping.ts +75 -0
  354. package/tui/src/tools/PowerShellTool/schemas.ts +40 -0
  355. package/tui/src/tools/PowerShellTool/shellExecution.tsx +258 -0
  356. package/tui/src/tools/PowerShellTool/symlinkModeValidation.ts +44 -0
  357. package/tui/src/tools/PowerShellTool/uiLoader.ts +37 -0
  358. package/tui/src/tools/PowerShellTool/validation.ts +39 -0
  359. package/tui/src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts +19 -3
  360. package/tui/src/tools/ResolveLocationPrimitive/ResolveLocationPrimitive.ts +1 -11
  361. package/tui/src/tools/ResolveLocationPrimitive/prompt.ts +2 -6
  362. package/tui/src/tools/SkillTool/SkillTool.ts +2 -2
  363. package/tui/src/tools/SubmitPrimitive/SubmitPrimitive.ts +27 -10
  364. package/tui/src/tools/TaskCreateTool/TaskCreateTool.ts +16 -2
  365. package/tui/src/tools/TaskGetTool/TaskGetTool.ts +23 -3
  366. package/tui/src/tools/TaskListTool/TaskListTool.ts +22 -4
  367. package/tui/src/tools/TaskOutputTool/TaskOutputTool.tsx +46 -547
  368. package/tui/src/tools/TaskOutputTool/lookup.ts +216 -0
  369. package/tui/src/tools/TaskOutputTool/render.tsx +257 -0
  370. package/tui/src/tools/TaskOutputTool/schemas.ts +55 -0
  371. package/tui/src/tools/TaskOutputTool/serialization.ts +36 -0
  372. package/tui/src/tools/TaskStopTool/TaskStopTool.ts +10 -0
  373. package/tui/src/tools/TaskUpdateTool/TaskUpdateTool.ts +14 -364
  374. package/tui/src/tools/TaskUpdateTool/completion.ts +62 -0
  375. package/tui/src/tools/TaskUpdateTool/schemas.ts +62 -0
  376. package/tui/src/tools/TaskUpdateTool/serialization.ts +46 -0
  377. package/tui/src/tools/TaskUpdateTool/statusUpdate.ts +247 -0
  378. package/tui/src/tools/TodoWriteTool/TodoWriteTool.ts +21 -2
  379. package/tui/src/tools/ToolSearchTool/ToolSearchTool.ts +21 -302
  380. package/tui/src/tools/ToolSearchTool/ccSupportTools.ts +223 -0
  381. package/tui/src/tools/ToolSearchTool/descriptionCache.ts +50 -0
  382. package/tui/src/tools/ToolSearchTool/keywordSearch.ts +216 -0
  383. package/tui/src/tools/ToolSearchTool/prompt.ts +10 -4
  384. package/tui/src/tools/ToolSearchTool/resultMapping.ts +30 -0
  385. package/tui/src/tools/ToolSearchTool/schemas.ts +30 -0
  386. package/tui/src/tools/ToolSearchTool/searchPool.ts +47 -0
  387. package/tui/src/tools/ToolSearchTool/supportIntentHints.ts +140 -0
  388. package/tui/src/tools/TranslateTool/TranslateTool.ts +1 -1
  389. package/tui/src/tools/VerifyPrimitive/VerifyPrimitive.ts +2 -1
  390. package/tui/src/tools/WebFetchTool/WebFetchTool.ts +43 -138
  391. package/tui/src/tools/WebFetchTool/call.ts +227 -0
  392. package/tui/src/tools/WebFetchTool/resolvedAddressSafety.ts +78 -0
  393. package/tui/src/tools/WebFetchTool/sourceVerification.ts +204 -0
  394. package/tui/src/tools/WebFetchTool/types.ts +23 -0
  395. package/tui/src/tools/WebFetchTool/urlSafety.ts +181 -0
  396. package/tui/src/tools/WebFetchTool/utils.ts +1 -1
  397. package/tui/src/tools/WebSearchTool/UI.tsx +0 -1
  398. package/tui/src/tools/WebSearchTool/WebSearchTool.ts +9 -313
  399. package/tui/src/tools/WebSearchTool/call.ts +33 -0
  400. package/tui/src/tools/WebSearchTool/responseMapping.ts +190 -0
  401. package/tui/src/tools/WebSearchTool/resultBlock.ts +47 -0
  402. package/tui/src/tools/WebSearchTool/schemas.ts +47 -0
  403. package/tui/src/tools/WebSearchTool/toolSchema.ts +12 -0
  404. package/tui/src/tools/WorkspaceToolAdapter/WorkspaceToolAdapter.ts +79 -0
  405. package/tui/src/tools/WorkspaceToolAdapter/allowedRootPolicy.ts +85 -0
  406. package/tui/src/tools/WorkspaceToolAdapter/documentFormatGuards.ts +73 -0
  407. package/tui/src/tools/WorkspaceToolAdapter/inputNormalization.ts +105 -0
  408. package/tui/src/tools/WorkspaceToolAdapter/mcpExposurePolicy.ts +64 -0
  409. package/tui/src/tools/WorkspaceToolAdapter/toolDefFactory.ts +215 -0
  410. package/tui/src/tools/WorkspaceToolAdapter/toolNames.ts +6 -0
  411. package/tui/src/tools/WorkspaceToolAdapter/workspacePolicy.ts +15 -0
  412. package/tui/src/tools/_shared/dispatchPrimitive.ts +6 -6
  413. package/tui/src/tools/_shared/documentChangeToPatch.ts +125 -0
  414. package/tui/src/tools/_shared/documentDispatchArguments.ts +87 -0
  415. package/tui/src/tools/_shared/documentPrimitiveTimeout.ts +13 -0
  416. package/tui/src/tools/_shared/documentToolResultRender.ts +98 -0
  417. package/tui/src/tools/_shared/pendingCallRegistry.ts +1 -6
  418. package/tui/src/tools/_shared/rootPrimitiveInput.ts +1 -0
  419. package/tui/src/tools/_shared/toolChoiceRepair/documentCompletionPatterns.ts +58 -0
  420. package/tui/src/tools/_shared/toolChoiceRepair/documentCompletionPrompt.ts +271 -0
  421. package/tui/src/tools/_shared/toolChoiceRepair/documentRepair.ts +452 -0
  422. package/tui/src/tools/_shared/toolChoiceRepair/messageAccess.ts +80 -0
  423. package/tui/src/tools/_shared/toolChoiceRepair/publicDataRepair.ts +92 -0
  424. package/tui/src/tools/_shared/toolChoiceRepair/supportRepair.ts +135 -0
  425. package/tui/src/tools/_shared/toolChoiceRepair.ts +55 -860
  426. package/tui/src/tools/shared/mockDisclaimer.ts +1 -1
  427. package/tui/src/tools.ts +39 -190
  428. package/tui/src/types/fileSuggestion.ts +4 -26
  429. package/tui/src/types/generated/events_mono/claude_code/v1/claude_code_internal_event.ts +186 -148
  430. package/tui/src/types/generated/events_mono/common/v1/auth.ts +25 -11
  431. package/tui/src/types/generated/events_mono/growthbook/v1/growthbook_experiment_event.ts +47 -30
  432. package/tui/src/types/generated/google/protobuf/timestamp.ts +21 -7
  433. package/tui/src/types/message.ts +80 -102
  434. package/tui/src/types/messageQueueTypes.ts +6 -28
  435. package/tui/src/types/notebook.ts +16 -38
  436. package/tui/src/types/statusLine.ts +4 -26
  437. package/tui/src/types/tools.ts +24 -46
  438. package/tui/src/types/utils.ts +6 -28
  439. package/tui/src/upstreamproxy/relay.ts +7 -3
  440. package/tui/src/upstreamproxy/upstreamproxy.ts +1 -1
  441. package/tui/src/utils/assistantMessageFactories.ts +9 -3
  442. package/tui/src/utils/auth.ts +129 -139
  443. package/tui/src/utils/bash/ast.ts +23 -23
  444. package/tui/src/utils/bash/bashParser.ts +5 -5
  445. package/tui/src/utils/billing.ts +1 -1
  446. package/tui/src/utils/claudeDesktop.ts +4 -4
  447. package/tui/src/utils/collapseReadSearch.ts +3 -3
  448. package/tui/src/utils/cronTasks.ts +1 -1
  449. package/tui/src/utils/execFileNoThrow.ts +1 -1
  450. package/tui/src/utils/filePersistence/types.ts +16 -38
  451. package/tui/src/utils/forkedAgent.ts +1 -1
  452. package/tui/src/utils/gracefulShutdown.ts +4 -4
  453. package/tui/src/utils/heapDumpService.ts +12 -8
  454. package/tui/src/utils/hooks/apiQueryHookHelper.ts +1 -1
  455. package/tui/src/utils/hooks/execPromptHook.ts +1 -1
  456. package/tui/src/utils/hooks/skillImprovement.ts +1 -1
  457. package/tui/src/utils/mcp/dateTimeParser.ts +1 -1
  458. package/tui/src/utils/messages.ts +18 -0
  459. package/tui/src/utils/migrateSessions.ts +3 -3
  460. package/tui/src/utils/model/model.ts +6 -6
  461. package/tui/src/utils/permissions/yoloClassifier.ts +1 -1
  462. package/tui/src/utils/plugins/headlessPluginInstall.ts +1 -1
  463. package/tui/src/utils/plugins/mcpPluginIntegration.ts +1 -1
  464. package/tui/src/utils/plugins/mcpbHandler.ts +1 -1
  465. package/tui/src/utils/plugins/pluginLoader.ts +8 -8
  466. package/tui/src/utils/protectedNamespace.ts +5 -3
  467. package/tui/src/utils/rawJsonToolCall.ts +242 -0
  468. package/tui/src/utils/ripgrep.ts +16 -7
  469. package/tui/src/utils/sessionTitle.ts +1 -1
  470. package/tui/src/utils/settings/permissionValidation.ts +14 -2
  471. package/tui/src/utils/shell/prefix.ts +1 -1
  472. package/tui/src/utils/sideQuery.ts +1 -1
  473. package/tui/src/utils/systemThemeWatcher.ts +13 -3
  474. package/tui/src/utils/teleport.tsx +1 -1
  475. package/uv.lock +426 -45
  476. package/tui/src/services/api/claude.ts +0 -3540
  477. package/tui/src/tools/_shared/directPublicDataGuard.ts +0 -362
  478. package/tui/src/tools/_shared/kmaAnalysisGuard.ts +0 -197
  479. package/tui/src/tools/_shared/kmaAviationGuard.ts +0 -70
  480. package/tui/src/tools/_shared/nmcAedGuard.ts +0 -234
  481. package/tui/src/tools/_shared/protectedCheckGuard.ts +0 -207
  482. package/tui/src/tools/_shared/textToolCallGuard.ts +0 -91
@@ -1,4 +1,8 @@
1
1
  import { z } from 'zod/v4'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { existsSync } from 'node:fs'
4
+ import { homedir } from 'node:os'
5
+ import { basename, join } from 'node:path'
2
6
  import {
3
7
  buildTool,
4
8
  type Tool,
@@ -14,89 +18,77 @@ import {
14
18
  import { getOrCreateUmmayaBridge } from '../../ipc/bridgeSingleton.js'
15
19
  import { getOrCreatePendingCallRegistry } from '../../ipc/pendingCallSingleton.js'
16
20
  import { dispatchPrimitive } from '../_shared/dispatchPrimitive.js'
17
- import { validateKmaAviationToolChoice } from '../_shared/kmaAviationGuard.js'
18
- import { validateKmaAnalysisToolChoice } from '../_shared/kmaAnalysisGuard.js'
19
- import { validateNmcAedToolChoice } from '../_shared/nmcAedGuard.js'
20
21
  import {
21
- normalizeDirectPublicDataToolInput,
22
- validateDirectPublicDataToolChoice,
23
- } from '../_shared/directPublicDataGuard.js'
22
+ applyDocumentVisualRenderGateToOutput,
23
+ extractDocumentToolResultPayload,
24
+ isDocumentVisualRenderFailedOutput,
25
+ renderDocumentToolResultIfPresent,
26
+ } from '../_shared/documentToolResultRender.js'
24
27
  import { LookupPrimitive } from '../LookupPrimitive/LookupPrimitive.js'
25
28
  import { ResolveLocationPrimitive } from '../ResolveLocationPrimitive/ResolveLocationPrimitive.js'
26
29
  import { SubmitPrimitive } from '../SubmitPrimitive/SubmitPrimitive.js'
27
30
  import { VerifyPrimitive } from '../VerifyPrimitive/VerifyPrimitive.js'
31
+ import { DocumentPrimitive } from '../DocumentPrimitive/DocumentPrimitive.js'
32
+ import { resolveDocumentPrimitiveTimeoutMs } from '../_shared/documentPrimitiveTimeout.js'
28
33
 
29
- type AdapterPrimitive = 'find' | 'locate' | 'send' | 'check'
34
+ type AdapterPrimitive = 'find' | 'locate' | 'send' | 'check' | 'document'
30
35
 
31
36
  type InputSchema = z.ZodType<{ [key: string]: unknown }>
32
37
 
33
- const ROOT_PRIMITIVE_TOOL_NAMES = new Set(['locate', 'find', 'check', 'send'])
34
- const KMA_URL_AIR_TOOL_NAMES = new Set([
35
- 'kma_apihub_url_air_amos_minute',
36
- 'kma_apihub_url_air_metar_decoded',
38
+ const ROOT_PRIMITIVE_TOOL_NAMES = new Set([
39
+ 'locate',
40
+ 'find',
41
+ 'check',
42
+ 'send',
43
+ 'document',
37
44
  ])
38
- const KMA_ANALYSIS_TOOL_NAMES = new Set([
39
- 'kma_apihub_url_high_resolution_grid_point',
40
- 'kma_apihub_url_aws_objective_analysis_grid',
41
- 'kma_apihub_url_analysis_weather_chart_image',
45
+ const DOCUMENT_TOOL_NAMES = new Set([
46
+ 'document',
47
+ 'document_inspect',
48
+ 'document_extract',
49
+ 'document_form_schema',
50
+ 'document_copy_for_edit',
51
+ 'document_apply_fill',
52
+ 'document_apply_style',
53
+ 'document_render',
54
+ 'document_validate_public_form',
55
+ 'document_save',
42
56
  ])
43
- const TAGO_BUS_TOOL_NAMES = new Set([
44
- 'tago_bus_station_search',
45
- 'tago_bus_arrival_search',
46
- 'tago_bus_route_search',
47
- 'tago_bus_route_station_search',
48
- 'tago_bus_location_search',
57
+ const DOCUMENT_MUTATION_TOOL_NAMES = new Set([
58
+ 'document_apply_fill',
59
+ 'document_apply_style',
49
60
  ])
50
- const KMA_GIMHAE_AIRPORT_RE = /(김해공항|gimhae|rkpk)/iu
51
- const KMA_GIMPO_AIRPORT_RE = /(김포공항|gimpo|rkss)/iu
52
- const KMA_AIRPORT_NAME_RE = /(공항|\bairport\b|\brk[a-z]{2}\b|station\s*\d{2,3})/iu
53
- const KMA_AIRPORT_AVIATION_RE =
54
- /(amos|metar|speci|rvr|항공기상|공항기상|활주로|runway|aviation|비행기|항공편|비행편|이륙|착륙|결항|지연|운항|뜰\s*만|뜨나|뜰\s*수|flight|take\s*off|landing|delay|cancel)/iu
55
- const KMA_RUNWAY_AREA_RE =
56
- /(amos|활주로|rvr|runway|시정|visibility|공항기상관측|매분)/iu
57
- const KMA_ANALYSIS_DATA_RE =
58
- /(분석자료|이미\s*분석|고해상도\s*격자|객관분석|aws\s*객관|지도\s*자료|일기도|분석일기도|비구름|바람\s*흐름|synoptic|weather\s*chart|objective\s*analysis|high[-\s]?resolution|grid)/iu
59
- const KMA_ANALYSIS_MAP_RE =
60
- /(일기도|분석일기도|지도\s*자료|비구름|바람\s*흐름|synoptic|weather\s*chart)/iu
61
- const KMA_ANALYSIS_POINT_RE =
62
- /(주변|근처|특정지점|좌표|위도|경도|\blat\b|\blon\b|공항\s*주변)/iu
63
- const KMA_LIFESTYLE_WEATHER_RE =
64
- /(날씨|현재\s*기상|실황|관측|예보|기온|습도|풍속|지금\s*비|비\s*(와|오|올|내리)|우산|강수|소나기|산책|퇴근|current\s+weather|forecast|rain|umbrella|precipitation|temperature)/iu
65
- const KMA_LIFESTYLE_WEATHER_TOOL_NAMES = new Set([
66
- 'kma_current_observation',
67
- 'kma_ultra_short_term_forecast',
68
- 'kma_short_term_forecast',
61
+ const DOCUMENT_ARTIFACT_FOLLOWUP_TOOL_NAMES = new Set([
62
+ 'document_apply_fill',
63
+ 'document_apply_style',
64
+ 'document_render',
65
+ 'document_validate_public_form',
66
+ 'document_save',
69
67
  ])
70
- const HIRA_MEDICAL_DETAIL_RE =
71
- /((병원|의료기관|의원).*(상세|진료과|진료과목|진료시간|주차)|(상세|진료시간|주차|응급실).*(병원|의료기관|의원)|ykiho|detail)/iu
72
- const MOIS_EMERGENCY_CALL_BOX_RE =
73
- /(안전\s*비상벨|비상벨|긴급\s*신고함|긴급신고함|방범벨|emergency\s+call\s+box)/iu
74
- const GYERYONG_ASSISTIVE_CHARGER_RE =
75
- /((전동보장구|전동\s*휠체어|보장구|장애인).*(충전|충전소|충전장소)|(충전|충전소|충전장소).*(전동보장구|전동\s*휠체어|보장구|장애인)|계룡시?.*(충전소|충전\s*장소))/iu
76
- const MOF_OCEAN_WATER_QUALITY_RE =
77
- /(해양\s*수질|해양수질|수질\s*자동\s*측정|용존산소|\bpH\b|water\s+quality|ocean\s+water)/iu
78
- const PPS_SHOPPING_RE = /(종합\s*쇼핑몰|쇼핑몰|계약\s*물품|물품\s*조회|shopping\s*mall)/iu
79
- const PPS_BID_RE = /(입찰|나라장터|조달청|\bbid\b|procurement|tender)/iu
80
- const PROTECTED_QUERY_RE =
81
- /(본인확인|인증|간편인증|모바일\s*(?:신분증|id)|mobile\s*id|마이데이터|mydata|증명원|소득금액증명|소득금액증명원|주민등록등본|민원|발급)/iu
82
- const PROTECTED_MOBILE_ID_RE = /(mobile\s*id|모바일\s*(?:신분증|id)|mobile_id)/iu
83
- const PROTECTED_SIMPLE_AUTH_RE =
84
- /(simple_auth|간편인증|ganpyeon|소득금액증명|증명원|민원|발급)/iu
85
- const PROTECTED_MYDATA_RE = /(mydata|마이데이터)/iu
86
- const PROTECTED_CHECK_TOOL_NAMES = [
87
- 'mock_verify_module_simple_auth',
88
- 'mock_verify_ganpyeon_injeung',
89
- 'mock_verify_mobile_id',
90
- 'mock_verify_mydata',
91
- ] as const
92
- const TAGO_BUS_RE =
93
- /(버스|시내버스|정류장|정류소|노선|도착|언제\s*와|몇\s*분|bus|route|arrival|station)/iu
94
- const AED_REQUEST_RE = /(aed|자동심장|심장충격|제세동)/iu
95
- const EMERGENCY_REQUEST_RE = /(응급|응급실|\ber\b|emergency)/iu
96
- const MEDICAL_COLLAPSE_RE =
97
- /(사람이\s*쓰러|쓰러졌|쓰러져|의식\s*잃|의식을\s*잃|심정지|호흡이\s*없|숨을\s*안|collapsed|unconscious|cardiac\s*arrest)/iu
98
- const TRAFFIC_HAZARD_RE =
99
- /(교통사고|사고\s*위험|사고다발|위험\s*(구간|도로|지점)|어린이보호구역|보호구역|도로\s*구간|accident|hazard|hotspot)/iu
68
+ // Purely mechanical pipeline steps that carry no user-meaningful change — only
69
+ // these are hidden on success. Substantive mutations (apply_fill / apply_style)
70
+ // now render their inline structural diff immediately (per-mutation trigger),
71
+ // the same way Claude Code shows a diff the moment an edit lands. See
72
+ // specs/2802-public-doc-harness/deep-research-migration-document-render.md.
73
+ const MECHANICAL_DOCUMENT_TOOL_NAMES = new Set(['document_copy_for_edit'])
74
+ const SAFE_DOCUMENT_ARTIFACT_ID_RE = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u
75
+ const EXPLICIT_DOCUMENT_ARTIFACT_MARKER_RE =
76
+ /(?:^|[\s"'`(])(?:artifact_id|artifact\s*id|artifact|아티팩트)\s*[:=]?\s*([A-Za-z0-9][A-Za-z0-9_.-]{0,127})(?=$|[^A-Za-z0-9_.-])/iu
77
+ const EXPLICIT_DOCUMENT_ARTIFACT_PREFIX_RE =
78
+ /(?:^|[\s"'`(])((?:source|working|derivative|render|export|viewport)-[A-Za-z0-9][A-Za-z0-9_.-]{0,127})(?=$|[^A-Za-z0-9_.-])/u
79
+ const DOCUMENT_FORMAT_RE = /\b(?:hwpx|hwp|docx|pdf|xlsx|pptx)\b/iu
80
+ const DOWNLOADS_FOLDER_PATH_RE = /(?:^|[\\/])Downloads$/u
81
+ const DOWNLOADS_PATH_SEGMENT_RE = /(?:^|[\\/])Downloads[\\/](?<tail>.+)$/iu
82
+ const DOCUMENT_EXTENSION_RE = /\.(?:hwpx|hwp|docx|pdf|xlsx|pptx)$/iu
83
+ const DOCUMENT_EXTENSION_TRAILING_PUNCT_RE =
84
+ /(\.(?:hwpx|hwp|docx|pdf|xlsx|pptx))[.。.]+$/iu
85
+ const EXPLICIT_LOCAL_DOCUMENT_PATH_RE =
86
+ /(?:^|[\s"'`(])(?<path>(?:~|\/|\.{1,2}\/|[A-Za-z]:\\)[^\n\r"'`]*?\.(?:hwpx|hwp|docx|pdf|xlsx|pptx))(?=$|[\s"'`),,。]|[가-힣])/giu
87
+ const HWPX_TEXT_TARGET_ALIAS_RE =
88
+ /^\/?hwp(?:x)?[-_/]text(?:[-_](?<indexA>\d+)|\[(?<indexB>\d+)\])(?:\/text\(\))?$/iu
89
+ const HWPX_TEXT_TARGET_RE = /^\/hwpx\/text\[\d+\]$/u
90
+ const HWPX_BLOCK_TABLE_CELL_TARGET_RE =
91
+ /^\/hwpx\/\[(?<tableId>hwpx-table-\d{3})\]\/cells\[(?<row>\d+)\]\[(?<column>\d+)\]$/u
100
92
 
101
93
  const fallbackInputSchema = z.object({}).passthrough() as InputSchema
102
94
 
@@ -110,6 +102,680 @@ function asJsonObject(value: unknown): JsonObject {
110
102
  return isJsonObject(value) ? value : {}
111
103
  }
112
104
 
105
+ function documentToolUseAction(toolId: string): string {
106
+ switch (toolId) {
107
+ case 'document':
108
+ return 'Prepare document workflow'
109
+ case 'document_inspect':
110
+ return 'Inspect document form'
111
+ case 'document_extract':
112
+ return 'Read document content'
113
+ case 'document_form_schema':
114
+ return 'Map document fields'
115
+ case 'document_apply_fill':
116
+ return 'Fill document fields'
117
+ case 'document_apply_style':
118
+ return 'Apply document formatting'
119
+ case 'document_render':
120
+ return 'Render document diff'
121
+ case 'document_validate_public_form':
122
+ return 'Validate public-form rules'
123
+ case 'document_save':
124
+ return 'Save document'
125
+ default:
126
+ return 'Process document'
127
+ }
128
+ }
129
+
130
+ function documentToolUseTarget(input: Record<string, unknown>): string | undefined {
131
+ const document = asJsonObject(input.document)
132
+ const path =
133
+ typeof document.path === 'string'
134
+ ? document.path
135
+ : typeof input.path === 'string'
136
+ ? input.path
137
+ : undefined
138
+ if (path !== undefined && path.trim()) {
139
+ return basename(path)
140
+ }
141
+ if (
142
+ typeof document.artifact_id === 'string' ||
143
+ typeof input.artifact_id === 'string'
144
+ ) {
145
+ return 'current document'
146
+ }
147
+ return undefined
148
+ }
149
+
150
+ function renderDocumentToolUseMessage(
151
+ toolId: string,
152
+ input: Record<string, unknown>,
153
+ ): string | null {
154
+ if (MECHANICAL_DOCUMENT_TOOL_NAMES.has(toolId)) return null
155
+ const action = documentToolUseAction(toolId)
156
+ const target = documentToolUseTarget(input)
157
+ return target === undefined ? action : `${action}: ${target}`
158
+ }
159
+
160
+ function messageInnerRecord(message: unknown): JsonObject {
161
+ return asJsonObject(asJsonObject(message).message)
162
+ }
163
+
164
+ function messageRole(message: unknown): string | undefined {
165
+ const inner = messageInnerRecord(message)
166
+ const outer = asJsonObject(message)
167
+ if (typeof inner.role === 'string') return inner.role
168
+ if (typeof outer.role === 'string') return outer.role
169
+ return typeof outer.type === 'string' ? outer.type : undefined
170
+ }
171
+
172
+ function messageContent(message: unknown): unknown {
173
+ const inner = messageInnerRecord(message)
174
+ return inner.content ?? asJsonObject(message).content
175
+ }
176
+
177
+ function isToolResultContent(content: unknown): boolean {
178
+ if (!Array.isArray(content)) return false
179
+ return content.some(block => asJsonObject(block).type === 'tool_result')
180
+ }
181
+
182
+ function textFromMessageContent(content: unknown): string {
183
+ if (typeof content === 'string') return content
184
+ if (!Array.isArray(content)) return ''
185
+ return content
186
+ .map(block => {
187
+ if (typeof block === 'string') return block
188
+ const record = asJsonObject(block)
189
+ if (record.type === 'tool_result') return ''
190
+ if (typeof record.text === 'string') return record.text
191
+ if (typeof record.content === 'string') return record.content
192
+ return ''
193
+ })
194
+ .filter(Boolean)
195
+ .join('\n')
196
+ }
197
+
198
+ function latestPlainUserText(messages: readonly unknown[]): string {
199
+ for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
200
+ const content = messageContent(messages[idx])
201
+ if (messageRole(messages[idx]) !== 'user' || isToolResultContent(content)) {
202
+ continue
203
+ }
204
+ const text = textFromMessageContent(content).trim()
205
+ if (text) return text
206
+ }
207
+ return ''
208
+ }
209
+
210
+ function safeDocumentArtifactId(value: unknown): string | undefined {
211
+ if (typeof value !== 'string') return undefined
212
+ const candidate = value.trim()
213
+ return SAFE_DOCUMENT_ARTIFACT_ID_RE.test(candidate) ? candidate : undefined
214
+ }
215
+
216
+ function explicitDocumentArtifactIdFromText(text: string): string | undefined {
217
+ const marked = EXPLICIT_DOCUMENT_ARTIFACT_MARKER_RE.exec(text)?.[1]
218
+ const markedArtifactId = safeDocumentArtifactId(marked)
219
+ if (markedArtifactId) return markedArtifactId
220
+ const prefixed = EXPLICIT_DOCUMENT_ARTIFACT_PREFIX_RE.exec(text)?.[1]
221
+ return safeDocumentArtifactId(prefixed)
222
+ }
223
+
224
+ function parseJsonObject(value: unknown): JsonObject | undefined {
225
+ if (isJsonObject(value)) return value
226
+ if (typeof value !== 'string' || !value.trim()) return undefined
227
+ try {
228
+ return asJsonObject(JSON.parse(value))
229
+ } catch {
230
+ return undefined
231
+ }
232
+ }
233
+
234
+ function documentToolResultPayload(message: unknown): JsonObject | undefined {
235
+ const directResult = asJsonObject(asJsonObject(message).toolUseResult).result
236
+ if (isJsonObject(directResult)) return directResult
237
+
238
+ const content = messageContent(message)
239
+ if (!Array.isArray(content)) return undefined
240
+ for (const block of content) {
241
+ const record = asJsonObject(block)
242
+ if (record.type !== 'tool_result') continue
243
+ const parsed = parseJsonObject(record.content)
244
+ const nestedResult = asJsonObject(parsed).result
245
+ if (isJsonObject(nestedResult)) return nestedResult
246
+ }
247
+ return undefined
248
+ }
249
+
250
+ function latestDocumentArtifactRef(
251
+ messages: readonly unknown[],
252
+ options: {
253
+ toolIds: ReadonlySet<string>
254
+ artifactPrefix: string
255
+ },
256
+ ): string | undefined {
257
+ for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
258
+ const payload = documentToolResultPayload(messages[idx])
259
+ if (!payload) continue
260
+ if (
261
+ typeof payload.tool_id !== 'string' ||
262
+ !options.toolIds.has(payload.tool_id)
263
+ ) continue
264
+ if (payload.status !== 'ok') continue
265
+ const refs = Array.isArray(payload.artifact_refs) ? payload.artifact_refs : []
266
+ for (let refIdx = refs.length - 1; refIdx >= 0; refIdx -= 1) {
267
+ const ref = safeDocumentArtifactId(refs[refIdx])
268
+ if (ref?.startsWith(options.artifactPrefix)) return ref
269
+ }
270
+ }
271
+ return undefined
272
+ }
273
+
274
+ function latestMutableDocumentArtifactRef(messages: readonly unknown[]): string | undefined {
275
+ return (
276
+ latestDocumentArtifactRef(messages, {
277
+ toolIds: DOCUMENT_MUTATION_TOOL_NAMES,
278
+ artifactPrefix: 'derivative-',
279
+ }) ??
280
+ latestDocumentArtifactRef(messages, {
281
+ toolIds: new Set([
282
+ 'document_copy_for_edit',
283
+ 'document_apply_fill',
284
+ 'document_apply_style',
285
+ ]),
286
+ artifactPrefix: 'working-',
287
+ })
288
+ )
289
+ }
290
+
291
+ function shouldHideSuccessfulIntermediateDocumentResult(output: unknown): boolean {
292
+ const payload = extractDocumentToolResultPayload(output)
293
+ return (
294
+ payload !== null &&
295
+ payload.status === 'ok' &&
296
+ typeof payload.tool_id === 'string' &&
297
+ MECHANICAL_DOCUMENT_TOOL_NAMES.has(payload.tool_id)
298
+ )
299
+ }
300
+
301
+ function documentCorrelationId(value: unknown): string {
302
+ return typeof value === 'string' && value.trim()
303
+ ? value.trim()
304
+ : `document-render-${randomUUID()}`
305
+ }
306
+
307
+ function documentFormatFromText(text: string): string | undefined {
308
+ return DOCUMENT_FORMAT_RE.exec(text)?.[0]?.toLowerCase()
309
+ }
310
+
311
+ function documentFormatFromPath(path: string): string | undefined {
312
+ const match = /\.(hwpx|hwp|docx|pdf|xlsx|pptx)$/iu.exec(path.trim())
313
+ return match?.[1]?.toLowerCase()
314
+ }
315
+
316
+ function inferredDownloadsDocumentPath(text: string): string | undefined {
317
+ if (!/(다운로드\s*폴더|downloads)/iu.test(text)) return undefined
318
+ const formatMatch = DOCUMENT_FORMAT_RE.exec(text)
319
+ const format = formatMatch?.[0]?.toLowerCase()
320
+ if (!format || formatMatch === null) return undefined
321
+ const beforeFormat = text.slice(0, formatMatch.index).trim()
322
+ const nameMatch =
323
+ /(?:다운로드\s*폴더|downloads)(?:에)?\s*(?:있는|의)?\s*(?<name>.+)$/iu.exec(
324
+ beforeFormat,
325
+ )
326
+ const rawName = nameMatch?.groups?.name?.trim()
327
+ if (!rawName) return undefined
328
+ const fileName = rawName
329
+ .replace(/[\\/:*?"<>|]/gu, ' ')
330
+ .replace(/\s+/gu, ' ')
331
+ .replace(/[.。.]+$/gu, '')
332
+ .trim()
333
+ if (!fileName) return undefined
334
+ return join(homedir(), 'Downloads', `${fileName}.${format}`)
335
+ }
336
+
337
+ function isDownloadsFolderLikePath(value: unknown): boolean {
338
+ if (typeof value !== 'string') return false
339
+ const normalized = value
340
+ .trim()
341
+ .replace(/^['"`]+|['"`]+$/gu, '')
342
+ .replace(/[\\/]+$/u, '')
343
+ .replace(/\.$/u, '')
344
+ return DOWNLOADS_FOLDER_PATH_RE.test(normalized)
345
+ }
346
+
347
+ function normalizeDownloadsDocumentPath(value: unknown): string | undefined {
348
+ if (typeof value !== 'string') return undefined
349
+ const cleaned = value
350
+ .trim()
351
+ .replace(/^['"`]+|['"`]+$/gu, '')
352
+ .replace(DOCUMENT_EXTENSION_TRAILING_PUNCT_RE, '$1')
353
+ if (!cleaned) return undefined
354
+ const downloadsPathMatch = DOWNLOADS_PATH_SEGMENT_RE.exec(cleaned)
355
+ if (DOCUMENT_EXTENSION_RE.test(cleaned) && downloadsPathMatch) {
356
+ const homeDownloads = `${homedir()}/Downloads/`
357
+ if (cleaned.startsWith(homeDownloads)) return cleaned
358
+ const tail = downloadsPathMatch.groups?.tail
359
+ if (!tail || tail.includes('..')) return undefined
360
+ const parts = tail.split(/[\\/]+/u).filter(Boolean)
361
+ if (parts.length === 0) return undefined
362
+ return join(homedir(), 'Downloads', ...parts)
363
+ }
364
+ return undefined
365
+ }
366
+
367
+ function cleanUserDocumentPath(value: string): string {
368
+ return value
369
+ .trim()
370
+ .replace(/^['"`]+|['"`]+$/gu, '')
371
+ .replace(DOCUMENT_EXTENSION_TRAILING_PUNCT_RE, '$1')
372
+ .replace(/^~/u, homedir())
373
+ }
374
+
375
+ function existingUserDocumentPathsFromText(text: string): string[] {
376
+ const paths: string[] = []
377
+ for (const match of text.matchAll(EXPLICIT_LOCAL_DOCUMENT_PATH_RE)) {
378
+ const rawPath = match.groups?.path
379
+ if (typeof rawPath !== 'string') continue
380
+ const candidate = cleanUserDocumentPath(rawPath)
381
+ if (!candidate || !DOCUMENT_EXTENSION_RE.test(candidate)) continue
382
+ if (!existsSync(candidate)) continue
383
+ if (!paths.includes(candidate)) paths.push(candidate)
384
+ }
385
+ return paths
386
+ }
387
+
388
+ function normalizeExactUserDocumentPathInput(
389
+ input: Record<string, unknown>,
390
+ document: JsonObject,
391
+ messages: readonly unknown[],
392
+ ): Record<string, unknown> | undefined {
393
+ if (document.artifact_id !== undefined) return undefined
394
+ const userPaths = existingUserDocumentPathsFromText(latestPlainUserText(messages))
395
+ if (userPaths.length !== 1) return undefined
396
+ const lockedPath = userPaths[0]
397
+ if (lockedPath === undefined) return undefined
398
+
399
+ const currentPath =
400
+ typeof document.path === 'string' ? cleanUserDocumentPath(document.path) : undefined
401
+ if (currentPath !== undefined && existsSync(currentPath)) return undefined
402
+ if (
403
+ currentPath !== undefined &&
404
+ basename(currentPath) !== basename(lockedPath)
405
+ ) {
406
+ return undefined
407
+ }
408
+
409
+ return {
410
+ ...input,
411
+ correlation_id: documentCorrelationId(input.correlation_id),
412
+ document: {
413
+ ...document,
414
+ path: lockedPath,
415
+ expected_format:
416
+ documentFormatFromPath(lockedPath) ??
417
+ document.expected_format ??
418
+ input.expected_format ??
419
+ documentFormatFromText(latestPlainUserText(messages)),
420
+ },
421
+ }
422
+ }
423
+
424
+ function normalizeDocumentInspectPathInput(
425
+ toolId: string,
426
+ input: Record<string, unknown>,
427
+ document: JsonObject,
428
+ messages: readonly unknown[],
429
+ ): Record<string, unknown> | undefined {
430
+ if (toolId !== 'document_inspect' && toolId !== 'document') return undefined
431
+ const path = document.path ?? input.path
432
+ const userText = latestPlainUserText(messages)
433
+ const cleanedPath = typeof path === 'string' ? cleanUserDocumentPath(path) : undefined
434
+ if (
435
+ cleanedPath !== undefined &&
436
+ DOCUMENT_EXTENSION_RE.test(cleanedPath) &&
437
+ existsSync(cleanedPath)
438
+ ) {
439
+ return undefined
440
+ }
441
+ const normalizedDownloadsPath = normalizeDownloadsDocumentPath(path)
442
+ const inferredPath = isDownloadsFolderLikePath(path)
443
+ ? inferredDownloadsDocumentPath(userText)
444
+ : undefined
445
+ const inferredDownloadsPath = inferredDownloadsDocumentPath(userText)
446
+ const shouldPreferUserTextDownloadsPath =
447
+ normalizedDownloadsPath !== undefined &&
448
+ inferredDownloadsPath !== undefined &&
449
+ normalizedDownloadsPath !== inferredDownloadsPath
450
+ const normalizedPath =
451
+ (shouldPreferUserTextDownloadsPath ? inferredDownloadsPath : undefined) ??
452
+ (normalizedDownloadsPath !== undefined && existsSync(normalizedDownloadsPath)
453
+ ? normalizedDownloadsPath
454
+ : undefined) ??
455
+ (inferredPath !== undefined && existsSync(inferredPath) ? inferredPath : undefined) ??
456
+ (inferredDownloadsPath !== undefined && existsSync(inferredDownloadsPath)
457
+ ? inferredDownloadsPath
458
+ : undefined) ??
459
+ inferredDownloadsPath ??
460
+ normalizedDownloadsPath ??
461
+ inferredPath
462
+ if (!normalizedPath) return undefined
463
+ return {
464
+ ...input,
465
+ correlation_id: documentCorrelationId(input.correlation_id),
466
+ document: {
467
+ ...document,
468
+ path: normalizedPath,
469
+ expected_format:
470
+ documentFormatFromPath(normalizedPath) ??
471
+ document.expected_format ??
472
+ input.expected_format ??
473
+ documentFormatFromText(userText),
474
+ },
475
+ }
476
+ }
477
+
478
+ function normalizeDocumentPathExpectedFormatInput(
479
+ input: Record<string, unknown>,
480
+ document: JsonObject,
481
+ ): Record<string, unknown> | undefined {
482
+ if (document.artifact_id !== undefined) return undefined
483
+ if (typeof document.path !== 'string') return undefined
484
+ const normalizedPath = cleanUserDocumentPath(document.path)
485
+ const pathFormat = documentFormatFromPath(normalizedPath)
486
+ if (pathFormat === undefined) return undefined
487
+ const currentFormat =
488
+ typeof document.expected_format === 'string'
489
+ ? document.expected_format.toLowerCase()
490
+ : undefined
491
+ if (currentFormat === pathFormat && document.path === normalizedPath) {
492
+ return undefined
493
+ }
494
+ return {
495
+ ...input,
496
+ correlation_id: documentCorrelationId(input.correlation_id),
497
+ document: {
498
+ ...document,
499
+ path: normalizedPath,
500
+ expected_format: pathFormat,
501
+ },
502
+ }
503
+ }
504
+
505
+ function normalizeHwpxTextTargetPath(value: unknown): string | undefined {
506
+ if (typeof value !== 'string') return undefined
507
+ const targetPath = value.trim()
508
+ const match = HWPX_TEXT_TARGET_ALIAS_RE.exec(targetPath)
509
+ const rawIndex = match?.groups?.indexA ?? match?.groups?.indexB
510
+ if (!rawIndex) return undefined
511
+ return `/hwpx/text[${Number(rawIndex)}]`
512
+ }
513
+
514
+ function latestDocumentFieldPaths(messages: readonly unknown[]): Set<string> | undefined {
515
+ for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
516
+ const payload = documentToolResultPayload(messages[idx])
517
+ const extraction = asJsonObject(payload?.extraction)
518
+ const fields = Array.isArray(extraction.fields) ? extraction.fields : []
519
+ const paths = new Set<string>()
520
+ for (const field of fields) {
521
+ const path = asJsonObject(field).path
522
+ if (typeof path === 'string' && path.trim()) {
523
+ paths.add(path.trim())
524
+ }
525
+ }
526
+ if (paths.size > 0) return paths
527
+ }
528
+ return undefined
529
+ }
530
+
531
+ function latestDocumentTableCellFieldAliases(
532
+ messages: readonly unknown[],
533
+ ): Map<string, string> | undefined {
534
+ for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
535
+ const payload = documentToolResultPayload(messages[idx])
536
+ const extraction = asJsonObject(payload?.extraction)
537
+ const tables = Array.isArray(extraction.tables) ? extraction.tables : []
538
+ const aliases = new Map<string, string>()
539
+ for (const tableValue of tables) {
540
+ const table = asJsonObject(tableValue)
541
+ const blockId = table.block_id
542
+ const cells = Array.isArray(table.cells) ? table.cells : []
543
+ if (typeof blockId !== 'string' || !blockId.trim()) continue
544
+ for (const cellValue of cells) {
545
+ const cell = asJsonObject(cellValue)
546
+ const rowIndex = cell.row_index
547
+ const columnIndex = cell.column_index
548
+ const fieldPath = cell.field_path
549
+ if (
550
+ typeof rowIndex !== 'number' ||
551
+ typeof columnIndex !== 'number' ||
552
+ typeof fieldPath !== 'string' ||
553
+ !fieldPath.trim()
554
+ ) {
555
+ continue
556
+ }
557
+ aliases.set(
558
+ `/hwpx/[${blockId.trim()}]/cells[${rowIndex}][${columnIndex}]`,
559
+ fieldPath.trim(),
560
+ )
561
+ }
562
+ }
563
+ if (aliases.size > 0) return aliases
564
+ }
565
+ return undefined
566
+ }
567
+
568
+ function normalizeHwpxTableCellTargetPath(
569
+ value: unknown,
570
+ aliases: ReadonlyMap<string, string> | undefined,
571
+ ): string | undefined {
572
+ if (typeof value !== 'string') return undefined
573
+ const targetPath = value.trim()
574
+ if (!HWPX_BLOCK_TABLE_CELL_TARGET_RE.test(targetPath)) return undefined
575
+ return aliases?.get(targetPath)
576
+ }
577
+
578
+ function normalizeDocumentPatchTargetPaths(
579
+ input: Record<string, unknown>,
580
+ messages: readonly unknown[],
581
+ ): Record<string, unknown> {
582
+ const patches = input.patches
583
+ if (!Array.isArray(patches)) return input
584
+
585
+ const fieldPaths = latestDocumentFieldPaths(messages)
586
+ const tableCellAliases = latestDocumentTableCellFieldAliases(messages)
587
+ let changed = false
588
+ const normalizedPatches = patches.flatMap(patch => {
589
+ if (!isJsonObject(patch)) return patch
590
+ const rawTargetPath =
591
+ typeof patch.target_path === 'string' ? patch.target_path.trim() : patch.target_path
592
+ const isHwpxTableCellTarget =
593
+ typeof rawTargetPath === 'string' && HWPX_BLOCK_TABLE_CELL_TARGET_RE.test(rawTargetPath)
594
+ const normalizedTargetPath =
595
+ normalizeHwpxTextTargetPath(patch.target_path) ??
596
+ normalizeHwpxTableCellTargetPath(patch.target_path, tableCellAliases)
597
+ const targetPath =
598
+ normalizedTargetPath ?? (
599
+ typeof rawTargetPath === 'string' ? rawTargetPath : patch.target_path
600
+ )
601
+ if (
602
+ isHwpxTableCellTarget &&
603
+ tableCellAliases !== undefined &&
604
+ normalizedTargetPath === undefined
605
+ ) {
606
+ changed = true
607
+ return []
608
+ }
609
+ if (
610
+ typeof targetPath === 'string' &&
611
+ fieldPaths !== undefined &&
612
+ HWPX_TEXT_TARGET_RE.test(targetPath) &&
613
+ !fieldPaths.has(targetPath)
614
+ ) {
615
+ changed = true
616
+ return []
617
+ }
618
+ if (
619
+ normalizedTargetPath === undefined ||
620
+ normalizedTargetPath === patch.target_path
621
+ ) {
622
+ return patch
623
+ }
624
+ changed = true
625
+ return {
626
+ ...patch,
627
+ target_path: normalizedTargetPath,
628
+ }
629
+ })
630
+
631
+ if (normalizedPatches.length === 0) return input
632
+ return changed ? { ...input, patches: normalizedPatches } : input
633
+ }
634
+
635
+ function normalizeDocumentPrimitiveInstructionInput(
636
+ toolId: string,
637
+ input: Record<string, unknown>,
638
+ messages: readonly unknown[],
639
+ ): Record<string, unknown> {
640
+ if (toolId !== 'document') return input
641
+ if (Array.isArray(input.patches) && input.patches.length > 0) return input
642
+
643
+ const userText = latestPlainUserText(messages)
644
+ if (!userText) return input
645
+
646
+ const instruction =
647
+ typeof input.instruction === 'string' ? input.instruction.trim() : ''
648
+ if (instruction.includes(userText)) return input
649
+
650
+ return {
651
+ ...input,
652
+ instruction: instruction
653
+ ? `${instruction}\n\nOriginal user request:\n${userText}`
654
+ : userText,
655
+ }
656
+ }
657
+
658
+ export function normalizeExplicitDocumentArtifactInput(
659
+ toolId: string,
660
+ input: Record<string, unknown>,
661
+ messages: readonly unknown[],
662
+ ): Record<string, unknown> {
663
+ if (!DOCUMENT_TOOL_NAMES.has(toolId)) return input
664
+
665
+ const instructionInput = normalizeDocumentPrimitiveInstructionInput(
666
+ toolId,
667
+ input,
668
+ messages,
669
+ )
670
+ const normalizedPatchInput = normalizeDocumentPatchTargetPaths(
671
+ instructionInput,
672
+ messages,
673
+ )
674
+ const { artifact_id: topLevelArtifactIdRaw, ...withoutTopLevelArtifactId } =
675
+ normalizedPatchInput
676
+ const document = asJsonObject(normalizedPatchInput.document)
677
+ const normalizedExactUserPathInput = normalizeExactUserDocumentPathInput(
678
+ normalizedPatchInput,
679
+ document,
680
+ messages,
681
+ )
682
+ if (normalizedExactUserPathInput) return normalizedExactUserPathInput
683
+ const normalizedInspectPathInput = normalizeDocumentInspectPathInput(
684
+ toolId,
685
+ normalizedPatchInput,
686
+ document,
687
+ messages,
688
+ )
689
+ if (normalizedInspectPathInput) return normalizedInspectPathInput
690
+ const { path: _documentPath, ...documentWithoutPath } = document
691
+ const existingDocumentArtifactId = safeDocumentArtifactId(document.artifact_id)
692
+ const inputArtifactId =
693
+ existingDocumentArtifactId ?? safeDocumentArtifactId(topLevelArtifactIdRaw)
694
+ const hasExtractionArtifactId = inputArtifactId?.startsWith('document-intake-') === true
695
+
696
+ if (DOCUMENT_ARTIFACT_FOLLOWUP_TOOL_NAMES.has(toolId)) {
697
+ const artifactId = latestMutableDocumentArtifactRef(messages)
698
+ if (
699
+ artifactId &&
700
+ (
701
+ document.path !== undefined ||
702
+ existingDocumentArtifactId?.startsWith('source-') ||
703
+ hasExtractionArtifactId
704
+ )
705
+ ) {
706
+ return {
707
+ ...withoutTopLevelArtifactId,
708
+ correlation_id: documentCorrelationId(input.correlation_id),
709
+ document: {
710
+ ...documentWithoutPath,
711
+ artifact_id: artifactId,
712
+ },
713
+ }
714
+ }
715
+ }
716
+
717
+ if (toolId === 'document_copy_for_edit' && hasExtractionArtifactId) {
718
+ const sourceArtifactId = latestDocumentArtifactRef(messages, {
719
+ toolIds: new Set(['document_inspect', 'document_extract', 'document_form_schema']),
720
+ artifactPrefix: 'source-',
721
+ })
722
+ if (sourceArtifactId) {
723
+ return {
724
+ ...withoutTopLevelArtifactId,
725
+ correlation_id: documentCorrelationId(input.correlation_id),
726
+ document: {
727
+ ...documentWithoutPath,
728
+ artifact_id: sourceArtifactId,
729
+ },
730
+ }
731
+ }
732
+ }
733
+
734
+ if (existingDocumentArtifactId) {
735
+ return topLevelArtifactIdRaw === undefined
736
+ ? { ...normalizedPatchInput, document: documentWithoutPath }
737
+ : { ...withoutTopLevelArtifactId, document: documentWithoutPath }
738
+ }
739
+
740
+ if (toolId === 'document_copy_for_edit' && document.path !== undefined) {
741
+ const sourceArtifactId = latestDocumentArtifactRef(messages, {
742
+ toolIds: new Set(['document_inspect', 'document_extract', 'document_form_schema']),
743
+ artifactPrefix: 'source-',
744
+ })
745
+ if (sourceArtifactId) {
746
+ return {
747
+ ...withoutTopLevelArtifactId,
748
+ correlation_id: documentCorrelationId(input.correlation_id),
749
+ document: {
750
+ ...documentWithoutPath,
751
+ artifact_id: sourceArtifactId,
752
+ },
753
+ }
754
+ }
755
+ }
756
+
757
+ const topLevelArtifactId = safeDocumentArtifactId(topLevelArtifactIdRaw)
758
+ const currentTextArtifactId = explicitDocumentArtifactIdFromText(
759
+ latestPlainUserText(messages),
760
+ )
761
+ const artifactId = topLevelArtifactId ?? currentTextArtifactId
762
+ if (!artifactId) {
763
+ return (
764
+ normalizeDocumentPathExpectedFormatInput(normalizedPatchInput, document) ??
765
+ normalizedPatchInput
766
+ )
767
+ }
768
+
769
+ return {
770
+ ...withoutTopLevelArtifactId,
771
+ correlation_id: documentCorrelationId(input.correlation_id),
772
+ document: {
773
+ ...documentWithoutPath,
774
+ artifact_id: artifactId,
775
+ },
776
+ }
777
+ }
778
+
113
779
  function asStringArray(value: unknown): string[] {
114
780
  return Array.isArray(value)
115
781
  ? value.filter((item): item is string => typeof item === 'string')
@@ -270,6 +936,8 @@ function primitiveToolFor(primitive: AdapterPrimitive): Tool {
270
936
  return ResolveLocationPrimitive as Tool
271
937
  case 'send':
272
938
  return SubmitPrimitive as Tool
939
+ case 'document':
940
+ return DocumentPrimitive as Tool
273
941
  case 'check':
274
942
  return VerifyPrimitive as Tool
275
943
  case 'find':
@@ -285,21 +953,20 @@ function rootInputFor(entry: AdapterManifestEntry, input: Record<string, unknown
285
953
  }
286
954
  }
287
955
 
288
- function validateAdapterContractInput(
289
- toolId: string,
956
+ function concreteAdapterCallInputFor(
957
+ entry: AdapterManifestEntry,
290
958
  input: Record<string, unknown>,
959
+ ): Record<string, unknown> {
960
+ if (input.tool_id !== entry.tool_id) return input
961
+ const params = input.params
962
+ return isJsonObject(params) ? params : input
963
+ }
964
+
965
+ function validateAdapterContractInput(
966
+ _toolId: string,
967
+ _input: Record<string, unknown>,
291
968
  ) {
292
- if (toolId !== 'kma_apihub_url_analysis_weather_chart_image') return undefined
293
- const analTime = input.anal_time
294
- if (typeof analTime === 'string' && /^\d{12}$/u.test(analTime)) return undefined
295
- return {
296
- result: false as const,
297
- message:
298
- 'KMA analysis weather-chart schema mismatch: anal_time is required as UTC YYYYMMDDHHMM. ' +
299
- "Use a 12-digit official analysis time with minutes, for example '202605281200', not a 10-digit KST hour. " +
300
- 'If the citizen asks for now/today, choose the latest completed official UTC analysis slot and report upstream failure directly if APIHub has no chart.',
301
- errorCode: 1,
302
- }
969
+ return undefined
303
970
  }
304
971
 
305
972
  export function isAdapterToolName(name: string): boolean {
@@ -323,694 +990,491 @@ function searchTokens(text: string): string[] {
323
990
  return text.toLowerCase().match(/[\p{L}\p{N}_-]+/gu) ?? []
324
991
  }
325
992
 
326
- function expandedQueryTokens(query: string): Set<string> {
327
- const tokens = new Set(searchTokens(query))
328
- const compact = query.toLowerCase()
329
- const airportAviationQuery = isAirportAviationQuery(query)
330
- if (/[날씨기상비강수기온습도풍속예보관측실황]/u.test(compact)) {
331
- for (const token of [
332
- '날씨',
333
- '기상',
334
- '현재',
335
- '관측',
336
- '실황',
337
- 'weather',
338
- 'current',
339
- 'observation',
340
- 'forecast',
341
- 'temperature',
342
- 'precipitation',
343
- 'humidity',
344
- 'wind',
345
- 'kma',
346
- '초단기실황',
347
- '초단기예보',
348
- '단기예보',
349
- '우산',
350
- 'nx',
351
- 'ny',
352
- 'base_date',
353
- 'base_time',
354
- ]) {
355
- tokens.add(token)
356
- }
357
- }
358
- const medicalEmergencyQuery = isMedicalEmergencyQuery(query)
359
- const collapseOrAedQuery = isCollapseOrAedQuery(query)
360
- if (EMERGENCY_REQUEST_RE.test(compact) && medicalEmergencyQuery) {
361
- for (const token of [
362
- '응급',
363
- '응급실',
364
- '응급의료',
365
- '응급의료센터',
366
- '실시간',
367
- '병상',
368
- '야간',
369
- 'emergency',
370
- 'room',
371
- 'er',
372
- 'nmc',
373
- ]) {
374
- tokens.add(token)
375
- }
376
- }
377
- if (collapseOrAedQuery) {
378
- for (const token of [
379
- '응급',
380
- '응급실',
381
- '응급의료',
382
- '응급의료센터',
383
- 'aed',
384
- '자동심장충격기',
385
- '자동제세동기',
386
- '심장충격기',
387
- '응급처치',
388
- '심정지',
389
- '의식불명',
390
- 'emergency',
391
- 'room',
392
- 'er',
393
- 'defibrillator',
394
- 'cardiac',
395
- 'arrest',
396
- 'nmc',
397
- ]) {
398
- tokens.add(token)
399
- }
400
- }
401
- if (/(병원|의료|진료|약국|hospital|clinic|medical)/u.test(compact)) {
402
- for (const token of [
403
- '병원',
404
- '의료기관',
405
- '진료',
406
- '진료과목',
407
- '야간',
408
- '약국',
409
- 'hospital',
410
- 'clinic',
411
- 'medical',
412
- 'nearby',
413
- ]) {
414
- tokens.add(token)
415
- }
416
- }
417
- if (HIRA_MEDICAL_DETAIL_RE.test(query)) {
418
- for (const token of [
419
- '상세정보',
420
- '진료과',
421
- '진료과목',
422
- '진료시간',
423
- '주차',
424
- '요양기호',
425
- 'ykiho',
426
- 'hira',
427
- 'detail',
428
- 'specialty',
429
- ]) {
430
- tokens.add(token)
431
- }
432
- }
433
- if (MOIS_EMERGENCY_CALL_BOX_RE.test(query)) {
434
- for (const token of [
435
- '안전비상벨',
436
- '비상벨',
437
- '긴급신고함',
438
- '방범',
439
- '행정안전부',
440
- 'mois',
441
- 'emergency',
442
- 'call',
443
- 'box',
444
- ]) {
445
- tokens.add(token)
446
- }
447
- }
448
- if (GYERYONG_ASSISTIVE_CHARGER_RE.test(query)) {
449
- for (const token of [
450
- '계룡시',
451
- '전동보장구',
452
- '전동휠체어',
453
- '보장구',
454
- '장애인',
455
- '충전소',
456
- '충전장소',
457
- 'accessibility',
458
- 'charger',
459
- ]) {
460
- tokens.add(token)
461
- }
462
- }
463
- if (MOF_OCEAN_WATER_QUALITY_RE.test(query)) {
464
- for (const token of [
465
- '해양수산부',
466
- '해양수질',
467
- '수질자동측정망',
468
- '관측소',
469
- 'sea3003',
470
- '용존산소',
471
- 'water',
472
- 'quality',
473
- 'ocean',
474
- ]) {
475
- tokens.add(token)
476
- }
477
- }
478
- if (isPpsBidQuery(query)) {
479
- for (const token of [
480
- '조달청',
481
- '나라장터',
482
- '입찰공고',
483
- '공사입찰',
484
- 'bidntcenm',
485
- 'inqrybgndt',
486
- 'inqryenddt',
487
- 'pps',
488
- 'bid',
489
- 'procurement',
490
- ]) {
491
- tokens.add(token)
492
- }
493
- }
494
- if (isProtectedCheckQuery(query)) {
495
- for (const token of [
496
- '본인확인',
497
- '인증',
498
- '간편인증',
499
- '모바일신분증',
500
- '모바일id',
501
- '소득금액증명원',
502
- '증명원',
503
- '홈택스',
504
- '정부24',
505
- 'check',
506
- 'verify',
507
- 'identity',
508
- 'simple',
509
- 'auth',
510
- 'mobile',
511
- 'id',
512
- 'mydata',
513
- ]) {
514
- tokens.add(token)
515
- }
516
- }
517
- if (isTagoBusQuery(query)) {
518
- for (const token of [
519
- '국토교통부',
520
- 'tago',
521
- '버스',
522
- '시내버스',
523
- '버스정류소',
524
- '정류장',
525
- '정류소',
526
- '노선',
527
- '노선번호',
528
- '버스도착',
529
- '도착',
530
- 'nodeid',
531
- 'nodenm',
532
- 'nodeno',
533
- 'routeid',
534
- 'routeno',
535
- 'citycode',
536
- 'bus',
537
- 'station',
538
- 'route',
539
- 'arrival',
540
- ]) {
541
- tokens.add(token)
542
- }
543
- }
544
- if (collapseOrAedQuery) {
545
- for (const token of ['aed', '자동심장충격기', '자동제세동기', '심장충격기', '위치']) {
546
- tokens.add(token)
547
- }
548
- }
549
- if (/(미세먼지|초미세|대기질|공기질|airquality|air quality)/u.test(compact)) {
550
- for (const token of ['미세먼지', '대기질', '대기오염', 'airkorea', 'air', 'quality']) {
551
- tokens.add(token)
552
- }
553
- }
554
- if (/(법률|변호사|무료상담|상담)/u.test(compact)) {
555
- for (const token of ['법률', '변호사', '마을변호사', '상담', 'legal', 'lawyer']) {
556
- tokens.add(token)
557
- }
558
- }
559
- if (/(장례|화장|봉안|장사|funeral)/u.test(compact)) {
560
- for (const token of ['장례', '장례식장', '시설사용료', 'funeral', 'fee']) {
561
- tokens.add(token)
562
- }
563
- }
564
- if (/(취업|채용|공고|공무원|job|recruit)/u.test(compact)) {
565
- for (const token of ['취업', '채용', '공고', '공무원', 'public', 'job']) {
566
- tokens.add(token)
567
- }
993
+ const KOREAN_TRAILING_PARTICLES = [
994
+ '으로부터',
995
+ '에서부터',
996
+ '에게서',
997
+ '한테서',
998
+ '까지',
999
+ '부터',
1000
+ '으로',
1001
+ '에서',
1002
+ '에게',
1003
+ '한테',
1004
+ '처럼',
1005
+ '보다',
1006
+ '하고',
1007
+ '이며',
1008
+ '이고',
1009
+ '',
1010
+ '',
1011
+ '',
1012
+ '',
1013
+ '',
1014
+ '',
1015
+ '',
1016
+ '',
1017
+ '',
1018
+ '',
1019
+ '',
1020
+ '',
1021
+ '만',
1022
+ ] as const
1023
+
1024
+ const LOW_SIGNAL_DISCOVERY_TOKENS = new Set([
1025
+ '지금',
1026
+ '현재',
1027
+ '오늘',
1028
+ '내일',
1029
+ '모레',
1030
+ '이번',
1031
+ '근처',
1032
+ '주변',
1033
+ '어디',
1034
+ '어떻게',
1035
+ 'now',
1036
+ 'current',
1037
+ 'today',
1038
+ 'tomorrow',
1039
+ 'latest',
1040
+ 'nearby',
1041
+ 'realtime',
1042
+ 'real-time',
1043
+ ])
1044
+
1045
+ const KMA_LIFESTYLE_WEATHER_RE =
1046
+ /(날씨|현재\s*기상|실황|관측|예보|기온|습도|풍속|지금\s*비|비\s*(?:와|오|올|내리)|우산|강수|소나기|산책|퇴근|current\s+weather|forecast|rain|umbrella|precipitation|temperature)/iu
1047
+ const HEALTHCARE_RE =
1048
+ /(응급|응급실|응급의료|야간\s*진료|야간진료|병원|의원|의료기관|진료\s*가능|\bemergency\b|\ber\b|\bhospital\b|\bclinic\b)/iu
1049
+ const AIR_QUALITY_RE =
1050
+ /(미세먼지|초미세먼지|초미세|대기질|대기오염|공기질|마스크|pm\s*2\.?5|pm\s*10|air\s*korea|airkorea|air\s*quality|airquality)/iu
1051
+ const KMA_ANALYSIS_RE =
1052
+ /(분석자료|이미\s*분석|고해상도\s*격자|객관분석|AWS\s*객관|지도\s*자료|일기도|분석일기도|비구름|바람\s*흐름|날씨\s*흐름|전국\s*날씨|synoptic|weather\s*chart|objective\s*analysis|high[-\s]?resolution|grid)/iu
1053
+ const AIRPORT_AVIATION_RE =
1054
+ /(AMOS|METAR|SPECI|RVR|항공기상|공항기상|활주로|runway|aviation|비행기|항공편|비행편|이륙|착륙|결항|지연|운항|뜰\s*만|뜨나|뜰\s*수|flight|take\s*off|landing|delay|cancel)/iu
1055
+ const POI_LOCATION_RE =
1056
+ /(근처|주변|주위|인근|가까운|우리\s*동네|여기|이\s*근처|현재\s*위치|내\s*위치|역|터미널|공항|캠퍼스|대학교|대학|해수욕장|시장|공원|랜드마크)/iu
1057
+ const ADMIN_LOCATION_RE =
1058
+ /(?:[가-힣]{2,}(?:시|군|구|동|읍|면)\b|[가-힣0-9]{2,}(?<!으)(?:로|길)\b)/iu
1059
+ const COORDINATE_PAIR_RE =
1060
+ /[+-]?\d{1,2}(?:\.\d+)?\s*,\s*[+-]?\d{2,3}(?:\.\d+)?/u
1061
+ const PRIOR_LOCATION_CONTEXT_RE = /\[prior_location_context\]/u
1062
+ const GOV24_RE = /(정부24|gov24|주민등록등본|등본|증명서|민원)/iu
1063
+ const GOV24_READ_ONLY_RE = /(가능\s*여부|준비물|확인|조회|안내|알려)/iu
1064
+ const GOV24_ACTION_RE = /(신청|진행|제출|접수|발급\s*신청|apply|submit|issue)/iu
1065
+ const WELFARE_RE =
1066
+ /(생활비|기초생활|주거급여|긴급복지|저소득|차상위|복지혜택|지원금|진료비\s*바우처|출산휴가|임신|아동수당|첫만남이용권)/iu
1067
+ const CIVIL_BIRTH_HANDOFF_RE =
1068
+ /(출생신고|아기가\s*태어|아동수당|첫만남이용권|피부양자\s*등록)/iu
1069
+ const UTILITY_RE = /(전기|수도|도시가스|요금|자동이체|공과금|고지서|납부)/iu
1070
+ const HOUSING_HANDOFF_RE =
1071
+ /(생애최초\s*주택구입|주택구입|대출|취득세|등기|전입)/iu
1072
+ const CIVIL_DEATH_RE = /(사망|돌아가|장례|유족|상속|재산|국민연금)/iu
1073
+
1074
+ const LOCATION_TOOL_IDS = new Set([
1075
+ 'locate',
1076
+ 'kakao_address_search',
1077
+ 'kakao_keyword_search',
1078
+ 'kakao_coord_to_region',
1079
+ 'juso_adm_cd_lookup',
1080
+ 'sgis_adm_cd_lookup',
1081
+ ])
1082
+ const KMA_LIFESTYLE_WEATHER_TOOL_IDS = new Set([
1083
+ 'kma_current_observation',
1084
+ 'kma_ultra_short_term_forecast',
1085
+ 'kma_short_term_forecast',
1086
+ 'kma_forecast_fetch',
1087
+ ])
1088
+ const EMERGENCY_TOOL_IDS = new Set([
1089
+ 'nmc_emergency_search',
1090
+ 'nmc_aed_site_locate',
1091
+ 'hira_hospital_search',
1092
+ 'hira_medical_institution_detail',
1093
+ ])
1094
+ const GOV24_LOOKUP_TOOL_IDS = new Set(['mock_lookup_module_gov24_certificate'])
1095
+ const GOV24_ACTION_TOOL_IDS = new Set([
1096
+ 'mock_lookup_module_gov24_certificate',
1097
+ 'mock_verify_module_simple_auth',
1098
+ 'mock_verify_ganpyeon_injeung',
1099
+ 'mock_verify_mobile_id',
1100
+ 'mock_submit_module_gov24_minwon',
1101
+ ])
1102
+ const WELFARE_TOOL_IDS = new Set([
1103
+ 'mohw_welfare_eligibility_search',
1104
+ 'mock_welfare_application_submit_v1',
1105
+ ])
1106
+ const UTILITY_TOOL_IDS = new Set([
1107
+ 'kepco_contract_power_usage',
1108
+ 'mock_kftc_opengiro_bill_send_v1',
1109
+ 'mock_kftc_opengiro_payment_send_v1',
1110
+ ])
1111
+ const CIVIL_DEATH_TOOL_IDS = new Set([
1112
+ 'bfc_funeral_area_fee',
1113
+ 'reb_real_estate_stat_table',
1114
+ 'mohw_welfare_eligibility_search',
1115
+ ])
1116
+
1117
+ type ProviderRoutingIntent = {
1118
+ readonly hasCoordinateLocationAnchor: boolean
1119
+ readonly hasAdminLocationAnchor: boolean
1120
+ readonly hasPriorLocationContext: boolean
1121
+ readonly hasLocationAnchor: boolean
1122
+ readonly hasLifestyleWeather: boolean
1123
+ readonly hasEmergencyMedical: boolean
1124
+ readonly hasGov24ReadOnly: boolean
1125
+ readonly hasGov24Action: boolean
1126
+ readonly hasWelfare: boolean
1127
+ readonly hasCivilBirthHandoff: boolean
1128
+ readonly hasUtility: boolean
1129
+ readonly hasHousingHandoff: boolean
1130
+ readonly hasCivilDeath: boolean
1131
+ }
1132
+
1133
+ type AdapterSelectionOptions = {
1134
+ readonly hasCurrentTurnLocationContext?: boolean
1135
+ }
1136
+
1137
+ function hasHangul(text: string): boolean {
1138
+ return /\p{Script=Hangul}/u.test(text)
1139
+ }
1140
+
1141
+ function extractProviderRoutingIntent(query: string): ProviderRoutingIntent {
1142
+ const hasEmergencyMedical = HEALTHCARE_RE.test(query)
1143
+ const hasGov24 = GOV24_RE.test(query)
1144
+ const hasGov24Action = hasGov24 && GOV24_ACTION_RE.test(query)
1145
+ const hasCoordinateLocationAnchor = COORDINATE_PAIR_RE.test(query)
1146
+ const hasAdminLocationAnchor = ADMIN_LOCATION_RE.test(query)
1147
+ const hasPoiLocationAnchor = POI_LOCATION_RE.test(query)
1148
+ const hasPriorLocationContext = PRIOR_LOCATION_CONTEXT_RE.test(query)
1149
+ return {
1150
+ hasCoordinateLocationAnchor,
1151
+ hasAdminLocationAnchor,
1152
+ hasPriorLocationContext,
1153
+ hasLocationAnchor:
1154
+ hasCoordinateLocationAnchor ||
1155
+ hasPoiLocationAnchor ||
1156
+ hasAdminLocationAnchor ||
1157
+ hasPriorLocationContext,
1158
+ hasLifestyleWeather:
1159
+ KMA_LIFESTYLE_WEATHER_RE.test(query) &&
1160
+ !hasEmergencyMedical &&
1161
+ !AIR_QUALITY_RE.test(query) &&
1162
+ !KMA_ANALYSIS_RE.test(query) &&
1163
+ !AIRPORT_AVIATION_RE.test(query),
1164
+ hasEmergencyMedical,
1165
+ hasGov24ReadOnly:
1166
+ hasGov24 &&
1167
+ GOV24_READ_ONLY_RE.test(query) &&
1168
+ !hasGov24Action,
1169
+ hasGov24Action,
1170
+ hasWelfare: WELFARE_RE.test(query),
1171
+ hasCivilBirthHandoff: CIVIL_BIRTH_HANDOFF_RE.test(query),
1172
+ hasUtility: UTILITY_RE.test(query),
1173
+ hasHousingHandoff: HOUSING_HANDOFF_RE.test(query),
1174
+ hasCivilDeath: CIVIL_DEATH_RE.test(query),
568
1175
  }
569
- if (/(대학|등록금|유학생|tuition|university)/u.test(compact)) {
570
- for (const token of ['대학', '등록금', '유학생', '대학알리미', 'tuition', 'university']) {
571
- tokens.add(token)
1176
+ }
1177
+
1178
+ function addSetValues(target: Set<string>, values: ReadonlySet<string>): void {
1179
+ for (const value of values) target.add(value)
1180
+ }
1181
+
1182
+ function restrictiveToolIdsForIntent(
1183
+ intent: ProviderRoutingIntent,
1184
+ options: AdapterSelectionOptions = {},
1185
+ ): Set<string> | undefined {
1186
+ const allowed = new Set<string>()
1187
+ let restrictive = false
1188
+
1189
+ if (intent.hasGov24ReadOnly) {
1190
+ restrictive = true
1191
+ addSetValues(allowed, GOV24_LOOKUP_TOOL_IDS)
1192
+ } else if (intent.hasGov24Action) {
1193
+ restrictive = true
1194
+ addSetValues(allowed, GOV24_ACTION_TOOL_IDS)
1195
+ }
1196
+
1197
+ if (intent.hasLifestyleWeather) {
1198
+ restrictive = true
1199
+ if (
1200
+ options.hasCurrentTurnLocationContext !== true &&
1201
+ !intent.hasPriorLocationContext
1202
+ ) {
1203
+ addSetValues(allowed, LOCATION_TOOL_IDS)
572
1204
  }
1205
+ addSetValues(allowed, KMA_LIFESTYLE_WEATHER_TOOL_IDS)
573
1206
  }
574
- if (/(전력|전기|한전|계약종별|power|kepco)/u.test(compact)) {
575
- for (const token of ['전력', '전기사용량', '계약종별', '한전', 'kepco', 'power', 'usage']) {
576
- tokens.add(token)
1207
+
1208
+ if (intent.hasEmergencyMedical) {
1209
+ restrictive = true
1210
+ if (
1211
+ options.hasCurrentTurnLocationContext !== true &&
1212
+ !intent.hasPriorLocationContext &&
1213
+ intent.hasLocationAnchor
1214
+ ) {
1215
+ addSetValues(allowed, LOCATION_TOOL_IDS)
577
1216
  }
578
- }
579
- if (/(특보|예비특보|경보|주의보|태풍|warning|alert)/u.test(compact)) {
580
- for (const token of ['특보', '예비특보', '경보', '주의보', '기상청', 'weather', 'alert']) {
581
- tokens.add(token)
1217
+ if (
1218
+ options.hasCurrentTurnLocationContext === true ||
1219
+ intent.hasPriorLocationContext ||
1220
+ intent.hasCoordinateLocationAnchor ||
1221
+ intent.hasAdminLocationAnchor
1222
+ ) {
1223
+ addSetValues(allowed, EMERGENCY_TOOL_IDS)
582
1224
  }
583
1225
  }
584
- if (airportAviationQuery) {
585
- for (const token of [
586
- 'metar',
587
- 'speci',
588
- 'amos',
589
- '항공기상',
590
- '공항기상',
591
- '항공',
592
- '비행기',
593
- '항공편',
594
- '운항',
595
- '이륙',
596
- '시정',
597
- 'rvr',
598
- 'wind',
599
- 'visibility',
600
- ]) {
601
- tokens.add(token)
602
- }
603
- if (KMA_GIMPO_AIRPORT_RE.test(query) && KMA_RUNWAY_AREA_RE.test(query)) {
604
- for (const token of [
605
- 'amos',
606
- '공항기상관측',
607
- '매분자료',
608
- '활주로',
609
- '김포공항',
610
- 'stn110',
611
- 'runway',
612
- 'visibility',
613
- ]) {
614
- tokens.add(token)
615
- }
616
- }
1226
+
1227
+ if (intent.hasWelfare) {
1228
+ restrictive = true
1229
+ addSetValues(allowed, WELFARE_TOOL_IDS)
617
1230
  }
618
- if (KMA_ANALYSIS_DATA_RE.test(query)) {
619
- for (const token of [
620
- '분석자료',
621
- '고해상도',
622
- '격자자료',
623
- '객관분석',
624
- 'aws',
625
- '분석일기도',
626
- '지도',
627
- '비구름',
628
- '바람흐름',
629
- 'objective',
630
- 'analysis',
631
- 'grid',
632
- 'chart',
633
- ]) {
634
- tokens.add(token)
635
- }
1231
+
1232
+ if (intent.hasCivilBirthHandoff) {
1233
+ restrictive = true
636
1234
  }
637
- if (/(교통사고|사고\s*위험|사고다발|위험\s*(구간|도로|지점)|어린이보호구역|보호구역|도로\s*구간|accident|hazard|hotspot)/u.test(compact)) {
638
- for (const token of [
639
- '교통사고',
640
- '사고',
641
- '위험',
642
- '위험지점',
643
- '사고다발',
644
- '사고다발구역',
645
- '어린이보호구역',
646
- '행정동코드',
647
- 'koroad',
648
- 'accident',
649
- 'hazard',
650
- 'hotspot',
651
- ]) {
652
- tokens.add(token)
653
- }
1235
+
1236
+ if (intent.hasUtility) {
1237
+ restrictive = true
1238
+ addSetValues(allowed, UTILITY_TOOL_IDS)
654
1239
  }
655
- if (/(주소|위치|좌표|행정|[가-힣]+(시|군|구|동|읍|면|로|길))/u.test(compact)) {
656
- for (const token of [
657
- 'locate',
658
- '위치',
659
- '주소',
660
- '좌표',
661
- '행정동',
662
- '법정동',
663
- 'geocode',
664
- 'address',
665
- 'kakao',
666
- ]) {
667
- tokens.add(token)
668
- }
1240
+
1241
+ if (intent.hasHousingHandoff) {
1242
+ restrictive = true
669
1243
  }
670
- if (
671
- !airportAviationQuery &&
672
- /(근처|주변|인근|가까운|역|터미널|공항|캠퍼스|대학교|대학|해수욕장|시장|공원|랜드마크|nearby|around)/u.test(compact)
673
- ) {
674
- for (const token of [
675
- '장소',
676
- '키워드',
677
- 'poi',
678
- '랜드마크',
679
- '역',
680
- 'keyword',
681
- 'station',
682
- 'place',
683
- ]) {
684
- tokens.add(token)
685
- }
1244
+
1245
+ if (intent.hasCivilDeath) {
1246
+ restrictive = true
1247
+ addSetValues(allowed, CIVIL_DEATH_TOOL_IDS)
686
1248
  }
687
- return tokens
688
- }
689
1249
 
690
- type ScoredAdapterEntry = {
691
- entry: AdapterManifestEntry
692
- score: number
1250
+ return restrictive ? allowed : undefined
693
1251
  }
694
1252
 
695
- function queryExplicitlyMentionsCoordinates(query: string): boolean {
696
- return /좌표|위도|경도|\blat\b|\blon\b|\blongitude\b|\blatitude\b|wgs84|coord|coord2region|reverse geocode|q0|q1/i.test(query)
1253
+ function routingIntentBoostForTool(
1254
+ toolId: string,
1255
+ intent: ProviderRoutingIntent,
1256
+ ): number {
1257
+ if (intent.hasGov24ReadOnly && GOV24_LOOKUP_TOOL_IDS.has(toolId)) return 1200
1258
+ if (intent.hasGov24Action && GOV24_ACTION_TOOL_IDS.has(toolId)) return 1000
1259
+ if (intent.hasLifestyleWeather) {
1260
+ if (toolId === 'kakao_keyword_search') return 1100
1261
+ if (toolId === 'kakao_address_search') return 1000
1262
+ if (toolId === 'kma_current_observation') return 900
1263
+ if (toolId === 'kma_ultra_short_term_forecast') return 800
1264
+ if (toolId === 'kma_short_term_forecast') return 650
1265
+ if (LOCATION_TOOL_IDS.has(toolId)) return 260
1266
+ }
1267
+ if (intent.hasEmergencyMedical && intent.hasLocationAnchor) {
1268
+ if (toolId === 'nmc_emergency_search') return 1200
1269
+ if (toolId === 'nmc_aed_site_locate') return 950
1270
+ if (toolId === 'kakao_keyword_search') return 900
1271
+ if (toolId === 'kakao_address_search') return 800
1272
+ if (toolId === 'kakao_coord_to_region') return 500
1273
+ if (toolId === 'hira_hospital_search') return 250
1274
+ if (toolId === 'hira_medical_institution_detail') return 200
1275
+ if (LOCATION_TOOL_IDS.has(toolId)) return 300
1276
+ }
1277
+ if (intent.hasWelfare && WELFARE_TOOL_IDS.has(toolId)) return 1000
1278
+ if (intent.hasUtility && UTILITY_TOOL_IDS.has(toolId)) return 1000
1279
+ if (intent.hasCivilDeath && CIVIL_DEATH_TOOL_IDS.has(toolId)) return 1000
1280
+ return 0
697
1281
  }
698
1282
 
699
- function isReverseGeocodeAdapter(toolId: string): boolean {
700
- return toolId === 'kakao_coord_to_region' || toolId === 'sgis_adm_cd_lookup'
1283
+ function koreanParticleStrippedVariants(token: string): string[] {
1284
+ if (!hasHangul(token)) return []
1285
+ const variants: string[] = []
1286
+ let current = token
1287
+ for (let i = 0; i < 2; i += 1) {
1288
+ const nextSuffix = KOREAN_TRAILING_PARTICLES.find(
1289
+ suffix => current.length > suffix.length + 1 && current.endsWith(suffix),
1290
+ )
1291
+ if (!nextSuffix) break
1292
+ current = current.slice(0, -nextSuffix.length)
1293
+ variants.push(current)
1294
+ }
1295
+ return variants
701
1296
  }
702
1297
 
703
- function queryTargetsKoroadHazardDataset(query: string): boolean {
704
- return /(사고\s*위험|위험\s*(구간|도로|지점)|도로\s*구간|어린이보호구역|보호구역|스쿨존|행정동코드|adm_cd|hazard|hotspot)/iu.test(query)
1298
+ function expandedTokensForText(text: string): Set<string> {
1299
+ const tokens = new Set<string>()
1300
+ for (const token of searchTokens(text)) {
1301
+ tokens.add(token)
1302
+ for (const variant of koreanParticleStrippedVariants(token)) {
1303
+ tokens.add(variant)
1304
+ }
1305
+ }
1306
+ return tokens
705
1307
  }
706
1308
 
707
- function isAirportAviationQuery(query: string): boolean {
708
- return KMA_AIRPORT_NAME_RE.test(query) && KMA_AIRPORT_AVIATION_RE.test(query)
1309
+ function expandedQueryTokens(query: string): Set<string> {
1310
+ return expandedTokensForText(query)
709
1311
  }
710
1312
 
711
- function isMedicalEmergencyQuery(query: string): boolean {
712
- return (
713
- (EMERGENCY_REQUEST_RE.test(query) ||
714
- AED_REQUEST_RE.test(query) ||
715
- MEDICAL_COLLAPSE_RE.test(query)) &&
716
- !MOIS_EMERGENCY_CALL_BOX_RE.test(query)
717
- )
1313
+ function isUsefulDiscoveryToken(token: string): boolean {
1314
+ const compact = token.replace(/[_-]/gu, '')
1315
+ if (compact.length === 0) return false
1316
+ if (LOW_SIGNAL_DISCOVERY_TOKENS.has(token)) return false
1317
+ if (hasHangul(compact)) return compact.length >= 2
1318
+ if (compact === 'er') return true
1319
+ return compact.length >= 3
718
1320
  }
719
1321
 
720
- function isCollapseOrAedQuery(query: string): boolean {
1322
+ function isSingleHangulPlaceSuffixMatch(
1323
+ fieldToken: string,
1324
+ queryToken: string,
1325
+ ): boolean {
721
1326
  return (
722
- (AED_REQUEST_RE.test(query) || MEDICAL_COLLAPSE_RE.test(query)) &&
723
- !MOIS_EMERGENCY_CALL_BOX_RE.test(query)
1327
+ hasHangul(fieldToken) &&
1328
+ fieldToken.length === 1 &&
1329
+ hasHangul(queryToken) &&
1330
+ queryToken.length >= 3 &&
1331
+ queryToken.endsWith(fieldToken)
724
1332
  )
725
1333
  }
726
1334
 
727
- function isLocationAdapter(entry: AdapterManifestEntry): boolean {
728
- return entry.primitive === 'locate' || ROOT_PRIMITIVE_TOOL_NAMES.has(entry.tool_id)
1335
+ function fieldMatchesToken(
1336
+ fieldTokens: Set<string>,
1337
+ fieldText: string,
1338
+ queryToken: string,
1339
+ ): boolean {
1340
+ if (fieldTokens.has(queryToken) || fieldText.includes(queryToken)) {
1341
+ return true
1342
+ }
1343
+ if (!isUsefulDiscoveryToken(queryToken)) return false
1344
+ for (const fieldToken of fieldTokens) {
1345
+ if (isSingleHangulPlaceSuffixMatch(fieldToken, queryToken)) {
1346
+ return true
1347
+ }
1348
+ if (!isUsefulDiscoveryToken(fieldToken)) continue
1349
+ if (fieldToken.includes(queryToken) || queryToken.includes(fieldToken)) {
1350
+ return true
1351
+ }
1352
+ }
1353
+ return false
729
1354
  }
730
1355
 
731
- function isKmaAnalysisQuery(query: string): boolean {
732
- return KMA_ANALYSIS_DATA_RE.test(query)
1356
+ function requiredInputFieldsFor(entry: AdapterManifestEntry): string[] {
1357
+ return asStringArray(asJsonObject(entry.input_schema_json).required)
733
1358
  }
734
1359
 
735
- function isLifestyleWeatherQuery(query: string): boolean {
1360
+ function isOpaqueProviderIdentifierField(fieldName: string): boolean {
736
1361
  return (
737
- KMA_LIFESTYLE_WEATHER_RE.test(query) &&
738
- !isAirportAviationQuery(query) &&
739
- !isKmaAnalysisQuery(query) &&
740
- !isMedicalEmergencyQuery(query) &&
741
- !TRAFFIC_HAZARD_RE.test(query) &&
742
- !MOF_OCEAN_WATER_QUALITY_RE.test(query)
1362
+ fieldName === 'ykiho' ||
1363
+ fieldName === 'id' ||
1364
+ fieldName.endsWith('_id')
743
1365
  )
744
1366
  }
745
1367
 
746
- function isPpsBidQuery(query: string): boolean {
747
- return PPS_BID_RE.test(query) && !PPS_SHOPPING_RE.test(query)
748
- }
749
-
750
- function isProtectedCheckQuery(query: string): boolean {
751
- return PROTECTED_QUERY_RE.test(query)
752
- }
753
-
754
- function protectedCheckToolPreference(query: string): string[] {
755
- const preferred = [
756
- PROTECTED_MOBILE_ID_RE.test(query) ? 'mock_verify_mobile_id' : undefined,
757
- PROTECTED_SIMPLE_AUTH_RE.test(query) ? 'mock_verify_module_simple_auth' : undefined,
758
- PROTECTED_SIMPLE_AUTH_RE.test(query) ? 'mock_verify_ganpyeon_injeung' : undefined,
759
- PROTECTED_MYDATA_RE.test(query) ? 'mock_verify_mydata' : undefined,
760
- ...PROTECTED_CHECK_TOOL_NAMES,
761
- ].filter((toolName): toolName is string => typeof toolName === 'string')
762
- return [...new Set(preferred)]
763
- }
764
-
765
- function isTagoBusQuery(query: string): boolean {
766
- return TAGO_BUS_RE.test(query)
767
- }
768
-
769
- function isKmaAnalysisMapQuery(query: string): boolean {
770
- return KMA_ANALYSIS_MAP_RE.test(query)
1368
+ function isOpaqueIdentifierOnlyInitialCandidate(
1369
+ entry: AdapterManifestEntry,
1370
+ queryTokens: Set<string>,
1371
+ query: string,
1372
+ ): boolean {
1373
+ const requiredFields = requiredInputFieldsFor(entry)
1374
+ if (requiredFields.length === 0) return false
1375
+ if (!requiredFields.every(isOpaqueProviderIdentifierField)) return false
1376
+
1377
+ const normalizedQuery = query.toLowerCase()
1378
+ if (normalizedQuery.includes(entry.tool_id.toLowerCase())) return false
1379
+ return !requiredFields.some(fieldName => queryTokens.has(fieldName.toLowerCase()))
771
1380
  }
772
1381
 
773
- function isKmaAnalysisPointQuery(query: string): boolean {
774
- return KMA_ANALYSIS_POINT_RE.test(query) && !isKmaAnalysisMapQuery(query)
1382
+ type AdapterScore = {
1383
+ score: number
1384
+ qualifyingDiscoveryMatches: number
775
1385
  }
776
1386
 
777
- function queryPrefersPoiLocation(query: string): boolean {
778
- return /(근처|주변|인근|가까운|역|터미널|공항|캠퍼스|대학교|대학|해수욕장|시장|공원|랜드마크|nearby|around)/iu.test(query)
1387
+ type ScoredAdapterEntry = {
1388
+ entry: AdapterManifestEntry
1389
+ score: number
779
1390
  }
780
1391
 
781
1392
  function scoreAdapterEntry(
782
1393
  entry: AdapterManifestEntry,
783
1394
  queryTokens: Set<string>,
784
1395
  query: string,
785
- ): number {
1396
+ ): AdapterScore {
1397
+ const toolId = entry.tool_id.toLowerCase()
1398
+ const name = entry.name.toLowerCase()
786
1399
  const searchHint = entry.search_hint.toLowerCase()
787
1400
  const description = (entry.llm_description ?? '').toLowerCase()
788
1401
  const haystack = [
789
- entry.tool_id,
790
- entry.name,
1402
+ toolId,
1403
+ name,
791
1404
  entry.primitive,
792
1405
  searchHint,
793
1406
  description,
794
1407
  ].join(' ').toLowerCase()
795
- const hintTokens = new Set(searchTokens(searchHint))
1408
+ const toolIdTokens = expandedTokensForText(toolId)
1409
+ const nameTokens = expandedTokensForText(name)
1410
+ const hintTokens = expandedTokensForText(searchHint)
796
1411
  let score = 0
1412
+ let qualifyingDiscoveryMatches = 0
797
1413
  for (const token of queryTokens) {
798
1414
  if (!token) continue
799
- if (entry.tool_id.toLowerCase().includes(token)) score += 12
800
- if (hintTokens.has(token)) score += 8
801
- else if (searchHint.includes(token)) score += 4
802
- if (description.includes(token)) score += 2
803
- if (haystack.includes(token)) score += 1
804
- }
805
- if (query.toLowerCase().includes(entry.tool_id.toLowerCase())) score += 1000
806
- if (
807
- isReverseGeocodeAdapter(entry.tool_id) &&
808
- !queryExplicitlyMentionsCoordinates(query)
809
- ) {
810
- score = Math.max(0, score - 24)
811
- }
812
- if (queryTargetsKoroadHazardDataset(query)) {
813
- if (entry.tool_id === 'koroad_accident_hazard_search') score += 32
814
- if (entry.tool_id === 'koroad_accident_search') score = 0
815
- }
816
- if (isKmaAnalysisQuery(query)) {
817
- if (entry.tool_id === 'kma_apihub_url_analysis_weather_chart_image') {
818
- score += isKmaAnalysisMapQuery(query) ? 900 : isKmaAnalysisPointQuery(query) ? -20 : 150
819
- }
820
- if (entry.tool_id === 'kma_apihub_url_high_resolution_grid_point') {
821
- score += isKmaAnalysisPointQuery(query) ? 900 : 450
822
- }
823
- if (entry.tool_id === 'kma_apihub_url_aws_objective_analysis_grid') {
824
- score += isKmaAnalysisPointQuery(query) ? 800 : 400
1415
+ let matchedDiscovery = false
1416
+ if (fieldMatchesToken(toolIdTokens, toolId, token)) {
1417
+ score += 12
1418
+ matchedDiscovery = true
825
1419
  }
826
- if (isKmaAnalysisPointQuery(query) && queryPrefersPoiLocation(query)) {
827
- if (entry.tool_id === 'kakao_keyword_search') score += 30
828
- if (entry.tool_id === 'kakao_address_search') score = Math.max(1, score - 15)
1420
+ if (fieldMatchesToken(hintTokens, searchHint, token)) {
1421
+ score += 8
1422
+ matchedDiscovery = true
829
1423
  }
830
- }
831
- if (isLifestyleWeatherQuery(query)) {
832
- if (entry.tool_id === 'kakao_keyword_search') score += 1100
833
- if (entry.tool_id === 'kakao_address_search') score += 1000
834
- if (entry.tool_id === 'kma_current_observation') score += 900
835
- if (entry.tool_id === 'kma_ultra_short_term_forecast') score += 800
836
- if (entry.tool_id === 'kma_short_term_forecast') score += 650
837
- if (entry.tool_id === 'kakao_coord_to_region') score += 260
838
- if (entry.tool_id === 'juso_adm_cd_lookup') score += 260
839
- if (entry.tool_id === 'sgis_adm_cd_lookup') score += 260
840
- }
841
- if (HIRA_MEDICAL_DETAIL_RE.test(query)) {
842
- if (entry.tool_id === 'hira_medical_institution_detail') score += 650
843
- }
844
- if (MOIS_EMERGENCY_CALL_BOX_RE.test(query)) {
845
- if (entry.tool_id === 'mois_emergency_call_box_lookup') score += 1000
846
- }
847
- if (GYERYONG_ASSISTIVE_CHARGER_RE.test(query)) {
848
- if (entry.tool_id === 'gyeryong_assistive_device_charging_place_locate') {
849
- score += 1000
1424
+ if (fieldMatchesToken(nameTokens, name, token)) {
1425
+ score += 4
1426
+ matchedDiscovery = true
850
1427
  }
851
- }
852
- if (MOF_OCEAN_WATER_QUALITY_RE.test(query)) {
853
- if (entry.tool_id === 'mof_ocean_water_quality_check') score += 1000
854
- }
855
- if (isPpsBidQuery(query)) {
856
- if (entry.tool_id === 'pps_bid_public_info') score += 1000
857
- }
858
- if (isProtectedCheckQuery(query) && entry.primitive === 'check') {
859
- const preference = protectedCheckToolPreference(query)
860
- const index = preference.indexOf(entry.tool_id)
861
- score += index >= 0 ? 1000 - index * 20 : 500
862
- }
863
- if (isTagoBusQuery(query)) {
864
- if (entry.tool_id === 'tago_bus_station_search') score += 1050
865
- if (entry.tool_id === 'tago_bus_arrival_search') score += 1000
866
- if (entry.tool_id === 'tago_bus_route_station_search') score += 950
867
- if (entry.tool_id === 'tago_bus_route_search') score += 850
868
- if (entry.tool_id === 'tago_bus_location_search') score += 650
869
- }
870
- if (isCollapseOrAedQuery(query)) {
871
- if (entry.tool_id === 'nmc_aed_site_locate') score += 900
872
- if (entry.tool_id === 'nmc_emergency_search') score += 700
873
- if (queryPrefersPoiLocation(query) && entry.tool_id === 'kakao_keyword_search') score += 120
874
- }
875
- if (
876
- KMA_GIMPO_AIRPORT_RE.test(query) &&
877
- KMA_RUNWAY_AREA_RE.test(query) &&
878
- KMA_AIRPORT_AVIATION_RE.test(query) &&
879
- entry.tool_id === 'kma_apihub_url_air_amos_minute'
880
- ) {
881
- score += 500
882
- }
883
- return score
884
- }
885
-
886
- function filterSpecialCaseRanked(
887
- query: string,
888
- ranked: ScoredAdapterEntry[],
889
- ): ScoredAdapterEntry[] {
890
- let filtered = ranked
891
- if (isKmaAnalysisQuery(query)) {
892
- const allowLocation = isKmaAnalysisPointQuery(query)
893
- const preferPoiLocation = queryPrefersPoiLocation(query)
894
- filtered = filtered
895
- .filter(candidate => {
896
- if (KMA_ANALYSIS_TOOL_NAMES.has(candidate.entry.tool_id)) return true
897
- return allowLocation && isLocationAdapter(candidate.entry)
898
- })
899
- .map(candidate => {
900
- if (!allowLocation || !isLocationAdapter(candidate.entry)) return candidate
901
- let score = Math.max(1, candidate.score - 10)
902
- if (preferPoiLocation && candidate.entry.tool_id === 'kakao_keyword_search') {
903
- score += 30
904
- } else if (preferPoiLocation && candidate.entry.tool_id === 'kakao_address_search') {
905
- score = Math.max(1, score - 15)
906
- }
907
- return { ...candidate, score }
908
- })
909
- .sort((a, b) => {
910
- if (b.score !== a.score) return b.score - a.score
911
- return a.entry.tool_id.localeCompare(b.entry.tool_id)
912
- })
913
- }
914
- if (isLifestyleWeatherQuery(query)) {
915
- const allowed = filtered.filter(
916
- candidate =>
917
- KMA_LIFESTYLE_WEATHER_TOOL_NAMES.has(candidate.entry.tool_id) ||
918
- isLocationAdapter(candidate.entry),
919
- )
920
- if (allowed.length > 0) {
921
- filtered = allowed.sort((a, b) => {
922
- if (b.score !== a.score) return b.score - a.score
923
- return a.entry.tool_id.localeCompare(b.entry.tool_id)
924
- })
925
- }
926
- }
927
- if (isPpsBidQuery(query)) {
928
- const allowed = filtered.filter(candidate => candidate.entry.tool_id === 'pps_bid_public_info')
929
- if (allowed.length > 0) {
930
- filtered = allowed.sort((a, b) => {
931
- if (b.score !== a.score) return b.score - a.score
932
- return a.entry.tool_id.localeCompare(b.entry.tool_id)
933
- })
934
- }
935
- }
936
- if (isProtectedCheckQuery(query)) {
937
- const preference = protectedCheckToolPreference(query)
938
- const allowed = filtered.filter(candidate => candidate.entry.primitive === 'check')
939
- if (allowed.length > 0) {
940
- filtered = allowed.sort((a, b) => {
941
- const aIndex = preference.indexOf(a.entry.tool_id)
942
- const bIndex = preference.indexOf(b.entry.tool_id)
943
- const aRank = aIndex >= 0 ? aIndex : Number.MAX_SAFE_INTEGER
944
- const bRank = bIndex >= 0 ? bIndex : Number.MAX_SAFE_INTEGER
945
- if (aRank !== bRank) return aRank - bRank
946
- if (b.score !== a.score) return b.score - a.score
947
- return a.entry.tool_id.localeCompare(b.entry.tool_id)
948
- })
949
- }
950
- }
951
- if (isTagoBusQuery(query)) {
952
- const allowed = filtered.filter(candidate => TAGO_BUS_TOOL_NAMES.has(candidate.entry.tool_id))
953
- if (allowed.length > 0) {
954
- filtered = allowed.sort((a, b) => {
955
- if (b.score !== a.score) return b.score - a.score
956
- return a.entry.tool_id.localeCompare(b.entry.tool_id)
957
- })
958
- }
959
- }
960
- if (isCollapseOrAedQuery(query)) {
961
- const allowed = filtered.filter(candidate => {
962
- if (candidate.entry.tool_id === 'nmc_aed_site_locate') return true
963
- if (candidate.entry.tool_id === 'nmc_emergency_search') return true
964
- return isLocationAdapter(candidate.entry)
965
- })
966
- if (allowed.some(candidate => candidate.entry.tool_id === 'nmc_aed_site_locate')) {
967
- filtered = allowed.sort((a, b) => {
968
- if (b.score !== a.score) return b.score - a.score
969
- return a.entry.tool_id.localeCompare(b.entry.tool_id)
970
- })
1428
+ if (description.includes(token)) score += 2
1429
+ if (haystack.includes(token)) score += 1
1430
+ if (matchedDiscovery && isUsefulDiscoveryToken(token)) {
1431
+ qualifyingDiscoveryMatches += 1
971
1432
  }
972
1433
  }
973
- if (KMA_GIMHAE_AIRPORT_RE.test(query) && KMA_AIRPORT_AVIATION_RE.test(query)) {
974
- filtered = filtered.filter(
975
- candidate => candidate.entry.tool_id !== 'kma_apihub_url_air_amos_minute',
976
- )
977
- }
978
- if (isAirportAviationQuery(query)) {
979
- const hasAirUrlCandidate = filtered.some(candidate =>
980
- KMA_URL_AIR_TOOL_NAMES.has(candidate.entry.tool_id),
981
- )
982
- if (hasAirUrlCandidate) {
983
- filtered = filtered.filter(
984
- candidate =>
985
- !isLocationAdapter(candidate.entry) &&
986
- candidate.entry.tool_id !== 'kma_current_observation',
987
- )
988
- }
1434
+ if (query.toLowerCase().includes(toolId)) {
1435
+ score += 1000
1436
+ qualifyingDiscoveryMatches += 1
989
1437
  }
990
- return filtered
1438
+ return { score, qualifyingDiscoveryMatches }
991
1439
  }
992
1440
 
993
1441
  export function selectTopKAdapterToolNamesForQuery(
994
1442
  query: string,
995
1443
  maxResults = 5,
1444
+ options: AdapterSelectionOptions = {},
996
1445
  ): string[] {
997
1446
  const normalizedQuery = query.trim()
998
1447
  if (!normalizedQuery || maxResults <= 0) return []
999
1448
  const queryTokens = expandedQueryTokens(normalizedQuery)
1000
- const ranked = filterSpecialCaseRanked(
1001
- normalizedQuery,
1002
- listAdapters()
1449
+ const routingIntent = extractProviderRoutingIntent(normalizedQuery)
1450
+ const restrictiveToolIds = restrictiveToolIdsForIntent(routingIntent, options)
1451
+ const ranked = listAdapters()
1003
1452
  .filter(entry => !ROOT_PRIMITIVE_TOOL_NAMES.has(entry.tool_id))
1004
- .map(entry => ({
1005
- entry,
1006
- score: scoreAdapterEntry(entry, queryTokens, normalizedQuery),
1007
- }))
1008
- .filter(candidate => candidate.score > 0)
1453
+ .map(entry => {
1454
+ const result = scoreAdapterEntry(entry, queryTokens, normalizedQuery)
1455
+ const routingBoost = routingIntentBoostForTool(entry.tool_id, routingIntent)
1456
+ return {
1457
+ entry,
1458
+ score: result.score + routingBoost,
1459
+ qualifyingDiscoveryMatches:
1460
+ result.qualifyingDiscoveryMatches + (routingBoost > 0 ? 1 : 0),
1461
+ }
1462
+ })
1463
+ .filter(candidate =>
1464
+ (restrictiveToolIds === undefined ||
1465
+ restrictiveToolIds.has(candidate.entry.tool_id)) &&
1466
+ candidate.score > 0 &&
1467
+ candidate.qualifyingDiscoveryMatches > 0 &&
1468
+ !isOpaqueIdentifierOnlyInitialCandidate(
1469
+ candidate.entry,
1470
+ queryTokens,
1471
+ normalizedQuery,
1472
+ )
1473
+ )
1009
1474
  .sort((a, b) => {
1010
1475
  if (b.score !== a.score) return b.score - a.score
1011
1476
  return a.entry.tool_id.localeCompare(b.entry.tool_id)
1012
- }),
1013
- )
1477
+ })
1014
1478
 
1015
1479
  return pickDiverseAdapterToolNames(ranked, maxResults)
1016
1480
  }
@@ -1047,6 +1511,8 @@ function buildAdapterTool(entry: AdapterManifestEntry): Tool {
1047
1511
  const primitiveTool = primitiveToolFor(primitive)
1048
1512
  const adapterInputSchema = inputSchemaFor(entry)
1049
1513
  const adapterInputJSONSchema = inputJSONSchemaFor(entry)
1514
+ const directCheckAdapterRequiresPermission =
1515
+ primitive === 'check' && !ROOT_PRIMITIVE_TOOL_NAMES.has(entry.tool_id)
1050
1516
 
1051
1517
  return buildTool({
1052
1518
  name: entry.tool_id,
@@ -1077,13 +1543,19 @@ function buildAdapterTool(entry: AdapterManifestEntry): Tool {
1077
1543
  },
1078
1544
 
1079
1545
  isReadOnly(input) {
1546
+ if (directCheckAdapterRequiresPermission) return false
1080
1547
  return primitiveTool.isReadOnly(rootInputFor(entry, input))
1081
1548
  },
1082
1549
 
1083
1550
  isDestructive(input) {
1551
+ if (directCheckAdapterRequiresPermission) return true
1084
1552
  return primitiveTool.isDestructive?.(rootInputFor(entry, input)) ?? false
1085
1553
  },
1086
1554
 
1555
+ async checkPermissions(input, context) {
1556
+ return primitiveTool.checkPermissions(rootInputFor(entry, input), context)
1557
+ },
1558
+
1087
1559
  async description() {
1088
1560
  return entry.name
1089
1561
  },
@@ -1098,18 +1570,6 @@ function buildAdapterTool(entry: AdapterManifestEntry): Tool {
1098
1570
  },
1099
1571
 
1100
1572
  async validateInput(input, context) {
1101
- const directPublicDataChoice = validateDirectPublicDataToolChoice(
1102
- entry.tool_id,
1103
- context,
1104
- input,
1105
- )
1106
- if (directPublicDataChoice) return directPublicDataChoice
1107
- const kmaAviationChoice = validateKmaAviationToolChoice(entry.tool_id, context)
1108
- if (kmaAviationChoice) return kmaAviationChoice
1109
- const kmaAnalysisChoice = validateKmaAnalysisToolChoice(entry.tool_id, context)
1110
- if (kmaAnalysisChoice) return kmaAnalysisChoice
1111
- const nmcAedChoice = validateNmcAedToolChoice(entry.tool_id, context)
1112
- if (nmcAedChoice) return nmcAedChoice
1113
1573
  if (!resolveAdapter(entry.tool_id)) {
1114
1574
  return {
1115
1575
  result: false as const,
@@ -1133,30 +1593,55 @@ function buildAdapterTool(entry: AdapterManifestEntry): Tool {
1133
1593
  },
1134
1594
 
1135
1595
  async call(input, context) {
1136
- const normalizedInput = normalizeDirectPublicDataToolInput(
1596
+ const normalizedDocumentInput = normalizeExplicitDocumentArtifactInput(
1137
1597
  entry.tool_id,
1138
- context,
1139
1598
  input,
1599
+ context.messages,
1600
+ )
1601
+ const adapterCallInput = concreteAdapterCallInputFor(
1602
+ entry,
1603
+ normalizedDocumentInput,
1140
1604
  )
1141
- return dispatchPrimitive({
1605
+ const result = await dispatchPrimitive({
1142
1606
  primitive,
1143
1607
  toolName: entry.tool_id,
1144
- args: normalizedInput,
1608
+ args: adapterCallInput,
1145
1609
  context,
1146
1610
  registry: getOrCreatePendingCallRegistry(),
1147
1611
  bridge: getOrCreateUmmayaBridge(),
1612
+ timeoutMs:
1613
+ primitive === 'document'
1614
+ ? resolveDocumentPrimitiveTimeoutMs()
1615
+ : undefined,
1148
1616
  })
1617
+ return {
1618
+ ...result,
1619
+ data: applyDocumentVisualRenderGateToOutput(result.data),
1620
+ }
1149
1621
  },
1150
1622
 
1151
1623
  userFacingName(input) {
1624
+ if (DOCUMENT_TOOL_NAMES.has(entry.tool_id)) {
1625
+ return 'Document'
1626
+ }
1152
1627
  return primitiveTool.userFacingName(rootInputFor(entry, input ?? {}))
1153
1628
  },
1154
1629
 
1155
1630
  mapToolResultToToolResultBlockParam(output, toolUseID) {
1156
- return primitiveTool.mapToolResultToToolResultBlockParam(output, toolUseID)
1631
+ const gatedOutput = applyDocumentVisualRenderGateToOutput(output)
1632
+ const block = primitiveTool.mapToolResultToToolResultBlockParam(gatedOutput, toolUseID)
1633
+ return isDocumentVisualRenderFailedOutput(gatedOutput)
1634
+ ? { ...block, is_error: true }
1635
+ : block
1157
1636
  },
1158
1637
 
1159
1638
  renderToolUseMessage(input, options) {
1639
+ if (DOCUMENT_TOOL_NAMES.has(entry.tool_id)) {
1640
+ return renderDocumentToolUseMessage(
1641
+ entry.tool_id,
1642
+ input as Record<string, unknown>,
1643
+ )
1644
+ }
1160
1645
  const rendered = primitiveTool.renderToolUseMessage(
1161
1646
  rootInputFor(entry, input),
1162
1647
  options,
@@ -1165,8 +1650,16 @@ function buildAdapterTool(entry: AdapterManifestEntry): Tool {
1165
1650
  },
1166
1651
 
1167
1652
  renderToolResultMessage(output, progressMessagesForMessage, options) {
1653
+ const gatedOutput = applyDocumentVisualRenderGateToOutput(output)
1654
+ if (shouldHideSuccessfulIntermediateDocumentResult(gatedOutput)) {
1655
+ return null
1656
+ }
1657
+ const documentResult = renderDocumentToolResultIfPresent(gatedOutput, options)
1658
+ if (documentResult !== null) {
1659
+ return documentResult
1660
+ }
1168
1661
  return primitiveTool.renderToolResultMessage?.(
1169
- output,
1662
+ gatedOutput,
1170
1663
  progressMessagesForMessage,
1171
1664
  options,
1172
1665
  ) ?? null