thatgfsj-code 2.2.9 → 3.0.1

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 (190) hide show
  1. package/CHANGELOG.md +15 -67
  2. package/dist/app/index.d.ts +28 -5
  3. package/dist/app/index.d.ts.map +1 -1
  4. package/dist/app/index.js +76 -10
  5. package/dist/app/index.js.map +1 -1
  6. package/dist/cache/fingerprint.d.ts +49 -0
  7. package/dist/cache/fingerprint.d.ts.map +1 -0
  8. package/dist/cache/fingerprint.js +100 -0
  9. package/dist/cache/fingerprint.js.map +1 -0
  10. package/dist/cache/index.d.ts +15 -0
  11. package/dist/cache/index.d.ts.map +1 -0
  12. package/dist/cache/index.js +15 -0
  13. package/dist/cache/index.js.map +1 -0
  14. package/dist/cache/smartModel.d.ts +40 -0
  15. package/dist/cache/smartModel.d.ts.map +1 -0
  16. package/dist/cache/smartModel.js +43 -0
  17. package/dist/cache/smartModel.js.map +1 -0
  18. package/dist/cache/stats.d.ts +93 -0
  19. package/dist/cache/stats.d.ts.map +1 -0
  20. package/dist/cache/stats.js +155 -0
  21. package/dist/cache/stats.js.map +1 -0
  22. package/dist/cache/volatile.d.ts +32 -0
  23. package/dist/cache/volatile.d.ts.map +1 -0
  24. package/dist/cache/volatile.js +44 -0
  25. package/dist/cache/volatile.js.map +1 -0
  26. package/dist/cmd/index.js +49 -57
  27. package/dist/cmd/index.js.map +1 -1
  28. package/dist/config/index.d.ts.map +1 -1
  29. package/dist/config/index.js +8 -0
  30. package/dist/config/index.js.map +1 -1
  31. package/dist/config/types.d.ts +14 -0
  32. package/dist/config/types.d.ts.map +1 -1
  33. package/dist/llm/anthropic.d.ts +55 -5
  34. package/dist/llm/anthropic.d.ts.map +1 -1
  35. package/dist/llm/anthropic.js +172 -26
  36. package/dist/llm/anthropic.js.map +1 -1
  37. package/dist/llm/index.d.ts +26 -8
  38. package/dist/llm/index.d.ts.map +1 -1
  39. package/dist/llm/index.js +83 -15
  40. package/dist/llm/index.js.map +1 -1
  41. package/dist/llm/openai.d.ts +28 -8
  42. package/dist/llm/openai.d.ts.map +1 -1
  43. package/dist/llm/openai.js +58 -8
  44. package/dist/llm/openai.js.map +1 -1
  45. package/dist/llm/provider.d.ts +40 -8
  46. package/dist/llm/provider.d.ts.map +1 -1
  47. package/dist/llm/provider.js +8 -1
  48. package/dist/llm/provider.js.map +1 -1
  49. package/dist/prompts/index.d.ts +32 -0
  50. package/dist/prompts/index.d.ts.map +1 -1
  51. package/dist/prompts/index.js +33 -11
  52. package/dist/prompts/index.js.map +1 -1
  53. package/dist/session/index.d.ts +32 -3
  54. package/dist/session/index.d.ts.map +1 -1
  55. package/dist/session/index.js +51 -36
  56. package/dist/session/index.js.map +1 -1
  57. package/dist/skills/brainstorming.js +33 -33
  58. package/dist/skills/code-review.js +35 -35
  59. package/dist/skills/executing-plans.js +18 -18
  60. package/dist/skills/frontend-design.js +25 -25
  61. package/dist/skills/git-workflow.js +26 -26
  62. package/dist/skills/improve-architecture.js +28 -28
  63. package/dist/skills/neuroweave.js +37 -37
  64. package/dist/skills/playwright.js +62 -62
  65. package/dist/skills/prototype.js +20 -20
  66. package/dist/skills/subagent.js +19 -19
  67. package/dist/skills/supabase.js +34 -34
  68. package/dist/skills/systematic-debugging.js +34 -34
  69. package/dist/skills/tdd.js +29 -29
  70. package/dist/skills/triage.js +25 -25
  71. package/dist/skills/verification.js +22 -22
  72. package/dist/skills/writing-plans.js +32 -32
  73. package/dist/tools/nwt.js +16 -16
  74. package/dist/tui/app.d.ts.map +1 -1
  75. package/dist/tui/app.js +9 -2
  76. package/dist/tui/app.js.map +1 -1
  77. package/dist/tui/components/Header.d.ts +8 -0
  78. package/dist/tui/components/Header.d.ts.map +1 -1
  79. package/dist/tui/components/Header.js +17 -4
  80. package/dist/tui/components/Header.js.map +1 -1
  81. package/dist/tui/components/InitWizard.d.ts +12 -1
  82. package/dist/tui/components/InitWizard.d.ts.map +1 -1
  83. package/dist/tui/components/InitWizard.js +58 -3
  84. package/dist/tui/components/InitWizard.js.map +1 -1
  85. package/dist/tui/components/ModelSelector.d.ts +3 -1
  86. package/dist/tui/components/ModelSelector.d.ts.map +1 -1
  87. package/dist/tui/components/ModelSelector.js +24 -7
  88. package/dist/tui/components/ModelSelector.js.map +1 -1
  89. package/dist/tui/components/ToolCall.d.ts.map +1 -1
  90. package/dist/tui/components/ToolCall.js +1 -6
  91. package/dist/tui/components/ToolCall.js.map +1 -1
  92. package/dist/tui/components/UserInput.d.ts.map +1 -1
  93. package/dist/tui/components/UserInput.js +20 -0
  94. package/dist/tui/components/UserInput.js.map +1 -1
  95. package/dist/tui/hooks/useChat.d.ts +22 -0
  96. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  97. package/dist/tui/hooks/useChat.js +82 -46
  98. package/dist/tui/hooks/useChat.js.map +1 -1
  99. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  100. package/dist/tui/hooks/useCommands.js +57 -0
  101. package/dist/tui/hooks/useCommands.js.map +1 -1
  102. package/dist/tui/welcome.d.ts.map +1 -1
  103. package/dist/tui/welcome.js +2 -3
  104. package/dist/tui/welcome.js.map +1 -1
  105. package/dist/types.d.ts +79 -6
  106. package/dist/types.d.ts.map +1 -1
  107. package/dist/types.js +7 -0
  108. package/dist/types.js.map +1 -1
  109. package/dist/utils/stableStringify.d.ts +21 -0
  110. package/dist/utils/stableStringify.d.ts.map +1 -0
  111. package/dist/utils/stableStringify.js +53 -0
  112. package/dist/utils/stableStringify.js.map +1 -0
  113. package/dist/utils/thinking.d.ts.map +1 -1
  114. package/dist/utils/thinking.js +0 -3
  115. package/dist/utils/thinking.js.map +1 -1
  116. package/package.json +7 -6
  117. package/src/app/index.ts +189 -124
  118. package/src/cache/fingerprint.ts +101 -0
  119. package/src/cache/index.ts +15 -0
  120. package/src/cache/smartModel.ts +52 -0
  121. package/src/cache/stats.ts +199 -0
  122. package/src/cache/volatile.ts +47 -0
  123. package/src/cmd/index.tsx +288 -292
  124. package/src/config/index.ts +156 -148
  125. package/src/config/providers.ts +234 -234
  126. package/src/config/types.ts +67 -53
  127. package/src/hooks/index.ts +111 -111
  128. package/src/llm/anthropic.ts +388 -243
  129. package/src/llm/gemini.ts +169 -169
  130. package/src/llm/index.ts +265 -200
  131. package/src/llm/openai.ts +243 -196
  132. package/src/llm/provider.ts +66 -34
  133. package/src/prompts/index.ts +260 -220
  134. package/src/session/compactor.ts +103 -103
  135. package/src/session/index.ts +181 -168
  136. package/src/session/message.ts +42 -42
  137. package/src/skills/brainstorming.ts +43 -43
  138. package/src/skills/code-review.ts +45 -45
  139. package/src/skills/executing-plans.ts +27 -27
  140. package/src/skills/frontend-design.ts +35 -35
  141. package/src/skills/git-workflow.ts +36 -36
  142. package/src/skills/improve-architecture.ts +38 -38
  143. package/src/skills/index.ts +136 -136
  144. package/src/skills/neuroweave.ts +47 -47
  145. package/src/skills/playwright.ts +72 -72
  146. package/src/skills/prototype.ts +30 -30
  147. package/src/skills/subagent.ts +28 -28
  148. package/src/skills/supabase.ts +44 -44
  149. package/src/skills/systematic-debugging.ts +44 -44
  150. package/src/skills/tdd.ts +39 -39
  151. package/src/skills/triage.ts +35 -35
  152. package/src/skills/verification.ts +31 -31
  153. package/src/skills/writing-plans.ts +42 -42
  154. package/src/tools/nwt.ts +598 -598
  155. package/src/tools/types.ts +122 -122
  156. package/src/tui/app.tsx +200 -186
  157. package/src/tui/components/ChatList.tsx +41 -41
  158. package/src/tui/components/ChatMessage.tsx +54 -54
  159. package/src/tui/components/Header.tsx +56 -29
  160. package/src/tui/components/InitWizard.tsx +217 -132
  161. package/src/tui/components/Markdown.tsx +35 -35
  162. package/src/tui/components/ModelSelector.tsx +107 -87
  163. package/src/tui/components/StatusBar.tsx +30 -30
  164. package/src/tui/components/Thinking.tsx +17 -17
  165. package/src/tui/components/ToolCall.tsx +102 -107
  166. package/src/tui/components/UserInput.tsx +151 -131
  167. package/src/tui/hooks/useChat.ts +287 -238
  168. package/src/tui/hooks/useCommands.ts +234 -176
  169. package/src/tui/index.ts +6 -6
  170. package/src/tui/welcome.ts +177 -178
  171. package/src/types.ts +104 -42
  172. package/src/utils/diff.ts +71 -71
  173. package/src/utils/project.ts +99 -99
  174. package/src/utils/stableStringify.ts +47 -0
  175. package/src/utils/thinking.ts +118 -121
  176. package/tests/cache/fingerprint.test.ts +75 -0
  177. package/tests/cache/providerCatalog.test.ts +31 -0
  178. package/tests/cache/stableStringify.test.ts +43 -0
  179. package/tests/cache/stats.test.ts +146 -0
  180. package/tests/cache/volatile.test.ts +75 -0
  181. package/tests/smoke-pollution.mjs +78 -0
  182. package/tests/smoke-thinking.mjs +110 -0
  183. package/tests/smoke-tool-stream.mjs +69 -0
  184. package/tests/smoke-tool.mjs +117 -0
  185. package/.nwt/meta.json +0 -5
  186. package/dist/version.d.ts +0 -16
  187. package/dist/version.d.ts.map +0 -1
  188. package/dist/version.js +0 -16
  189. package/dist/version.js.map +0 -1
  190. package/src/version.ts +0 -16
package/CHANGELOG.md CHANGED
@@ -4,77 +4,25 @@ All notable changes to **Thatgfsj Code** are documented here. The format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/) and the project adheres to
5
5
  [Semantic Versioning](https://semver.org/).
6
6
 
7
- ## [2.2.8 / 产品 0.4.5] - 2026-07-06 - 边界测试驱动的 5 处修复
7
+ ## [3.0.0 / 产品 0.5.0] - 2026-08-08 - Reasonix 风格 Prompt Caching 架构
8
8
 
9
- > 双版本号方案:
9
+ > **重大版本变更**:流式协议 `AsyncGenerator<string>` → `AsyncGenerator<StreamChunk>`。
10
+ > 自定义依赖 `LLMService.chatStream` / `App.streamResponse` 的下游代码需要适配。
10
11
  >
11
- > | 位置 | 版本 | 含义 |
12
- > |---|---|---|
13
- > | `package.json` `"version"` | `2.2.8` | npm 包版本,正常递增 (+0.0.1) |
14
- > | `src/cmd/index.tsx` `.version(...)` | `0.4.5` | **产品版本** (`gfcode --version` 看到的) |
15
-
16
- ### Fixed (边界测试 `tests/edge-*.mjs` 捕获的真实 bug)
17
-
18
- **Bug 1: ToolCall 在 `result === null` 时误渲染** — 旧代码
19
- `tool.result !== undefined ? ... : null` 把 `null` 当有效结果,空字符串
20
- 被显示。改成 `tool.result !== undefined && tool.result !== null`,
21
- `null` 现在进入 pending 状态,显示 ⏳ running。
22
-
23
- **Bug 2: `SessionManager.truncate(N)` 不生效** — compactor 的
24
- `preserveRecent` 默认 10,比 `maxMessages` 大,所以 `truncate(2)`
25
- 触发 compact 时,`others.length <= preserveRecent` 分支返回原消息。
26
- 改成 `truncate(N)` 时同步 `preserveRecent = N - 1`,让 max 边界真
27
- 的生效。
28
-
29
- **Bug 3: thinking 正则漏掉 `<thinking>` delimiter** — 用户实际会话
30
- 里模型用的是 `<thinking>...</thinking>` 不是 `<<<think>...</think>`。
31
- 加上 `<thinking>` / `<THINK>` / `[think]` 三种额外格式,总共 6 个
32
- delimiter。
33
-
34
- **Bug 4: pollution 过滤 STRONG 模式过宽** — `/\[已中断\]/` 在消息
35
- **任何位置** 都触发 drop,导致 300 字的中段提到这个标记的合法
36
- 解释型消息被误删。改成所有 STRONG 也受 length/question/temporal
37
- 三个门控,长消息中段出现的引用不会被误杀。
38
-
39
- **Bug 5: 1.0.4 源码里没有 `npm test` script** — `prepublishOnly`
40
- 引用了它,但 1.0.4 tarball 缺这个 script,导致 release 后用户跑
41
- `npm test` 会失败。加上 npm test 聚合脚本 + `prepublishOnly`
42
- 显式跑 build + test。
12
+ > 参考:DeepSeek-Reasonix (esengine/DeepSeek-Reasonix) 的"不可变前缀 + 追加日志 + 易变草稿"三支柱。
43
13
 
44
14
  ### Added
45
-
46
- - **5 套边界测试** (`tests/edge-*.mjs`),共 **118 个用例**,覆盖
47
- - thinking compression: 嵌套 delimiter、无 closing tag、大小写、
48
- whitespace-only、超长输入、混合 `bcdf` (Case O)
49
- - pollution filter: 大小写、unicode 变种、`?` / 中文 `?` 时间
50
- 副词、`null byte`、markdown 装饰
51
- - ToolCall 渲染: `null` vs `undefined`, `\\r\\n`, 10000 字符
52
- 单行,边界 8/9 行
53
- - SessionManager: user msg、1MB msg、`getMessages()` 不可变性、
54
- `truncate(N)` 边界
55
- - CLI 参数: -V, --version, -h, --show-thinking, -m, -i, 1000 char
56
- prompt, unicode prompt, 含 shell 元字符的 prompt
57
- - **`npm test`** 脚本 (聚合 8 套测试, 181 / 181 pass)
58
- - **`prepublishOnly`** (build + test,确保发布前必过)
59
- - **`tests/` 加入 `.gitignore`** — 之前的 tarball 误把 dev-time
60
- smoke 测试发到 npm 了
61
-
62
- ### Test Summary
63
-
64
- ```
65
- smoke-thinking: 28 / 28 pass
66
- smoke-tool: 18 / 18 pass
67
- smoke-pollution: 17 / 17 pass
68
- edge-thinking: 28 / 28 pass
69
- edge-tool: 25 / 25 pass
70
- edge-pollution: 23 / 23 pass
71
- edge-session: 27 / 27 pass
72
- edge-cli: 15 / 15 pass
73
- ────────────────
74
- total: 181 / 181 pass
75
- ```
76
-
77
- ---
15
+ - **M1 — 流式协议重构**:干掉 `@@TOOL@@` 哨兵字符串;`StreamChunk` 升级为结构化 union(text / tool_calls / thinking / usage)。
16
+ - **M2 Anthropic prompt cache**:`system` 改为 block 数组,最后一块挂 `cache_control: { type: 'ephemeral' }`;工具列表最后一个也挂 marker;新增 `anthropic-beta: prompt-caching-2024-07-31` header;修「第二条 system 消息被丢弃」bug。
17
+ - **M3 缓存统计与 UI**:`CacheStatsStore` 持久化到 `~/.thatgfsj/cache-stats.json`;TUI Header 显示 `⚡ 命中率 87%`;新增 `/cache` 命令显示 24h 命中率曲线。
18
+ - **M4 — 高级特性**:`VolatileScratch` 隔离 thinking;Tool-call Repair(append `[TOOL_REPAIR]` 而非修改既有消息);`smartModel.shouldDowngrade` 智能路由;`Config.cache` 字段 + InitWizard 两步缓存配置。
19
+ - **OpenAI provider 缓存友好**:`stableStringify` 保证 JSON 字节级稳定;流末尾 yield `{ type: 'usage' }` chunk。
20
+ - **28 个 vitest 单元测试**(`tests/cache/`)。
21
+
22
+ ### Changed
23
+ - `SessionManager.autoCompact` 不再 mutate messages,改为通过 `onSuggestNewSession` 回调通知用户调 `/new`。Reasonix 原则:超长开新会话,保护上游缓存前缀。
24
+ - `system prompt` 片段顺序调整:NWT history 和 Date 移到末尾(易变区),前 6 段是 Immutable Prefix。
25
+ - `LLMService.fromConfig` 接收 `Config.cache` 透传给 provider。
78
26
 
79
27
  ## [2.2.7 / 产品 0.4.4] - 2026-07-06 - 烟囱测试驱动的 anti-pollution 过滤收紧
80
28
 
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * App - Core application singleton
3
3
  * Simplified: directly uses LLMService (which has built-in agent loop)
4
+ *
5
+ * v3.0.0+: streamResponse yields structured StreamChunk
6
+ * - runPrompt streams { type: 'text' } chunks to stdout and captures the
7
+ * final usage for cache stats recording.
4
8
  */
5
9
  import { ConfigManager } from '../config/index.js';
6
10
  import { LLMService } from '../llm/index.js';
@@ -9,7 +13,8 @@ import { ToolRegistry } from '../tools/index.js';
9
13
  import { HookManager } from '../hooks/index.js';
10
14
  import { SystemPromptBuilder } from '../prompts/index.js';
11
15
  import { SkillRegistry } from '../skills/index.js';
12
- import type { ChatMessage } from '../types.js';
16
+ import { CacheStatsStore } from '../cache/stats.js';
17
+ import type { ChatMessage, ChatResponse, StreamChunk, Usage } from '../types.js';
13
18
  export declare class App {
14
19
  config: ConfigManager;
15
20
  llm: LLMService;
@@ -19,7 +24,13 @@ export declare class App {
19
24
  prompts: SystemPromptBuilder;
20
25
  skills: SkillRegistry;
21
26
  /**
22
- * v2.2.5 (product 0.4.2): toggle <think> block compression. Default
27
+ * v3.0.0: persistent cache stats store. The single source of truth for
28
+ * cache hit-rate and estimated savings, surfaced through the TUI Header
29
+ * and the /cache command.
30
+ */
31
+ cacheStats: CacheStatsStore;
32
+ /**
33
+ * v2.2.5 (product 0.4.2): toggle block compression. Default
23
34
  * true. Toggled by `--show-thinking` on the CLI or `/thinking on|off`
24
35
  * in the REPL.
25
36
  */
@@ -29,8 +40,17 @@ export declare class App {
29
40
  /**
30
41
  * Stream a response for the current session messages.
31
42
  * The LLMService handles the full agent loop internally.
43
+ *
44
+ * Yields structured StreamChunks. Returns the final ChatResponse (with usage
45
+ * if the provider reported it) so the caller can record cache stats.
46
+ *
47
+ * Implementation note: we drain the inner stream manually so the final
48
+ * ChatResponse returned by LLMService.chatStream is propagated as this
49
+ * generator's return value. Using yield* doesn't carry the return value
50
+ * through TS's AsyncGenerator<T, R> type inference in this version of
51
+ * TypeScript, so we wrap with an inner for-await and explicit return.
32
52
  */
33
- streamResponse(messages?: ChatMessage[]): AsyncGenerator<string, void>;
53
+ streamResponse(messages?: ChatMessage[]): AsyncGenerator<StreamChunk, ChatResponse>;
34
54
  /**
35
55
  * Run a single prompt (non-interactive mode)
36
56
  *
@@ -38,10 +58,13 @@ export declare class App {
38
58
  * uses addMessageSafe, which drops the message if it contains
39
59
  * pollution markers like "[已中断]".
40
60
  *
41
- * v2.2.5 (product 0.4.2): persistence also strips <think> blocks
61
+ * v2.2.5 (product 0.4.2): persistence also strips blocks
42
62
  * (and similar reasoning delimiters) when showThinking is false,
43
63
  * so the conversation log stays compact.
64
+ *
65
+ * v3.0.0: yields structured StreamChunks; final usage is captured
66
+ * into onUsage callback for cache stats persistence.
44
67
  */
45
- runPrompt(prompt: string): Promise<string>;
68
+ runPrompt(prompt: string, onUsage?: (usage: Usage) => void): Promise<string>;
46
69
  }
47
70
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD,OAAO,KAAK,EAAE,WAAW,EAAgB,MAAM,aAAa,CAAC;AAE7D,qBAAa,GAAG;IACd,MAAM,EAAE,aAAa,CAAC;IACtB,GAAG,EAAE,UAAU,CAAC;IAChB,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,YAAY,CAAC;IACpB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAS;IAE9B,OAAO;WAkBM,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC;IA+BnC;;;OAGG;IACI,cAAc,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,GAAG,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC;IAO7E;;;;;;;;;;OAUG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAsBjD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEjF,qBAAa,GAAG;IACd,MAAM,EAAE,aAAa,CAAC;IACtB,GAAG,EAAE,UAAU,CAAC;IAChB,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,YAAY,CAAC;IACpB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,mBAAmB,CAAC;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB;;;;OAIG;IACH,UAAU,EAAE,eAAe,CAAC;IAC5B;;;;OAIG;IACH,YAAY,EAAE,OAAO,CAAS;IAE9B,OAAO;WAoBM,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC;IAyCnC;;;;;;;;;;;;OAYG;IACI,cAAc,CAAC,QAAQ,CAAC,EAAE,WAAW,EAAE,GAAG,cAAc,CAAC,WAAW,EAAE,YAAY,CAAC;IA2B1F;;;;;;;;;;;;;OAaG;IACG,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;CAgCnF"}
package/dist/app/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * App - Core application singleton
3
3
  * Simplified: directly uses LLMService (which has built-in agent loop)
4
+ *
5
+ * v3.0.0+: streamResponse yields structured StreamChunk
6
+ * - runPrompt streams { type: 'text' } chunks to stdout and captures the
7
+ * final usage for cache stats recording.
4
8
  */
5
9
  import { ConfigManager } from '../config/index.js';
6
10
  import { LLMService } from '../llm/index.js';
@@ -9,6 +13,7 @@ import { ToolRegistry } from '../tools/index.js';
9
13
  import { HookManager } from '../hooks/index.js';
10
14
  import { SystemPromptBuilder } from '../prompts/index.js';
11
15
  import { SkillRegistry } from '../skills/index.js';
16
+ import { CacheStatsStore } from '../cache/stats.js';
12
17
  import { compressThinking } from '../utils/thinking.js';
13
18
  export class App {
14
19
  config;
@@ -19,12 +24,18 @@ export class App {
19
24
  prompts;
20
25
  skills;
21
26
  /**
22
- * v2.2.5 (product 0.4.2): toggle <think> block compression. Default
27
+ * v3.0.0: persistent cache stats store. The single source of truth for
28
+ * cache hit-rate and estimated savings, surfaced through the TUI Header
29
+ * and the /cache command.
30
+ */
31
+ cacheStats;
32
+ /**
33
+ * v2.2.5 (product 0.4.2): toggle block compression. Default
23
34
  * true. Toggled by `--show-thinking` on the CLI or `/thinking on|off`
24
35
  * in the REPL.
25
36
  */
26
37
  showThinking = false;
27
- constructor(config, llm, session, tools, hooks, prompts, skills) {
38
+ constructor(config, llm, session, tools, hooks, prompts, skills, cacheStats) {
28
39
  this.config = config;
29
40
  this.llm = llm;
30
41
  this.session = session;
@@ -32,12 +43,21 @@ export class App {
32
43
  this.hooks = hooks;
33
44
  this.prompts = prompts;
34
45
  this.skills = skills;
46
+ this.cacheStats = cacheStats;
35
47
  }
36
48
  static async create() {
37
49
  const config = await ConfigManager.load();
38
50
  const aiConfig = config.getAIConfig();
39
51
  const llm = LLMService.fromConfig(aiConfig);
52
+ const cacheStats = new CacheStatsStore();
40
53
  const session = new SessionManager(config.get().contextLength || 50);
54
+ // v3.0.0: do not mutate messages when context grows — instead, surface
55
+ // a "consider /new" toast via onSuggestNewSession. The TUI wires this
56
+ // up in app.tsx; in CLI single-prompt mode it's a no-op (one-shot).
57
+ session.onSuggestNewSession = (info) => {
58
+ console.warn(`\n ⚠️ 上下文较长(${info.currentLength}/${info.max})。` +
59
+ `建议调 /new 开新会话(NWT 已自动归档历史)\n`);
60
+ };
41
61
  const tools = new ToolRegistry();
42
62
  const hooks = new HookManager();
43
63
  const skills = new SkillRegistry();
@@ -56,17 +76,47 @@ export class App {
56
76
  skillsPrompt: skills.getActivePrompts(),
57
77
  });
58
78
  session.addMessage('system', prompts.build());
59
- return new App(config, llm, session, tools, hooks, prompts, skills);
79
+ return new App(config, llm, session, tools, hooks, prompts, skills, cacheStats);
60
80
  }
61
81
  /**
62
82
  * Stream a response for the current session messages.
63
83
  * The LLMService handles the full agent loop internally.
84
+ *
85
+ * Yields structured StreamChunks. Returns the final ChatResponse (with usage
86
+ * if the provider reported it) so the caller can record cache stats.
87
+ *
88
+ * Implementation note: we drain the inner stream manually so the final
89
+ * ChatResponse returned by LLMService.chatStream is propagated as this
90
+ * generator's return value. Using yield* doesn't carry the return value
91
+ * through TS's AsyncGenerator<T, R> type inference in this version of
92
+ * TypeScript, so we wrap with an inner for-await and explicit return.
64
93
  */
65
94
  async *streamResponse(messages) {
66
95
  const msgs = messages || this.session.getMessages();
67
- for await (const chunk of this.llm.chatStream(msgs)) {
68
- yield chunk;
96
+ const inner = this.llm.chatStream(msgs);
97
+ const debugUsage = !!process.env.GFCODE_DEBUG_USAGE;
98
+ let next = await inner.next();
99
+ while (!next.done) {
100
+ // Forward chunks unchanged, but capture usage into the persistent
101
+ // cache stats store so the TUI Header / /cache command can read it.
102
+ if (next.value && next.value.type === 'usage') {
103
+ try {
104
+ this.cacheStats.record(next.value.usage);
105
+ }
106
+ catch { /* best-effort */ }
107
+ if (debugUsage) {
108
+ // v3.0.0 DEBUG: dump raw usage fields to stderr so the user can
109
+ // confirm whether the upstream provider/relay forwards cache stats.
110
+ // Enable with: GFCODE_DEBUG_USAGE=1 gfcode ...
111
+ process.stderr.write('[debug_usage] ' + JSON.stringify(next.value.usage) + '\n');
112
+ }
113
+ }
114
+ yield next.value;
115
+ next = await inner.next();
69
116
  }
117
+ // The generator's return value (ChatResponse with usage) is propagated
118
+ // to callers via `for await ... await streamResponse.next()` semantics.
119
+ return next.value;
70
120
  }
71
121
  /**
72
122
  * Run a single prompt (non-interactive mode)
@@ -75,17 +125,26 @@ export class App {
75
125
  * uses addMessageSafe, which drops the message if it contains
76
126
  * pollution markers like "[已中断]".
77
127
  *
78
- * v2.2.5 (product 0.4.2): persistence also strips <think> blocks
128
+ * v2.2.5 (product 0.4.2): persistence also strips blocks
79
129
  * (and similar reasoning delimiters) when showThinking is false,
80
130
  * so the conversation log stays compact.
131
+ *
132
+ * v3.0.0: yields structured StreamChunks; final usage is captured
133
+ * into onUsage callback for cache stats persistence.
81
134
  */
82
- async runPrompt(prompt) {
135
+ async runPrompt(prompt, onUsage) {
83
136
  this.session.addMessage('user', prompt);
84
137
  let fullResponse = '';
138
+ let finalUsage;
85
139
  try {
86
140
  for await (const chunk of this.streamResponse()) {
87
- process.stdout.write(chunk);
88
- fullResponse += chunk;
141
+ if (chunk.type === 'text' && chunk.content) {
142
+ process.stdout.write(chunk.content);
143
+ fullResponse += chunk.content;
144
+ }
145
+ else if (chunk.type === 'usage') {
146
+ finalUsage = chunk.usage;
147
+ }
89
148
  }
90
149
  }
91
150
  catch (err) {
@@ -95,9 +154,16 @@ export class App {
95
154
  throw err;
96
155
  }
97
156
  console.log();
98
- // v2.2.5: compress <think> blocks before persisting.
157
+ // v2.2.5: compress blocks before persisting.
99
158
  const toPersist = compressThinking(fullResponse, this.showThinking);
100
159
  this.session.addMessageSafe('assistant', toPersist);
160
+ // v3.0.0: forward usage to caller (CLI single-shot mode records stats too)
161
+ if (finalUsage && onUsage) {
162
+ try {
163
+ onUsage(finalUsage);
164
+ }
165
+ catch { /* best-effort */ }
166
+ }
101
167
  return fullResponse;
102
168
  }
103
169
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/app/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Cache fingerprints — small, stable hashes used to detect when the
3
+ * "cacheable prefix" of an LLM request changes between rounds.
4
+ *
5
+ * Why this exists:
6
+ * Anthropic prompt cache and DeepSeek automatic prefix cache both break
7
+ * when the request body changes upstream of the cache breakpoint. To
8
+ * debug "why is my hit-rate 0%", we want a quick fingerprint of the
9
+ * things that *should* be cacheable (tools + system prefix) so we can
10
+ * log it on every request and compare across rounds.
11
+ *
12
+ * The fingerprint itself is NOT used to decide anything (the upstream
13
+ * provider does the actual cache lookup). It's purely a debug /
14
+ * observability tool — same role as `reasoning_content` in the message
15
+ * schema: captured for transparency, not for control flow.
16
+ *
17
+ * Implementation:
18
+ * - Uses stableStringify (sibling module) so logically equal objects
19
+ * always produce the same JSON.
20
+ * - Hashes with sha256 (Node built-in `crypto`), truncated to 16 chars.
21
+ * 16 hex chars = 64 bits, which is plenty for collision-resistance
22
+ * across a single user's session.
23
+ */
24
+ import type { Tool } from '../tools/types.js';
25
+ import type { SystemSegment } from '../prompts/index.js';
26
+ /**
27
+ * Hash a stable JSON representation of `value` and return a 16-char hex prefix.
28
+ * Recursively normalizes object keys (sorted) so insertion order does not
29
+ * affect the output.
30
+ */
31
+ export declare function fingerprint(value: unknown): string;
32
+ /**
33
+ * Compute a fingerprint of the registered tool schemas.
34
+ *
35
+ * We deliberately pick only `name`, `description`, and `inputSchema` — the
36
+ * three fields that matter for the upstream prompt cache. Other Tool
37
+ * metadata (version strings, descriptions of internal handlers, etc) is
38
+ * ignored because it never reaches the wire.
39
+ */
40
+ export declare function fingerprintTools(tools: Tool[]): string;
41
+ /**
42
+ * Compute a fingerprint of the system prompt's *immutable* portion.
43
+ *
44
+ * Volatile segments (NWT history, current time) are deliberately excluded
45
+ * because they change between rounds and would defeat the fingerprint's
46
+ * purpose as a "did the cacheable prefix change?" signal.
47
+ */
48
+ export declare function fingerprintSystemPrefix(segments: SystemSegment[]): string;
49
+ //# sourceMappingURL=fingerprint.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.d.ts","sourceRoot":"","sources":["../../src/cache/fingerprint.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUlD;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,MAAM,CAOtD;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,aAAa,EAAE,GAAG,MAAM,CAGzE"}
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Cache fingerprints — small, stable hashes used to detect when the
3
+ * "cacheable prefix" of an LLM request changes between rounds.
4
+ *
5
+ * Why this exists:
6
+ * Anthropic prompt cache and DeepSeek automatic prefix cache both break
7
+ * when the request body changes upstream of the cache breakpoint. To
8
+ * debug "why is my hit-rate 0%", we want a quick fingerprint of the
9
+ * things that *should* be cacheable (tools + system prefix) so we can
10
+ * log it on every request and compare across rounds.
11
+ *
12
+ * The fingerprint itself is NOT used to decide anything (the upstream
13
+ * provider does the actual cache lookup). It's purely a debug /
14
+ * observability tool — same role as `reasoning_content` in the message
15
+ * schema: captured for transparency, not for control flow.
16
+ *
17
+ * Implementation:
18
+ * - Uses stableStringify (sibling module) so logically equal objects
19
+ * always produce the same JSON.
20
+ * - Hashes with sha256 (Node built-in `crypto`), truncated to 16 chars.
21
+ * 16 hex chars = 64 bits, which is plenty for collision-resistance
22
+ * across a single user's session.
23
+ */
24
+ import { createHash } from 'crypto';
25
+ /**
26
+ * Hash a stable JSON representation of `value` and return a 16-char hex prefix.
27
+ * Recursively normalizes object keys (sorted) so insertion order does not
28
+ * affect the output.
29
+ */
30
+ export function fingerprint(value) {
31
+ // stableStringify is imported lazily to avoid a circular dep — fingerprint
32
+ // is consumed by both providers (which sit above utils/) and the cache
33
+ // stats store (which sits next to fingerprint). Top-level import works
34
+ // because utils has no other consumers besides fingerprint, so we just
35
+ // import statically.
36
+ return createHash('sha256')
37
+ .update(stableStringifyLocal(value))
38
+ .digest('hex')
39
+ .slice(0, 16);
40
+ }
41
+ /**
42
+ * Compute a fingerprint of the registered tool schemas.
43
+ *
44
+ * We deliberately pick only `name`, `description`, and `inputSchema` — the
45
+ * three fields that matter for the upstream prompt cache. Other Tool
46
+ * metadata (version strings, descriptions of internal handlers, etc) is
47
+ * ignored because it never reaches the wire.
48
+ */
49
+ export function fingerprintTools(tools) {
50
+ const minimal = tools.map(t => ({
51
+ name: t.name,
52
+ description: t.description,
53
+ inputSchema: t.inputSchema,
54
+ }));
55
+ return fingerprint({ tools: minimal });
56
+ }
57
+ /**
58
+ * Compute a fingerprint of the system prompt's *immutable* portion.
59
+ *
60
+ * Volatile segments (NWT history, current time) are deliberately excluded
61
+ * because they change between rounds and would defeat the fingerprint's
62
+ * purpose as a "did the cacheable prefix change?" signal.
63
+ */
64
+ export function fingerprintSystemPrefix(segments) {
65
+ const immutable = segments.filter(s => !s.volatile);
66
+ return fingerprint({ system: immutable.map(s => ({ name: s.name, content: s.content })) });
67
+ }
68
+ // -- Local copy of stableStringify to avoid a circular dep. This must stay
69
+ // byte-for-byte identical to src/utils/stableStringify.ts. If you change
70
+ // one, change both.
71
+ function stableStringifyLocal(value) {
72
+ if (value === null)
73
+ return 'null';
74
+ if (value === undefined)
75
+ return 'null';
76
+ const t = typeof value;
77
+ if (t === 'string')
78
+ return JSON.stringify(value);
79
+ if (t === 'number')
80
+ return Number.isFinite(value) ? String(value) : 'null';
81
+ if (t === 'boolean')
82
+ return value ? 'true' : 'false';
83
+ if (Array.isArray(value)) {
84
+ return '[' + value.map((v) => stableStringifyLocal(v)).join(',') + ']';
85
+ }
86
+ if (t === 'object') {
87
+ const obj = value;
88
+ const keys = Object.keys(obj).sort();
89
+ const parts = [];
90
+ for (const k of keys) {
91
+ const v = obj[k];
92
+ if (v === undefined)
93
+ continue;
94
+ parts.push(JSON.stringify(k) + ':' + stableStringifyLocal(v));
95
+ }
96
+ return '{' + parts.join(',') + '}';
97
+ }
98
+ return 'null';
99
+ }
100
+ //# sourceMappingURL=fingerprint.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fingerprint.js","sourceRoot":"","sources":["../../src/cache/fingerprint.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Cache subsystem barrel. Re-exports the public surface used by the TUI,
3
+ * the App singleton, and the LLM service.
4
+ *
5
+ * Modules:
6
+ * - fingerprint.ts: stable hashes for tools + immutable system prefix
7
+ * - stats.ts: CacheStatsStore — per-round hit/miss + cost savings
8
+ * - volatile.ts: VolatileScratch — per-round scratch that never reaches the API
9
+ * - smartModel.ts: smart-model routing hook (simple → mini, complex → main)
10
+ */
11
+ export { fingerprint, fingerprintTools, fingerprintSystemPrefix } from './fingerprint.js';
12
+ export { CacheStatsStore, estimateSavingsCNY, type CacheStats, type CacheSnapshot } from './stats.js';
13
+ export { VolatileScratch } from './volatile.js';
14
+ export { shouldDowngrade, type SmartModelDecision } from './smartModel.js';
15
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AACtG,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAAE,KAAK,kBAAkB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Cache subsystem barrel. Re-exports the public surface used by the TUI,
3
+ * the App singleton, and the LLM service.
4
+ *
5
+ * Modules:
6
+ * - fingerprint.ts: stable hashes for tools + immutable system prefix
7
+ * - stats.ts: CacheStatsStore — per-round hit/miss + cost savings
8
+ * - volatile.ts: VolatileScratch — per-round scratch that never reaches the API
9
+ * - smartModel.ts: smart-model routing hook (simple → mini, complex → main)
10
+ */
11
+ export { fingerprint, fingerprintTools, fingerprintSystemPrefix } from './fingerprint.js';
12
+ export { CacheStatsStore, estimateSavingsCNY } from './stats.js';
13
+ export { VolatileScratch } from './volatile.js';
14
+ export { shouldDowngrade } from './smartModel.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,WAAW,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC1F,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAuC,MAAM,YAAY,CAAC;AACtG,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,eAAe,EAA2B,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Smart-model routing (P3 of the Reasonix plan).
3
+ *
4
+ * Idea: classify the user's input as "simple" or "complex" and route
5
+ * simple queries to a cheaper / faster model while keeping the main model
6
+ * for complex work. The classification is intentionally trivial — prompt
7
+ * length + presence of recent tool calls — because the cost of a
8
+ * misclassification (a slightly worse answer on a simple query) is much
9
+ * lower than the cost of running a frontier model on every greeting.
10
+ *
11
+ * Trade-off: each routing decision adds one model swap (different
12
+ * `model` field on the wire). If the cache prefix is keyed on the model
13
+ * string, the cache miss rate can spike on every simple query. To keep
14
+ * the prefix stable, callers should pass a *family* (e.g. "anthropic" or
15
+ * "deepseek") rather than a specific model name when forwarding the
16
+ * prompt to the downstream provider — that's outside the scope of this
17
+ * hook; this module only answers the question "should I downgrade?".
18
+ *
19
+ * v3.0.0: shipped as a stub. Real decision logic is conservative
20
+ * (downgrade only when prompt is short AND no recent tool activity).
21
+ * Future iterations can add heuristic or classifier-based routing.
22
+ */
23
+ import type { ChatMessage } from '../types.js';
24
+ export interface SmartModelDecision {
25
+ /** Should we route this turn to a cheaper model? */
26
+ downgrade: boolean;
27
+ /** Why we made this call — useful for `/cache` debug output. */
28
+ reason: string;
29
+ }
30
+ /**
31
+ * Heuristic: downgrade when
32
+ * 1. The latest user message is short (< 200 chars), AND
33
+ * 2. The last 3 messages contain no assistant tool_calls.
34
+ *
35
+ * This covers greetings, one-line questions, and simple follow-ups
36
+ * ("thanks", "ok", "explain this one line") while routing anything
37
+ * that triggered a tool to the main model.
38
+ */
39
+ export declare function shouldDowngrade(messages: ChatMessage[], lastUserInput: string): SmartModelDecision;
40
+ //# sourceMappingURL=smartModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smartModel.d.ts","sourceRoot":"","sources":["../../src/cache/smartModel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,WAAW,kBAAkB;IACjC,oDAAoD;IACpD,SAAS,EAAE,OAAO,CAAC;IACnB,gEAAgE;IAChE,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,WAAW,EAAE,EAAE,aAAa,EAAE,MAAM,GAAG,kBAAkB,CAUlG"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Smart-model routing (P3 of the Reasonix plan).
3
+ *
4
+ * Idea: classify the user's input as "simple" or "complex" and route
5
+ * simple queries to a cheaper / faster model while keeping the main model
6
+ * for complex work. The classification is intentionally trivial — prompt
7
+ * length + presence of recent tool calls — because the cost of a
8
+ * misclassification (a slightly worse answer on a simple query) is much
9
+ * lower than the cost of running a frontier model on every greeting.
10
+ *
11
+ * Trade-off: each routing decision adds one model swap (different
12
+ * `model` field on the wire). If the cache prefix is keyed on the model
13
+ * string, the cache miss rate can spike on every simple query. To keep
14
+ * the prefix stable, callers should pass a *family* (e.g. "anthropic" or
15
+ * "deepseek") rather than a specific model name when forwarding the
16
+ * prompt to the downstream provider — that's outside the scope of this
17
+ * hook; this module only answers the question "should I downgrade?".
18
+ *
19
+ * v3.0.0: shipped as a stub. Real decision logic is conservative
20
+ * (downgrade only when prompt is short AND no recent tool activity).
21
+ * Future iterations can add heuristic or classifier-based routing.
22
+ */
23
+ /**
24
+ * Heuristic: downgrade when
25
+ * 1. The latest user message is short (< 200 chars), AND
26
+ * 2. The last 3 messages contain no assistant tool_calls.
27
+ *
28
+ * This covers greetings, one-line questions, and simple follow-ups
29
+ * ("thanks", "ok", "explain this one line") while routing anything
30
+ * that triggered a tool to the main model.
31
+ */
32
+ export function shouldDowngrade(messages, lastUserInput) {
33
+ if (lastUserInput.length > 200) {
34
+ return { downgrade: false, reason: 'prompt-too-long' };
35
+ }
36
+ const recent = messages.slice(-3);
37
+ const hasRecentToolCalls = recent.some(m => m.tool_calls && m.tool_calls.length > 0);
38
+ if (hasRecentToolCalls) {
39
+ return { downgrade: false, reason: 'recent-tool-call' };
40
+ }
41
+ return { downgrade: true, reason: 'short-no-tools' };
42
+ }
43
+ //# sourceMappingURL=smartModel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"smartModel.js","sourceRoot":"","sources":["../../src/cache/smartModel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAWH;;;;;;;;GAQG;AACH,MAAM,UAAU,eAAe,CAAC,QAAuB,EAAE,aAAqB;IAC5E,IAAI,aAAa,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;QAC/B,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACzD,CAAC;IACD,MAAM,MAAM,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAClC,MAAM,kBAAkB,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACrF,IAAI,kBAAkB,EAAE,CAAC;QACvB,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC1D,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;AACvD,CAAC"}