@zwbigi/ink-xy 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (598) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +2 -2
  3. package/.next/build-manifest.json +2 -2
  4. package/.next/server/app/_global-error.html +1 -1
  5. package/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  7. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  8. package/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  9. package/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  10. package/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  12. package/.next/server/app/_not-found.html +1 -1
  13. package/.next/server/app/_not-found.rsc +1 -1
  14. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  15. package/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  16. package/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  17. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  20. package/.next/server/app/api/inkos/route.js.nft.json +1 -1
  21. package/.next/server/app/api/skills/install/route.js.nft.json +1 -1
  22. package/.next/server/app/api/skills/search/route.js.nft.json +1 -1
  23. package/.next/server/app/index.html +1 -1
  24. package/.next/server/app/index.rsc +1 -1
  25. package/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  26. package/.next/server/app/index.segments/_full.segment.rsc +1 -1
  27. package/.next/server/app/index.segments/_head.segment.rsc +1 -1
  28. package/.next/server/app/index.segments/_index.segment.rsc +1 -1
  29. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  30. package/.next/server/app-paths-manifest.json +2 -2
  31. package/.next/server/chunks/162.js +1 -1
  32. package/.next/server/middleware-build-manifest.js +1 -1
  33. package/.next/server/pages/404.html +1 -1
  34. package/.next/server/pages/500.html +1 -1
  35. package/.next/trace +4 -4
  36. package/.next/trace-build +1 -1
  37. package/bin/pi-web.js +4 -1
  38. package/inkos/.env.example +20 -0
  39. package/inkos/.node-version +1 -0
  40. package/inkos/.nvmrc +1 -0
  41. package/inkos/CHANGELOG.md +787 -0
  42. package/inkos/CONTRIBUTING.md +89 -0
  43. package/inkos/LICENSE +661 -0
  44. package/inkos/README.en.md +483 -0
  45. package/inkos/README.ja.md +461 -0
  46. package/inkos/README.md +272 -0
  47. package/inkos/assets/15qun.jpg +0 -0
  48. package/inkos/assets/41777702961_.pic.jpg +0 -0
  49. package/inkos/assets/inkos-short-demo-cover.png +0 -0
  50. package/inkos/assets/inkos-text.svg +40 -0
  51. package/inkos/assets/logo.svg +47 -0
  52. package/inkos/assets/screenshot-chapters.png +0 -0
  53. package/inkos/assets/screenshot-pipeline.png +0 -0
  54. package/inkos/assets/screenshot-state.png +0 -0
  55. package/inkos/assets/screenshot-terminal.png +0 -0
  56. package/inkos/assets/wechat-group-v8.jpg +0 -0
  57. package/inkos/package.json +42 -0
  58. package/inkos/packages/cli/package.json +74 -0
  59. package/inkos/packages/cli/src/__tests__/analytics.test.ts +154 -0
  60. package/inkos/packages/cli/src/__tests__/cli-integration.test.ts +1031 -0
  61. package/inkos/packages/cli/src/__tests__/daemon.test.ts +93 -0
  62. package/inkos/packages/cli/src/__tests__/doctor.test.ts +36 -0
  63. package/inkos/packages/cli/src/__tests__/interact-command.test.ts +142 -0
  64. package/inkos/packages/cli/src/__tests__/interaction-tools.test.ts +107 -0
  65. package/inkos/packages/cli/src/__tests__/llm-overrides.test.ts +25 -0
  66. package/inkos/packages/cli/src/__tests__/localization.test.ts +121 -0
  67. package/inkos/packages/cli/src/__tests__/progress-text.test.ts +92 -0
  68. package/inkos/packages/cli/src/__tests__/project-bootstrap.test.ts +71 -0
  69. package/inkos/packages/cli/src/__tests__/publish-package.test.ts +272 -0
  70. package/inkos/packages/cli/src/__tests__/revision-command.test.ts +82 -0
  71. package/inkos/packages/cli/src/__tests__/runtime-requirements.test.ts +89 -0
  72. package/inkos/packages/cli/src/__tests__/short-fiction-command.test.ts +48 -0
  73. package/inkos/packages/cli/src/__tests__/studio-runtime.test.ts +142 -0
  74. package/inkos/packages/cli/src/__tests__/studio.test.ts +87 -0
  75. package/inkos/packages/cli/src/__tests__/tui-activity-state.test.ts +20 -0
  76. package/inkos/packages/cli/src/__tests__/tui-agent-session.test.ts +213 -0
  77. package/inkos/packages/cli/src/__tests__/tui-chat-depth.test.ts +27 -0
  78. package/inkos/packages/cli/src/__tests__/tui-chat-draft.test.ts +44 -0
  79. package/inkos/packages/cli/src/__tests__/tui-command.test.ts +86 -0
  80. package/inkos/packages/cli/src/__tests__/tui-composer-caret.test.ts +46 -0
  81. package/inkos/packages/cli/src/__tests__/tui-composer-display.test.ts +40 -0
  82. package/inkos/packages/cli/src/__tests__/tui-dashboard.test.tsx +219 -0
  83. package/inkos/packages/cli/src/__tests__/tui-effects-i18n.test.ts +29 -0
  84. package/inkos/packages/cli/src/__tests__/tui-i18n.test.ts +22 -0
  85. package/inkos/packages/cli/src/__tests__/tui-input-chrome.test.ts +10 -0
  86. package/inkos/packages/cli/src/__tests__/tui-input-history.test.ts +40 -0
  87. package/inkos/packages/cli/src/__tests__/tui-layout.test.ts +55 -0
  88. package/inkos/packages/cli/src/__tests__/tui-local-commands.test.ts +47 -0
  89. package/inkos/packages/cli/src/__tests__/tui-session-store.test.ts +81 -0
  90. package/inkos/packages/cli/src/__tests__/tui-setup-i18n.test.ts +31 -0
  91. package/inkos/packages/cli/src/__tests__/tui-slash-autocomplete.test.ts +33 -0
  92. package/inkos/packages/cli/src/commands/agent.ts +65 -0
  93. package/inkos/packages/cli/src/commands/analytics.ts +77 -0
  94. package/inkos/packages/cli/src/commands/audit.ts +52 -0
  95. package/inkos/packages/cli/src/commands/book.ts +260 -0
  96. package/inkos/packages/cli/src/commands/compose.ts +50 -0
  97. package/inkos/packages/cli/src/commands/config.ts +328 -0
  98. package/inkos/packages/cli/src/commands/consolidate.ts +50 -0
  99. package/inkos/packages/cli/src/commands/daemon.ts +121 -0
  100. package/inkos/packages/cli/src/commands/detect.ts +125 -0
  101. package/inkos/packages/cli/src/commands/doctor.ts +391 -0
  102. package/inkos/packages/cli/src/commands/draft.ts +43 -0
  103. package/inkos/packages/cli/src/commands/eval.ts +217 -0
  104. package/inkos/packages/cli/src/commands/export.ts +45 -0
  105. package/inkos/packages/cli/src/commands/fanfic.ts +183 -0
  106. package/inkos/packages/cli/src/commands/genre.ts +160 -0
  107. package/inkos/packages/cli/src/commands/import.ts +158 -0
  108. package/inkos/packages/cli/src/commands/init.ts +47 -0
  109. package/inkos/packages/cli/src/commands/interact.ts +109 -0
  110. package/inkos/packages/cli/src/commands/plan.ts +54 -0
  111. package/inkos/packages/cli/src/commands/radar.ts +60 -0
  112. package/inkos/packages/cli/src/commands/review.ts +253 -0
  113. package/inkos/packages/cli/src/commands/revise.ts +58 -0
  114. package/inkos/packages/cli/src/commands/short-fiction.ts +294 -0
  115. package/inkos/packages/cli/src/commands/status.ts +138 -0
  116. package/inkos/packages/cli/src/commands/studio.ts +194 -0
  117. package/inkos/packages/cli/src/commands/style.ts +99 -0
  118. package/inkos/packages/cli/src/commands/tui.ts +18 -0
  119. package/inkos/packages/cli/src/commands/update.ts +45 -0
  120. package/inkos/packages/cli/src/commands/write.ts +324 -0
  121. package/inkos/packages/cli/src/index.ts +5 -0
  122. package/inkos/packages/cli/src/interaction/tools.ts +49 -0
  123. package/inkos/packages/cli/src/localization.ts +215 -0
  124. package/inkos/packages/cli/src/program.ts +106 -0
  125. package/inkos/packages/cli/src/progress-text.ts +85 -0
  126. package/inkos/packages/cli/src/project-bootstrap.ts +175 -0
  127. package/inkos/packages/cli/src/runtime-requirements.ts +135 -0
  128. package/inkos/packages/cli/src/tui/__tests__/markdown.test.ts +64 -0
  129. package/inkos/packages/cli/src/tui/activity-state.ts +41 -0
  130. package/inkos/packages/cli/src/tui/agent-input.ts +264 -0
  131. package/inkos/packages/cli/src/tui/ansi.ts +72 -0
  132. package/inkos/packages/cli/src/tui/app.ts +130 -0
  133. package/inkos/packages/cli/src/tui/chat-depth.ts +22 -0
  134. package/inkos/packages/cli/src/tui/chat-draft.ts +42 -0
  135. package/inkos/packages/cli/src/tui/composer-caret.ts +22 -0
  136. package/inkos/packages/cli/src/tui/composer-display.ts +26 -0
  137. package/inkos/packages/cli/src/tui/dashboard-model.ts +164 -0
  138. package/inkos/packages/cli/src/tui/dashboard.tsx +544 -0
  139. package/inkos/packages/cli/src/tui/effects.ts +542 -0
  140. package/inkos/packages/cli/src/tui/i18n.ts +278 -0
  141. package/inkos/packages/cli/src/tui/input-history.ts +69 -0
  142. package/inkos/packages/cli/src/tui/local-commands.ts +55 -0
  143. package/inkos/packages/cli/src/tui/markdown.ts +64 -0
  144. package/inkos/packages/cli/src/tui/session-store.ts +6 -0
  145. package/inkos/packages/cli/src/tui/setup.ts +397 -0
  146. package/inkos/packages/cli/src/tui/slash-autocomplete.ts +62 -0
  147. package/inkos/packages/cli/src/tui/theme.ts +17 -0
  148. package/inkos/packages/cli/src/utils.ts +222 -0
  149. package/inkos/packages/cli/tsconfig.json +9 -0
  150. package/inkos/packages/core/genres/cozy.md +43 -0
  151. package/inkos/packages/core/genres/cultivation.md +42 -0
  152. package/inkos/packages/core/genres/dungeon-core.md +40 -0
  153. package/inkos/packages/core/genres/horror.md +51 -0
  154. package/inkos/packages/core/genres/isekai.md +43 -0
  155. package/inkos/packages/core/genres/litrpg.md +43 -0
  156. package/inkos/packages/core/genres/other.md +24 -0
  157. package/inkos/packages/core/genres/progression.md +41 -0
  158. package/inkos/packages/core/genres/romantasy.md +45 -0
  159. package/inkos/packages/core/genres/sci-fi.md +42 -0
  160. package/inkos/packages/core/genres/system-apocalypse.md +40 -0
  161. package/inkos/packages/core/genres/tower-climber.md +41 -0
  162. package/inkos/packages/core/genres/urban.md +53 -0
  163. package/inkos/packages/core/genres/xianxia.md +46 -0
  164. package/inkos/packages/core/genres/xuanhuan.md +64 -0
  165. package/inkos/packages/core/package.json +61 -0
  166. package/inkos/packages/core/src/__tests__/agent-max-tokens-policy.test.ts +29 -0
  167. package/inkos/packages/core/src/__tests__/agent-session.test.ts +866 -0
  168. package/inkos/packages/core/src/__tests__/agent-system-prompt.test.ts +167 -0
  169. package/inkos/packages/core/src/__tests__/agent-tools-params.test.ts +197 -0
  170. package/inkos/packages/core/src/__tests__/agent-tools.test.ts +421 -0
  171. package/inkos/packages/core/src/__tests__/ai-tells.test.ts +90 -0
  172. package/inkos/packages/core/src/__tests__/architect-phase5-consolidated.test.ts +445 -0
  173. package/inkos/packages/core/src/__tests__/architect-phase5.test.ts +455 -0
  174. package/inkos/packages/core/src/__tests__/architect-phase7.test.ts +210 -0
  175. package/inkos/packages/core/src/__tests__/architect.test.ts +859 -0
  176. package/inkos/packages/core/src/__tests__/audit-parse.test.ts +78 -0
  177. package/inkos/packages/core/src/__tests__/book-id.test.ts +26 -0
  178. package/inkos/packages/core/src/__tests__/book-session-store.test.ts +447 -0
  179. package/inkos/packages/core/src/__tests__/book-session.test.ts +113 -0
  180. package/inkos/packages/core/src/__tests__/chapter-analyzer.test.ts +574 -0
  181. package/inkos/packages/core/src/__tests__/chapter-memo-parser.test.ts +247 -0
  182. package/inkos/packages/core/src/__tests__/chapter-persistence.test.ts +198 -0
  183. package/inkos/packages/core/src/__tests__/chapter-review-cycle.test.ts +294 -0
  184. package/inkos/packages/core/src/__tests__/chapter-splitter.test.ts +156 -0
  185. package/inkos/packages/core/src/__tests__/chapter-state-recovery.test.ts +235 -0
  186. package/inkos/packages/core/src/__tests__/chapter-truth-validation.test.ts +253 -0
  187. package/inkos/packages/core/src/__tests__/composer.test.ts +627 -0
  188. package/inkos/packages/core/src/__tests__/config-loader.test.ts +325 -0
  189. package/inkos/packages/core/src/__tests__/config-migration.test.ts +102 -0
  190. package/inkos/packages/core/src/__tests__/consolidator.test.ts +32 -0
  191. package/inkos/packages/core/src/__tests__/context-filter.test.ts +60 -0
  192. package/inkos/packages/core/src/__tests__/context-transform.test.ts +108 -0
  193. package/inkos/packages/core/src/__tests__/continuity.test.ts +391 -0
  194. package/inkos/packages/core/src/__tests__/detection-insights.test.ts +59 -0
  195. package/inkos/packages/core/src/__tests__/detector.test.ts +86 -0
  196. package/inkos/packages/core/src/__tests__/draft-directive-parser.test.ts +386 -0
  197. package/inkos/packages/core/src/__tests__/edit-controller.test.ts +190 -0
  198. package/inkos/packages/core/src/__tests__/effective-llm-config.test.ts +486 -0
  199. package/inkos/packages/core/src/__tests__/fanfic-dimensions.test.ts +58 -0
  200. package/inkos/packages/core/src/__tests__/fanfic-models.test.ts +69 -0
  201. package/inkos/packages/core/src/__tests__/governed-working-set.test.ts +155 -0
  202. package/inkos/packages/core/src/__tests__/hook-arbiter.test.ts +124 -0
  203. package/inkos/packages/core/src/__tests__/hook-governance.test.ts +228 -0
  204. package/inkos/packages/core/src/__tests__/hook-health.test.ts +166 -0
  205. package/inkos/packages/core/src/__tests__/hook-ledger-validator.test.ts +236 -0
  206. package/inkos/packages/core/src/__tests__/hook-promotion.test.ts +192 -0
  207. package/inkos/packages/core/src/__tests__/hook-stale-detection.test.ts +136 -0
  208. package/inkos/packages/core/src/__tests__/index-notify-lazy.test.ts +20 -0
  209. package/inkos/packages/core/src/__tests__/interaction-chat-tokens.test.ts +170 -0
  210. package/inkos/packages/core/src/__tests__/interaction-models.test.ts +155 -0
  211. package/inkos/packages/core/src/__tests__/interaction-nl-router.test.ts +223 -0
  212. package/inkos/packages/core/src/__tests__/interaction-runtime.test.ts +633 -0
  213. package/inkos/packages/core/src/__tests__/interaction-tools.test.ts +343 -0
  214. package/inkos/packages/core/src/__tests__/length-metrics.test.ts +82 -0
  215. package/inkos/packages/core/src/__tests__/length-normalizer.test.ts +331 -0
  216. package/inkos/packages/core/src/__tests__/list-models.test.ts +109 -0
  217. package/inkos/packages/core/src/__tests__/llm-env.test.ts +31 -0
  218. package/inkos/packages/core/src/__tests__/logger.test.ts +175 -0
  219. package/inkos/packages/core/src/__tests__/long-span-fatigue.test.ts +160 -0
  220. package/inkos/packages/core/src/__tests__/memory-retrieval.test.ts +1303 -0
  221. package/inkos/packages/core/src/__tests__/models.test.ts +918 -0
  222. package/inkos/packages/core/src/__tests__/outline-paths.test.ts +97 -0
  223. package/inkos/packages/core/src/__tests__/path-safety.test.ts +22 -0
  224. package/inkos/packages/core/src/__tests__/persisted-governed-plan.test.ts +134 -0
  225. package/inkos/packages/core/src/__tests__/phase5-cleanup.test.ts +393 -0
  226. package/inkos/packages/core/src/__tests__/phase5-hotfix.test.ts +288 -0
  227. package/inkos/packages/core/src/__tests__/phase7-hotfix.test.ts +614 -0
  228. package/inkos/packages/core/src/__tests__/pipeline-agent.test.ts +354 -0
  229. package/inkos/packages/core/src/__tests__/pipeline-runner-memory-sync.test.ts +317 -0
  230. package/inkos/packages/core/src/__tests__/pipeline-runner.test.ts +5200 -0
  231. package/inkos/packages/core/src/__tests__/planner-context.test.ts +137 -0
  232. package/inkos/packages/core/src/__tests__/planner-prompts-ratio.test.ts +11 -0
  233. package/inkos/packages/core/src/__tests__/planner-prompts.test.ts +171 -0
  234. package/inkos/packages/core/src/__tests__/planner.test.ts +362 -0
  235. package/inkos/packages/core/src/__tests__/planning-materials.test.ts +90 -0
  236. package/inkos/packages/core/src/__tests__/polisher.test.ts +189 -0
  237. package/inkos/packages/core/src/__tests__/post-write-validator.test.ts +291 -0
  238. package/inkos/packages/core/src/__tests__/probe.test.ts +77 -0
  239. package/inkos/packages/core/src/__tests__/project-interaction.test.ts +241 -0
  240. package/inkos/packages/core/src/__tests__/provider.test.ts +953 -0
  241. package/inkos/packages/core/src/__tests__/providers-group.test.ts +34 -0
  242. package/inkos/packages/core/src/__tests__/providers-lookup.test.ts +81 -0
  243. package/inkos/packages/core/src/__tests__/providers-schema.test.ts +158 -0
  244. package/inkos/packages/core/src/__tests__/proxy-fetch.test.ts +75 -0
  245. package/inkos/packages/core/src/__tests__/revise-foundation.test.ts +514 -0
  246. package/inkos/packages/core/src/__tests__/reviser.test.ts +859 -0
  247. package/inkos/packages/core/src/__tests__/runtime-state-store.test.ts +388 -0
  248. package/inkos/packages/core/src/__tests__/scheduler.test.ts +123 -0
  249. package/inkos/packages/core/src/__tests__/secrets-migration.test.ts +71 -0
  250. package/inkos/packages/core/src/__tests__/secrets.test.ts +95 -0
  251. package/inkos/packages/core/src/__tests__/sensitive-words.test.ts +88 -0
  252. package/inkos/packages/core/src/__tests__/service-presets-regression.test.ts +73 -0
  253. package/inkos/packages/core/src/__tests__/service-resolver-regression.test.ts +75 -0
  254. package/inkos/packages/core/src/__tests__/service-resolver.test.ts +228 -0
  255. package/inkos/packages/core/src/__tests__/session-transcript-restore.test.ts +1311 -0
  256. package/inkos/packages/core/src/__tests__/session-transcript.test.ts +195 -0
  257. package/inkos/packages/core/src/__tests__/settler-delta-parser.test.ts +133 -0
  258. package/inkos/packages/core/src/__tests__/short-fiction-public.test.ts +241 -0
  259. package/inkos/packages/core/src/__tests__/spot-fix-patches.test.ts +104 -0
  260. package/inkos/packages/core/src/__tests__/state-manager.test.ts +1298 -0
  261. package/inkos/packages/core/src/__tests__/state-projections.test.ts +130 -0
  262. package/inkos/packages/core/src/__tests__/state-reducer.test.ts +372 -0
  263. package/inkos/packages/core/src/__tests__/state-validator-agent.test.ts +165 -0
  264. package/inkos/packages/core/src/__tests__/state-validator.test.ts +122 -0
  265. package/inkos/packages/core/src/__tests__/style-analyzer.test.ts +61 -0
  266. package/inkos/packages/core/src/__tests__/temperature-constraints.test.ts +57 -0
  267. package/inkos/packages/core/src/__tests__/v13-hotfix-round4.test.ts +343 -0
  268. package/inkos/packages/core/src/__tests__/verify-service.test.ts +77 -0
  269. package/inkos/packages/core/src/__tests__/webhook.test.ts +91 -0
  270. package/inkos/packages/core/src/__tests__/writer-parser.test.ts +348 -0
  271. package/inkos/packages/core/src/__tests__/writer-prompts.test.ts +269 -0
  272. package/inkos/packages/core/src/__tests__/writer.test.ts +1360 -0
  273. package/inkos/packages/core/src/agent/agent-session.ts +737 -0
  274. package/inkos/packages/core/src/agent/agent-system-prompt.ts +199 -0
  275. package/inkos/packages/core/src/agent/agent-tools.ts +835 -0
  276. package/inkos/packages/core/src/agent/context-transform.ts +85 -0
  277. package/inkos/packages/core/src/agent/index.ts +14 -0
  278. package/inkos/packages/core/src/agents/ai-tells.ts +161 -0
  279. package/inkos/packages/core/src/agents/architect.ts +1291 -0
  280. package/inkos/packages/core/src/agents/base.ts +100 -0
  281. package/inkos/packages/core/src/agents/chapter-analyzer.ts +634 -0
  282. package/inkos/packages/core/src/agents/composer.ts +469 -0
  283. package/inkos/packages/core/src/agents/consolidator.ts +218 -0
  284. package/inkos/packages/core/src/agents/continuity.ts +824 -0
  285. package/inkos/packages/core/src/agents/detection-insights.ts +72 -0
  286. package/inkos/packages/core/src/agents/detector.ts +224 -0
  287. package/inkos/packages/core/src/agents/en-prompt-sections.ts +129 -0
  288. package/inkos/packages/core/src/agents/fanfic-canon-importer.ts +146 -0
  289. package/inkos/packages/core/src/agents/fanfic-dimensions.ts +87 -0
  290. package/inkos/packages/core/src/agents/fanfic-prompt-sections.ts +109 -0
  291. package/inkos/packages/core/src/agents/foundation-reviewer.ts +204 -0
  292. package/inkos/packages/core/src/agents/length-normalizer.ts +218 -0
  293. package/inkos/packages/core/src/agents/observer-prompts.ts +127 -0
  294. package/inkos/packages/core/src/agents/planner-context.ts +297 -0
  295. package/inkos/packages/core/src/agents/planner-prompts.ts +404 -0
  296. package/inkos/packages/core/src/agents/planner.ts +783 -0
  297. package/inkos/packages/core/src/agents/polisher.ts +153 -0
  298. package/inkos/packages/core/src/agents/post-write-validator.ts +873 -0
  299. package/inkos/packages/core/src/agents/radar-source.ts +123 -0
  300. package/inkos/packages/core/src/agents/radar.ts +120 -0
  301. package/inkos/packages/core/src/agents/reviser.ts +701 -0
  302. package/inkos/packages/core/src/agents/rules-reader.ts +155 -0
  303. package/inkos/packages/core/src/agents/sensitive-words.ts +142 -0
  304. package/inkos/packages/core/src/agents/settler-delta-parser.ts +53 -0
  305. package/inkos/packages/core/src/agents/settler-parser.ts +38 -0
  306. package/inkos/packages/core/src/agents/settler-prompts.ts +230 -0
  307. package/inkos/packages/core/src/agents/short-fiction.ts +429 -0
  308. package/inkos/packages/core/src/agents/state-validator.ts +322 -0
  309. package/inkos/packages/core/src/agents/style-analyzer.ts +93 -0
  310. package/inkos/packages/core/src/agents/writer-parser.ts +178 -0
  311. package/inkos/packages/core/src/agents/writer-prompts.ts +899 -0
  312. package/inkos/packages/core/src/agents/writer.ts +1450 -0
  313. package/inkos/packages/core/src/index.ts +392 -0
  314. package/inkos/packages/core/src/interaction/book-session-store.ts +226 -0
  315. package/inkos/packages/core/src/interaction/draft-directive-parser.ts +266 -0
  316. package/inkos/packages/core/src/interaction/edit-controller.ts +270 -0
  317. package/inkos/packages/core/src/interaction/events.ts +41 -0
  318. package/inkos/packages/core/src/interaction/export-artifact.ts +151 -0
  319. package/inkos/packages/core/src/interaction/intents.ts +63 -0
  320. package/inkos/packages/core/src/interaction/modes.ts +13 -0
  321. package/inkos/packages/core/src/interaction/nl-router.ts +258 -0
  322. package/inkos/packages/core/src/interaction/project-control.ts +150 -0
  323. package/inkos/packages/core/src/interaction/project-session-store.ts +81 -0
  324. package/inkos/packages/core/src/interaction/project-tools.ts +704 -0
  325. package/inkos/packages/core/src/interaction/request-router.ts +5 -0
  326. package/inkos/packages/core/src/interaction/runtime.ts +1167 -0
  327. package/inkos/packages/core/src/interaction/session-transcript-legacy.ts +113 -0
  328. package/inkos/packages/core/src/interaction/session-transcript-restore.ts +607 -0
  329. package/inkos/packages/core/src/interaction/session-transcript-schema.ts +76 -0
  330. package/inkos/packages/core/src/interaction/session-transcript.ts +189 -0
  331. package/inkos/packages/core/src/interaction/session.ts +226 -0
  332. package/inkos/packages/core/src/interaction/truth-authority.ts +45 -0
  333. package/inkos/packages/core/src/llm/config-migration.ts +58 -0
  334. package/inkos/packages/core/src/llm/cover-providers.ts +45 -0
  335. package/inkos/packages/core/src/llm/provider.ts +1331 -0
  336. package/inkos/packages/core/src/llm/providers/endpoints/ai360.ts +42 -0
  337. package/inkos/packages/core/src/llm/providers/endpoints/anthropic.ts +82 -0
  338. package/inkos/packages/core/src/llm/providers/endpoints/astronCodingPlan.ts +30 -0
  339. package/inkos/packages/core/src/llm/providers/endpoints/baichuan.ts +28 -0
  340. package/inkos/packages/core/src/llm/providers/endpoints/bailian.ts +65 -0
  341. package/inkos/packages/core/src/llm/providers/endpoints/bailianCodingPlan.ts +30 -0
  342. package/inkos/packages/core/src/llm/providers/endpoints/custom.ts +22 -0
  343. package/inkos/packages/core/src/llm/providers/endpoints/deepseek.ts +35 -0
  344. package/inkos/packages/core/src/llm/providers/endpoints/giteeai.ts +41 -0
  345. package/inkos/packages/core/src/llm/providers/endpoints/githubCopilot.ts +43 -0
  346. package/inkos/packages/core/src/llm/providers/endpoints/glmCodingPlan.ts +28 -0
  347. package/inkos/packages/core/src/llm/providers/endpoints/google.ts +51 -0
  348. package/inkos/packages/core/src/llm/providers/endpoints/hunyuan.ts +42 -0
  349. package/inkos/packages/core/src/llm/providers/endpoints/infiniai.ts +72 -0
  350. package/inkos/packages/core/src/llm/providers/endpoints/internlm.ts +28 -0
  351. package/inkos/packages/core/src/llm/providers/endpoints/kimiCode.ts +23 -0
  352. package/inkos/packages/core/src/llm/providers/endpoints/kimiCodingPlan.ts +24 -0
  353. package/inkos/packages/core/src/llm/providers/endpoints/kkaiapi.ts +56 -0
  354. package/inkos/packages/core/src/llm/providers/endpoints/longcat.ts +25 -0
  355. package/inkos/packages/core/src/llm/providers/endpoints/minimax.ts +39 -0
  356. package/inkos/packages/core/src/llm/providers/endpoints/minimaxCodingPlan.ts +28 -0
  357. package/inkos/packages/core/src/llm/providers/endpoints/mistral.ts +40 -0
  358. package/inkos/packages/core/src/llm/providers/endpoints/modelscope.ts +30 -0
  359. package/inkos/packages/core/src/llm/providers/endpoints/moonshot.ts +39 -0
  360. package/inkos/packages/core/src/llm/providers/endpoints/newapi.ts +21 -0
  361. package/inkos/packages/core/src/llm/providers/endpoints/ollama.ts +73 -0
  362. package/inkos/packages/core/src/llm/providers/endpoints/openai.ts +77 -0
  363. package/inkos/packages/core/src/llm/providers/endpoints/opencodeCodingPlan.ts +30 -0
  364. package/inkos/packages/core/src/llm/providers/endpoints/openrouter.ts +87 -0
  365. package/inkos/packages/core/src/llm/providers/endpoints/ppio.ts +86 -0
  366. package/inkos/packages/core/src/llm/providers/endpoints/qiniu.ts +32 -0
  367. package/inkos/packages/core/src/llm/providers/endpoints/sensenova.ts +45 -0
  368. package/inkos/packages/core/src/llm/providers/endpoints/siliconcloud.ts +126 -0
  369. package/inkos/packages/core/src/llm/providers/endpoints/spark.ts +33 -0
  370. package/inkos/packages/core/src/llm/providers/endpoints/stepfun.ts +35 -0
  371. package/inkos/packages/core/src/llm/providers/endpoints/tencentcloud.ts +25 -0
  372. package/inkos/packages/core/src/llm/providers/endpoints/volcengine.ts +52 -0
  373. package/inkos/packages/core/src/llm/providers/endpoints/volcengineCodingPlan.ts +42 -0
  374. package/inkos/packages/core/src/llm/providers/endpoints/wenxin.ts +106 -0
  375. package/inkos/packages/core/src/llm/providers/endpoints/xai.ts +34 -0
  376. package/inkos/packages/core/src/llm/providers/endpoints/xiaomimimo.ts +26 -0
  377. package/inkos/packages/core/src/llm/providers/endpoints/zeroone.ts +34 -0
  378. package/inkos/packages/core/src/llm/providers/endpoints/zhipu.ts +61 -0
  379. package/inkos/packages/core/src/llm/providers/index.ts +71 -0
  380. package/inkos/packages/core/src/llm/providers/lookup.ts +70 -0
  381. package/inkos/packages/core/src/llm/providers/probe.ts +35 -0
  382. package/inkos/packages/core/src/llm/providers/types.ts +89 -0
  383. package/inkos/packages/core/src/llm/providers/verify.ts +104 -0
  384. package/inkos/packages/core/src/llm/secrets.ts +77 -0
  385. package/inkos/packages/core/src/llm/service-presets.ts +215 -0
  386. package/inkos/packages/core/src/llm/service-resolver.ts +91 -0
  387. package/inkos/packages/core/src/models/book-rules.ts +126 -0
  388. package/inkos/packages/core/src/models/book.ts +70 -0
  389. package/inkos/packages/core/src/models/chapter.ts +42 -0
  390. package/inkos/packages/core/src/models/detection.ts +25 -0
  391. package/inkos/packages/core/src/models/genre-profile.ts +36 -0
  392. package/inkos/packages/core/src/models/input-governance.ts +99 -0
  393. package/inkos/packages/core/src/models/length-governance.ts +46 -0
  394. package/inkos/packages/core/src/models/project.ts +161 -0
  395. package/inkos/packages/core/src/models/runtime-state.ts +144 -0
  396. package/inkos/packages/core/src/models/state.ts +52 -0
  397. package/inkos/packages/core/src/models/style-profile.ts +15 -0
  398. package/inkos/packages/core/src/notify/dispatcher.ts +96 -0
  399. package/inkos/packages/core/src/notify/feishu.ts +34 -0
  400. package/inkos/packages/core/src/notify/telegram.ts +25 -0
  401. package/inkos/packages/core/src/notify/webhook.ts +58 -0
  402. package/inkos/packages/core/src/notify/wechat-work.ts +22 -0
  403. package/inkos/packages/core/src/pipeline/agent.ts +691 -0
  404. package/inkos/packages/core/src/pipeline/chapter-persistence.ts +79 -0
  405. package/inkos/packages/core/src/pipeline/chapter-review-cycle.ts +324 -0
  406. package/inkos/packages/core/src/pipeline/chapter-state-recovery.ts +236 -0
  407. package/inkos/packages/core/src/pipeline/chapter-truth-validation.ts +145 -0
  408. package/inkos/packages/core/src/pipeline/detection-runner.ts +164 -0
  409. package/inkos/packages/core/src/pipeline/persisted-governed-plan.ts +216 -0
  410. package/inkos/packages/core/src/pipeline/runner.ts +3438 -0
  411. package/inkos/packages/core/src/pipeline/scheduler.ts +411 -0
  412. package/inkos/packages/core/src/pipeline/short-fiction-runner.ts +801 -0
  413. package/inkos/packages/core/src/prompts/index.ts +1 -0
  414. package/inkos/packages/core/src/prompts/short-fiction.ts +273 -0
  415. package/inkos/packages/core/src/state/manager.ts +560 -0
  416. package/inkos/packages/core/src/state/memory-db.ts +359 -0
  417. package/inkos/packages/core/src/state/runtime-state-store.ts +164 -0
  418. package/inkos/packages/core/src/state/state-bootstrap.ts +657 -0
  419. package/inkos/packages/core/src/state/state-projections.ts +255 -0
  420. package/inkos/packages/core/src/state/state-reducer.ts +260 -0
  421. package/inkos/packages/core/src/state/state-validator.ts +117 -0
  422. package/inkos/packages/core/src/utils/analytics.ts +92 -0
  423. package/inkos/packages/core/src/utils/book-id.ts +31 -0
  424. package/inkos/packages/core/src/utils/cadence-policy.ts +46 -0
  425. package/inkos/packages/core/src/utils/chapter-cadence.ts +211 -0
  426. package/inkos/packages/core/src/utils/chapter-memo-parser.ts +157 -0
  427. package/inkos/packages/core/src/utils/chapter-splitter.ts +80 -0
  428. package/inkos/packages/core/src/utils/config-loader.ts +29 -0
  429. package/inkos/packages/core/src/utils/context-assembly.ts +98 -0
  430. package/inkos/packages/core/src/utils/context-filter.ts +190 -0
  431. package/inkos/packages/core/src/utils/effective-llm-config.ts +529 -0
  432. package/inkos/packages/core/src/utils/governed-context.ts +101 -0
  433. package/inkos/packages/core/src/utils/governed-working-set.ts +395 -0
  434. package/inkos/packages/core/src/utils/hook-arbiter.ts +332 -0
  435. package/inkos/packages/core/src/utils/hook-governance.ts +199 -0
  436. package/inkos/packages/core/src/utils/hook-health.ts +189 -0
  437. package/inkos/packages/core/src/utils/hook-ledger-validator.ts +277 -0
  438. package/inkos/packages/core/src/utils/hook-lifecycle.ts +224 -0
  439. package/inkos/packages/core/src/utils/hook-policy.ts +115 -0
  440. package/inkos/packages/core/src/utils/hook-promotion.ts +313 -0
  441. package/inkos/packages/core/src/utils/hook-stale-detection.ts +168 -0
  442. package/inkos/packages/core/src/utils/length-metrics.ts +123 -0
  443. package/inkos/packages/core/src/utils/llm-endpoint-auth.ts +40 -0
  444. package/inkos/packages/core/src/utils/llm-env.ts +74 -0
  445. package/inkos/packages/core/src/utils/logger.ts +123 -0
  446. package/inkos/packages/core/src/utils/long-span-fatigue.ts +545 -0
  447. package/inkos/packages/core/src/utils/memory-retrieval.ts +527 -0
  448. package/inkos/packages/core/src/utils/narrative-control.ts +177 -0
  449. package/inkos/packages/core/src/utils/outline-paths.ts +275 -0
  450. package/inkos/packages/core/src/utils/path-safety.ts +11 -0
  451. package/inkos/packages/core/src/utils/planning-materials.ts +185 -0
  452. package/inkos/packages/core/src/utils/pov-filter.ts +149 -0
  453. package/inkos/packages/core/src/utils/proxy-fetch.ts +44 -0
  454. package/inkos/packages/core/src/utils/runtime-writer.ts +41 -0
  455. package/inkos/packages/core/src/utils/spot-fix-patches.ts +189 -0
  456. package/inkos/packages/core/src/utils/story-markdown.ts +346 -0
  457. package/inkos/packages/core/src/utils/web-search.ts +82 -0
  458. package/inkos/packages/core/src/utils/writing-methodology.ts +164 -0
  459. package/inkos/packages/core/tsconfig.json +8 -0
  460. package/inkos/packages/core/vitest.config.ts +7 -0
  461. package/inkos/packages/studio/components.json +25 -0
  462. package/inkos/packages/studio/index.html +13 -0
  463. package/inkos/packages/studio/package.json +72 -0
  464. package/inkos/packages/studio/postcss.config.js +3 -0
  465. package/inkos/packages/studio/src/App.test.ts +25 -0
  466. package/inkos/packages/studio/src/App.tsx +280 -0
  467. package/inkos/packages/studio/src/api/__tests__/normalize-base-url.test.ts +40 -0
  468. package/inkos/packages/studio/src/api/book-create.test.ts +104 -0
  469. package/inkos/packages/studio/src/api/book-create.ts +94 -0
  470. package/inkos/packages/studio/src/api/errors.ts +17 -0
  471. package/inkos/packages/studio/src/api/index.ts +30 -0
  472. package/inkos/packages/studio/src/api/lib/run-store.ts +177 -0
  473. package/inkos/packages/studio/src/api/lib/sse.ts +50 -0
  474. package/inkos/packages/studio/src/api/phase5-hotfix.test.ts +335 -0
  475. package/inkos/packages/studio/src/api/safety.ts +6 -0
  476. package/inkos/packages/studio/src/api/server.test.ts +3162 -0
  477. package/inkos/packages/studio/src/api/server.ts +3666 -0
  478. package/inkos/packages/studio/src/api/v13-hotfix-round4.test.ts +226 -0
  479. package/inkos/packages/studio/src/app-state.test.ts +8 -0
  480. package/inkos/packages/studio/src/app-state.ts +1 -0
  481. package/inkos/packages/studio/src/components/ConfirmDialog.tsx +95 -0
  482. package/inkos/packages/studio/src/components/ServiceConfigSourceCard.tsx +139 -0
  483. package/inkos/packages/studio/src/components/ServiceQuickLinks.tsx +65 -0
  484. package/inkos/packages/studio/src/components/Sidebar.tsx +652 -0
  485. package/inkos/packages/studio/src/components/ai-elements/code-block.tsx +562 -0
  486. package/inkos/packages/studio/src/components/ai-elements/confirmation.tsx +174 -0
  487. package/inkos/packages/studio/src/components/ai-elements/message.tsx +360 -0
  488. package/inkos/packages/studio/src/components/ai-elements/prompt-input.tsx +1457 -0
  489. package/inkos/packages/studio/src/components/ai-elements/reasoning.tsx +226 -0
  490. package/inkos/packages/studio/src/components/ai-elements/shimmer.tsx +77 -0
  491. package/inkos/packages/studio/src/components/ai-elements/tool.tsx +173 -0
  492. package/inkos/packages/studio/src/components/chat/BookSidebar.tsx +291 -0
  493. package/inkos/packages/studio/src/components/chat/ChatMessage.tsx +39 -0
  494. package/inkos/packages/studio/src/components/chat/QuickActions.tsx +73 -0
  495. package/inkos/packages/studio/src/components/chat/ToolExecutionSteps.tsx +320 -0
  496. package/inkos/packages/studio/src/components/chat/__tests__/ToolExecutionSteps.test.ts +114 -0
  497. package/inkos/packages/studio/src/components/chat-utils.ts +56 -0
  498. package/inkos/packages/studio/src/components/chatbar-state.test.ts +69 -0
  499. package/inkos/packages/studio/src/components/sidebar/ChaptersSection.tsx +66 -0
  500. package/inkos/packages/studio/src/components/sidebar/CharacterSection.tsx +129 -0
  501. package/inkos/packages/studio/src/components/sidebar/FoundationSection.tsx +61 -0
  502. package/inkos/packages/studio/src/components/sidebar/ProgressSection.tsx +124 -0
  503. package/inkos/packages/studio/src/components/sidebar/SidebarCard.tsx +30 -0
  504. package/inkos/packages/studio/src/components/sidebar/SummarySection.tsx +89 -0
  505. package/inkos/packages/studio/src/components/ui/alert.tsx +76 -0
  506. package/inkos/packages/studio/src/components/ui/badge.tsx +52 -0
  507. package/inkos/packages/studio/src/components/ui/button-group.tsx +87 -0
  508. package/inkos/packages/studio/src/components/ui/button.tsx +58 -0
  509. package/inkos/packages/studio/src/components/ui/collapsible.tsx +19 -0
  510. package/inkos/packages/studio/src/components/ui/command.tsx +194 -0
  511. package/inkos/packages/studio/src/components/ui/dialog.tsx +158 -0
  512. package/inkos/packages/studio/src/components/ui/dropdown-menu.tsx +266 -0
  513. package/inkos/packages/studio/src/components/ui/hover-card.tsx +51 -0
  514. package/inkos/packages/studio/src/components/ui/input-group.tsx +158 -0
  515. package/inkos/packages/studio/src/components/ui/input.tsx +20 -0
  516. package/inkos/packages/studio/src/components/ui/select.tsx +199 -0
  517. package/inkos/packages/studio/src/components/ui/separator.tsx +23 -0
  518. package/inkos/packages/studio/src/components/ui/spinner.tsx +10 -0
  519. package/inkos/packages/studio/src/components/ui/textarea.tsx +18 -0
  520. package/inkos/packages/studio/src/components/ui/tooltip.tsx +66 -0
  521. package/inkos/packages/studio/src/constants/service-groups.ts +29 -0
  522. package/inkos/packages/studio/src/hooks/use-api.test.ts +93 -0
  523. package/inkos/packages/studio/src/hooks/use-api.ts +189 -0
  524. package/inkos/packages/studio/src/hooks/use-book-activity.test.ts +129 -0
  525. package/inkos/packages/studio/src/hooks/use-book-activity.ts +180 -0
  526. package/inkos/packages/studio/src/hooks/use-colors.ts +27 -0
  527. package/inkos/packages/studio/src/hooks/use-hash-route.test.ts +101 -0
  528. package/inkos/packages/studio/src/hooks/use-hash-route.ts +89 -0
  529. package/inkos/packages/studio/src/hooks/use-i18n.ts +289 -0
  530. package/inkos/packages/studio/src/hooks/use-session-events.ts +64 -0
  531. package/inkos/packages/studio/src/hooks/use-sse.test.ts +52 -0
  532. package/inkos/packages/studio/src/hooks/use-sse.ts +92 -0
  533. package/inkos/packages/studio/src/hooks/use-theme.test.ts +31 -0
  534. package/inkos/packages/studio/src/hooks/use-theme.ts +75 -0
  535. package/inkos/packages/studio/src/index.css +323 -0
  536. package/inkos/packages/studio/src/lib/error-copy.test.ts +34 -0
  537. package/inkos/packages/studio/src/lib/error-copy.ts +37 -0
  538. package/inkos/packages/studio/src/lib/utils.ts +6 -0
  539. package/inkos/packages/studio/src/main.tsx +10 -0
  540. package/inkos/packages/studio/src/pages/Analytics.tsx +80 -0
  541. package/inkos/packages/studio/src/pages/BookCreate.tsx +895 -0
  542. package/inkos/packages/studio/src/pages/BookDetail.tsx +652 -0
  543. package/inkos/packages/studio/src/pages/ChapterReader.tsx +266 -0
  544. package/inkos/packages/studio/src/pages/ChatPage.tsx +521 -0
  545. package/inkos/packages/studio/src/pages/DaemonControl.tsx +116 -0
  546. package/inkos/packages/studio/src/pages/Dashboard.tsx +379 -0
  547. package/inkos/packages/studio/src/pages/DoctorView.tsx +82 -0
  548. package/inkos/packages/studio/src/pages/GenreManager.tsx +464 -0
  549. package/inkos/packages/studio/src/pages/ImportManager.tsx +216 -0
  550. package/inkos/packages/studio/src/pages/LanguageSelector.tsx +74 -0
  551. package/inkos/packages/studio/src/pages/LogViewer.tsx +82 -0
  552. package/inkos/packages/studio/src/pages/RadarView.tsx +157 -0
  553. package/inkos/packages/studio/src/pages/ServiceDetailPage.tsx +393 -0
  554. package/inkos/packages/studio/src/pages/ServiceListPage.tsx +463 -0
  555. package/inkos/packages/studio/src/pages/StyleManager.tsx +225 -0
  556. package/inkos/packages/studio/src/pages/TruthFiles.tsx +194 -0
  557. package/inkos/packages/studio/src/pages/chat-page-state.test.ts +206 -0
  558. package/inkos/packages/studio/src/pages/chat-page-state.ts +112 -0
  559. package/inkos/packages/studio/src/pages/page-state.test.ts +258 -0
  560. package/inkos/packages/studio/src/pages/service-detail-state.test.ts +294 -0
  561. package/inkos/packages/studio/src/pages/service-detail-state.ts +234 -0
  562. package/inkos/packages/studio/src/pages/style-manager-state.test.ts +22 -0
  563. package/inkos/packages/studio/src/pages/truth-files-state.test.ts +61 -0
  564. package/inkos/packages/studio/src/shared/contracts.ts +143 -0
  565. package/inkos/packages/studio/src/store/chat/__tests__/message-parts.test.ts +172 -0
  566. package/inkos/packages/studio/src/store/chat/index.ts +3 -0
  567. package/inkos/packages/studio/src/store/chat/initialState.ts +8 -0
  568. package/inkos/packages/studio/src/store/chat/message-policy.test.ts +16 -0
  569. package/inkos/packages/studio/src/store/chat/message-policy.ts +5 -0
  570. package/inkos/packages/studio/src/store/chat/parts-builder.ts +187 -0
  571. package/inkos/packages/studio/src/store/chat/selectors.ts +13 -0
  572. package/inkos/packages/studio/src/store/chat/slices/create/action.ts +10 -0
  573. package/inkos/packages/studio/src/store/chat/slices/create/initialState.ts +9 -0
  574. package/inkos/packages/studio/src/store/chat/slices/message/action.ts +417 -0
  575. package/inkos/packages/studio/src/store/chat/slices/message/initialState.ts +10 -0
  576. package/inkos/packages/studio/src/store/chat/slices/message/runtime.test.ts +21 -0
  577. package/inkos/packages/studio/src/store/chat/slices/message/runtime.ts +233 -0
  578. package/inkos/packages/studio/src/store/chat/slices/message/stream-events.ts +272 -0
  579. package/inkos/packages/studio/src/store/chat/store.ts +11 -0
  580. package/inkos/packages/studio/src/store/chat/types.ts +169 -0
  581. package/inkos/packages/studio/src/store/service/index.ts +2 -0
  582. package/inkos/packages/studio/src/store/service/store.ts +123 -0
  583. package/inkos/packages/studio/src/store/service/types.ts +50 -0
  584. package/inkos/packages/studio/tsconfig.json +24 -0
  585. package/inkos/packages/studio/tsconfig.server.json +11 -0
  586. package/inkos/packages/studio/vite.config.ts +34 -0
  587. package/inkos/packages/studio/vitest.config.ts +14 -0
  588. package/inkos/pnpm-lock.yaml +9569 -0
  589. package/inkos/pnpm-workspace.yaml +2 -0
  590. package/inkos/scripts/prepare-package-for-publish.mjs +135 -0
  591. package/inkos/scripts/restore-package-json.mjs +31 -0
  592. package/inkos/scripts/set-package-versions.mjs +74 -0
  593. package/inkos/scripts/verify-no-workspace-protocol.mjs +140 -0
  594. package/inkos/skills/SKILL.md +654 -0
  595. package/inkos/tsconfig.json +19 -0
  596. package/package.json +4 -3
  597. /package/.next/static/{F2hMZMf1IyCVAWpkbtRz7 → -3vIrBZXdQ0rp7Wa3Kz40}/_buildManifest.js +0 -0
  598. /package/.next/static/{F2hMZMf1IyCVAWpkbtRz7 → -3vIrBZXdQ0rp7Wa3Kz40}/_ssgManifest.js +0 -0
@@ -0,0 +1,1331 @@
1
+ import type { LLMConfig } from "../models/project.js";
2
+ import {
3
+ streamSimple as piStreamSimple,
4
+ stream as piStream,
5
+ completeSimple as piCompleteSimple,
6
+ complete as piComplete,
7
+ } from "@mariozechner/pi-ai";
8
+ import type {
9
+ Api as PiApi,
10
+ Model as PiModel,
11
+ Context as PiContext,
12
+ AssistantMessageEvent,
13
+ Tool as PiTool,
14
+ TextContent as PiTextContent,
15
+ ToolCall as PiToolCall,
16
+ } from "@mariozechner/pi-ai";
17
+ import { resolveServicePreset } from "./service-presets.js";
18
+ import { getEndpoint } from "./providers/index.js";
19
+ import { lookupModel } from "./providers/lookup.js";
20
+ import { fetchWithProxy } from "../utils/proxy-fetch.js";
21
+ import { isApiKeyOptionalForEndpoint } from "../utils/llm-endpoint-auth.js";
22
+
23
+
24
+ // === Streaming Monitor Types ===
25
+
26
+ export interface StreamProgress {
27
+ readonly elapsedMs: number;
28
+ readonly totalChars: number;
29
+ readonly chineseChars: number;
30
+ readonly status: "streaming" | "done";
31
+ }
32
+
33
+ export type OnStreamProgress = (progress: StreamProgress) => void;
34
+
35
+ const INKOS_USER_AGENT = "InkOS/1.3.5";
36
+ const UNKNOWN_MODEL_FALLBACK_MAX_TOKENS = 8192 * 3;
37
+ const TRANSIENT_LLM_RETRIES = 2;
38
+
39
+ function isByteString(value: string): boolean {
40
+ for (let i = 0; i < value.length; i++) {
41
+ if (value.charCodeAt(i) > 255) return false;
42
+ }
43
+ return true;
44
+ }
45
+
46
+ function isValidHeaderName(value: string): boolean {
47
+ return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(value);
48
+ }
49
+
50
+ function sanitizeHttpHeaders(headers?: Record<string, string>): Record<string, string> | undefined {
51
+ if (!headers) return undefined;
52
+ const sanitized: Record<string, string> = {};
53
+ for (const [key, value] of Object.entries(headers)) {
54
+ if (!isValidHeaderName(key)) continue;
55
+ if (!isByteString(value)) continue;
56
+ sanitized[key] = value;
57
+ }
58
+ return Object.keys(sanitized).length > 0 ? sanitized : undefined;
59
+ }
60
+
61
+ function mergeUserAgent(headers?: Record<string, string>): Record<string, string> {
62
+ return { "User-Agent": INKOS_USER_AGENT, ...(sanitizeHttpHeaders(headers) ?? {}) };
63
+ }
64
+
65
+ export function createStreamMonitor(
66
+ onProgress?: OnStreamProgress,
67
+ intervalMs: number = 30000,
68
+ ): { readonly onChunk: (text: string) => void; readonly stop: () => void } {
69
+ let totalChars = 0;
70
+ let chineseChars = 0;
71
+ const startTime = Date.now();
72
+ let timer: ReturnType<typeof setInterval> | undefined;
73
+
74
+ if (onProgress) {
75
+ timer = setInterval(() => {
76
+ onProgress({
77
+ elapsedMs: Date.now() - startTime,
78
+ totalChars,
79
+ chineseChars,
80
+ status: "streaming",
81
+ });
82
+ }, intervalMs);
83
+ }
84
+
85
+ return {
86
+ onChunk(text: string): void {
87
+ totalChars += text.length;
88
+ chineseChars += (text.match(/[\u4e00-\u9fff]/g) || []).length;
89
+ },
90
+ stop(): void {
91
+ if (timer !== undefined) {
92
+ clearInterval(timer);
93
+ timer = undefined;
94
+ }
95
+ onProgress?.({
96
+ elapsedMs: Date.now() - startTime,
97
+ totalChars,
98
+ chineseChars,
99
+ status: "done",
100
+ });
101
+ },
102
+ };
103
+ }
104
+
105
+ // === Shared Types ===
106
+
107
+ export interface LLMResponse {
108
+ readonly content: string;
109
+ readonly usage: {
110
+ readonly promptTokens: number;
111
+ readonly completionTokens: number;
112
+ readonly totalTokens: number;
113
+ };
114
+ }
115
+
116
+ export interface LLMMessage {
117
+ readonly role: "system" | "user" | "assistant";
118
+ readonly content: string;
119
+ }
120
+
121
+ export interface LLMClient {
122
+ readonly provider: "openai" | "anthropic";
123
+ readonly service?: string;
124
+ readonly configSource?: LLMConfig["configSource"];
125
+ readonly apiFormat: "chat" | "responses";
126
+ readonly stream: boolean;
127
+ readonly proxyUrl?: string;
128
+ readonly _piModel?: PiModel<PiApi>;
129
+ readonly _apiKey?: string;
130
+ readonly defaults: {
131
+ readonly temperature: number;
132
+ /**
133
+ * Per-call fallback: 当 agent 调 chat() 不传 options.maxTokens 时用这个值。
134
+ * 命中模型卡时来自 providers bank 的 modelCard.maxOutput;未知模型走写作兜底预算。
135
+ */
136
+ readonly maxTokens: number;
137
+ /**
138
+ * Legacy mock compatibility only. v2 provider resolution no longer caps
139
+ * per-call maxTokens from project config; model max output comes from the
140
+ * provider bank.
141
+ */
142
+ readonly maxTokensCap?: number | null;
143
+ readonly thinkingBudget: number;
144
+ readonly extra: Record<string, unknown>;
145
+ };
146
+ }
147
+
148
+ // === Tool-calling Types ===
149
+
150
+ export interface ToolDefinition {
151
+ readonly name: string;
152
+ readonly description: string;
153
+ readonly parameters: Record<string, unknown>;
154
+ }
155
+
156
+ export interface ToolCall {
157
+ readonly id: string;
158
+ readonly name: string;
159
+ readonly arguments: string;
160
+ }
161
+
162
+ export type AgentMessage =
163
+ | { readonly role: "system"; readonly content: string }
164
+ | { readonly role: "user"; readonly content: string }
165
+ | { readonly role: "assistant"; readonly content: string | null; readonly toolCalls?: ReadonlyArray<ToolCall> }
166
+ | { readonly role: "tool"; readonly toolCallId: string; readonly content: string };
167
+
168
+ export interface ChatWithToolsResult {
169
+ readonly content: string;
170
+ readonly toolCalls: ReadonlyArray<ToolCall>;
171
+ }
172
+
173
+ // === Factory ===
174
+
175
+ export function createLLMClient(config: LLMConfig): LLMClient {
176
+ // C1 (v2.0.0):config.maxTokens / maxTokensCap 已删除;defaults.maxTokens 完全从 modelCard 推导。
177
+ const _earlyCard = lookupModel(config.service ?? "custom", config.model);
178
+ const defaults = {
179
+ temperature: config.temperature ?? 0.7,
180
+ maxTokens: _earlyCard?.maxOutput ?? UNKNOWN_MODEL_FALLBACK_MAX_TOKENS,
181
+ thinkingBudget: config.thinkingBudget ?? 0,
182
+ extra: config.extra ?? {},
183
+ };
184
+
185
+ const apiFormat = config.apiFormat ?? "chat";
186
+ const stream = config.stream ?? true;
187
+
188
+ // --- Build pi-ai Model object ---
189
+ const serviceName = config.service ?? "custom";
190
+ const preset = resolveServicePreset(serviceName);
191
+ const inkosProvider = getEndpoint(serviceName);
192
+ const modelCard = lookupModel(serviceName, config.model);
193
+
194
+ const piApi = resolvePiApi(serviceName, config.apiFormat, (inkosProvider?.api ?? preset?.api) as PiApi) as PiApi;
195
+ const baseUrl = config.baseUrl || inkosProvider?.baseUrl || preset?.baseUrl || "";
196
+ const extraHeaders = sanitizeHttpHeaders(config.headers ?? parseEnvHeaders());
197
+ const compat = piApi === "openai-completions"
198
+ ? resolveProviderCompat(inkosProvider, baseUrl)
199
+ : undefined;
200
+
201
+ const provider = config.provider === "anthropic" ? "anthropic" : "openai";
202
+ // pi-ai provider 字段:大多数情况 pi-ai 会按 baseUrl 自动嗅探(openrouter.ai / api.z.ai /
203
+ // api.x.ai / deepseek.com / anthropic.com 等)。这里只列 pi-ai 嗅探不到、需要显式指定的少数情况。
204
+ let piProvider: string;
205
+ if (inkosProvider?.id === "google") piProvider = "google";
206
+ else if (inkosProvider?.id === "zhipu") piProvider = "zai";
207
+ else if (inkosProvider?.id === "openrouter") piProvider = "openrouter";
208
+ else if (inkosProvider?.id === "githubCopilot") piProvider = "githubCopilot";
209
+ else if (inkosProvider?.id === "ollama") piProvider = "ollama";
210
+ else if (inkosProvider?.api === "anthropic-messages") piProvider = "anthropic";
211
+ else piProvider = provider;
212
+
213
+ const piModel: PiModel<PiApi> = {
214
+ id: modelCard?.deploymentName ?? config.model,
215
+ name: config.model,
216
+ api: piApi,
217
+ provider: piProvider,
218
+ baseUrl,
219
+ // 注意:piModel.reasoning 是"激活 reasoning 模式"标志(会让 pi-ai 把 system 改成 developer role 等),
220
+ // 不是"模型能力"标签。只有用户显式配了 thinkingBudget > 0 才启用 reasoning mode。
221
+ // 千万不要从 lobe abilities.reasoning 自动推导,否则 Moonshot 这类不支持 developer role 的服务
222
+ // 会把 content 吃掉,只返回 reasoning_content(见 R4 bug 1 诊断)。
223
+ reasoning: (config.thinkingBudget ?? 0) > 0,
224
+ input: ["text"] as ("text" | "image")[],
225
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
226
+ contextWindow: modelCard?.contextWindowTokens ?? 128_000,
227
+ maxTokens: modelCard?.maxOutput ?? UNKNOWN_MODEL_FALLBACK_MAX_TOKENS,
228
+ ...(extraHeaders ? { headers: extraHeaders } : {}),
229
+ ...(compat ? { compat } : {}),
230
+ };
231
+
232
+ return {
233
+ provider,
234
+ service: serviceName,
235
+ configSource: config.configSource,
236
+ apiFormat,
237
+ stream,
238
+ proxyUrl: config.proxyUrl,
239
+ _piModel: piModel,
240
+ _apiKey: config.apiKey,
241
+ defaults,
242
+ };
243
+ }
244
+
245
+ function resolvePiApi(
246
+ serviceName: string,
247
+ apiFormat: LLMConfig["apiFormat"] | undefined,
248
+ presetApi: PiApi | undefined,
249
+ ): PiApi {
250
+ if (serviceName === "custom") {
251
+ return apiFormat === "responses" ? "openai-responses" : "openai-completions";
252
+ }
253
+ return (presetApi ?? "openai-completions") as PiApi;
254
+ }
255
+
256
+ function resolveProviderCompat(
257
+ provider: ReturnType<typeof getEndpoint>,
258
+ baseUrl: string,
259
+ ): Record<string, unknown> | undefined {
260
+ const compat = {
261
+ ...(provider?.compat ?? {}),
262
+ ...(baseUrl.includes("generativelanguage.googleapis.com") ? { supportsStore: false } : {}),
263
+ };
264
+ return Object.keys(compat).length > 0 ? compat : undefined;
265
+ }
266
+
267
+ function parseEnvHeaders(): Record<string, string> | undefined {
268
+ const raw = process.env.INKOS_LLM_HEADERS;
269
+ if (!raw) return undefined;
270
+ try {
271
+ const parsed: unknown = JSON.parse(raw);
272
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
273
+ return parsed as Record<string, string>;
274
+ }
275
+ } catch {
276
+ // not JSON — treat as single "Key: Value" pair
277
+ const idx = raw.indexOf(":");
278
+ if (idx > 0) {
279
+ return { [raw.slice(0, idx).trim()]: raw.slice(idx + 1).trim() };
280
+ }
281
+ }
282
+ return undefined;
283
+ }
284
+
285
+ // === Partial Response (stream interrupted but usable content received) ===
286
+
287
+ export class PartialResponseError extends Error {
288
+ readonly partialContent: string;
289
+ constructor(partialContent: string, cause: unknown) {
290
+ super(`Stream interrupted after ${partialContent.length} chars: ${String(cause)}`);
291
+ this.name = "PartialResponseError";
292
+ this.partialContent = partialContent;
293
+ }
294
+ }
295
+
296
+ /** Minimum chars to consider a partial response salvageable (Chinese ~2 chars/word → 500 chars ≈ 250 words) */
297
+ const MIN_SALVAGEABLE_CHARS = 500;
298
+
299
+ /** Keys managed by the provider layer — prevent extra from overriding them. */
300
+ const RESERVED_KEYS = new Set(["max_tokens", "temperature", "model", "messages", "stream"]);
301
+
302
+ function stripReservedKeys(extra: Record<string, unknown>): Record<string, unknown> {
303
+ const result: Record<string, unknown> = {};
304
+ for (const [key, value] of Object.entries(extra)) {
305
+ if (!RESERVED_KEYS.has(key)) result[key] = value;
306
+ }
307
+ return result;
308
+ }
309
+
310
+ // === Fixed-Temperature Model Clamp ===
311
+ //
312
+ // 部分 thinking 模型(如 Moonshot kimi-k2.5/k2.6、kimi-k2-thinking)的 API
313
+ // 硬要求 temperature === 1,其他值会被直接 400 拒绝(Moonshot 返回
314
+ // `invalid temperature: only 1 is allowed for this model`)。
315
+ //
316
+ // inkos 让 writer/validator/architect 各自带 per-call 温度(0.1~1.5),
317
+ // 所以 provider 层统一夹制:如果 bank 里模型卡标了 temperature 字段,
318
+ // 就把 per-call 温度 clamp 到那个值,并对每个模型名打一次 warning。
319
+ //
320
+ // 这个字段只表达"服务端硬约束",普通模型不要标,避免误伤 per-call 调参。
321
+
322
+ const warnedFixedTemperatureModels = new Set<string>();
323
+
324
+ function clampTemperatureForModel(
325
+ service: string | undefined,
326
+ model: string,
327
+ requested: number,
328
+ ): number {
329
+ const card = service ? lookupModel(service, model) : undefined;
330
+ if (card?.temperature === undefined) return requested;
331
+ const locked = card.temperature;
332
+ if (requested === locked) return locked;
333
+ if (!warnedFixedTemperatureModels.has(model)) {
334
+ warnedFixedTemperatureModels.add(model);
335
+ console.warn(
336
+ `[inkos] 模型 "${model}" API 要求 temperature=${locked},已 clamp(原值 ${requested})`,
337
+ );
338
+ }
339
+ return locked;
340
+ }
341
+
342
+ // 仅测试用:清空 warning 去重集合。
343
+ export function __resetFixedTemperatureWarnings(): void {
344
+ warnedFixedTemperatureModels.clear();
345
+ }
346
+
347
+ // === Error Wrapping ===
348
+
349
+ function wrapLLMError(error: unknown, context?: { readonly baseUrl?: string; readonly model?: string; readonly service?: string }): Error {
350
+ const msg = String(error);
351
+ const ctxLine = context
352
+ ? `\n (baseUrl: ${context.baseUrl}, model: ${context.model})`
353
+ : "";
354
+
355
+ if (msg.includes("400")) {
356
+ // 抽上游 error body 的 message / reason / code(和下方 5xx 一致),让真实错因浮到用户面前
357
+ let detail = "";
358
+ if (error && typeof error === "object") {
359
+ const err = error as { error?: unknown; body?: unknown; message?: string };
360
+ const bodyLike = err.error ?? err.body;
361
+ if (bodyLike && typeof bodyLike === "object") {
362
+ const b = bodyLike as { reason?: string; message?: string; code?: number | string; type?: string };
363
+ if (b.message) detail = b.type ? `${b.type}: ${b.message}` : b.message;
364
+ else if (b.reason) detail = b.reason;
365
+ }
366
+ }
367
+ return new Error(
368
+ `API 返回 400(请求参数错误)。${detail ? `上游详情:${detail}。\n` : ""}` +
369
+ `常见原因:\n` +
370
+ ` 1. temperature / max_tokens 超出模型约束(如 Moonshot kimi-k2.X 强制 temperature=1)\n` +
371
+ ` 2. 模型名称不正确或未上架\n` +
372
+ ` 3. 消息格式不兼容(部分服务不支持 system role 或 developer role)${ctxLine}`,
373
+ );
374
+ }
375
+ if (msg.includes("403")) {
376
+ return new Error(
377
+ `API 返回 403 (请求被拒绝)。可能原因:\n` +
378
+ ` 1. API Key 无效或过期\n` +
379
+ ` 2. API 提供方的内容审查拦截了请求(公益/免费 API 常见)\n` +
380
+ ` 3. 账户余额不足\n` +
381
+ ` 建议:用 inkos doctor 测试 API 连通性,或换一个不限制内容的 API 提供方${ctxLine}`,
382
+ );
383
+ }
384
+ if (msg.includes("401")) {
385
+ return new Error(
386
+ `API 返回 401 (未授权)。请检查 .env 中的 INKOS_LLM_API_KEY 是否正确。${ctxLine}`,
387
+ );
388
+ }
389
+ if (msg.includes("429")) {
390
+ return new Error(
391
+ `API 返回 429 (请求过多)。请稍后重试,或检查 API 配额。${ctxLine}`,
392
+ );
393
+ }
394
+ if (
395
+ msg.includes("Connection error")
396
+ || msg.includes("ECONNREFUSED")
397
+ || msg.includes("ENOTFOUND")
398
+ || msg.includes("fetch failed")
399
+ || msg.includes("terminated")
400
+ || msg.includes("UND_ERR_SOCKET")
401
+ || msg.includes("ECONNRESET")
402
+ || msg.includes("ETIMEDOUT")
403
+ || msg.includes("EPIPE")
404
+ ) {
405
+ return new Error(
406
+ `无法连接到 API 服务。可能原因:\n` +
407
+ ` 1. baseUrl 地址不正确(当前:${context?.baseUrl ?? "未知"})\n` +
408
+ ` 2. 网络不通或被防火墙拦截\n` +
409
+ ` 3. API 服务暂时不可用\n` +
410
+ ` 建议:检查 INKOS_LLM_BASE_URL 是否包含完整路径(如 /v1)`,
411
+ );
412
+ }
413
+ // R4 Bug 2: 5xx "status code (no body)" — 尝试从 OpenAI SDK APIError 里抽 body 给用户看具体原因
414
+ // (如 PPIO 的 {"code":500,"reason":"MODEL_NOT_AVAILABLE","message":"model not available"})
415
+ if (msg.includes("status code") && msg.includes("no body")) {
416
+ let detail = "";
417
+ if (error && typeof error === "object") {
418
+ const err = error as { error?: unknown; body?: unknown; message?: string };
419
+ const bodyLike = err.error ?? err.body;
420
+ if (bodyLike && typeof bodyLike === "object") {
421
+ const b = bodyLike as { reason?: string; message?: string; code?: number | string };
422
+ if (b.reason) detail = `${b.reason}${b.message ? `: ${b.message}` : ""}`;
423
+ else if (b.message) detail = b.message;
424
+ }
425
+ }
426
+ return new Error(
427
+ `API 返回 5xx(上游服务异常)。${detail ? `上游详情:${detail}。` : ""}\n` +
428
+ `可能原因:\n` +
429
+ ` 1. 模型在 /models 列表但 inference 未上架(如 PPIO 返回 MODEL_NOT_AVAILABLE)\n` +
430
+ ` 2. 服务端临时故障,稍后重试\n` +
431
+ ` 3. 当前 apikey 无权限调用该模型${ctxLine}`,
432
+ );
433
+ }
434
+ return error instanceof Error ? error : new Error(msg);
435
+ }
436
+
437
+ function collectErrorText(error: unknown, depth = 0): string {
438
+ if (depth > 4 || error === null || error === undefined) return "";
439
+ const parts = [String(error)];
440
+ if (error instanceof Error) {
441
+ parts.push(error.name, error.message);
442
+ const cause = (error as Error & { cause?: unknown }).cause;
443
+ if (cause) parts.push(collectErrorText(cause, depth + 1));
444
+ } else if (typeof error === "object") {
445
+ const err = error as { code?: unknown; cause?: unknown; message?: unknown; name?: unknown };
446
+ if (err.name) parts.push(String(err.name));
447
+ if (err.message) parts.push(String(err.message));
448
+ if (err.code) parts.push(String(err.code));
449
+ if (err.cause) parts.push(collectErrorText(err.cause, depth + 1));
450
+ }
451
+ return parts.join("\n");
452
+ }
453
+
454
+ function isTransientLLMTransportError(error: unknown): boolean {
455
+ const text = collectErrorText(error);
456
+ return [
457
+ "terminated",
458
+ "UND_ERR_SOCKET",
459
+ "ECONNRESET",
460
+ "ETIMEDOUT",
461
+ "EPIPE",
462
+ "socket hang up",
463
+ "other side closed",
464
+ "network socket disconnected",
465
+ ].some((needle) => text.includes(needle));
466
+ }
467
+
468
+ async function withTransientLLMRetry<T>(
469
+ run: () => Promise<T>,
470
+ options?: { readonly enabled?: boolean },
471
+ ): Promise<T> {
472
+ const enabled = options?.enabled ?? true;
473
+ let lastError: unknown;
474
+ for (let attempt = 0; attempt <= TRANSIENT_LLM_RETRIES; attempt++) {
475
+ try {
476
+ return await run();
477
+ } catch (error) {
478
+ lastError = error;
479
+ if (
480
+ !enabled
481
+ || attempt >= TRANSIENT_LLM_RETRIES
482
+ || error instanceof PartialResponseError
483
+ || !isTransientLLMTransportError(error)
484
+ ) {
485
+ throw error;
486
+ }
487
+ }
488
+ }
489
+ throw lastError;
490
+ }
491
+
492
+ function shouldUseNativeCustomTransport(client: LLMClient): boolean {
493
+ if (client.service === "kkaiapi" && client.provider === "openai") {
494
+ return true;
495
+ }
496
+ if (client.service === "custom") {
497
+ if (
498
+ client.configSource === "studio"
499
+ && (client.provider === "openai" || client.provider === "anthropic")
500
+ ) {
501
+ return true;
502
+ }
503
+ return client.provider === "openai" && shouldUseNativeLocalOpenAICompatibleTransport(client);
504
+ }
505
+ return client.service === "ollama"
506
+ && client.provider === "openai"
507
+ && shouldUseNativeLocalOpenAICompatibleTransport(client);
508
+ }
509
+
510
+ function shouldUseNativeLocalOpenAICompatibleTransport(client: LLMClient): boolean {
511
+ return !client._apiKey
512
+ && isApiKeyOptionalForEndpoint({
513
+ provider: client.provider,
514
+ baseUrl: client._piModel?.baseUrl,
515
+ });
516
+ }
517
+
518
+ function buildCustomHeaders(client: LLMClient): Record<string, string> {
519
+ const apiKey = sanitizeHeaderApiKey(client._apiKey);
520
+ return sanitizeHttpHeaders({
521
+ "Content-Type": "application/json",
522
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
523
+ ...(client._piModel?.headers ?? {}),
524
+ }) ?? { "Content-Type": "application/json" };
525
+ }
526
+
527
+ function sanitizeHeaderApiKey(apiKey: string | undefined): string {
528
+ const trimmed = apiKey?.trim() ?? "";
529
+ if (!trimmed) return "";
530
+ if (!/^[\x20-\x7e]+$/.test(trimmed)) {
531
+ throw new Error("API Key contains non-ASCII characters; please remove any pasted Chinese notes or whitespace.");
532
+ }
533
+ return trimmed;
534
+ }
535
+
536
+ function joinSystemPrompt(messages: ReadonlyArray<LLMMessage>): string | undefined {
537
+ const systemParts = messages
538
+ .filter((message) => message.role === "system" && message.content.trim().length > 0)
539
+ .map((message) => message.content.trim());
540
+ return systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
541
+ }
542
+
543
+ function buildChatMessages(messages: ReadonlyArray<LLMMessage>): Array<{ role: string; content: string }> {
544
+ return messages
545
+ .filter((message) => message.role !== "system")
546
+ .map((message) => ({
547
+ role: message.role,
548
+ content: message.content,
549
+ }));
550
+ }
551
+
552
+ function buildAnthropicMessages(messages: ReadonlyArray<LLMMessage>): Array<{ role: "user" | "assistant"; content: string }> {
553
+ return messages
554
+ .filter((message): message is Readonly<LLMMessage> & { role: "user" | "assistant" } => message.role === "user" || message.role === "assistant")
555
+ .map((message) => ({
556
+ role: message.role,
557
+ content: message.content,
558
+ }));
559
+ }
560
+
561
+ function buildResponsesInput(messages: ReadonlyArray<LLMMessage>): Array<{ role: string; content: Array<{ type: "input_text"; text: string }> }> {
562
+ return messages
563
+ .filter((message) => message.role !== "system")
564
+ .map((message) => ({
565
+ role: message.role,
566
+ content: [{ type: "input_text", text: message.content }],
567
+ }));
568
+ }
569
+
570
+ function hasSystemMessages(messages: ReadonlyArray<LLMMessage>): boolean {
571
+ return messages.some((message) => message.role === "system" && message.content.trim().length > 0);
572
+ }
573
+
574
+ function foldSystemMessagesIntoFirstUser(messages: ReadonlyArray<LLMMessage>): LLMMessage[] {
575
+ const system = joinSystemPrompt(messages);
576
+ const nonSystemMessages = messages.filter((message) => message.role !== "system");
577
+ if (!system) return [...nonSystemMessages];
578
+
579
+ const firstUserIndex = nonSystemMessages.findIndex((message) => message.role === "user");
580
+ const prefix = `System instructions:\n${system}\n\nUser request:\n`;
581
+ if (firstUserIndex < 0) {
582
+ return [{ role: "user", content: `System instructions:\n${system}` }, ...nonSystemMessages];
583
+ }
584
+
585
+ return nonSystemMessages.map((message, index) => index === firstUserIndex
586
+ ? { ...message, content: `${prefix}${message.content}` }
587
+ : message);
588
+ }
589
+
590
+ function isSystemRoleUnsupportedErrorText(text: string): boolean {
591
+ const normalized = text.toLowerCase();
592
+ const mentionsSystemRole = normalized.includes("system") && normalized.includes("role");
593
+ if (!mentionsSystemRole) return false;
594
+ return normalized.includes("unsupported")
595
+ || normalized.includes("not support")
596
+ || normalized.includes("does not support")
597
+ || normalized.includes("invalid")
598
+ || normalized.includes("不支持")
599
+ || normalized.includes("不允许");
600
+ }
601
+
602
+ async function readErrorResponse(res: Response): Promise<string> {
603
+ const text = await res.text().catch(() => "");
604
+ try {
605
+ const json = JSON.parse(text) as { error?: { message?: string } | string; detail?: string };
606
+ if (typeof json.error === "string" && json.error) return `${res.status} ${json.error}`;
607
+ if (json.error && typeof json.error === "object" && typeof json.error.message === "string") {
608
+ return `${res.status} ${json.error.message}`;
609
+ }
610
+ if (typeof json.detail === "string" && json.detail) return `${res.status} ${json.detail}`;
611
+ } catch {
612
+ // fall through
613
+ }
614
+ return `${res.status} ${text || res.statusText}`.trim();
615
+ }
616
+
617
+ type ParsedSseEvent = {
618
+ readonly event?: string;
619
+ readonly data?: string;
620
+ };
621
+
622
+ function parseSseEvents(buffer: string): { readonly events: ParsedSseEvent[]; readonly rest: string } {
623
+ const chunks = buffer.split(/\n\n/);
624
+ const rest = chunks.pop() ?? "";
625
+ const events: ParsedSseEvent[] = [];
626
+
627
+ for (const chunk of chunks) {
628
+ const lines = chunk.split(/\r?\n/);
629
+ let eventName: string | undefined;
630
+ const dataLines: string[] = [];
631
+ for (const line of lines) {
632
+ if (line.startsWith("event:")) {
633
+ eventName = line.slice("event:".length).trim();
634
+ } else if (line.startsWith("data:")) {
635
+ dataLines.push(line.slice("data:".length).trimStart());
636
+ }
637
+ }
638
+ if (eventName || dataLines.length > 0) {
639
+ events.push({
640
+ ...(eventName ? { event: eventName } : {}),
641
+ ...(dataLines.length > 0 ? { data: dataLines.join("\n") } : {}),
642
+ });
643
+ }
644
+ }
645
+
646
+ return { events, rest };
647
+ }
648
+
649
+ function extractOpenAITextPart(value: any): string {
650
+ if (typeof value === "string") return value;
651
+ if (Array.isArray(value)) {
652
+ return value
653
+ .map((item) => typeof item?.text === "string" ? item.text : typeof item?.content === "string" ? item.content : "")
654
+ .join("");
655
+ }
656
+ return "";
657
+ }
658
+
659
+ function extractChatContent(json: any): string {
660
+ const message = json?.choices?.[0]?.message;
661
+ return extractOpenAITextPart(message?.content) || extractOpenAITextPart(message?.reasoning_content);
662
+ }
663
+
664
+ function extractChatDeltaContent(json: any): string {
665
+ return extractOpenAITextPart(json?.choices?.[0]?.delta?.content);
666
+ }
667
+
668
+ function extractChatDeltaReasoningContent(json: any): string {
669
+ return extractOpenAITextPart(json?.choices?.[0]?.delta?.reasoning_content);
670
+ }
671
+
672
+ function extractResponsesContent(json: any): string {
673
+ const output = Array.isArray(json?.output) ? json.output : [];
674
+ return output
675
+ .flatMap((item: any) => Array.isArray(item?.content) ? item.content : [])
676
+ .map((part: any) => {
677
+ if (typeof part?.text === "string") return part.text;
678
+ if (typeof part?.content === "string") return part.content;
679
+ if (typeof part?.output_text === "string") return part.output_text;
680
+ return "";
681
+ })
682
+ .join("");
683
+ }
684
+
685
+ function extractAnthropicContent(json: any): string {
686
+ const content = Array.isArray(json?.content) ? json.content : [];
687
+ return content
688
+ .map((part: any) => typeof part?.text === "string" ? part.text : "")
689
+ .join("");
690
+ }
691
+
692
+ async function chatCompletionViaCustomAnthropicCompatible(
693
+ client: LLMClient,
694
+ model: string,
695
+ messages: ReadonlyArray<LLMMessage>,
696
+ resolved: { readonly temperature: number; readonly maxTokens: number; readonly extra: Record<string, unknown> },
697
+ onStreamProgress?: OnStreamProgress,
698
+ onTextDelta?: (text: string) => void,
699
+ ): Promise<LLMResponse> {
700
+ const baseUrl = client._piModel?.baseUrl ?? "";
701
+ const errorCtx = { baseUrl, model, service: client.service };
702
+ const extra = stripReservedKeys(resolved.extra);
703
+ const payload: Record<string, unknown> = {
704
+ model,
705
+ messages: buildAnthropicMessages(messages),
706
+ stream: client.stream,
707
+ max_tokens: resolved.maxTokens,
708
+ temperature: resolved.temperature,
709
+ ...extra,
710
+ };
711
+ const system = joinSystemPrompt(messages);
712
+ if (system) payload.system = system;
713
+
714
+ const apiKey = sanitizeHeaderApiKey(client._apiKey);
715
+ const response = await fetchWithProxy(`${baseUrl.replace(/\/$/, "")}/messages`, {
716
+ method: "POST",
717
+ headers: sanitizeHttpHeaders({
718
+ "User-Agent": INKOS_USER_AGENT,
719
+ "x-api-key": apiKey,
720
+ "anthropic-version": "2023-06-01",
721
+ "Content-Type": "application/json",
722
+ ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
723
+ ...(client._piModel?.headers ?? {}),
724
+ }) ?? { "Content-Type": "application/json" },
725
+ body: JSON.stringify(payload),
726
+ }, client.proxyUrl);
727
+
728
+ if (!response.ok) {
729
+ throw wrapLLMError(new Error(await readErrorResponse(response)), errorCtx);
730
+ }
731
+
732
+ if (!client.stream) {
733
+ const json = await response.json() as any;
734
+ const content = extractAnthropicContent(json);
735
+ if (!content) {
736
+ throw wrapLLMError(new Error("LLM returned empty response"), errorCtx);
737
+ }
738
+ return {
739
+ content,
740
+ usage: {
741
+ promptTokens: json?.usage?.input_tokens ?? 0,
742
+ completionTokens: json?.usage?.output_tokens ?? 0,
743
+ totalTokens: (json?.usage?.input_tokens ?? 0) + (json?.usage?.output_tokens ?? 0),
744
+ },
745
+ };
746
+ }
747
+
748
+ const reader = response.body?.getReader();
749
+ if (!reader) throw wrapLLMError(new Error("Streaming body unavailable"), errorCtx);
750
+ const decoder = new TextDecoder();
751
+ let buffer = "";
752
+ let content = "";
753
+ let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
754
+ const monitor = createStreamMonitor(onStreamProgress);
755
+
756
+ try {
757
+ while (true) {
758
+ const { value, done } = await reader.read();
759
+ if (done) break;
760
+ buffer += decoder.decode(value, { stream: true });
761
+ const parsed = parseSseEvents(buffer);
762
+ buffer = parsed.rest;
763
+ for (const event of parsed.events) {
764
+ if (!event.data) continue;
765
+ const json = JSON.parse(event.data);
766
+ if (json.type === "message_start" && json.message?.usage) {
767
+ usage.promptTokens = json.message.usage.input_tokens ?? usage.promptTokens;
768
+ }
769
+ if (json.type === "content_block_delta" && json.delta?.type === "text_delta" && typeof json.delta.text === "string") {
770
+ content += json.delta.text;
771
+ monitor.onChunk(json.delta.text);
772
+ onTextDelta?.(json.delta.text);
773
+ }
774
+ if (json.type === "message_delta" && json.usage) {
775
+ usage.completionTokens = json.usage.output_tokens ?? usage.completionTokens;
776
+ }
777
+ if (json.type === "message_stop") {
778
+ usage.totalTokens = usage.promptTokens + usage.completionTokens;
779
+ }
780
+ }
781
+ }
782
+ } finally {
783
+ monitor.stop();
784
+ }
785
+
786
+ if (!content) {
787
+ throw wrapLLMError(new Error("LLM returned empty response from stream"), errorCtx);
788
+ }
789
+ if (!usage.totalTokens) {
790
+ usage.totalTokens = usage.promptTokens + usage.completionTokens;
791
+ }
792
+ return { content, usage };
793
+ }
794
+
795
+ async function chatCompletionViaCustomOpenAICompatible(
796
+ client: LLMClient,
797
+ model: string,
798
+ messages: ReadonlyArray<LLMMessage>,
799
+ resolved: { readonly temperature: number; readonly maxTokens: number; readonly extra: Record<string, unknown> },
800
+ onStreamProgress?: OnStreamProgress,
801
+ onTextDelta?: (text: string) => void,
802
+ allowSystemRoleFallback = true,
803
+ ): Promise<LLMResponse> {
804
+ if (client.provider === "anthropic") {
805
+ return chatCompletionViaCustomAnthropicCompatible(client, model, messages, resolved, onStreamProgress, onTextDelta);
806
+ }
807
+ const baseUrl = client._piModel?.baseUrl ?? "";
808
+ const headers = buildCustomHeaders(client);
809
+ const errorCtx = { baseUrl, model, service: client.service };
810
+ const extra = stripReservedKeys(resolved.extra);
811
+
812
+ if (client.apiFormat === "responses") {
813
+ const payload: Record<string, unknown> = {
814
+ model,
815
+ input: buildResponsesInput(messages),
816
+ stream: client.stream,
817
+ store: false,
818
+ max_output_tokens: resolved.maxTokens,
819
+ temperature: resolved.temperature,
820
+ ...extra,
821
+ };
822
+ const instructions = joinSystemPrompt(messages);
823
+ if (instructions) payload.instructions = instructions;
824
+
825
+ const response = await fetchWithProxy(`${baseUrl.replace(/\/$/, "")}/responses`, {
826
+ method: "POST",
827
+ headers,
828
+ body: JSON.stringify(payload),
829
+ }, client.proxyUrl);
830
+ if (!response.ok) {
831
+ throw wrapLLMError(new Error(await readErrorResponse(response)), errorCtx);
832
+ }
833
+
834
+ if (!client.stream) {
835
+ const json = await response.json() as any;
836
+ const content = extractResponsesContent(json);
837
+ if (!content) {
838
+ throw wrapLLMError(new Error("LLM returned empty response"), errorCtx);
839
+ }
840
+ return {
841
+ content,
842
+ usage: {
843
+ promptTokens: json?.usage?.input_tokens ?? 0,
844
+ completionTokens: json?.usage?.output_tokens ?? 0,
845
+ totalTokens: json?.usage?.total_tokens ?? 0,
846
+ },
847
+ };
848
+ }
849
+
850
+ const reader = response.body?.getReader();
851
+ if (!reader) throw wrapLLMError(new Error("Streaming body unavailable"), errorCtx);
852
+ const decoder = new TextDecoder();
853
+ let buffer = "";
854
+ let content = "";
855
+ let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
856
+ const monitor = createStreamMonitor(onStreamProgress);
857
+
858
+ try {
859
+ while (true) {
860
+ const { value, done } = await reader.read();
861
+ if (done) break;
862
+ buffer += decoder.decode(value, { stream: true });
863
+ const parsed = parseSseEvents(buffer);
864
+ buffer = parsed.rest;
865
+ for (const event of parsed.events) {
866
+ if (!event.data) continue;
867
+ const json = JSON.parse(event.data);
868
+ if (json.type === "response.output_text.delta" && typeof json.delta === "string") {
869
+ content += json.delta;
870
+ monitor.onChunk(json.delta);
871
+ onTextDelta?.(json.delta);
872
+ }
873
+ if (json.type === "response.completed") {
874
+ usage = {
875
+ promptTokens: json.response?.usage?.input_tokens ?? 0,
876
+ completionTokens: json.response?.usage?.output_tokens ?? 0,
877
+ totalTokens: json.response?.usage?.total_tokens ?? 0,
878
+ };
879
+ if (!content) {
880
+ content = extractResponsesContent(json.response);
881
+ }
882
+ }
883
+ }
884
+ }
885
+ } finally {
886
+ monitor.stop();
887
+ }
888
+
889
+ if (!content) {
890
+ throw wrapLLMError(new Error("LLM returned empty response from stream"), errorCtx);
891
+ }
892
+ return { content, usage };
893
+ }
894
+
895
+ const payload: Record<string, unknown> = {
896
+ model,
897
+ messages: [
898
+ ...messages
899
+ .filter((message) => message.role === "system")
900
+ .map((message) => ({ role: "system", content: message.content })),
901
+ ...buildChatMessages(messages),
902
+ ],
903
+ stream: client.stream,
904
+ temperature: resolved.temperature,
905
+ max_tokens: resolved.maxTokens,
906
+ ...extra,
907
+ };
908
+ if (client.stream) {
909
+ payload.stream_options = { include_usage: true };
910
+ }
911
+
912
+ const response = await fetchWithProxy(`${baseUrl.replace(/\/$/, "")}/chat/completions`, {
913
+ method: "POST",
914
+ headers,
915
+ body: JSON.stringify(payload),
916
+ }, client.proxyUrl);
917
+ if (!response.ok) {
918
+ const detail = await readErrorResponse(response);
919
+ if (allowSystemRoleFallback && hasSystemMessages(messages) && isSystemRoleUnsupportedErrorText(detail)) {
920
+ return chatCompletionViaCustomOpenAICompatible(
921
+ client,
922
+ model,
923
+ foldSystemMessagesIntoFirstUser(messages),
924
+ resolved,
925
+ onStreamProgress,
926
+ onTextDelta,
927
+ false,
928
+ );
929
+ }
930
+ throw wrapLLMError(new Error(detail), errorCtx);
931
+ }
932
+
933
+ if (!client.stream) {
934
+ const json = await response.json() as any;
935
+ const content = extractChatContent(json);
936
+ if (!content) {
937
+ throw wrapLLMError(new Error("LLM returned empty response"), errorCtx);
938
+ }
939
+ return {
940
+ content,
941
+ usage: {
942
+ promptTokens: json?.usage?.prompt_tokens ?? 0,
943
+ completionTokens: json?.usage?.completion_tokens ?? 0,
944
+ totalTokens: json?.usage?.total_tokens ?? 0,
945
+ },
946
+ };
947
+ }
948
+
949
+ const reader = response.body?.getReader();
950
+ if (!reader) throw wrapLLMError(new Error("Streaming body unavailable"), errorCtx);
951
+ const decoder = new TextDecoder();
952
+ let buffer = "";
953
+ let content = "";
954
+ let reasoningContent = "";
955
+ let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
956
+ const monitor = createStreamMonitor(onStreamProgress);
957
+
958
+ try {
959
+ while (true) {
960
+ const { value, done } = await reader.read();
961
+ if (done) break;
962
+ buffer += decoder.decode(value, { stream: true });
963
+ const parsed = parseSseEvents(buffer);
964
+ buffer = parsed.rest;
965
+ for (const event of parsed.events) {
966
+ if (!event.data || event.data === "[DONE]") continue;
967
+ const json = JSON.parse(event.data);
968
+ const delta = extractChatDeltaContent(json);
969
+ if (delta) {
970
+ content += delta;
971
+ monitor.onChunk(delta);
972
+ onTextDelta?.(delta);
973
+ } else {
974
+ const reasoningDelta = extractChatDeltaReasoningContent(json);
975
+ if (reasoningDelta) {
976
+ reasoningContent += reasoningDelta;
977
+ monitor.onChunk(reasoningDelta);
978
+ }
979
+ }
980
+ if (json?.usage) {
981
+ usage = {
982
+ promptTokens: json.usage.prompt_tokens ?? usage.promptTokens,
983
+ completionTokens: json.usage.completion_tokens ?? usage.completionTokens,
984
+ totalTokens: json.usage.total_tokens ?? usage.totalTokens,
985
+ };
986
+ }
987
+ }
988
+ }
989
+ } finally {
990
+ monitor.stop();
991
+ }
992
+
993
+ const finalContent = content || reasoningContent;
994
+ if (!finalContent) {
995
+ throw wrapLLMError(new Error("LLM returned empty response from stream"), errorCtx);
996
+ }
997
+ return { content: finalContent, usage };
998
+ }
999
+
1000
+ // === Simple Chat (used by all agents via BaseAgent.chat()) ===
1001
+
1002
+ export async function chatCompletion(
1003
+ client: LLMClient,
1004
+ model: string,
1005
+ messages: ReadonlyArray<LLMMessage>,
1006
+ options?: {
1007
+ readonly temperature?: number;
1008
+ readonly maxTokens?: number;
1009
+ readonly webSearch?: boolean;
1010
+ readonly onStreamProgress?: OnStreamProgress;
1011
+ readonly onTextDelta?: (text: string) => void;
1012
+ },
1013
+ ): Promise<LLMResponse> {
1014
+ // C1 (v2.0.0):删除 maxTokensCap 机制。per-call 显式传的 maxTokens 永远不被裁剪。
1015
+ const resolved = {
1016
+ temperature: clampTemperatureForModel(
1017
+ client.service,
1018
+ model,
1019
+ options?.temperature ?? client.defaults.temperature,
1020
+ ),
1021
+ maxTokens: options?.maxTokens ?? client.defaults.maxTokens,
1022
+ extra: client.defaults.extra,
1023
+ };
1024
+ const onStreamProgress = options?.onStreamProgress;
1025
+ const onTextDelta = options?.onTextDelta;
1026
+ const errorCtx = { baseUrl: client._piModel?.baseUrl ?? "(unknown)", model, service: client.service };
1027
+
1028
+ try {
1029
+ return await withTransientLLMRetry(
1030
+ async () => {
1031
+ if (shouldUseNativeCustomTransport(client)) {
1032
+ return chatCompletionViaCustomOpenAICompatible(client, model, messages, resolved, onStreamProgress, onTextDelta);
1033
+ }
1034
+ return chatCompletionViaPiAi(client, model, messages, resolved, onStreamProgress, onTextDelta);
1035
+ },
1036
+ // Retrying after UI text deltas have been emitted can duplicate visible text.
1037
+ { enabled: !onTextDelta },
1038
+ );
1039
+ } catch (error) {
1040
+ // Stream interrupted but partial content is usable — return truncated response
1041
+ if (error instanceof PartialResponseError) {
1042
+ return {
1043
+ content: error.partialContent,
1044
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
1045
+ };
1046
+ }
1047
+ throw wrapLLMError(error, errorCtx);
1048
+ }
1049
+ }
1050
+
1051
+ // === Tool-calling Chat (used by agent loop) ===
1052
+
1053
+ export async function chatWithTools(
1054
+ client: LLMClient,
1055
+ model: string,
1056
+ messages: ReadonlyArray<AgentMessage>,
1057
+ tools: ReadonlyArray<ToolDefinition>,
1058
+ options?: {
1059
+ readonly temperature?: number;
1060
+ readonly maxTokens?: number;
1061
+ },
1062
+ ): Promise<ChatWithToolsResult> {
1063
+ const errorCtx = { baseUrl: client._piModel?.baseUrl ?? "(unknown)", model, service: client.service };
1064
+ try {
1065
+ const resolved = {
1066
+ temperature: clampTemperatureForModel(
1067
+ client.service,
1068
+ model,
1069
+ options?.temperature ?? client.defaults.temperature,
1070
+ ),
1071
+ maxTokens: options?.maxTokens ?? client.defaults.maxTokens,
1072
+ };
1073
+ return await chatWithToolsViaPiAi(client, model, messages, tools, resolved);
1074
+ } catch (error) {
1075
+ throw wrapLLMError(error, errorCtx);
1076
+ }
1077
+ }
1078
+
1079
+ // === pi-ai Unified Implementation ===
1080
+
1081
+ /**
1082
+ * Build a pi-ai Model<Api> for a specific per-call model name.
1083
+ * The base template comes from client._piModel (created in createLLMClient);
1084
+ * we override .id / .name when the caller passes a different model string
1085
+ * (e.g. agent overrides).
1086
+ */
1087
+ function resolvePiModel(client: LLMClient, model: string): PiModel<PiApi> {
1088
+ const base = client._piModel!;
1089
+ if (base.id === model) return base;
1090
+ return { ...base, id: model, name: model };
1091
+ }
1092
+
1093
+ /** Convert inkos LLMMessage[] to pi-ai Context. */
1094
+ function toPiContext(messages: ReadonlyArray<LLMMessage>): PiContext {
1095
+ const systemParts = messages.filter((m) => m.role === "system").map((m) => m.content);
1096
+ const systemPrompt = systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
1097
+ const piMessages = messages
1098
+ .filter((m) => m.role !== "system")
1099
+ .map((m) => {
1100
+ if (m.role === "user") {
1101
+ return { role: "user" as const, content: m.content, timestamp: Date.now() };
1102
+ }
1103
+ // assistant
1104
+ return {
1105
+ role: "assistant" as const,
1106
+ content: [{ type: "text" as const, text: m.content }],
1107
+ api: "openai-completions" as PiApi,
1108
+ provider: "openai",
1109
+ model: "",
1110
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
1111
+ stopReason: "stop" as const,
1112
+ timestamp: Date.now(),
1113
+ };
1114
+ });
1115
+ return { systemPrompt, messages: piMessages };
1116
+ }
1117
+
1118
+ /** Convert inkos AgentMessage[] to pi-ai Context (with tool calls/results). */
1119
+ function agentMessagesToPiContext(messages: ReadonlyArray<AgentMessage>): PiContext {
1120
+ const systemParts = messages.filter((m) => m.role === "system").map((m) => (m as { content: string }).content);
1121
+ const systemPrompt = systemParts.length > 0 ? systemParts.join("\n\n") : undefined;
1122
+ const piMessages: PiContext["messages"] = [];
1123
+ for (const msg of messages) {
1124
+ if (msg.role === "system") continue;
1125
+ if (msg.role === "user") {
1126
+ piMessages.push({ role: "user", content: msg.content, timestamp: Date.now() });
1127
+ continue;
1128
+ }
1129
+ if (msg.role === "assistant") {
1130
+ const content: (PiTextContent | PiToolCall)[] = [];
1131
+ if (msg.content) content.push({ type: "text", text: msg.content });
1132
+ if (msg.toolCalls) {
1133
+ for (const tc of msg.toolCalls) {
1134
+ content.push({
1135
+ type: "toolCall",
1136
+ id: tc.id,
1137
+ name: tc.name,
1138
+ arguments: JSON.parse(tc.arguments),
1139
+ });
1140
+ }
1141
+ }
1142
+ if (content.length === 0) content.push({ type: "text", text: "" });
1143
+ piMessages.push({
1144
+ role: "assistant",
1145
+ content,
1146
+ api: "openai-completions" as PiApi,
1147
+ provider: "openai",
1148
+ model: "",
1149
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
1150
+ stopReason: "stop",
1151
+ timestamp: Date.now(),
1152
+ });
1153
+ continue;
1154
+ }
1155
+ if (msg.role === "tool") {
1156
+ piMessages.push({
1157
+ role: "toolResult",
1158
+ toolCallId: msg.toolCallId,
1159
+ toolName: "",
1160
+ content: [{ type: "text", text: msg.content }],
1161
+ isError: false,
1162
+ timestamp: Date.now(),
1163
+ });
1164
+ }
1165
+ }
1166
+ return { systemPrompt, messages: piMessages };
1167
+ }
1168
+
1169
+ /** Convert inkos ToolDefinition[] to pi-ai Tool[]. */
1170
+ function toPiTools(tools: ReadonlyArray<ToolDefinition>): PiTool[] {
1171
+ return tools.map((t) => ({
1172
+ name: t.name,
1173
+ description: t.description,
1174
+ parameters: t.parameters as PiTool["parameters"],
1175
+ }));
1176
+ }
1177
+
1178
+ async function chatCompletionViaPiAi(
1179
+ client: LLMClient,
1180
+ model: string,
1181
+ messages: ReadonlyArray<LLMMessage>,
1182
+ resolved: { readonly temperature: number; readonly maxTokens: number; readonly extra: Record<string, unknown> },
1183
+ onStreamProgress?: OnStreamProgress,
1184
+ onTextDelta?: (text: string) => void,
1185
+ ): Promise<LLMResponse> {
1186
+ const piModel = resolvePiModel(client, model);
1187
+ const context = toPiContext(messages);
1188
+ const streamOpts = {
1189
+ temperature: resolved.temperature,
1190
+ maxTokens: resolved.maxTokens,
1191
+ apiKey: client._apiKey,
1192
+ headers: mergeUserAgent(piModel.headers),
1193
+ };
1194
+
1195
+ if (!client.stream) {
1196
+ const response = await piCompleteSimple(piModel, context, streamOpts);
1197
+ if (response.stopReason === "error" && response.errorMessage) {
1198
+ throw new Error(response.errorMessage);
1199
+ }
1200
+ const content = response.content
1201
+ .filter((block): block is { type: "text"; text: string } => block.type === "text")
1202
+ .map((block) => block.text)
1203
+ .join("");
1204
+ if (!content) {
1205
+ const diag = `usage=${response.usage.input}+${response.usage.output}`;
1206
+ console.warn(`[inkos] LLM 非流式响应无文本内容 (${diag})`);
1207
+ throw new Error(`LLM returned empty response (${diag})`);
1208
+ }
1209
+ return {
1210
+ content,
1211
+ usage: {
1212
+ promptTokens: response.usage.input,
1213
+ completionTokens: response.usage.output,
1214
+ totalTokens: response.usage.totalTokens,
1215
+ },
1216
+ };
1217
+ }
1218
+
1219
+ const eventStream = piStreamSimple(piModel, context, streamOpts);
1220
+ const chunks: string[] = [];
1221
+ const monitor = createStreamMonitor(onStreamProgress);
1222
+ let inputTokens = 0;
1223
+ let outputTokens = 0;
1224
+
1225
+ try {
1226
+ for await (const event of eventStream) {
1227
+ if (event.type === "text_delta") {
1228
+ chunks.push(event.delta);
1229
+ monitor.onChunk(event.delta);
1230
+ onTextDelta?.(event.delta);
1231
+ }
1232
+ if (event.type === "done" || event.type === "error") {
1233
+ const msg = event.type === "done" ? event.message : event.error;
1234
+ inputTokens = msg.usage.input;
1235
+ outputTokens = msg.usage.output;
1236
+ if (event.type === "error" && msg.errorMessage) {
1237
+ const partial = chunks.join("");
1238
+ if (partial.length >= MIN_SALVAGEABLE_CHARS) {
1239
+ throw new PartialResponseError(partial, new Error(msg.errorMessage));
1240
+ }
1241
+ throw new Error(msg.errorMessage);
1242
+ }
1243
+ }
1244
+ }
1245
+ } catch (streamError) {
1246
+ monitor.stop();
1247
+ if (streamError instanceof PartialResponseError) throw streamError;
1248
+ const partial = chunks.join("");
1249
+ if (partial.length >= MIN_SALVAGEABLE_CHARS) {
1250
+ throw new PartialResponseError(partial, streamError);
1251
+ }
1252
+ throw streamError;
1253
+ } finally {
1254
+ monitor.stop();
1255
+ }
1256
+
1257
+ const content = chunks.join("");
1258
+ if (!content) {
1259
+ const diag = `usage=${inputTokens}+${outputTokens}`;
1260
+ console.warn(`[inkos] LLM 流式响应无文本内容 (${diag})`);
1261
+ throw new Error(`LLM returned empty response from stream (${diag})`);
1262
+ }
1263
+
1264
+ return {
1265
+ content,
1266
+ usage: {
1267
+ promptTokens: inputTokens,
1268
+ completionTokens: outputTokens,
1269
+ totalTokens: inputTokens + outputTokens,
1270
+ },
1271
+ };
1272
+ }
1273
+
1274
+ async function chatWithToolsViaPiAi(
1275
+ client: LLMClient,
1276
+ model: string,
1277
+ messages: ReadonlyArray<AgentMessage>,
1278
+ tools: ReadonlyArray<ToolDefinition>,
1279
+ resolved: { readonly temperature: number; readonly maxTokens: number },
1280
+ ): Promise<ChatWithToolsResult> {
1281
+ const piModel = resolvePiModel(client, model);
1282
+ const context = agentMessagesToPiContext(messages);
1283
+ context.tools = toPiTools(tools);
1284
+ const streamOpts = {
1285
+ temperature: resolved.temperature,
1286
+ maxTokens: resolved.maxTokens,
1287
+ apiKey: client._apiKey,
1288
+ headers: mergeUserAgent(piModel.headers),
1289
+ };
1290
+
1291
+ if (!client.stream) {
1292
+ const response = await piComplete(piModel, context, streamOpts);
1293
+ if (response.stopReason === "error" && response.errorMessage) {
1294
+ throw new Error(response.errorMessage);
1295
+ }
1296
+ const content = response.content
1297
+ .filter((block): block is { type: "text"; text: string } => block.type === "text")
1298
+ .map((block) => block.text)
1299
+ .join("");
1300
+ const toolCalls = response.content
1301
+ .filter((block): block is PiToolCall => block.type === "toolCall")
1302
+ .map((block) => ({
1303
+ id: block.id,
1304
+ name: block.name,
1305
+ arguments: JSON.stringify(block.arguments),
1306
+ }));
1307
+ return { content, toolCalls };
1308
+ }
1309
+
1310
+ const eventStream = piStream(piModel, context, streamOpts);
1311
+ let content = "";
1312
+ const toolCalls: ToolCall[] = [];
1313
+
1314
+ for await (const event of eventStream) {
1315
+ if (event.type === "text_delta") {
1316
+ content += event.delta;
1317
+ }
1318
+ if (event.type === "toolcall_end") {
1319
+ toolCalls.push({
1320
+ id: event.toolCall.id,
1321
+ name: event.toolCall.name,
1322
+ arguments: JSON.stringify(event.toolCall.arguments),
1323
+ });
1324
+ }
1325
+ if (event.type === "error" && event.error.errorMessage) {
1326
+ throw new Error(event.error.errorMessage);
1327
+ }
1328
+ }
1329
+
1330
+ return { content, toolCalls };
1331
+ }