thatgfsj-code 2.2.9 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) 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 +69 -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/ToolCall.d.ts.map +1 -1
  86. package/dist/tui/components/ToolCall.js +1 -6
  87. package/dist/tui/components/ToolCall.js.map +1 -1
  88. package/dist/tui/hooks/useChat.d.ts +22 -0
  89. package/dist/tui/hooks/useChat.d.ts.map +1 -1
  90. package/dist/tui/hooks/useChat.js +82 -46
  91. package/dist/tui/hooks/useChat.js.map +1 -1
  92. package/dist/tui/hooks/useCommands.d.ts.map +1 -1
  93. package/dist/tui/hooks/useCommands.js +57 -0
  94. package/dist/tui/hooks/useCommands.js.map +1 -1
  95. package/dist/tui/welcome.d.ts.map +1 -1
  96. package/dist/tui/welcome.js +2 -3
  97. package/dist/tui/welcome.js.map +1 -1
  98. package/dist/types.d.ts +79 -6
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/types.js +7 -0
  101. package/dist/types.js.map +1 -1
  102. package/dist/utils/stableStringify.d.ts +21 -0
  103. package/dist/utils/stableStringify.d.ts.map +1 -0
  104. package/dist/utils/stableStringify.js +53 -0
  105. package/dist/utils/stableStringify.js.map +1 -0
  106. package/dist/utils/thinking.d.ts.map +1 -1
  107. package/dist/utils/thinking.js +0 -3
  108. package/dist/utils/thinking.js.map +1 -1
  109. package/package.json +6 -6
  110. package/src/app/index.ts +180 -124
  111. package/src/cache/fingerprint.ts +101 -0
  112. package/src/cache/index.ts +15 -0
  113. package/src/cache/smartModel.ts +52 -0
  114. package/src/cache/stats.ts +199 -0
  115. package/src/cache/volatile.ts +47 -0
  116. package/src/cmd/index.tsx +288 -292
  117. package/src/config/index.ts +156 -148
  118. package/src/config/providers.ts +234 -234
  119. package/src/config/types.ts +67 -53
  120. package/src/hooks/index.ts +111 -111
  121. package/src/llm/anthropic.ts +388 -243
  122. package/src/llm/gemini.ts +169 -169
  123. package/src/llm/index.ts +265 -200
  124. package/src/llm/openai.ts +243 -196
  125. package/src/llm/provider.ts +66 -34
  126. package/src/prompts/index.ts +260 -220
  127. package/src/session/compactor.ts +103 -103
  128. package/src/session/index.ts +181 -168
  129. package/src/session/message.ts +42 -42
  130. package/src/skills/brainstorming.ts +43 -43
  131. package/src/skills/code-review.ts +45 -45
  132. package/src/skills/executing-plans.ts +27 -27
  133. package/src/skills/frontend-design.ts +35 -35
  134. package/src/skills/git-workflow.ts +36 -36
  135. package/src/skills/improve-architecture.ts +38 -38
  136. package/src/skills/index.ts +136 -136
  137. package/src/skills/neuroweave.ts +47 -47
  138. package/src/skills/playwright.ts +72 -72
  139. package/src/skills/prototype.ts +30 -30
  140. package/src/skills/subagent.ts +28 -28
  141. package/src/skills/supabase.ts +44 -44
  142. package/src/skills/systematic-debugging.ts +44 -44
  143. package/src/skills/tdd.ts +39 -39
  144. package/src/skills/triage.ts +35 -35
  145. package/src/skills/verification.ts +31 -31
  146. package/src/skills/writing-plans.ts +42 -42
  147. package/src/tools/nwt.ts +598 -598
  148. package/src/tools/types.ts +122 -122
  149. package/src/tui/app.tsx +199 -186
  150. package/src/tui/components/ChatList.tsx +41 -41
  151. package/src/tui/components/ChatMessage.tsx +54 -54
  152. package/src/tui/components/Header.tsx +56 -29
  153. package/src/tui/components/InitWizard.tsx +217 -132
  154. package/src/tui/components/Markdown.tsx +35 -35
  155. package/src/tui/components/ModelSelector.tsx +87 -87
  156. package/src/tui/components/StatusBar.tsx +30 -30
  157. package/src/tui/components/Thinking.tsx +17 -17
  158. package/src/tui/components/ToolCall.tsx +102 -107
  159. package/src/tui/components/UserInput.tsx +131 -131
  160. package/src/tui/hooks/useChat.ts +287 -238
  161. package/src/tui/hooks/useCommands.ts +234 -176
  162. package/src/tui/index.ts +6 -6
  163. package/src/tui/welcome.ts +177 -178
  164. package/src/types.ts +104 -42
  165. package/src/utils/diff.ts +71 -71
  166. package/src/utils/project.ts +99 -99
  167. package/src/utils/stableStringify.ts +47 -0
  168. package/src/utils/thinking.ts +118 -121
  169. package/tests/cache/fingerprint.test.ts +75 -0
  170. package/tests/cache/stableStringify.test.ts +43 -0
  171. package/tests/cache/stats.test.ts +146 -0
  172. package/tests/cache/volatile.test.ts +75 -0
  173. package/tests/smoke-pollution.mjs +78 -0
  174. package/tests/smoke-thinking.mjs +110 -0
  175. package/tests/smoke-tool-stream.mjs +69 -0
  176. package/tests/smoke-tool.mjs +117 -0
  177. package/.nwt/meta.json +0 -5
  178. package/dist/version.d.ts +0 -16
  179. package/dist/version.d.ts.map +0 -1
  180. package/dist/version.js +0 -16
  181. package/dist/version.js.map +0 -1
  182. package/src/version.ts +0 -16
package/src/llm/index.ts CHANGED
@@ -1,200 +1,265 @@
1
- /**
2
- * LLM Service - Factory for creating providers
3
- * Supports all providers + custom relay stations (中转站)
4
- */
5
-
6
- import chalk from 'chalk';
7
- import type { ChatMessage, ChatResponse, ChatOptions, ToolCall } from '../types.js';
8
- import type { Tool } from '../tools/types.js';
9
- import type { LLMProvider, StreamChunk } from './provider.js';
10
- import type { AIConfig, ProviderName } from '../config/types.js';
11
- import { PROVIDERS } from '../config/providers.js';
12
- import { OpenAIProvider } from './openai.js';
13
- import { AnthropicProvider } from './anthropic.js';
14
- import { GeminiProvider } from './gemini.js';
15
-
16
- export class LLMService {
17
- private provider: LLMProvider;
18
- private tools: Map<string, Tool> = new Map();
19
- private apiKey: string;
20
-
21
- constructor(provider: LLMProvider, apiKey: string) {
22
- this.provider = provider;
23
- this.apiKey = apiKey;
24
- }
25
-
26
- static fromConfig(config: AIConfig): LLMService {
27
- const providerName = config.provider || 'siliconflow';
28
- const providerConfig = PROVIDERS[providerName];
29
-
30
- const providerCfg = {
31
- apiKey: config.apiKey || '',
32
- model: config.model || providerConfig.defaultModel,
33
- baseUrl: config.baseUrl || providerConfig.baseUrl,
34
- temperature: config.temperature ?? 0.7,
35
- maxTokens: config.maxTokens ?? 4096,
36
- };
37
-
38
- const format = providerConfig.format;
39
- let provider: LLMProvider;
40
-
41
- switch (format) {
42
- case 'anthropic':
43
- provider = new AnthropicProvider(providerCfg);
44
- break;
45
- case 'gemini':
46
- provider = new GeminiProvider(providerCfg);
47
- break;
48
- default:
49
- provider = new OpenAIProvider(providerCfg);
50
- }
51
-
52
- return new LLMService(provider, providerCfg.apiKey);
53
- }
54
-
55
- registerTools(tools: Tool[]): void {
56
- for (const tool of tools) {
57
- this.tools.set(tool.name, tool);
58
- }
59
- }
60
-
61
- getProviderName(): string { return this.provider.name; }
62
- hasApiKey(): boolean { return !!this.apiKey; }
63
-
64
- async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
65
- if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
66
- const toolsArray = [...this.tools.values()];
67
- return this.provider.chat(messages, options, toolsArray.length > 0 ? toolsArray : undefined);
68
- }
69
-
70
- /**
71
- * Streaming chat with agent loop (tool call support)
72
- * - Streams text to the user
73
- * - Detects structured tool calls from the API
74
- * - Executes tools and loops back for more
75
- */
76
- async *chatStream(
77
- messages: ChatMessage[],
78
- options?: ChatOptions & { maxIterations?: number }
79
- ): AsyncGenerator<string, ChatResponse> {
80
- if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
81
-
82
- const maxIterations = options?.maxIterations ?? 10;
83
- let currentMessages = [...messages];
84
- let iterations = 0;
85
-
86
- while (iterations < maxIterations) {
87
- iterations++;
88
- const toolsArray = [...this.tools.values()];
89
- const hasTools = toolsArray.length > 0;
90
-
91
- let fullContent = '';
92
- let detectedToolCalls: ToolCall[] | undefined;
93
-
94
- const stream = this.provider.chatStream(currentMessages, options, hasTools ? toolsArray : undefined);
95
-
96
- for await (const chunk of stream) {
97
- if (chunk.type === 'text' && chunk.content) {
98
- fullContent += chunk.content;
99
- yield chunk.content;
100
- } else if (chunk.type === 'tool_calls' && chunk.toolCalls) {
101
- detectedToolCalls = chunk.toolCalls;
102
- }
103
- }
104
-
105
- // If we got tool calls, execute them and loop
106
- if (detectedToolCalls && detectedToolCalls.length > 0) {
107
- // Add assistant message with tool calls
108
- currentMessages.push({
109
- role: 'assistant',
110
- content: fullContent || '',
111
- tool_calls: detectedToolCalls,
112
- });
113
-
114
- // Execute each tool
115
- for (const toolCall of detectedToolCalls) {
116
- const tool = this.tools.get(toolCall.function.name);
117
-
118
- // Yield structured tool call as JSON line
119
- yield `\n@@TOOL@@${JSON.stringify({ action: 'call', name: toolCall.function.name, args: toolCall.function.arguments })}\n`;
120
-
121
- if (!tool) {
122
- const errMsg = `Tool "${toolCall.function.name}" not found`;
123
- currentMessages.push({
124
- role: 'tool',
125
- content: errMsg,
126
- tool_call_id: toolCall.id,
127
- name: toolCall.function.name,
128
- });
129
- yield `@@TOOL@@${JSON.stringify({ action: 'result', error: errMsg })}\n`;
130
- continue;
131
- }
132
-
133
- try {
134
- const params = JSON.parse(toolCall.function.arguments || '{}');
135
- const result = await tool.execute(params);
136
- const output = result.success ? (result.output || JSON.stringify(result.data)) : (result.error || 'Tool failed');
137
-
138
- currentMessages.push({
139
- role: 'tool',
140
- content: output,
141
- tool_call_id: toolCall.id,
142
- name: toolCall.function.name,
143
- });
144
-
145
- yield `@@TOOL@@${JSON.stringify({ action: 'result', output })}\n`;
146
- } catch (error: any) {
147
- const errMsg = `Error: ${error.message}`;
148
- currentMessages.push({
149
- role: 'tool',
150
- content: errMsg,
151
- tool_call_id: toolCall.id,
152
- name: toolCall.function.name,
153
- });
154
- yield `@@TOOL@@${JSON.stringify({ action: 'result', error: errMsg })}\n`;
155
- }
156
- }
157
-
158
- yield '\n';
159
- continue;
160
- }
161
-
162
- // No tool calls - done
163
- return { content: fullContent, role: 'assistant' };
164
- }
165
-
166
- return { content: '[Agent loop exceeded maximum iterations]', role: 'assistant' };
167
- }
168
-
169
- private truncateArgs(args: string): string {
170
- try {
171
- const obj = JSON.parse(args || '{}');
172
- const entries = Object.entries(obj);
173
- if (entries.length === 0) return '';
174
- return entries.map(([k, v]) => {
175
- const val = typeof v === 'string' && v.length > 50 ? v.slice(0, 50) + '...' : v;
176
- return `${k}: ${JSON.stringify(val)}`;
177
- }).join(', ');
178
- } catch {
179
- return args.length > 80 ? args.slice(0, 80) + '...' : args;
180
- }
181
- }
182
-
183
- private getNoKeyMessage(): string {
184
- return [
185
- '❌ 未配置 API Key,无法调用 AI。',
186
- '',
187
- '请先运行: gfcode init',
188
- '',
189
- '或设置环境变量:',
190
- ' export SILICONFLOW_API_KEY="sk-..."',
191
- ' export OPENAI_API_KEY="sk-..."',
192
- ' export DEEPSEEK_API_KEY="sk-..."',
193
- ].join('\n');
194
- }
195
- }
196
-
197
- export type { LLMProvider, ProviderConfig, StreamChunk } from './provider.js';
198
- export { OpenAIProvider } from './openai.js';
199
- export { AnthropicProvider } from './anthropic.js';
200
- export { GeminiProvider } from './gemini.js';
1
+ /**
2
+ * LLM Service - Factory for creating providers
3
+ * Supports all providers + custom relay stations (中转站)
4
+ *
5
+ * v3.0.0+: streaming protocol migrated to structured StreamChunk
6
+ * - chatStream yields { type: 'text' | 'tool_calls' | 'thinking' | 'usage' }
7
+ * - Tool call execution is now part of the agent loop; consumers see
8
+ * { type: 'tool_calls', toolCalls } for dispatch and { type: 'text', content }
9
+ * for model output. The legacy @@TOOL@@ sentinel-string protocol is GONE.
10
+ * - Usage / cache stats are emitted as the final { type: 'usage' } chunk,
11
+ * which the TUI cache stats store consumes.
12
+ */
13
+
14
+ import chalk from 'chalk';
15
+ import type { ChatMessage, ChatResponse, ChatOptions, ToolCall, StreamChunk } from '../types.js';
16
+ import type { Tool } from '../tools/types.js';
17
+ import type { LLMProvider } from './provider.js';
18
+ import type { AIConfig, Config, ProviderName } from '../config/types.js';
19
+ import { PROVIDERS } from '../config/providers.js';
20
+ import { OpenAIProvider } from './openai.js';
21
+ import { AnthropicProvider } from './anthropic.js';
22
+ import { GeminiProvider } from './gemini.js';
23
+
24
+ export class LLMService {
25
+ private provider: LLMProvider;
26
+ private tools: Map<string, Tool> = new Map();
27
+ private apiKey: string;
28
+
29
+ constructor(provider: LLMProvider, apiKey: string) {
30
+ this.provider = provider;
31
+ this.apiKey = apiKey;
32
+ }
33
+
34
+ static fromConfig(config: AIConfig & { cache?: Config['cache'] }): LLMService {
35
+ const providerName = config.provider || 'siliconflow';
36
+ const providerConfig = PROVIDERS[providerName];
37
+
38
+ const providerCfg = {
39
+ apiKey: config.apiKey || '',
40
+ model: config.model || providerConfig.defaultModel,
41
+ baseUrl: config.baseUrl || providerConfig.baseUrl,
42
+ temperature: config.temperature ?? 0.7,
43
+ maxTokens: config.maxTokens ?? 4096,
44
+ // v3.0.0: forward cache policy. Anthropic reads this to decide
45
+ // whether to attach cache_control markers; other providers ignore it.
46
+ cache: config.cache ?? { enabled: true, ttl: '5m', strategy: 'auto' as const },
47
+ };
48
+
49
+ const format = providerConfig.format;
50
+ let provider: LLMProvider;
51
+
52
+ switch (format) {
53
+ case 'anthropic':
54
+ provider = new AnthropicProvider(providerCfg);
55
+ break;
56
+ case 'gemini':
57
+ provider = new GeminiProvider(providerCfg);
58
+ break;
59
+ default:
60
+ provider = new OpenAIProvider(providerCfg);
61
+ }
62
+
63
+ return new LLMService(provider, providerCfg.apiKey);
64
+ }
65
+
66
+ registerTools(tools: Tool[]): void {
67
+ for (const tool of tools) {
68
+ this.tools.set(tool.name, tool);
69
+ }
70
+ }
71
+
72
+ getProviderName(): string { return this.provider.name; }
73
+ hasApiKey(): boolean { return !!this.apiKey; }
74
+
75
+ async chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse> {
76
+ if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
77
+ const toolsArray = [...this.tools.values()];
78
+ return this.provider.chat(messages, options, toolsArray.length > 0 ? toolsArray : undefined);
79
+ }
80
+
81
+ /**
82
+ * Streaming chat with agent loop (tool call support).
83
+ *
84
+ * Structured chunk protocol (replaces the @@TOOL@@ sentinel strings):
85
+ * { type: 'text', content: string } model output, accumulate + render
86
+ * { type: 'tool_calls', toolCalls: ToolCall[] } dispatch tools, results will
87
+ * be folded into the next request
88
+ * { type: 'thinking', content: string } reasoning text (consumers may
89
+ * display in debug mode)
90
+ * { type: 'usage', usage: Usage } cache hit/miss + token counts;
91
+ * emitted on the final round
92
+ *
93
+ * Returns the final ChatResponse of the agent loop when done.
94
+ */
95
+ async *chatStream(
96
+ messages: ChatMessage[],
97
+ options?: ChatOptions & { maxIterations?: number }
98
+ ): AsyncGenerator<StreamChunk, ChatResponse> {
99
+ if (!this.hasApiKey()) throw new Error(this.getNoKeyMessage());
100
+
101
+ const maxIterations = options?.maxIterations ?? 10;
102
+ let currentMessages = [...messages];
103
+ let iterations = 0;
104
+ let lastUsage: ChatResponse['usage'] | undefined;
105
+
106
+ while (iterations < maxIterations) {
107
+ iterations++;
108
+ const toolsArray = [...this.tools.values()];
109
+ const hasTools = toolsArray.length > 0;
110
+
111
+ let fullContent = '';
112
+ let detectedToolCalls: ToolCall[] | undefined;
113
+
114
+ // Forward stream chunks from the provider. We collect text internally for
115
+ // tool-call persistence but always re-emit the original chunks unchanged.
116
+ const stream = this.provider.chatStream(currentMessages, options, hasTools ? toolsArray : undefined);
117
+
118
+ for await (const chunk of stream) {
119
+ if (chunk.type === 'text' && chunk.content) {
120
+ fullContent += chunk.content;
121
+ yield chunk;
122
+ } else if (chunk.type === 'tool_calls' && chunk.toolCalls) {
123
+ detectedToolCalls = chunk.toolCalls;
124
+ // Don't yield the raw tool_calls chunk here — we emit one combined
125
+ // chunk after persisting the assistant message so consumers don't
126
+ // double-render.
127
+ } else if (chunk.type === 'thinking') {
128
+ yield chunk;
129
+ } else if (chunk.type === 'usage') {
130
+ lastUsage = chunk.usage;
131
+ yield chunk;
132
+ }
133
+ }
134
+
135
+ // If we got tool calls, execute them and loop
136
+ if (detectedToolCalls && detectedToolCalls.length > 0) {
137
+ // Add assistant message with tool calls (append-only, preserves prefix cache)
138
+ currentMessages.push({
139
+ role: 'assistant',
140
+ content: fullContent || '',
141
+ tool_calls: detectedToolCalls,
142
+ });
143
+
144
+ // Execute each tool and emit a single structured tool_calls chunk
145
+ // describing the dispatch plan. Per-tool results are appended to
146
+ // currentMessages but NOT yielded as additional chunks — consumers
147
+ // that want to display result text can introspect tool_calls[*].result
148
+ // on a synthesized combined chunk, OR we re-emit one tool_calls chunk
149
+ // at the end with results attached. We choose the simpler approach:
150
+ // emit ONE tool_calls chunk per iteration with all the calls; results
151
+ // are surfaced through the next assistant turn's text content.
152
+ //
153
+ // v3.0.0 Tool-call Repair (Reasonix P2): on tool failure we APPEND
154
+ // a `[TOOL_REPAIR]` system message rather than mutating the existing
155
+ // tool_call message. This preserves the upstream cache prefix —
156
+ // re-writing an earlier message would shift the prefix by N bytes
157
+ // and bust the cache for every subsequent round.
158
+ for (const toolCall of detectedToolCalls) {
159
+ const tool = this.tools.get(toolCall.function.name);
160
+
161
+ if (!tool) {
162
+ const errMsg = `Tool "${toolCall.function.name}" not found`;
163
+ // Repair message explain the failure and tell the model to
164
+ // try a different tool. Cache-safe because we are APPENDING,
165
+ // never modifying existing messages.
166
+ currentMessages.push({
167
+ role: 'system',
168
+ content: `[TOOL_REPAIR] Previous tool_call "${toolCall.function.name}" (id=${toolCall.id}) failed: ${errMsg}. Available tools: ${[...this.tools.keys()].join(', ')}.`,
169
+ });
170
+ currentMessages.push({
171
+ role: 'tool',
172
+ content: errMsg,
173
+ tool_call_id: toolCall.id,
174
+ name: toolCall.function.name,
175
+ });
176
+ continue;
177
+ }
178
+
179
+ try {
180
+ const params = JSON.parse(toolCall.function.arguments || '{}');
181
+ const result = await tool.execute(params);
182
+ const output = result.success ? (result.output || JSON.stringify(result.data)) : (result.error || 'Tool failed');
183
+
184
+ currentMessages.push({
185
+ role: 'tool',
186
+ content: output,
187
+ tool_call_id: toolCall.id,
188
+ name: toolCall.function.name,
189
+ });
190
+
191
+ if (!result.success) {
192
+ // Soft failure: tool returned success=false. Same repair pattern.
193
+ currentMessages.push({
194
+ role: 'system',
195
+ content: `[TOOL_REPAIR] Tool "${toolCall.function.name}" returned success=false: ${output}. Consider correcting the arguments and retrying.`,
196
+ });
197
+ }
198
+ } catch (error: any) {
199
+ const errMsg = `Error: ${error.message}`;
200
+ currentMessages.push({
201
+ role: 'tool',
202
+ content: errMsg,
203
+ tool_call_id: toolCall.id,
204
+ name: toolCall.function.name,
205
+ });
206
+ // Hard failure: tool.execute threw. Repair message so the model
207
+ // can see the failure next round and adjust (e.g. fix a path
208
+ // typo, retry without the optional arg).
209
+ currentMessages.push({
210
+ role: 'system',
211
+ content: `[TOOL_REPAIR] Tool "${toolCall.function.name}" threw an exception: ${errMsg}. Inspect the arguments and retry with a corrected call.`,
212
+ });
213
+ }
214
+ }
215
+
216
+ // Emit one tool_calls chunk for this iteration. TUI renders this as a
217
+ // "tool invoked" entry. Per-tool outputs are summarized on the next
218
+ // assistant turn.
219
+ yield { type: 'tool_calls', toolCalls: detectedToolCalls };
220
+ continue;
221
+ }
222
+
223
+ // No tool calls - done. Return final response (with usage if we have it).
224
+ return {
225
+ content: fullContent,
226
+ role: 'assistant',
227
+ usage: lastUsage,
228
+ };
229
+ }
230
+
231
+ return { content: '[Agent loop exceeded maximum iterations]', role: 'assistant' };
232
+ }
233
+
234
+ private truncateArgs(args: string): string {
235
+ try {
236
+ const obj = JSON.parse(args || '{}');
237
+ const entries = Object.entries(obj);
238
+ if (entries.length === 0) return '';
239
+ return entries.map(([k, v]) => {
240
+ const val = typeof v === 'string' && v.length > 50 ? v.slice(0, 50) + '...' : v;
241
+ return `${k}: ${JSON.stringify(val)}`;
242
+ }).join(', ');
243
+ } catch {
244
+ return args.length > 80 ? args.slice(0, 80) + '...' : args;
245
+ }
246
+ }
247
+
248
+ private getNoKeyMessage(): string {
249
+ return [
250
+ '❌ 未配置 API Key,无法调用 AI。',
251
+ '',
252
+ '请先运行: gfcode init',
253
+ '',
254
+ '或设置环境变量:',
255
+ ' export SILICONFLOW_API_KEY="sk-..."',
256
+ ' export OPENAI_API_KEY="sk-..."',
257
+ ' export DEEPSEEK_API_KEY="sk-..."',
258
+ ].join('\n');
259
+ }
260
+ }
261
+
262
+ export type { LLMProvider, ProviderConfig, StreamChunk } from './provider.js';
263
+ export { OpenAIProvider } from './openai.js';
264
+ export { AnthropicProvider } from './anthropic.js';
265
+ export { GeminiProvider } from './gemini.js';