@linxiraos/pi-ai 1.0.0

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 (639) hide show
  1. package/CHANGELOG.md +5066 -0
  2. package/README.md +1195 -0
  3. package/dist/types/api-registry.d.ts +30 -0
  4. package/dist/types/auth/sqlite-credential-store.d.ts +117 -0
  5. package/dist/types/auth-broker/client.d.ts +93 -0
  6. package/dist/types/auth-broker/discover.d.ts +39 -0
  7. package/dist/types/auth-broker/index.d.ts +7 -0
  8. package/dist/types/auth-broker/refresher.d.ts +25 -0
  9. package/dist/types/auth-broker/remote-store.d.ts +136 -0
  10. package/dist/types/auth-broker/server.d.ts +48 -0
  11. package/dist/types/auth-broker/snapshot-cache.d.ts +17 -0
  12. package/dist/types/auth-broker/types.d.ts +152 -0
  13. package/dist/types/auth-broker/wire-schema-resource.d.ts +53 -0
  14. package/dist/types/auth-broker/wire-schemas.d.ts +5 -0
  15. package/dist/types/auth-gateway/http.d.ts +56 -0
  16. package/dist/types/auth-gateway/index.d.ts +3 -0
  17. package/dist/types/auth-gateway/server.d.ts +36 -0
  18. package/dist/types/auth-gateway/types.d.ts +127 -0
  19. package/dist/types/auth-retry.d.ts +150 -0
  20. package/dist/types/auth-storage.d.ts +1258 -0
  21. package/dist/types/dialect/anthropic.d.ts +15 -0
  22. package/dist/types/dialect/catalog.d.ts +3 -0
  23. package/dist/types/dialect/coercion.d.ts +23 -0
  24. package/dist/types/dialect/deepseek.d.ts +14 -0
  25. package/dist/types/dialect/demotion.d.ts +30 -0
  26. package/dist/types/dialect/examples.d.ts +18 -0
  27. package/dist/types/dialect/factory.d.ts +3 -0
  28. package/dist/types/dialect/fenced-thinking.d.ts +53 -0
  29. package/dist/types/dialect/gemini.d.ts +17 -0
  30. package/dist/types/dialect/gemma.d.ts +15 -0
  31. package/dist/types/dialect/glm.d.ts +9 -0
  32. package/dist/types/dialect/harmony.d.ts +8 -0
  33. package/dist/types/dialect/hermes.d.ts +9 -0
  34. package/dist/types/dialect/history.d.ts +3 -0
  35. package/dist/types/dialect/index.d.ts +11 -0
  36. package/dist/types/dialect/inventory.d.ts +9 -0
  37. package/dist/types/dialect/kimi.d.ts +14 -0
  38. package/dist/types/dialect/minimax.d.ts +3 -0
  39. package/dist/types/dialect/owned-stream.d.ts +4 -0
  40. package/dist/types/dialect/qwen3.d.ts +9 -0
  41. package/dist/types/dialect/rendering.d.ts +54 -0
  42. package/dist/types/dialect/thinking.d.ts +6 -0
  43. package/dist/types/dialect/types.d.ts +68 -0
  44. package/dist/types/dialect/xml.d.ts +9 -0
  45. package/dist/types/error/abort.d.ts +14 -0
  46. package/dist/types/error/auth-classify.d.ts +20 -0
  47. package/dist/types/error/auth.d.ts +27 -0
  48. package/dist/types/error/aws.d.ts +27 -0
  49. package/dist/types/error/classes.d.ts +106 -0
  50. package/dist/types/error/finalize.d.ts +39 -0
  51. package/dist/types/error/flags.d.ts +94 -0
  52. package/dist/types/error/format.d.ts +20 -0
  53. package/dist/types/error/gateway.d.ts +20 -0
  54. package/dist/types/error/index.d.ts +14 -0
  55. package/dist/types/error/oauth.d.ts +43 -0
  56. package/dist/types/error/provider.d.ts +42 -0
  57. package/dist/types/error/rate-limit.d.ts +80 -0
  58. package/dist/types/error/retryable.d.ts +27 -0
  59. package/dist/types/error/validation.d.ts +32 -0
  60. package/dist/types/index.d.ts +51 -0
  61. package/dist/types/provider-details.d.ts +24 -0
  62. package/dist/types/providers/amazon-bedrock.d.ts +39 -0
  63. package/dist/types/providers/anthropic-client.d.ts +106 -0
  64. package/dist/types/providers/anthropic-messages-server-schema.d.ts +937 -0
  65. package/dist/types/providers/anthropic-messages-server.d.ts +17 -0
  66. package/dist/types/providers/anthropic-wire.d.ts +345 -0
  67. package/dist/types/providers/anthropic.d.ts +261 -0
  68. package/dist/types/providers/aws-credentials.d.ts +48 -0
  69. package/dist/types/providers/aws-eventstream.d.ts +39 -0
  70. package/dist/types/providers/aws-sigv4.d.ts +55 -0
  71. package/dist/types/providers/azure-openai-responses.d.ts +16 -0
  72. package/dist/types/providers/bedrock-mantle.d.ts +13 -0
  73. package/dist/types/providers/claude-code-fingerprint.d.ts +19 -0
  74. package/dist/types/providers/cowork-fetch.d.ts +3 -0
  75. package/dist/types/providers/cursor/exec-modern.d.ts +98 -0
  76. package/dist/types/providers/cursor-pi-args.d.ts +105 -0
  77. package/dist/types/providers/cursor.d.ts +221 -0
  78. package/dist/types/providers/devin.d.ts +12 -0
  79. package/dist/types/providers/error-message.d.ts +25 -0
  80. package/dist/types/providers/github-copilot-headers.d.ts +40 -0
  81. package/dist/types/providers/gitlab-duo-workflow.d.ts +254 -0
  82. package/dist/types/providers/gitlab-duo.d.ts +27 -0
  83. package/dist/types/providers/google-auth.d.ts +24 -0
  84. package/dist/types/providers/google-gemini-cli.d.ts +120 -0
  85. package/dist/types/providers/google-shared.d.ts +203 -0
  86. package/dist/types/providers/google-types.d.ts +155 -0
  87. package/dist/types/providers/google-vertex.d.ts +7 -0
  88. package/dist/types/providers/google.d.ts +4 -0
  89. package/dist/types/providers/grammar.d.ts +1 -0
  90. package/dist/types/providers/kimi.d.ts +27 -0
  91. package/dist/types/providers/mock.d.ts +179 -0
  92. package/dist/types/providers/ollama.d.ts +8 -0
  93. package/dist/types/providers/openai-anthropic-shim.d.ts +35 -0
  94. package/dist/types/providers/openai-chat-server-schema.d.ts +1311 -0
  95. package/dist/types/providers/openai-chat-server.d.ts +16 -0
  96. package/dist/types/providers/openai-chat-wire.d.ts +669 -0
  97. package/dist/types/providers/openai-codex/request-transformer.d.ts +109 -0
  98. package/dist/types/providers/openai-codex/response-handler.d.ts +26 -0
  99. package/dist/types/providers/openai-codex-responses.d.ts +238 -0
  100. package/dist/types/providers/openai-completions.d.ts +48 -0
  101. package/dist/types/providers/openai-reasoning-fallback.d.ts +25 -0
  102. package/dist/types/providers/openai-responses-server-schema.d.ts +1314 -0
  103. package/dist/types/providers/openai-responses-server.d.ts +17 -0
  104. package/dist/types/providers/openai-responses-wire.d.ts +6099 -0
  105. package/dist/types/providers/openai-responses.d.ts +131 -0
  106. package/dist/types/providers/openai-shared.d.ts +624 -0
  107. package/dist/types/providers/pi-native-client.d.ts +13 -0
  108. package/dist/types/providers/pi-native-server.d.ts +69 -0
  109. package/dist/types/providers/register-builtins.d.ts +37 -0
  110. package/dist/types/providers/synthetic.d.ts +26 -0
  111. package/dist/types/providers/transform-messages.d.ts +32 -0
  112. package/dist/types/providers/vision-guard.d.ts +20 -0
  113. package/dist/types/registry/aiand.d.ts +7 -0
  114. package/dist/types/registry/aimlapi.d.ts +4 -0
  115. package/dist/types/registry/alibaba-coding-plan.d.ts +8 -0
  116. package/dist/types/registry/alibaba-token-plan.d.ts +18 -0
  117. package/dist/types/registry/amazon-bedrock.d.ts +5 -0
  118. package/dist/types/registry/anthropic.d.ts +10 -0
  119. package/dist/types/registry/api-key-login.d.ts +42 -0
  120. package/dist/types/registry/api-key-validation.d.ts +43 -0
  121. package/dist/types/registry/aws.d.ts +13 -0
  122. package/dist/types/registry/azure.d.ts +4 -0
  123. package/dist/types/registry/baseten.d.ts +7 -0
  124. package/dist/types/registry/bedrock-mantle.d.ts +22 -0
  125. package/dist/types/registry/cerebras.d.ts +7 -0
  126. package/dist/types/registry/cloudflare-ai-gateway.d.ts +13 -0
  127. package/dist/types/registry/coreweave.d.ts +7 -0
  128. package/dist/types/registry/cursor.d.ts +7 -0
  129. package/dist/types/registry/deepseek.d.ts +8 -0
  130. package/dist/types/registry/derived.d.ts +5 -0
  131. package/dist/types/registry/devin.d.ts +8 -0
  132. package/dist/types/registry/exa.d.ts +8 -0
  133. package/dist/types/registry/firepass.d.ts +16 -0
  134. package/dist/types/registry/fireworks.d.ts +7 -0
  135. package/dist/types/registry/github-copilot.d.ts +7 -0
  136. package/dist/types/registry/gitlab-duo-workflow.d.ts +10 -0
  137. package/dist/types/registry/gitlab-duo.d.ts +9 -0
  138. package/dist/types/registry/gmi-cloud.d.ts +7 -0
  139. package/dist/types/registry/google-antigravity.d.ts +9 -0
  140. package/dist/types/registry/google-gemini-cli.d.ts +9 -0
  141. package/dist/types/registry/google-vertex.d.ts +5 -0
  142. package/dist/types/registry/google.d.ts +4 -0
  143. package/dist/types/registry/groq.d.ts +4 -0
  144. package/dist/types/registry/huggingface.d.ts +7 -0
  145. package/dist/types/registry/index.d.ts +4 -0
  146. package/dist/types/registry/kagi.d.ts +14 -0
  147. package/dist/types/registry/kilo.d.ts +7 -0
  148. package/dist/types/registry/kimi-code.d.ts +7 -0
  149. package/dist/types/registry/litellm.d.ts +13 -0
  150. package/dist/types/registry/llama-cpp.d.ts +8 -0
  151. package/dist/types/registry/lm-studio.d.ts +8 -0
  152. package/dist/types/registry/meta.d.ts +7 -0
  153. package/dist/types/registry/minimax-code-cn.d.ts +6 -0
  154. package/dist/types/registry/minimax-code.d.ts +6 -0
  155. package/dist/types/registry/minimax.d.ts +4 -0
  156. package/dist/types/registry/mistral.d.ts +4 -0
  157. package/dist/types/registry/moonshot.d.ts +7 -0
  158. package/dist/types/registry/nanogpt.d.ts +7 -0
  159. package/dist/types/registry/novita.d.ts +6 -0
  160. package/dist/types/registry/nvidia.d.ts +7 -0
  161. package/dist/types/registry/oauth/anthropic-constants.d.ts +12 -0
  162. package/dist/types/registry/oauth/anthropic.d.ts +24 -0
  163. package/dist/types/registry/oauth/callback-server.d.ts +74 -0
  164. package/dist/types/registry/oauth/cursor.d.ts +16 -0
  165. package/dist/types/registry/oauth/device-code.d.ts +25 -0
  166. package/dist/types/registry/oauth/devin.d.ts +5 -0
  167. package/dist/types/registry/oauth/github-copilot.d.ts +30 -0
  168. package/dist/types/registry/oauth/gitlab-duo-workflow.d.ts +6 -0
  169. package/dist/types/registry/oauth/gitlab-duo.d.ts +3 -0
  170. package/dist/types/registry/oauth/google-antigravity.d.ts +11 -0
  171. package/dist/types/registry/oauth/google-gemini-cli.d.ts +22 -0
  172. package/dist/types/registry/oauth/google-oauth-shared.d.ts +56 -0
  173. package/dist/types/registry/oauth/index.d.ts +45 -0
  174. package/dist/types/registry/oauth/kimi.d.ts +21 -0
  175. package/dist/types/registry/oauth/minimax-code.d.ts +27 -0
  176. package/dist/types/registry/oauth/openai-codex.d.ts +33 -0
  177. package/dist/types/registry/oauth/opencode.d.ts +18 -0
  178. package/dist/types/registry/oauth/perplexity.d.ts +9 -0
  179. package/dist/types/registry/oauth/pkce.d.ts +8 -0
  180. package/dist/types/registry/oauth/types.d.ts +87 -0
  181. package/dist/types/registry/oauth/wafer.d.ts +1 -0
  182. package/dist/types/registry/oauth/xai-oauth.d.ts +46 -0
  183. package/dist/types/registry/oauth/xiaomi.d.ts +25 -0
  184. package/dist/types/registry/oauth/zai.d.ts +25 -0
  185. package/dist/types/registry/ollama-cloud.d.ts +7 -0
  186. package/dist/types/registry/ollama.d.ts +12 -0
  187. package/dist/types/registry/openai-codex-device.d.ts +8 -0
  188. package/dist/types/registry/openai-codex.d.ts +9 -0
  189. package/dist/types/registry/openai.d.ts +4 -0
  190. package/dist/types/registry/opencode-go.d.ts +6 -0
  191. package/dist/types/registry/opencode-zen.d.ts +6 -0
  192. package/dist/types/registry/openrouter.d.ts +13 -0
  193. package/dist/types/registry/parallel.d.ts +14 -0
  194. package/dist/types/registry/perplexity.d.ts +7 -0
  195. package/dist/types/registry/qianfan.d.ts +7 -0
  196. package/dist/types/registry/qwen-portal.d.ts +7 -0
  197. package/dist/types/registry/registry.d.ts +367 -0
  198. package/dist/types/registry/sakana.d.ts +7 -0
  199. package/dist/types/registry/siliconflow-cn.d.ts +7 -0
  200. package/dist/types/registry/siliconflow.d.ts +7 -0
  201. package/dist/types/registry/synthetic.d.ts +6 -0
  202. package/dist/types/registry/tavily.d.ts +14 -0
  203. package/dist/types/registry/together.d.ts +6 -0
  204. package/dist/types/registry/types.d.ts +75 -0
  205. package/dist/types/registry/umans.d.ts +7 -0
  206. package/dist/types/registry/venice.d.ts +13 -0
  207. package/dist/types/registry/vercel-ai-gateway.d.ts +7 -0
  208. package/dist/types/registry/vllm.d.ts +7 -0
  209. package/dist/types/registry/wafer-serverless.d.ts +6 -0
  210. package/dist/types/registry/xai-oauth.d.ts +7 -0
  211. package/dist/types/registry/xai.d.ts +7 -0
  212. package/dist/types/registry/xiaomi-token-plan-ams.d.ts +6 -0
  213. package/dist/types/registry/xiaomi-token-plan-cn.d.ts +6 -0
  214. package/dist/types/registry/xiaomi-token-plan-sgp.d.ts +6 -0
  215. package/dist/types/registry/xiaomi.d.ts +6 -0
  216. package/dist/types/registry/zai.d.ts +15 -0
  217. package/dist/types/registry/zenmux.d.ts +7 -0
  218. package/dist/types/registry/zhipu-coding-plan.d.ts +7 -0
  219. package/dist/types/stream.d.ts +46 -0
  220. package/dist/types/types.d.ts +1064 -0
  221. package/dist/types/usage/alibaba-token-plan.d.ts +3 -0
  222. package/dist/types/usage/claude.d.ts +4 -0
  223. package/dist/types/usage/cursor.d.ts +4 -0
  224. package/dist/types/usage/gemini.d.ts +2 -0
  225. package/dist/types/usage/github-copilot.d.ts +7 -0
  226. package/dist/types/usage/google-antigravity.d.ts +15 -0
  227. package/dist/types/usage/kimi.d.ts +2 -0
  228. package/dist/types/usage/minimax-code.d.ts +3 -0
  229. package/dist/types/usage/ollama.d.ts +5 -0
  230. package/dist/types/usage/openai-codex-base-url.d.ts +18 -0
  231. package/dist/types/usage/openai-codex-reset.d.ts +88 -0
  232. package/dist/types/usage/openai-codex.d.ts +10 -0
  233. package/dist/types/usage/opencode-go.d.ts +2 -0
  234. package/dist/types/usage/shared.d.ts +1 -0
  235. package/dist/types/usage/synthetic.d.ts +2 -0
  236. package/dist/types/usage/umans.d.ts +2 -0
  237. package/dist/types/usage/xai-oauth.d.ts +12 -0
  238. package/dist/types/usage/zai.d.ts +3 -0
  239. package/dist/types/usage.d.ts +527 -0
  240. package/dist/types/utils/abort.d.ts +25 -0
  241. package/dist/types/utils/anthropic-auth.d.ts +35 -0
  242. package/dist/types/utils/aws-profile.d.ts +17 -0
  243. package/dist/types/utils/block-symbols.d.ts +62 -0
  244. package/dist/types/utils/deterministic-id.d.ts +16 -0
  245. package/dist/types/utils/empty-completion-retry.d.ts +21 -0
  246. package/dist/types/utils/event-stream.d.ts +39 -0
  247. package/dist/types/utils/foundry.d.ts +1 -0
  248. package/dist/types/utils/google-validation.d.ts +2 -0
  249. package/dist/types/utils/harmony-leak.d.ts +135 -0
  250. package/dist/types/utils/http-inspector.d.ts +49 -0
  251. package/dist/types/utils/idle-iterator.d.ts +149 -0
  252. package/dist/types/utils/leaked-thinking-stream.d.ts +33 -0
  253. package/dist/types/utils/openai-http.d.ts +48 -0
  254. package/dist/types/utils/openrouter-headers.d.ts +1 -0
  255. package/dist/types/utils/parse-bind.d.ts +23 -0
  256. package/dist/types/utils/provider-response.d.ts +3 -0
  257. package/dist/types/utils/proxy.d.ts +39 -0
  258. package/dist/types/utils/request-debug.d.ts +29 -0
  259. package/dist/types/utils/retry-after.d.ts +4 -0
  260. package/dist/types/utils/retry.d.ts +14 -0
  261. package/dist/types/utils/schema/adapt.d.ts +24 -0
  262. package/dist/types/utils/schema/compatibility.d.ts +30 -0
  263. package/dist/types/utils/schema/dereference.d.ts +11 -0
  264. package/dist/types/utils/schema/draft.d.ts +10 -0
  265. package/dist/types/utils/schema/equality.d.ts +4 -0
  266. package/dist/types/utils/schema/fields.d.ts +54 -0
  267. package/dist/types/utils/schema/index.d.ts +14 -0
  268. package/dist/types/utils/schema/json-schema-validator.d.ts +20 -0
  269. package/dist/types/utils/schema/meta-validator.d.ts +2 -0
  270. package/dist/types/utils/schema/normalize.d.ts +153 -0
  271. package/dist/types/utils/schema/spill.d.ts +8 -0
  272. package/dist/types/utils/schema/stamps.d.ts +17 -0
  273. package/dist/types/utils/schema/strict-tool-validation.d.ts +16 -0
  274. package/dist/types/utils/schema/types.d.ts +4 -0
  275. package/dist/types/utils/schema/typescript.d.ts +24 -0
  276. package/dist/types/utils/schema/wire.d.ts +52 -0
  277. package/dist/types/utils/sdk-stream-timeout.d.ts +33 -0
  278. package/dist/types/utils/sse-debug.d.ts +5 -0
  279. package/dist/types/utils/stream-markup-healing.d.ts +87 -0
  280. package/dist/types/utils/thinking-loop.d.ts +102 -0
  281. package/dist/types/utils/tool-call-loop-guard.d.ts +26 -0
  282. package/dist/types/utils/tool-choice.d.ts +52 -0
  283. package/dist/types/utils/validation.d.ts +28 -0
  284. package/dist/types/utils.d.ts +57 -0
  285. package/package.json +138 -0
  286. package/src/api-registry.ts +109 -0
  287. package/src/auth/sqlite-credential-store.ts +2066 -0
  288. package/src/auth-broker/client.ts +471 -0
  289. package/src/auth-broker/discover.ts +310 -0
  290. package/src/auth-broker/index.ts +7 -0
  291. package/src/auth-broker/refresher.ts +117 -0
  292. package/src/auth-broker/remote-store.ts +1332 -0
  293. package/src/auth-broker/server.ts +898 -0
  294. package/src/auth-broker/snapshot-cache.ts +200 -0
  295. package/src/auth-broker/types.ts +193 -0
  296. package/src/auth-broker/wire-schema-resource.ts +487 -0
  297. package/src/auth-broker/wire-schemas.ts +43 -0
  298. package/src/auth-gateway/http.ts +227 -0
  299. package/src/auth-gateway/index.ts +3 -0
  300. package/src/auth-gateway/server.ts +836 -0
  301. package/src/auth-gateway/types.ts +153 -0
  302. package/src/auth-retry.ts +401 -0
  303. package/src/auth-storage.ts +6540 -0
  304. package/src/dialect/anthropic.md +31 -0
  305. package/src/dialect/anthropic.ts +608 -0
  306. package/src/dialect/catalog.ts +29 -0
  307. package/src/dialect/coercion.ts +136 -0
  308. package/src/dialect/deepseek.md +24 -0
  309. package/src/dialect/deepseek.ts +609 -0
  310. package/src/dialect/demotion.ts +40 -0
  311. package/src/dialect/examples.ts +71 -0
  312. package/src/dialect/factory.ts +34 -0
  313. package/src/dialect/fenced-thinking.ts +184 -0
  314. package/src/dialect/gemini.md +44 -0
  315. package/src/dialect/gemini.ts +583 -0
  316. package/src/dialect/gemma.md +33 -0
  317. package/src/dialect/gemma.ts +387 -0
  318. package/src/dialect/glm.md +32 -0
  319. package/src/dialect/glm.ts +579 -0
  320. package/src/dialect/harmony.md +31 -0
  321. package/src/dialect/harmony.ts +345 -0
  322. package/src/dialect/hermes.md +25 -0
  323. package/src/dialect/hermes.ts +206 -0
  324. package/src/dialect/history.ts +81 -0
  325. package/src/dialect/index.ts +15 -0
  326. package/src/dialect/inventory.ts +30 -0
  327. package/src/dialect/kimi.md +24 -0
  328. package/src/dialect/kimi.ts +340 -0
  329. package/src/dialect/minimax.md +31 -0
  330. package/src/dialect/minimax.ts +95 -0
  331. package/src/dialect/owned-stream.ts +481 -0
  332. package/src/dialect/prompt-template.md +12 -0
  333. package/src/dialect/qwen3.md +28 -0
  334. package/src/dialect/qwen3.ts +240 -0
  335. package/src/dialect/rendering.ts +304 -0
  336. package/src/dialect/thinking.ts +292 -0
  337. package/src/dialect/types.ts +56 -0
  338. package/src/dialect/xml.md +22 -0
  339. package/src/dialect/xml.ts +90 -0
  340. package/src/error/abort.ts +18 -0
  341. package/src/error/auth-classify.ts +47 -0
  342. package/src/error/auth.ts +48 -0
  343. package/src/error/aws.ts +35 -0
  344. package/src/error/classes.ts +281 -0
  345. package/src/error/finalize.ts +69 -0
  346. package/src/error/flags.ts +602 -0
  347. package/src/error/format.ts +45 -0
  348. package/src/error/gateway.ts +96 -0
  349. package/src/error/index.ts +14 -0
  350. package/src/error/oauth.ts +58 -0
  351. package/src/error/provider.ts +63 -0
  352. package/src/error/rate-limit.ts +303 -0
  353. package/src/error/retryable.ts +70 -0
  354. package/src/error/validation.ts +44 -0
  355. package/src/index.ts +51 -0
  356. package/src/provider-details.ts +90 -0
  357. package/src/providers/amazon-bedrock.ts +1064 -0
  358. package/src/providers/anthropic-client.ts +317 -0
  359. package/src/providers/anthropic-messages-server-schema.ts +252 -0
  360. package/src/providers/anthropic-messages-server.ts +818 -0
  361. package/src/providers/anthropic-wire.ts +359 -0
  362. package/src/providers/anthropic.ts +4539 -0
  363. package/src/providers/aws-credentials.ts +772 -0
  364. package/src/providers/aws-eventstream.ts +181 -0
  365. package/src/providers/aws-sigv4.ts +218 -0
  366. package/src/providers/azure-openai-responses.ts +438 -0
  367. package/src/providers/bedrock-mantle.ts +110 -0
  368. package/src/providers/claude-code-fingerprint.ts +20 -0
  369. package/src/providers/cowork-fetch.ts +201 -0
  370. package/src/providers/cursor/exec-modern.ts +496 -0
  371. package/src/providers/cursor/proto/agent.proto +4533 -0
  372. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  373. package/src/providers/cursor/proto/buf.yaml +17 -0
  374. package/src/providers/cursor-pi-args.ts +165 -0
  375. package/src/providers/cursor.ts +4689 -0
  376. package/src/providers/devin/proto/buf/validate/validate.proto +468 -0
  377. package/src/providers/devin/proto/buf.gen.yaml +33 -0
  378. package/src/providers/devin/proto/buf.yaml +17 -0
  379. package/src/providers/devin/proto/cel/expr/checked.proto +103 -0
  380. package/src/providers/devin/proto/cel/expr/eval.proto +38 -0
  381. package/src/providers/devin/proto/cel/expr/explain.proto +15 -0
  382. package/src/providers/devin/proto/cel/expr/syntax.proto +113 -0
  383. package/src/providers/devin/proto/cel/expr/value.proto +41 -0
  384. package/src/providers/devin/proto/connectext/grpc/status/v1/status.proto +11 -0
  385. package/src/providers/devin/proto/errorspb/errors.proto +56 -0
  386. package/src/providers/devin/proto/errorspb/hintdetail.proto +7 -0
  387. package/src/providers/devin/proto/errorspb/markers.proto +10 -0
  388. package/src/providers/devin/proto/errorspb/tags.proto +12 -0
  389. package/src/providers/devin/proto/errorspb/testing.proto +6 -0
  390. package/src/providers/devin/proto/exa/analytics_pb/analytics.proto +188 -0
  391. package/src/providers/devin/proto/exa/api_server_pb/api_server.proto +2461 -0
  392. package/src/providers/devin/proto/exa/auth_pb/auth.proto +19 -0
  393. package/src/providers/devin/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +79 -0
  394. package/src/providers/devin/proto/exa/browser_preview_pb/browser_preview.proto +32 -0
  395. package/src/providers/devin/proto/exa/bug_checker_pb/bug_checker.proto +22 -0
  396. package/src/providers/devin/proto/exa/cascade_plugins_pb/cascade_plugins.proto +262 -0
  397. package/src/providers/devin/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
  398. package/src/providers/devin/proto/exa/chat_pb/chat.proto +449 -0
  399. package/src/providers/devin/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
  400. package/src/providers/devin/proto/exa/codeium_common_pb/codeium_common.proto +4157 -0
  401. package/src/providers/devin/proto/exa/context_module_pb/context_module.proto +175 -0
  402. package/src/providers/devin/proto/exa/cortex_pb/cortex.proto +3268 -0
  403. package/src/providers/devin/proto/exa/dev_pb/dev.proto +26 -0
  404. package/src/providers/devin/proto/exa/diff_action_pb/diff_action.proto +75 -0
  405. package/src/providers/devin/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +103 -0
  406. package/src/providers/devin/proto/exa/eval_pb/eval.proto +1315 -0
  407. package/src/providers/devin/proto/exa/extension_server_pb/extension_server.proto +556 -0
  408. package/src/providers/devin/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
  409. package/src/providers/devin/proto/exa/index_pb/index.proto +461 -0
  410. package/src/providers/devin/proto/exa/knowledge_base_pb/knowledge_base.proto +144 -0
  411. package/src/providers/devin/proto/exa/language_server_pb/language_server.proto +2385 -0
  412. package/src/providers/devin/proto/exa/model_management_pb/model_management.proto +186 -0
  413. package/src/providers/devin/proto/exa/opensearch_clients_pb/opensearch_clients.proto +503 -0
  414. package/src/providers/devin/proto/exa/product_analytics_pb/product_analytics.proto +37 -0
  415. package/src/providers/devin/proto/exa/prompt_pb/prompt.proto +92 -0
  416. package/src/providers/devin/proto/exa/reactive_component_pb/reactive_component.proto +96 -0
  417. package/src/providers/devin/proto/exa/seat_management_pb/seat_management.proto +2680 -0
  418. package/src/providers/devin/proto/exa/tokenizer_pb/tokenizer.proto +37 -0
  419. package/src/providers/devin/proto/exa/trainer_pb/config.proto +647 -0
  420. package/src/providers/devin/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
  421. package/src/providers/devin/proto/exa/trust_pb/trust.proto +157 -0
  422. package/src/providers/devin/proto/exa/user_analytics_pb/user_analytics.proto +519 -0
  423. package/src/providers/devin/proto/google.golang.org/appengine/internal/base/api_base.proto +28 -0
  424. package/src/providers/devin/proto/google.golang.org/appengine/internal/datastore/datastore_v3.proto +484 -0
  425. package/src/providers/devin/proto/google.golang.org/appengine/internal/log/log_service.proto +136 -0
  426. package/src/providers/devin/proto/google.golang.org/appengine/internal/remote_api/remote_api.proto +42 -0
  427. package/src/providers/devin/proto/google.golang.org/appengine/internal/urlfetch/urlfetch_service.proto +61 -0
  428. package/src/providers/devin/proto/grpc/binlog/v1/binarylog.proto +84 -0
  429. package/src/providers/devin/proto/io/prometheus/client/metrics.proto +98 -0
  430. package/src/providers/devin.ts +679 -0
  431. package/src/providers/error-message.ts +23 -0
  432. package/src/providers/github-copilot-headers.ts +141 -0
  433. package/src/providers/gitlab-duo-workflow-chatml-note.md +1 -0
  434. package/src/providers/gitlab-duo-workflow.ts +3135 -0
  435. package/src/providers/gitlab-duo.ts +399 -0
  436. package/src/providers/google-auth.ts +330 -0
  437. package/src/providers/google-gemini-cli.ts +1370 -0
  438. package/src/providers/google-shared.ts +1122 -0
  439. package/src/providers/google-types.ts +180 -0
  440. package/src/providers/google-vertex.ts +135 -0
  441. package/src/providers/google.ts +47 -0
  442. package/src/providers/grammar.ts +70 -0
  443. package/src/providers/kimi.ts +51 -0
  444. package/src/providers/mock.ts +514 -0
  445. package/src/providers/ollama.ts +776 -0
  446. package/src/providers/openai-anthropic-shim.ts +166 -0
  447. package/src/providers/openai-chat-server-schema.ts +243 -0
  448. package/src/providers/openai-chat-server.ts +752 -0
  449. package/src/providers/openai-chat-wire.ts +859 -0
  450. package/src/providers/openai-codex/request-transformer.ts +491 -0
  451. package/src/providers/openai-codex/response-handler.ts +102 -0
  452. package/src/providers/openai-codex-responses.ts +4716 -0
  453. package/src/providers/openai-completions.ts +2389 -0
  454. package/src/providers/openai-reasoning-fallback.ts +269 -0
  455. package/src/providers/openai-responses-server-schema.ts +397 -0
  456. package/src/providers/openai-responses-server.ts +1466 -0
  457. package/src/providers/openai-responses-wire.ts +6416 -0
  458. package/src/providers/openai-responses.ts +1393 -0
  459. package/src/providers/openai-shared.ts +3500 -0
  460. package/src/providers/pi-native-client.ts +275 -0
  461. package/src/providers/pi-native-server.ts +245 -0
  462. package/src/providers/register-builtins.ts +503 -0
  463. package/src/providers/synthetic.ts +50 -0
  464. package/src/providers/transform-messages.ts +1083 -0
  465. package/src/providers/vision-guard.ts +54 -0
  466. package/src/registry/aiand.ts +22 -0
  467. package/src/registry/aimlapi.ts +6 -0
  468. package/src/registry/alibaba-coding-plan.ts +104 -0
  469. package/src/registry/alibaba-token-plan.ts +125 -0
  470. package/src/registry/amazon-bedrock.ts +22 -0
  471. package/src/registry/anthropic.ts +26 -0
  472. package/src/registry/api-key-login.ts +115 -0
  473. package/src/registry/api-key-validation.ts +145 -0
  474. package/src/registry/aws.ts +57 -0
  475. package/src/registry/azure.ts +6 -0
  476. package/src/registry/baseten.ts +22 -0
  477. package/src/registry/bedrock-mantle.ts +34 -0
  478. package/src/registry/cerebras.ts +23 -0
  479. package/src/registry/cloudflare-ai-gateway.ts +45 -0
  480. package/src/registry/coreweave.ts +40 -0
  481. package/src/registry/cursor.ts +20 -0
  482. package/src/registry/deepseek.ts +46 -0
  483. package/src/registry/derived.ts +9 -0
  484. package/src/registry/devin.ts +15 -0
  485. package/src/registry/exa.ts +19 -0
  486. package/src/registry/firepass.ts +32 -0
  487. package/src/registry/fireworks.ts +28 -0
  488. package/src/registry/github-copilot.ts +22 -0
  489. package/src/registry/gitlab-duo-workflow.ts +20 -0
  490. package/src/registry/gitlab-duo.ts +19 -0
  491. package/src/registry/gmi-cloud.ts +22 -0
  492. package/src/registry/google-antigravity.ts +22 -0
  493. package/src/registry/google-gemini-cli.ts +22 -0
  494. package/src/registry/google-vertex.ts +38 -0
  495. package/src/registry/google.ts +6 -0
  496. package/src/registry/groq.ts +6 -0
  497. package/src/registry/huggingface.ts +29 -0
  498. package/src/registry/index.ts +4 -0
  499. package/src/registry/kagi.ts +46 -0
  500. package/src/registry/kilo.ts +114 -0
  501. package/src/registry/kimi-code.ts +17 -0
  502. package/src/registry/litellm.ts +45 -0
  503. package/src/registry/llama-cpp.ts +35 -0
  504. package/src/registry/lm-studio.ts +31 -0
  505. package/src/registry/meta.ts +22 -0
  506. package/src/registry/minimax-code-cn.ts +12 -0
  507. package/src/registry/minimax-code.ts +12 -0
  508. package/src/registry/minimax.ts +6 -0
  509. package/src/registry/mistral.ts +6 -0
  510. package/src/registry/moonshot.ts +28 -0
  511. package/src/registry/nanogpt.ts +22 -0
  512. package/src/registry/novita.ts +25 -0
  513. package/src/registry/nvidia.ts +61 -0
  514. package/src/registry/oauth/anthropic-constants.ts +12 -0
  515. package/src/registry/oauth/anthropic.ts +346 -0
  516. package/src/registry/oauth/callback-server.ts +438 -0
  517. package/src/registry/oauth/cursor.ts +187 -0
  518. package/src/registry/oauth/device-code.ts +92 -0
  519. package/src/registry/oauth/devin.ts +124 -0
  520. package/src/registry/oauth/github-copilot.ts +369 -0
  521. package/src/registry/oauth/gitlab-duo-workflow.ts +146 -0
  522. package/src/registry/oauth/gitlab-duo.ts +222 -0
  523. package/src/registry/oauth/google-antigravity.ts +225 -0
  524. package/src/registry/oauth/google-gemini-cli.ts +297 -0
  525. package/src/registry/oauth/google-oauth-shared.ts +211 -0
  526. package/src/registry/oauth/index.ts +187 -0
  527. package/src/registry/oauth/kimi.ts +297 -0
  528. package/src/registry/oauth/minimax-code.ts +53 -0
  529. package/src/registry/oauth/oauth.html +317 -0
  530. package/src/registry/oauth/openai-codex.ts +384 -0
  531. package/src/registry/oauth/opencode.ts +50 -0
  532. package/src/registry/oauth/perplexity.ts +228 -0
  533. package/src/registry/oauth/pkce.ts +18 -0
  534. package/src/registry/oauth/types.ts +96 -0
  535. package/src/registry/oauth/wafer.ts +24 -0
  536. package/src/registry/oauth/xai-oauth.ts +559 -0
  537. package/src/registry/oauth/xiaomi.ts +211 -0
  538. package/src/registry/oauth/zai.ts +285 -0
  539. package/src/registry/ollama-cloud.ts +36 -0
  540. package/src/registry/ollama.ts +43 -0
  541. package/src/registry/openai-codex-device.ts +18 -0
  542. package/src/registry/openai-codex.ts +19 -0
  543. package/src/registry/openai.ts +6 -0
  544. package/src/registry/opencode-go.ts +12 -0
  545. package/src/registry/opencode-zen.ts +12 -0
  546. package/src/registry/openrouter.ts +28 -0
  547. package/src/registry/parallel.ts +45 -0
  548. package/src/registry/perplexity.ts +13 -0
  549. package/src/registry/qianfan.ts +27 -0
  550. package/src/registry/qwen-portal.ts +50 -0
  551. package/src/registry/registry.ts +182 -0
  552. package/src/registry/sakana.ts +22 -0
  553. package/src/registry/siliconflow-cn.ts +22 -0
  554. package/src/registry/siliconflow.ts +22 -0
  555. package/src/registry/synthetic.ts +21 -0
  556. package/src/registry/tavily.ts +45 -0
  557. package/src/registry/together.ts +22 -0
  558. package/src/registry/types.ts +86 -0
  559. package/src/registry/umans.ts +23 -0
  560. package/src/registry/venice.ts +33 -0
  561. package/src/registry/vercel-ai-gateway.ts +38 -0
  562. package/src/registry/vllm.ts +34 -0
  563. package/src/registry/wafer-serverless.ts +12 -0
  564. package/src/registry/xai-oauth.ts +17 -0
  565. package/src/registry/xai.ts +22 -0
  566. package/src/registry/xiaomi-token-plan-ams.ts +12 -0
  567. package/src/registry/xiaomi-token-plan-cn.ts +12 -0
  568. package/src/registry/xiaomi-token-plan-sgp.ts +12 -0
  569. package/src/registry/xiaomi.ts +12 -0
  570. package/src/registry/zai.ts +41 -0
  571. package/src/registry/zenmux.ts +22 -0
  572. package/src/registry/zhipu-coding-plan.ts +27 -0
  573. package/src/stream.ts +1944 -0
  574. package/src/types.ts +1243 -0
  575. package/src/usage/alibaba-token-plan.ts +230 -0
  576. package/src/usage/claude.ts +830 -0
  577. package/src/usage/cursor.ts +335 -0
  578. package/src/usage/gemini.ts +258 -0
  579. package/src/usage/github-copilot.ts +424 -0
  580. package/src/usage/google-antigravity.ts +497 -0
  581. package/src/usage/kimi.ts +277 -0
  582. package/src/usage/minimax-code.ts +291 -0
  583. package/src/usage/ollama.ts +41 -0
  584. package/src/usage/openai-codex-base-url.ts +35 -0
  585. package/src/usage/openai-codex-reset.ts +205 -0
  586. package/src/usage/openai-codex.ts +627 -0
  587. package/src/usage/opencode-go.ts +89 -0
  588. package/src/usage/shared.ts +10 -0
  589. package/src/usage/synthetic.ts +180 -0
  590. package/src/usage/umans.ts +192 -0
  591. package/src/usage/xai-oauth.ts +414 -0
  592. package/src/usage/zai.ts +370 -0
  593. package/src/usage.ts +411 -0
  594. package/src/utils/abort.ts +67 -0
  595. package/src/utils/anthropic-auth.ts +93 -0
  596. package/src/utils/aws-profile.ts +88 -0
  597. package/src/utils/block-symbols.ts +78 -0
  598. package/src/utils/deterministic-id.ts +20 -0
  599. package/src/utils/empty-completion-retry.ts +161 -0
  600. package/src/utils/event-stream.ts +202 -0
  601. package/src/utils/foundry.ts +8 -0
  602. package/src/utils/google-validation.ts +25 -0
  603. package/src/utils/harmony-leak.ts +500 -0
  604. package/src/utils/http-inspector.ts +196 -0
  605. package/src/utils/idle-iterator.ts +531 -0
  606. package/src/utils/leaked-thinking-stream.ts +483 -0
  607. package/src/utils/openai-http.ts +119 -0
  608. package/src/utils/openrouter-headers.ts +12 -0
  609. package/src/utils/parse-bind.ts +56 -0
  610. package/src/utils/provider-response.ts +30 -0
  611. package/src/utils/proxy.ts +314 -0
  612. package/src/utils/request-debug.ts +351 -0
  613. package/src/utils/retry-after.ts +121 -0
  614. package/src/utils/retry.ts +77 -0
  615. package/src/utils/schema/CONSTRAINTS.md +168 -0
  616. package/src/utils/schema/adapt.ts +36 -0
  617. package/src/utils/schema/compatibility.ts +435 -0
  618. package/src/utils/schema/dereference.ts +98 -0
  619. package/src/utils/schema/draft.ts +341 -0
  620. package/src/utils/schema/equality.ts +97 -0
  621. package/src/utils/schema/fields.ts +210 -0
  622. package/src/utils/schema/index.ts +14 -0
  623. package/src/utils/schema/json-schema-validator.ts +595 -0
  624. package/src/utils/schema/meta-validator.ts +167 -0
  625. package/src/utils/schema/normalize.ts +2314 -0
  626. package/src/utils/schema/spill.ts +43 -0
  627. package/src/utils/schema/stamps.ts +109 -0
  628. package/src/utils/schema/strict-tool-validation.ts +117 -0
  629. package/src/utils/schema/types.ts +10 -0
  630. package/src/utils/schema/typescript.ts +212 -0
  631. package/src/utils/schema/wire.ts +662 -0
  632. package/src/utils/sdk-stream-timeout.ts +43 -0
  633. package/src/utils/sse-debug.ts +18 -0
  634. package/src/utils/stream-markup-healing.ts +247 -0
  635. package/src/utils/thinking-loop.ts +552 -0
  636. package/src/utils/tool-call-loop-guard.ts +107 -0
  637. package/src/utils/tool-choice.ts +101 -0
  638. package/src/utils/validation.ts +1932 -0
  639. package/src/utils.ts +492 -0
@@ -0,0 +1,552 @@
1
+ /**
2
+ * Gemini thinking-loop guard.
3
+ *
4
+ * Gemini models (notably `gemini-3.5-flash` via OpenRouter) occasionally fall
5
+ * into a degenerate reasoning loop: they re-emit the same paragraph intent over
6
+ * and over with cosmetic wording drift ("Confirming Safety", "Verifying
7
+ * Completion", …), burning the entire output budget without ever calling a tool
8
+ * or answering. The runaway is *not* byte-identical, so a cheap verbatim
9
+ * tail-repeat check alone misses it.
10
+ *
11
+ * This guard watches the streamed `thinking` deltas and, on a match, terminates
12
+ * the stream with a synthetic `error` {@link AssistantMessage} that carries
13
+ * **no observable content**. An empty-content `stopReason: "error"` message tagged
14
+ * with `AIError.Flag.ThinkingLoop` lets result consumers and `AgentSession` discard
15
+ * the runaway and re-sample instead of committing garbage transcript.
16
+ *
17
+ * Three failure shapes are detected:
18
+ * 1. **Verbatim tail repetition** — a short unit repeated back-to-back (e.g.
19
+ * "🌊 🌊 🌊 …"). Caught from a rolling 250-char tail.
20
+ * 2. **Near-duplicate segments** — paragraphs that normalize to the same
21
+ * word-trigram fingerprint. Caught with a Jaccard window over recent
22
+ * paragraphs. Thresholds were calibrated on a real loop transcript plus
23
+ * 13.5k non-loop thinking blocks (zero false positives; hardest negative
24
+ * scored 3 against the trigger of 4).
25
+ * 3. **Progress-lexicon stall** — paragraphs that keep reshuffling the same
26
+ * motivational filler ("just doing it, pushing ahead, maintaining momentum")
27
+ * into fresh word order, so trigrams never match, yet introduce no new
28
+ * vocabulary and name nothing concrete. Caught by a run of low-novelty,
29
+ * anchor-free segments; a segment naming a path/identifier resets the run, so
30
+ * genuine but vocabulary-repetitive work (per-file templates) is spared.
31
+ *
32
+ * Scope is narrow: guarded Gemini/DeepSeek streams before any tool call. Native
33
+ * thinking is checked first; assistant text can also be checked for providers
34
+ * that surface reasoning as visible prose. On a hit the failed turn is emitted as
35
+ * an empty retryable stream-stall error; result-awaiting callers (`complete`,
36
+ * `completeSimple`) re-sample it a few times and then let a stubborn loop cook
37
+ * through one unguarded pass. Disable detection with `PI_NO_THINKING_LOOP_GUARD=1`.
38
+ */
39
+ import { logger } from "@linxiraos/pi-utils";
40
+ import * as AIError from "../error";
41
+ import type { Api, AssistantMessage, Model, StreamOptions } from "../types";
42
+ import { AssistantMessageEventStream } from "./event-stream";
43
+
44
+ /** Stable lead phrase of the guard's error message; exported for tests. The
45
+ * message also carries "stream stall" so the session + transport retry
46
+ * classifiers treat it as a transient (retryable) stop without bespoke rules. */
47
+ export const THINKING_LOOP_ERROR_MARKER = "Thinking loop detected";
48
+
49
+ /** Rolling tail (chars) inspected for verbatim back-to-back repetition. */
50
+ const VERBATIM_TAIL_WINDOW = 250;
51
+ /** Minimum total repeated chars before a verbatim run counts as a loop. */
52
+ const VERBATIM_MIN_REPEATED_CHARS = 180;
53
+ /** Longest unit length probed for a verbatim repeat. */
54
+ const VERBATIM_MAX_UNIT = 60;
55
+
56
+ /** Char cap for an unterminated segment; forces a flush so a wall-of-text loop
57
+ * (no blank lines / headings) still segments. */
58
+ const SEGMENT_CHAR_CAP = 700;
59
+ /** Normalized-length floor below which a segment is ignored (too short to be a
60
+ * meaningful paragraph; bare headings must not trip detection). */
61
+ const SEGMENT_MIN_NORM_CHARS = 60;
62
+ /** How many recent substantial segments are kept for similarity comparison. */
63
+ const SEGMENT_WINDOW = 16;
64
+ /** Word-trigram Jaccard at/above which two segments count as near-duplicates. */
65
+ const SEGMENT_SIMILARITY = 0.8;
66
+ /** Substantial segments required before detection may fire (warm-up). */
67
+ const SEGMENT_MIN_COUNT = 8;
68
+ /** Near-duplicate cluster size (current + matches) that trips the loop. */
69
+ const SEGMENT_MIN_CLUSTER = 4;
70
+
71
+ /** Recent segments whose pooled unigram vocabulary is the novelty baseline for
72
+ * progress-lexicon stall detection. */
73
+ const LEX_NOVELTY_WINDOW = 8;
74
+ /** Novelty (fraction of a segment's content words unseen across the recent
75
+ * window) at/below which a segment counts as recycling earlier wording.
76
+ * Calibrated against 536k real non-Gemini reasoning blocks: at 0.2 the longest
77
+ * low-information run any legitimate block reached was 7. */
78
+ const LEX_STALL_NOVELTY_FLOOR = 0.2;
79
+ /** Consecutive low-information segments that trip a progress-lexicon stall. Set
80
+ * to 8 (one above the worst legitimate run observed in the 536k-block corpus) so
81
+ * the heuristic stays clear of focused reasoning that briefly recycles wording;
82
+ * the real reasoning-summarizer loop sustains far longer runs (10+). */
83
+ const LEX_STALL_MIN_RUN = 8;
84
+
85
+ /** A concrete reference the model is actually reasoning about: a code span, a
86
+ * file extension / dotted member, a multi-segment path, or a snake/camel/Pascal
87
+ * identifier. A segment that introduces a NEW one resets the lexical-stall run —
88
+ * this spares genuine per-target work (per-file templates, focused single-symbol
89
+ * debugging) while still catching reworded filler that names nothing new ("just
90
+ * doing it, pushing ahead") or fixates on one unchanging reference. Excludes bare
91
+ * digits, abbreviations, and decimals (e.g. "Step 2", "i.e.", "1.2") so numbered
92
+ * or punctuated filler is not self-anchoring. Global flag: collected with
93
+ * matchAll, so never used with the stateful test(). */
94
+ const CONCRETE_ANCHOR =
95
+ /`[^`]+`|\b\w{2,}\.[a-zA-Z]\w{0,4}\b|[\w-]+(?:\/[\w-]+){2,}|\b\w+_\w+\b|\b[a-z]+[A-Z]\w*\b|\b[A-Z][a-z]+[A-Z]\w*\b/g;
96
+
97
+ const OPENAI_COMPAT_GUARDED_APIS: Partial<Record<Api, true>> = {
98
+ "openai-completions": true,
99
+ "openai-responses": true,
100
+ "azure-openai-responses": true,
101
+ "openai-codex-responses": true,
102
+ };
103
+
104
+ /**
105
+ * True when `model` is a Gemini model whose native thinking stream surfaces the
106
+ * "thought summary" titles this module's header guard counts.
107
+ *
108
+ * OpenAI-compat transports can serve Gemini under an arbitrary provider/id, so they
109
+ * carry the explicit `compat.enableGeminiThinkingLoopGuard` flag; direct Gemini
110
+ * transports carry a clearly shaped id/provider, so a string match is sufficient.
111
+ */
112
+ export function isGeminiThinkingModel(model: Model<Api>): boolean {
113
+ if (OPENAI_COMPAT_GUARDED_APIS[model.api]) {
114
+ const compat = model.compat as { enableGeminiThinkingLoopGuard?: boolean } | undefined;
115
+ return compat?.enableGeminiThinkingLoopGuard === true;
116
+ }
117
+ return /gemini/i.test(`${model.provider}/${model.id}`);
118
+ }
119
+
120
+ /**
121
+ * True when `model` should be guarded for thinking/response loops (Gemini & DeepSeek).
122
+ *
123
+ * OpenAI-compat transports can serve Gemini or DeepSeek under an arbitrary provider/id.
124
+ * Direct Gemini/DeepSeek transports carry a clearly shaped id/provider, so a string match
125
+ * is sufficient.
126
+ */
127
+ export function isLoopGuardedModel(model: Model<Api>, options?: StreamOptions): boolean {
128
+ if (options?.loopGuard?.enabled === false) return false;
129
+ const isDeepseek = /deepseek/i.test(`${model.provider}/${model.id}`);
130
+ return isGeminiThinkingModel(model) || isDeepseek;
131
+ }
132
+
133
+ /** @deprecated Use isLoopGuardedModel instead. */
134
+ export function isGeminiThinkingLoopModel(model: Model<Api>): boolean {
135
+ return isLoopGuardedModel(model);
136
+ }
137
+
138
+ /**
139
+ * Stateful detector fed the streamed thinking deltas. `push` returns a
140
+ * human-readable reason the first time a loop shape is recognized; the caller
141
+ * is responsible for stopping after the first hit.
142
+ */
143
+ export class ThinkingLoopDetector {
144
+ /** Rolling char tail for verbatim repeat detection. */
145
+ #tail = "";
146
+ /** Pending thinking text not yet split into completed segments. */
147
+ #pending = "";
148
+ /** Fingerprints of the most recent substantial segments (≤ SEGMENT_WINDOW). */
149
+ #window: Set<string>[] = [];
150
+ /** Count of substantial segments seen so far (warm-up gate). */
151
+ #count = 0;
152
+ /** Unigram word sets of the most recent segments (≤ LEX_NOVELTY_WINDOW); the
153
+ * novelty baseline for progress-lexicon stall detection. */
154
+ #wordWindow: Set<string>[] = [];
155
+ /** Consecutive low-information (low-novelty, anchor-free) segments seen. */
156
+ #lexStallRun = 0;
157
+ /** Concrete anchors seen per recent segment (≤ LEX_NOVELTY_WINDOW). A stall is
158
+ * only broken by a *new* reference, so filler repeating one fixed
159
+ * path/identifier every paragraph is still caught. */
160
+ #anchorWindow: Set<string>[] = [];
161
+
162
+ push(delta: string): string | null {
163
+ if (!delta) return null;
164
+
165
+ // 1. Verbatim back-to-back repetition over the rolling tail.
166
+ this.#tail += delta;
167
+ if (this.#tail.length > VERBATIM_TAIL_WINDOW) this.#tail = this.#tail.slice(-VERBATIM_TAIL_WINDOW);
168
+ const verbatim = detectVerbatimRepetition(this.#tail);
169
+ if (verbatim) {
170
+ const [unit, times] = verbatim;
171
+ return `repeated "${unit.trim()}" ${times}× back-to-back`;
172
+ }
173
+
174
+ // 2. Near-duplicate paragraph loop. Append, then drain completed segments.
175
+ this.#pending += delta;
176
+ while (true) {
177
+ const boundary = /\n\s*\n/.exec(this.#pending);
178
+ let raw: string;
179
+ if (boundary) {
180
+ raw = this.#pending.slice(0, boundary.index);
181
+ this.#pending = this.#pending.slice(boundary.index + boundary[0].length);
182
+ } else if (this.#pending.length > SEGMENT_CHAR_CAP) {
183
+ // No boundary yet but the segment is runaway-long: force a flush.
184
+ raw = this.#pending.slice(0, SEGMENT_CHAR_CAP);
185
+ this.#pending = this.#pending.slice(SEGMENT_CHAR_CAP);
186
+ } else {
187
+ return null;
188
+ }
189
+ // An over-long segment is chunked so each piece stays comparable.
190
+ for (let rest = raw; rest.length > 0; ) {
191
+ const chunk = rest.length > SEGMENT_CHAR_CAP ? rest.slice(0, SEGMENT_CHAR_CAP) : rest;
192
+ rest = rest.slice(chunk.length);
193
+ const hit = this.#consumeSegment(chunk);
194
+ if (hit) return hit;
195
+ }
196
+ }
197
+ }
198
+
199
+ /** Process the buffered trailing paragraph (one with no blank-line / heading
200
+ * terminator). Called when the thinking block ends so the final segment —
201
+ * which may be the one that completes a duplicate cluster — is not dropped. */
202
+ flush(): string | null {
203
+ if (!this.#pending) return null;
204
+ let rest = this.#pending;
205
+ this.#pending = "";
206
+ while (rest.length > 0) {
207
+ const chunk = rest.length > SEGMENT_CHAR_CAP ? rest.slice(0, SEGMENT_CHAR_CAP) : rest;
208
+ rest = rest.slice(chunk.length);
209
+ const hit = this.#consumeSegment(chunk);
210
+ if (hit) return hit;
211
+ }
212
+ return null;
213
+ }
214
+
215
+ #consumeSegment(raw: string): string | null {
216
+ // Reasoning-summarizer titles ("**Maintaining Momentum**", "## Heading")
217
+ // are per-thought formatting, not chain-of-thought; their ever-changing
218
+ // wording would otherwise mask a loop by inflating novelty. Strip them
219
+ // before analysis (a title-only segment then falls below the length gate).
220
+ const segment = raw.replace(/^[ \t]*#{1,6}[ \t].*$/gm, "").replace(/^[ \t]*\*{2,3}.+?\*{2,3}[ \t]*$/gm, "");
221
+ const normalized = normalizeSegment(segment);
222
+ if (normalized.length < SEGMENT_MIN_NORM_CHARS) return null;
223
+
224
+ // (a) Near-duplicate trigram cluster: the same paragraph reused with
225
+ // cosmetic wording drift (high word-trigram overlap).
226
+ const fingerprint = trigramShingles(normalized);
227
+ let cluster = 1;
228
+ for (const prev of this.#window) {
229
+ if (jaccard(fingerprint, prev) >= SEGMENT_SIMILARITY) cluster++;
230
+ }
231
+
232
+ // (b) Progress-lexicon stall: paragraphs that recycle the recent
233
+ // vocabulary (low novelty) and add no *new* concrete reference — reworded
234
+ // filler that burns budget without advancing. The trigram check above
235
+ // already claims high-overlap near-duplicates; this catches the
236
+ // low-overlap, reshuffled-wording shape it misses. Requiring a NEW anchor
237
+ // (not merely any anchor) still catches filler that name-drops one fixed
238
+ // path/identifier every paragraph, while sparing genuine per-target work
239
+ // that names a fresh file/symbol each time.
240
+ const words = new Set<string>(normalized.split(" ").filter(Boolean));
241
+ const priorVocab = new Set<string>();
242
+ for (const set of this.#wordWindow) for (const w of set) priorVocab.add(w);
243
+ let unseen = 0;
244
+ for (const w of words) if (!priorVocab.has(w)) unseen++;
245
+ const novelty = priorVocab.size === 0 ? 1 : unseen / words.size;
246
+
247
+ const anchors = new Set<string>();
248
+ // Canonicalize so the same reference written as `Foo`, Foo, or FOO is one
249
+ // anchor and cannot masquerade as "new" to keep a fixed-reference stall alive.
250
+ for (const match of segment.matchAll(CONCRETE_ANCHOR)) anchors.add(match[0].replace(/`/g, "").toLowerCase());
251
+ let newAnchor = false;
252
+ for (const anchor of anchors) {
253
+ if (this.#anchorWindow.every(seen => !seen.has(anchor))) {
254
+ newAnchor = true;
255
+ break;
256
+ }
257
+ }
258
+
259
+ if (novelty <= LEX_STALL_NOVELTY_FLOOR && !newAnchor) {
260
+ this.#lexStallRun++;
261
+ } else {
262
+ this.#lexStallRun = 0;
263
+ }
264
+
265
+ this.#window.push(fingerprint);
266
+ if (this.#window.length > SEGMENT_WINDOW) this.#window.shift();
267
+ this.#wordWindow.push(words);
268
+ if (this.#wordWindow.length > LEX_NOVELTY_WINDOW) this.#wordWindow.shift();
269
+ this.#anchorWindow.push(anchors);
270
+ if (this.#anchorWindow.length > LEX_NOVELTY_WINDOW) this.#anchorWindow.shift();
271
+ this.#count++;
272
+
273
+ if (this.#count >= SEGMENT_MIN_COUNT) {
274
+ if (cluster >= SEGMENT_MIN_CLUSTER) {
275
+ return `${cluster} near-identical segments within the last ${SEGMENT_WINDOW}`;
276
+ }
277
+ if (this.#lexStallRun >= LEX_STALL_MIN_RUN) {
278
+ return `${this.#lexStallRun} low-information segments recycling recent wording`;
279
+ }
280
+ }
281
+ return null;
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Consecutive Gemini thought-summary headers in one uninterrupted reasoning
287
+ * stream that trips the tool-call reminder. Gemini occasionally narrates a long
288
+ * chain of titled summaries ("Examining Result Handling", "Refining Result
289
+ * Rendering", …) without ever calling a tool, burning the whole budget on
290
+ * planning. This is the over-planning shape {@link ThinkingLoopDetector} misses —
291
+ * those titles are stripped before its similarity analysis precisely because their
292
+ * wording keeps changing, so a genuinely-distinct planning runaway never trips it.
293
+ *
294
+ * Set well above legitimate hard-problem depth: a capable model can emit ~10
295
+ * distinct, progressing hypotheses in a single reasoning block before acting (and
296
+ * a false trip is costly — the interrupt discards the whole reasoning turn). A
297
+ * real narration runaway burns dozens-to-hundreds of titles, so this still trips
298
+ * fast on the actual pathology.
299
+ */
300
+ export const GEMINI_HEADER_RUNAWAY_THRESHOLD = 24;
301
+
302
+ /**
303
+ * True when a single trimmed line is a Gemini reasoning-summary title: a markdown
304
+ * ATX heading (`## …`) or a whole-line bold / bold-italic run (`**Title**`,
305
+ * `***Title***`). Inline emphasis inside prose never matches — the bold run must
306
+ * span the entire line. Mirrors the title shapes {@link ThinkingLoopDetector}
307
+ * strips before similarity analysis.
308
+ */
309
+ export function isReasoningSummaryHeader(line: string): boolean {
310
+ return /^#{1,6}[ \t]+\S/.test(line) || /^\*{2,3}.+\*{2,3}$/.test(line);
311
+ }
312
+
313
+ /**
314
+ * Counts consecutive Gemini reasoning-summary headers across a streamed thinking
315
+ * block. {@link push} returns true exactly once — when the running header count
316
+ * first reaches {@link GEMINI_HEADER_RUNAWAY_THRESHOLD} — and the caller then
317
+ * interrupts the stream and reminds the model to issue a tool call. Paragraph
318
+ * lines between titles do NOT reset the run (Gemini emits header + paragraph per
319
+ * thought, so the run IS the number of summaries); leaving the reasoning channel
320
+ * does, via {@link reset} on a new thinking block / prose / tool call.
321
+ */
322
+ export class GeminiHeaderRunDetector {
323
+ /** Thinking text not yet split into completed lines. */
324
+ #pending = "";
325
+ /** Summary-title lines seen in the current run. */
326
+ #count = 0;
327
+ /** Latches after the first threshold hit so each run fires at most once. */
328
+ #fired = false;
329
+
330
+ /** Feed a thinking delta. Returns true the first time the run hits the threshold. */
331
+ push(delta: string): boolean {
332
+ if (this.#fired || !delta) return false;
333
+ this.#pending += delta;
334
+ let nl = this.#pending.indexOf("\n");
335
+ while (nl !== -1) {
336
+ const line = this.#pending.slice(0, nl).trim();
337
+ this.#pending = this.#pending.slice(nl + 1);
338
+ if (line !== "" && isReasoningSummaryHeader(line) && ++this.#count >= GEMINI_HEADER_RUNAWAY_THRESHOLD) {
339
+ this.#fired = true;
340
+ return true;
341
+ }
342
+ nl = this.#pending.indexOf("\n");
343
+ }
344
+ return false;
345
+ }
346
+
347
+ /** Number of summary titles counted in the current run (for the reminder/log). */
348
+ get count(): number {
349
+ return this.#count;
350
+ }
351
+
352
+ /** Re-arm for a fresh reasoning block: clears the buffer, count, and latch. */
353
+ reset(): void {
354
+ this.#pending = "";
355
+ this.#count = 0;
356
+ this.#fired = false;
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Wrap a provider stream with the loop guard. `controller` is the guard's own
362
+ * abort handle: aborting it (after wiring it into the provider's signal via
363
+ * {@link withGeminiThinkingLoopGuard}) tears down the upstream once a loop
364
+ * trips.
365
+ */
366
+ export function guardThinkingLoopStream(
367
+ inner: AssistantMessageEventStream,
368
+ model: Model<Api>,
369
+ controller: AbortController,
370
+ options?: StreamOptions,
371
+ ): AssistantMessageEventStream {
372
+ const outer = new AssistantMessageEventStream();
373
+ const thinkingDetector = new ThinkingLoopDetector();
374
+ const textDetector = new ThinkingLoopDetector();
375
+ const checkAssistantContent = options?.loopGuard?.checkAssistantContent !== false;
376
+
377
+ void (async () => {
378
+ let thinkingArmed = true;
379
+ let textArmed = checkAssistantContent;
380
+ try {
381
+ for await (const event of inner) {
382
+ let detail: string | null = null;
383
+ if (thinkingArmed && event.type === "thinking_delta") {
384
+ detail = thinkingDetector.push(event.delta);
385
+ } else if (thinkingArmed && event.type === "thinking_end") {
386
+ detail = thinkingDetector.flush();
387
+ thinkingArmed = false;
388
+ } else if (event.type === "text_start" || event.type === "text_delta") {
389
+ thinkingArmed = false;
390
+ if (textArmed && event.type === "text_delta") {
391
+ detail = textDetector.push(event.delta);
392
+ }
393
+ } else if (event.type === "toolcall_start" || event.type === "toolcall_delta") {
394
+ thinkingArmed = false;
395
+ textArmed = false;
396
+ } else if (event.type === "done") {
397
+ if (thinkingArmed) {
398
+ detail = thinkingDetector.flush();
399
+ }
400
+ if (textArmed) {
401
+ detail = detail || textDetector.flush();
402
+ }
403
+ }
404
+ if (detail) {
405
+ logger.warn("Thinking loop detected; aborting stream for retry.", {
406
+ model: model.id,
407
+ provider: model.provider,
408
+ detail,
409
+ });
410
+ controller.abort(
411
+ AIError.attach(new Error(THINKING_LOOP_ERROR_MARKER), AIError.create(AIError.Flag.ThinkingLoop)),
412
+ );
413
+ outer.push({
414
+ type: "error",
415
+ reason: "error",
416
+ error: buildThinkingLoopError(model, detail),
417
+ });
418
+ return;
419
+ }
420
+ outer.push(event);
421
+ if (outer.done) return;
422
+ }
423
+ if (!outer.done) {
424
+ try {
425
+ outer.end(await inner.result());
426
+ } catch (err) {
427
+ outer.fail(err);
428
+ }
429
+ }
430
+ } catch (err) {
431
+ if (!outer.done) outer.fail(err);
432
+ }
433
+ })();
434
+
435
+ return outer;
436
+ }
437
+
438
+ /**
439
+ * Apply the loop guard around a provider dispatch. For non-guarded models
440
+ * (or when disabled) this is a transparent pass-through. For guarded models it injects a
441
+ * guard abort signal into the provider call so a detected loop tears down the
442
+ * upstream, then wraps the returned stream. The guard only raises the retryable
443
+ * stall; bounding the re-samples and the final cook pass lives in the
444
+ * result-awaiting caller.
445
+ */
446
+ export function withGeminiThinkingLoopGuard<
447
+ O extends { signal?: AbortSignal; loopGuard?: { enabled?: boolean; checkAssistantContent?: boolean } },
448
+ >(
449
+ model: Model<Api>,
450
+ options: O | undefined,
451
+ dispatch: (options: O | undefined) => AssistantMessageEventStream,
452
+ ): AssistantMessageEventStream {
453
+ if (process.env.PI_NO_THINKING_LOOP_GUARD === "1" || !isLoopGuardedModel(model, options)) {
454
+ return dispatch(options);
455
+ }
456
+ const controller = new AbortController();
457
+ const caller = options?.signal;
458
+ const signal = caller ? AbortSignal.any([caller, controller.signal]) : controller.signal;
459
+ const merged = { ...(options ?? {}), signal } as O;
460
+ return guardThinkingLoopStream(dispatch(merged), model, controller, options);
461
+ }
462
+
463
+ function buildThinkingLoopError(model: Model<Api>, detail: string): AssistantMessage {
464
+ return {
465
+ role: "assistant",
466
+ // Empty content is load-bearing: loop-guard output is replay garbage, even
467
+ // when it arrived as assistant text instead of native thinking. Keeping it
468
+ // would persist the failed attempt before AgentSession retries.
469
+ content: [],
470
+ api: model.api,
471
+ provider: model.provider,
472
+ model: model.id,
473
+ usage: {
474
+ input: 0,
475
+ output: 0,
476
+ cacheRead: 0,
477
+ cacheWrite: 0,
478
+ totalTokens: 0,
479
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
480
+ },
481
+ stopReason: "error",
482
+ // "stream stall" makes the transport/session retry classifiers treat this
483
+ // as a transient (retryable) failure with no bespoke rule.
484
+ errorMessage: `${THINKING_LOOP_ERROR_MARKER}: the model repeated near-identical content (${detail}). Treating as a stream stall and retrying.`,
485
+ errorId: AIError.create(AIError.Flag.ThinkingLoop),
486
+ timestamp: Date.now(),
487
+ };
488
+ }
489
+
490
+ /**
491
+ * Detect a short unit repeated back-to-back at the tail (verbatim loop). Only a
492
+ * unit carrying a letter or pictographic emoji counts — runs of digits,
493
+ * whitespace or punctuation are legitimate in tabular / hex / numeric output.
494
+ */
495
+ function detectVerbatimRepetition(text: string): [unit: string, count: number] | null {
496
+ if (text.length < VERBATIM_MIN_REPEATED_CHARS) return null;
497
+ const windowSize = Math.min(text.length, VERBATIM_TAIL_WINDOW);
498
+ const searchSpace = text.slice(-windowSize);
499
+
500
+ for (let len = 2; len <= VERBATIM_MAX_UNIT; len++) {
501
+ if (searchSpace.length < len * 4) continue;
502
+ const unit = searchSpace.slice(-len);
503
+ if (!/[\p{L}\p{Extended_Pictographic}]/u.test(unit)) continue;
504
+
505
+ let count = 0;
506
+ let pos = searchSpace.length;
507
+ while (pos >= len) {
508
+ if (searchSpace.slice(pos - len, pos) === unit) {
509
+ count++;
510
+ pos -= len;
511
+ } else {
512
+ break;
513
+ }
514
+ }
515
+ if (count >= 4 && len * count >= VERBATIM_MIN_REPEATED_CHARS) return [unit, count];
516
+ }
517
+ return null;
518
+ }
519
+
520
+ /** Lowercase and tokenize prose plus code/path payloads, dropping pure numbers. */
521
+ function normalizeSegment(segment: string): string {
522
+ return segment
523
+ .toLowerCase()
524
+ .replace(/`([^`]*)`/g, " $1 ")
525
+ .replace(/[^a-z0-9]+/g, " ")
526
+ .split(/\s+/)
527
+ .filter(token => /[a-z]/.test(token))
528
+ .join(" ")
529
+ .trim();
530
+ }
531
+
532
+ /** Word-trigram shingle set of a normalized segment. */
533
+ function trigramShingles(normalized: string): Set<string> {
534
+ const words = normalized.split(" ").filter(Boolean);
535
+ if (words.length < 3) return new Set(words.length > 0 ? [words.join(" ")] : []);
536
+ const shingles = new Set<string>();
537
+ for (let i = 0; i + 3 <= words.length; i++) {
538
+ shingles.add(`${words[i]} ${words[i + 1]} ${words[i + 2]}`);
539
+ }
540
+ return shingles;
541
+ }
542
+
543
+ function jaccard(a: Set<string>, b: Set<string>): number {
544
+ if (a.size === 0 || b.size === 0) return 0;
545
+ const [small, large] = a.size < b.size ? [a, b] : [b, a];
546
+ let intersection = 0;
547
+ for (const x of small) {
548
+ if (large.has(x)) intersection++;
549
+ }
550
+ const union = a.size + b.size - intersection;
551
+ return union === 0 ? 0 : intersection / union;
552
+ }
@@ -0,0 +1,107 @@
1
+ import { INTENT_FIELD } from "@linxiraos/pi-wire";
2
+ import type { AssistantMessage, ToolCall, ToolResultMessage } from "../types";
3
+
4
+ const LEGACY_INTENT_FIELD = "__intent";
5
+ const RESULT_SUMMARY_LIMIT = 200;
6
+ const ARGUMENT_SUMMARY_LIMIT = 400;
7
+
8
+ /** Runtime settings for cross-turn tool-call repetition detection. */
9
+ export interface ToolCallLoopGuardOptions {
10
+ readonly threshold: number;
11
+ readonly exemptTools: readonly string[];
12
+ }
13
+
14
+ /** A completed assistant turn plus the tool results it produced. */
15
+ export interface ToolCallLoopTurn {
16
+ readonly message: AssistantMessage;
17
+ readonly toolResults: readonly ToolResultMessage[];
18
+ }
19
+
20
+ /** Details needed to steer the model away from a repeated tool call. */
21
+ export interface RepeatedToolCallDetection {
22
+ readonly kind: "repeated_tool_call";
23
+ readonly toolName: string;
24
+ readonly count: number;
25
+ readonly resultSummary: string;
26
+ readonly argumentsSummary: string;
27
+ }
28
+
29
+ function canonicalizeToolCallValue(value: unknown): unknown {
30
+ if (Array.isArray(value)) {
31
+ return value.map(item => canonicalizeToolCallValue(item));
32
+ }
33
+ if (!value || typeof value !== "object") {
34
+ return value;
35
+ }
36
+
37
+ const input = value as Record<string, unknown>;
38
+ const output: Record<string, unknown> = {};
39
+ for (const key of Object.keys(input).sort()) {
40
+ if (key === INTENT_FIELD || key === LEGACY_INTENT_FIELD) continue;
41
+ output[key] = canonicalizeToolCallValue(input[key]);
42
+ }
43
+ return output;
44
+ }
45
+
46
+ function summarizeText(text: string, limit: number): string {
47
+ let summary = text.replace(/\s+/g, " ").trim();
48
+ if (summary.length > limit) {
49
+ summary = `${summary.slice(0, limit)}…`;
50
+ }
51
+ return summary;
52
+ }
53
+
54
+ function summarizeToolResult(toolResults: readonly ToolResultMessage[], toolCallId: string): string {
55
+ const result = toolResults.find(candidate => candidate.toolCallId === toolCallId);
56
+ if (!result) return "";
57
+
58
+ const textParts: string[] = [];
59
+ for (const block of result.content) {
60
+ if (block.type === "text") {
61
+ textParts.push(block.text);
62
+ }
63
+ }
64
+ return summarizeText(textParts.join("\n"), RESULT_SUMMARY_LIMIT);
65
+ }
66
+
67
+ /** Detects consecutive identical assistant tool calls across model turns. */
68
+ export class ToolCallLoopGuard {
69
+ #threshold: number;
70
+ #exemptTools: ReadonlySet<string>;
71
+ #lastHash: string | undefined;
72
+ #count = 0;
73
+
74
+ constructor(options: ToolCallLoopGuardOptions) {
75
+ this.#threshold = Math.max(1, Math.trunc(options.threshold));
76
+ this.#exemptTools = new Set(options.exemptTools);
77
+ }
78
+
79
+ /** Records one completed turn and returns the threshold hit, if any. */
80
+ recordTurn(turn: ToolCallLoopTurn): RepeatedToolCallDetection | null {
81
+ const toolCalls = turn.message.content.filter((part): part is ToolCall => part.type === "toolCall");
82
+ if (toolCalls.length !== 1 || this.#exemptTools.has(toolCalls[0]!.name)) {
83
+ this.#lastHash = undefined;
84
+ this.#count = 0;
85
+ return null;
86
+ }
87
+
88
+ const toolCall = toolCalls[0]!;
89
+ const canonicalArgs = JSON.stringify(canonicalizeToolCallValue(toolCall.arguments));
90
+ const hash = `${toolCall.name}:${canonicalArgs}`;
91
+ if (hash === this.#lastHash) {
92
+ this.#count++;
93
+ } else {
94
+ this.#lastHash = hash;
95
+ this.#count = 1;
96
+ }
97
+
98
+ if (this.#count !== this.#threshold) return null;
99
+ return {
100
+ kind: "repeated_tool_call",
101
+ toolName: toolCall.name,
102
+ count: this.#count,
103
+ resultSummary: summarizeToolResult(turn.toolResults, toolCall.id),
104
+ argumentsSummary: summarizeText(canonicalArgs, ARGUMENT_SUMMARY_LIMIT),
105
+ };
106
+ }
107
+ }