closer-code 1.0.0 → 1.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.
- package/.closer-code.example.json +32 -0
- package/DUAL_OPTIMIZATION_COMPLETE.md +293 -0
- package/README.md +167 -557
- package/README_OPENAI.md +163 -0
- package/THINKING_THROTTLING_OPTIMIZATION.md +244 -0
- package/THROTTLING_1_5S_OPTIMIZATION.md +401 -0
- package/TOOLS_IMPROVEMENTS_SUMMARY.md +273 -0
- package/cloco.md +5 -1
- package/config.example.json +15 -94
- package/config.mcp.example.json +81 -0
- package/dist/bash-runner.js +5 -126
- package/dist/batch-cli.js +286 -20658
- package/dist/closer-cli.js +329 -21135
- package/dist/index.js +308 -31036
- package/docs/ANTHROPIC_TOOL_ERROR_HANDLING.md +220 -0
- package/docs/BUILD_COMMANDS.md +79 -0
- package/docs/CTRL_Z_SUPPORT.md +189 -0
- package/docs/DEEPSEEK_R1_INTEGRATION.md +427 -0
- package/docs/FIX_OPENAI_TOOL_ERROR_HANDLING.md +375 -0
- package/docs/FIX_OPENAI_TOOL_RESULT.md +198 -0
- package/docs/INPUT_ENHANCEMENTS.md +192 -0
- package/docs/MCP_IMPLEMENTATION_SUMMARY.md +428 -0
- package/docs/MCP_INTEGRATION.md +418 -0
- package/docs/MCP_QUICKSTART.md +299 -0
- package/docs/MCP_README.md +166 -0
- package/docs/MINIFY_BUILD.md +180 -0
- package/docs/MULTILINE_INPUT_FEATURE.md +119 -0
- package/docs/OPENAI_CLIENT.md +258 -0
- package/docs/PROJECT_LOCAL_CONFIG.md +471 -0
- package/docs/PROJECT_LOCAL_CONFIG_SUMMARY.md +407 -0
- package/docs/REFACTOR_CONVERSATION.md +306 -0
- package/docs/REGION_EDIT_DESIGN.md +475 -0
- package/docs/SIGNAL_HANDLING.md +171 -0
- package/docs/STREAM_UPDATE_THROTTLE.md +273 -0
- package/docs/TOOLS_REFACTOR_PLAN.md +520 -0
- package/ds_r1.md +249 -0
- package/examples/abort-fence-example.js +294 -0
- package/package.json +18 -4
- package/src/ai-client-legacy.js +6 -1
- package/src/ai-client-openai.js +672 -0
- package/src/ai-client.js +30 -13
- package/src/closer-cli.jsx +450 -162
- package/src/components/fullscreen-conversation.jsx +157 -0
- package/src/components/ink-text-input/index.jsx +324 -0
- package/src/components/multiline-text-input.jsx +614 -0
- package/src/components/progress-bar.jsx +135 -0
- package/src/components/tool-detail-view.jsx +82 -0
- package/src/components/tool-renderers/bash-renderer.jsx +197 -0
- package/src/components/tool-renderers/file-edit-renderer.jsx +247 -0
- package/src/components/tool-renderers/file-read-renderer.jsx +261 -0
- package/src/components/tool-renderers/file-write-renderer.jsx +222 -0
- package/src/components/tool-renderers/index.jsx +178 -0
- package/src/components/tool-renderers/list-renderer.jsx +274 -0
- package/src/components/tool-renderers/search-renderer.jsx +248 -0
- package/src/config.js +182 -20
- package/src/conversation/abort-fence.js +158 -0
- package/src/conversation/core.js +377 -0
- package/src/conversation/index.js +33 -0
- package/src/conversation/mcp-integration.js +96 -0
- package/src/conversation/plan-manager.js +295 -0
- package/src/conversation/stream-handler.js +154 -0
- package/src/conversation/tool-executor.js +264 -0
- package/src/conversation.js +23 -958
- package/src/hooks/use-throttled-state.js +158 -0
- package/src/input/enhanced-input.jsx +268 -0
- package/src/input/history.js +342 -0
- package/src/logger.js +20 -0
- package/src/mcp/client.js +275 -0
- package/src/mcp/tools-adapter.js +149 -0
- package/src/planner.js +18 -5
- package/src/prompt-builder.js +159 -0
- package/src/tools.js +457 -25
- package/src/utils/json-parser.js +231 -0
- package/src/utils/json-repair.js +146 -0
- package/src/utils/platform.js +259 -0
- package/test/test-ctrl-bf.js +121 -0
- package/test/test-deepseek-reasoning.js +118 -0
- package/test/test-history-navigation.js +80 -0
- package/test/test-input-fix.js +105 -0
- package/test/test-input-history.js +98 -0
- package/test/test-mcp.js +115 -0
- package/test/test-openai-client.js +152 -0
- package/test/test-openai-tool-result.js +199 -0
- package/test/test-project-config.js +106 -0
- package/test/test-shortcuts.js +79 -0
- package/test/test-stream-throttle.js +124 -0
- package/test/test-tool-error-handling.js +95 -0
- package/test/verify-input-fix.sh +35 -0
- package/test-abort-fence.js +263 -0
- package/test-abort-fix.js +54 -0
- package/test-abort-new-conversation.js +75 -0
- package/test-ctrl-z.js +54 -0
- package/test-file-read.js +105 -0
- package/test-tool-display.js +127 -0
- package/src/closer-cli.jsx.backup +0 -948
- package/test/workflows/longtalk/cloco.md +0 -19
- package/test/workflows/longtalk/emoji_500.txt +0 -63
- package/test/workflows/longtalk/emoji_list.txt +0 -20
- package/test-ctrl-c.jsx +0 -126
package/ds_r1.md
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
Thinking Mode
|
|
2
|
+
The DeepSeek model supports the thinking mode: before outputting the final answer, the model will first output a chain-of-thought reasoning to improve the accuracy of the final response. You can enable thinking mode using any of the following methods:
|
|
3
|
+
|
|
4
|
+
Set the model parameter: "model": "deepseek-reasoner"
|
|
5
|
+
|
|
6
|
+
Set the thinking parameter: "thinking": {"type": "enabled"}
|
|
7
|
+
|
|
8
|
+
If you are using the OpenAI SDK, when setting thinking parameter, you need to pass the thinking parameter within extra_body:
|
|
9
|
+
|
|
10
|
+
response = client.chat.completions.create(
|
|
11
|
+
model="deepseek-chat",
|
|
12
|
+
# ...
|
|
13
|
+
extra_body={"thinking": {"type": "enabled"}}
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
API Parameters
|
|
17
|
+
Input:
|
|
18
|
+
|
|
19
|
+
max_tokens:The maximum output length (including the COT part). Default to 32K, maximum to 64K.
|
|
20
|
+
Output:
|
|
21
|
+
|
|
22
|
+
reasoning_content:The content of the CoT,which is at the same level as content in the output structure. See API Example for details.
|
|
23
|
+
content: The content of the final answer.
|
|
24
|
+
tool_calls: The tool calls.
|
|
25
|
+
Supported Features:Json Output、Tool Calls、Chat Completion、Chat Prefix Completion (Beta)
|
|
26
|
+
|
|
27
|
+
Not Supported Features:FIM (Beta)
|
|
28
|
+
|
|
29
|
+
Not Supported Parameters:temperature、top_p、presence_penalty、frequency_penalty、logprobs、top_logprobs. Please note that to ensure compatibility with existing software, setting temperature、top_p、presence_penalty、frequency_penalty will not trigger an error but will also have no effect. Setting logprobs、top_logprobs will trigger an error.
|
|
30
|
+
|
|
31
|
+
Multi-turn Conversation
|
|
32
|
+
In each turn of the conversation, the model outputs the CoT (reasoning_content) and the final answer (content). In the next turn of the conversation, the CoT from previous turns is not concatenated into the context, as illustrated in the following diagram:
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
API Example
|
|
36
|
+
The following code, using Python as an example, demonstrates how to access the CoT and the final answer, as well as how to conduct multi-turn conversations. Note that in the code for the new turn of conversation, only the content from the previous turn's output is passed, while the reasoning_content is ignored.
|
|
37
|
+
|
|
38
|
+
from openai import OpenAI
|
|
39
|
+
client = OpenAI(api_key="<DeepSeek API Key>", base_url="https://api.deepseek.com")
|
|
40
|
+
|
|
41
|
+
# Turn 1
|
|
42
|
+
messages = [{"role": "user", "content": "9.11 and 9.8, which is greater?"}]
|
|
43
|
+
response = client.chat.completions.create(
|
|
44
|
+
model="deepseek-reasoner",
|
|
45
|
+
messages=messages,
|
|
46
|
+
stream=True
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
reasoning_content = ""
|
|
50
|
+
content = ""
|
|
51
|
+
|
|
52
|
+
for chunk in response:
|
|
53
|
+
if chunk.choices[0].delta.reasoning_content:
|
|
54
|
+
reasoning_content += chunk.choices[0].delta.reasoning_content
|
|
55
|
+
else:
|
|
56
|
+
content += chunk.choices[0].delta.content
|
|
57
|
+
|
|
58
|
+
# Turn 2
|
|
59
|
+
messages.append({"role": "assistant", "content": content})
|
|
60
|
+
messages.append({'role': 'user', 'content': "How many Rs are there in the word 'strawberry'?"})
|
|
61
|
+
response = client.chat.completions.create(
|
|
62
|
+
model="deepseek-reasoner",
|
|
63
|
+
messages=messages,
|
|
64
|
+
stream=True
|
|
65
|
+
)
|
|
66
|
+
# ...
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
from openai import OpenAI
|
|
72
|
+
client = OpenAI(api_key="<DeepSeek API Key>", base_url="https://api.deepseek.com")
|
|
73
|
+
|
|
74
|
+
# Turn 1
|
|
75
|
+
messages = [{"role": "user", "content": "9.11 and 9.8, which is greater?"}]
|
|
76
|
+
response = client.chat.completions.create(
|
|
77
|
+
model="deepseek-reasoner",
|
|
78
|
+
messages=messages
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
reasoning_content = response.choices[0].message.reasoning_content
|
|
82
|
+
content = response.choices[0].message.content
|
|
83
|
+
|
|
84
|
+
# Turn 2
|
|
85
|
+
messages.append({'role': 'assistant', 'content': content})
|
|
86
|
+
messages.append({'role': 'user', 'content': "How many Rs are there in the word 'strawberry'?"})
|
|
87
|
+
response = client.chat.completions.create(
|
|
88
|
+
model="deepseek-reasoner",
|
|
89
|
+
messages=messages
|
|
90
|
+
)
|
|
91
|
+
# ...
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
DeepSeek model's thinking mode now supports tool calls. Before outputting the final answer, the model can engage in multiple turns of reasoning and tool calls to improve the quality of the response. The calling pattern is illustrated below:
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
During the process of answering question 1 (Turn 1.1 - 1.3), the model performed multiple turns of thinking + tool calls before providing the answer. During this process, the user needs to send the reasoning content (reasoning_content) back to the API to allow the model to continue reasoning.
|
|
102
|
+
|
|
103
|
+
When the next user question begins (Turn 2.1), the previous reasoning_content should be removed, while keeping other elements to send to the API. If reasoning_content is retained and sent to the API, the API will ignore it.
|
|
104
|
+
|
|
105
|
+
Compatibility Notice
|
|
106
|
+
Since the tool invocation process in thinking mode requires users to pass back reasoning_content to the API, if your code does not correctly pass back reasoning_content, the API will return a 400 error. Please refer to the sample code below for the correct way.
|
|
107
|
+
|
|
108
|
+
Sample Code
|
|
109
|
+
Below is a simple sample code for tool calls in thinking mode:
|
|
110
|
+
|
|
111
|
+
import os
|
|
112
|
+
import json
|
|
113
|
+
from openai import OpenAI
|
|
114
|
+
|
|
115
|
+
# The definition of the tools
|
|
116
|
+
tools = [
|
|
117
|
+
{
|
|
118
|
+
"type": "function",
|
|
119
|
+
"function": {
|
|
120
|
+
"name": "get_date",
|
|
121
|
+
"description": "Get the current date",
|
|
122
|
+
"parameters": { "type": "object", "properties": {} },
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"type": "function",
|
|
127
|
+
"function": {
|
|
128
|
+
"name": "get_weather",
|
|
129
|
+
"description": "Get weather of a location, the user should supply the location and date.",
|
|
130
|
+
"parameters": {
|
|
131
|
+
"type": "object",
|
|
132
|
+
"properties": {
|
|
133
|
+
"location": { "type": "string", "description": "The city name" },
|
|
134
|
+
"date": { "type": "string", "description": "The date in format YYYY-mm-dd" },
|
|
135
|
+
},
|
|
136
|
+
"required": ["location", "date"]
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
# The mocked version of the tool calls
|
|
143
|
+
def get_date_mock():
|
|
144
|
+
return "2025-12-01"
|
|
145
|
+
|
|
146
|
+
def get_weather_mock(location, date):
|
|
147
|
+
return "Cloudy 7~13°C"
|
|
148
|
+
|
|
149
|
+
TOOL_CALL_MAP = {
|
|
150
|
+
"get_date": get_date_mock,
|
|
151
|
+
"get_weather": get_weather_mock
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
def clear_reasoning_content(messages):
|
|
155
|
+
for message in messages:
|
|
156
|
+
if hasattr(message, 'reasoning_content'):
|
|
157
|
+
message.reasoning_content = None
|
|
158
|
+
|
|
159
|
+
def run_turn(turn, messages):
|
|
160
|
+
sub_turn = 1
|
|
161
|
+
while True:
|
|
162
|
+
response = client.chat.completions.create(
|
|
163
|
+
model='deepseek-chat',
|
|
164
|
+
messages=messages,
|
|
165
|
+
tools=tools,
|
|
166
|
+
extra_body={ "thinking": { "type": "enabled" } }
|
|
167
|
+
)
|
|
168
|
+
messages.append(response.choices[0].message)
|
|
169
|
+
reasoning_content = response.choices[0].message.reasoning_content
|
|
170
|
+
content = response.choices[0].message.content
|
|
171
|
+
tool_calls = response.choices[0].message.tool_calls
|
|
172
|
+
print(f"Turn {turn}.{sub_turn}\n{reasoning_content=}\n{content=}\n{tool_calls=}")
|
|
173
|
+
# If there is no tool calls, then the model should get a final answer and we need to stop the loop
|
|
174
|
+
if tool_calls is None:
|
|
175
|
+
break
|
|
176
|
+
for tool in tool_calls:
|
|
177
|
+
tool_function = TOOL_CALL_MAP[tool.function.name]
|
|
178
|
+
tool_result = tool_function(**json.loads(tool.function.arguments))
|
|
179
|
+
print(f"tool result for {tool.function.name}: {tool_result}\n")
|
|
180
|
+
messages.append({
|
|
181
|
+
"role": "tool",
|
|
182
|
+
"tool_call_id": tool.id,
|
|
183
|
+
"content": tool_result,
|
|
184
|
+
})
|
|
185
|
+
sub_turn += 1
|
|
186
|
+
|
|
187
|
+
client = OpenAI(
|
|
188
|
+
api_key=os.environ.get('DEEPSEEK_API_KEY'),
|
|
189
|
+
base_url=os.environ.get('DEEPSEEK_BASE_URL'),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
# The user starts a question
|
|
193
|
+
turn = 1
|
|
194
|
+
messages = [{
|
|
195
|
+
"role": "user",
|
|
196
|
+
"content": "How's the weather in Hangzhou Tomorrow"
|
|
197
|
+
}]
|
|
198
|
+
run_turn(turn, messages)
|
|
199
|
+
|
|
200
|
+
# The user starts a new question
|
|
201
|
+
turn = 2
|
|
202
|
+
messages.append({
|
|
203
|
+
"role": "user",
|
|
204
|
+
"content": "How's the weather in Hangzhou Tomorrow"
|
|
205
|
+
})
|
|
206
|
+
# We recommended to clear the reasoning_content in history messages so as to save network bandwidth
|
|
207
|
+
clear_reasoning_content(messages)
|
|
208
|
+
run_turn(turn, messages)
|
|
209
|
+
|
|
210
|
+
In each sub-request of Turn 1, the reasoning_content generated during that turn is sent to the API, allowing the model to continue its previous reasoning. response.choices[0].message contains all necessary fields for the assistant message, including content, reasoning_content, and tool_calls. For simplicity, you can directly append the message to the end of the messages list using the following code:
|
|
211
|
+
|
|
212
|
+
messages.append(response.choices[0].message)
|
|
213
|
+
|
|
214
|
+
This line of code is equivalent to:
|
|
215
|
+
|
|
216
|
+
messages.append({
|
|
217
|
+
'role': 'assistant',
|
|
218
|
+
'content': response.choices[0].message.content,
|
|
219
|
+
'reasoning_content': response.choices[0].message.reasoning_content,
|
|
220
|
+
'tool_calls': response.choices[0].message.tool_calls,
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
At the beginning of Turn 2, we recommend discarding the reasoning_content from previous turns to save network bandwidth:
|
|
224
|
+
|
|
225
|
+
clear_reasoning_content(messages)
|
|
226
|
+
|
|
227
|
+
The sample output of this code is as follows:
|
|
228
|
+
|
|
229
|
+
Turn 1.1
|
|
230
|
+
reasoning_content="The user is asking about the weather in Hangzhou tomorrow. I need to get the current date first, then calculate tomorrow's date, and then call the weather API. Let me start by getting the current date."
|
|
231
|
+
content=''
|
|
232
|
+
tool_calls=[ChatCompletionMessageToolCall(id='call_00_Tcek83ZQ4fFb1RfPQnsPEE5w', function=Function(arguments='{}', name='get_date'), type='function', index=0)]
|
|
233
|
+
tool_result(get_date): 2025-12-01
|
|
234
|
+
|
|
235
|
+
Turn 1.2
|
|
236
|
+
reasoning_content='Today is December 1, 2025. Tomorrow is December 2, 2025. I need to format the date as YYYY-mm-dd: "2025-12-02". Now I can call get_weather with location Hangzhou and date 2025-12-02.'
|
|
237
|
+
content=''
|
|
238
|
+
tool_calls=[ChatCompletionMessageToolCall(id='call_00_V0Uwt4i63m5QnWRS1q1AO1tP', function=Function(arguments='{"location": "Hangzhou", "date": "2025-12-02"}', name='get_weather'), type='function', index=0)]
|
|
239
|
+
tool_result(get_weather): Cloudy 7~13°C
|
|
240
|
+
|
|
241
|
+
Turn 1.3
|
|
242
|
+
reasoning_content="I have the weather information: Cloudy with temperatures between 7 and 13°C. I should respond in a friendly, helpful manner. I'll mention that it's for tomorrow (December 2, 2025) and give the details. I can also ask if they need any other information. Let's craft the response."
|
|
243
|
+
content="Tomorrow (Tuesday, December 2, 2025) in Hangzhou will be **cloudy** with temperatures ranging from **7°C to 13°C**. \n\nIt might be a good idea to bring a light jacket if you're heading out. Is there anything else you'd like to know about the weather?"
|
|
244
|
+
tool_calls=None
|
|
245
|
+
|
|
246
|
+
Turn 2.1
|
|
247
|
+
reasoning_content="The user wants clothing advice for tomorrow based on the weather in Hangzhou. I know tomorrow's weather: cloudy, 7-13°C. That's cool but not freezing. I should suggest layered clothing, maybe a jacket, long pants, etc. I can also mention that since it's cloudy, an umbrella might not be needed unless there's rain chance, but the forecast didn't mention rain. I should be helpful and give specific suggestions. I can also ask if they have any specific activities planned to tailor the advice. Let me respond."
|
|
248
|
+
content="Based on tomorrow's forecast of **cloudy weather with temperatures between 7°C and 13°C** in Hangzhou, here are some clothing suggestions:\n\n**Recommended outfit:**\n- **Upper body:** A long-sleeve shirt or sweater, plus a light to medium jacket (like a fleece, windbreaker, or light coat)\n- **Lower body:** Long pants or jeans\n- **Footwear:** Closed-toe shoes or sneakers\n- **Optional:** A scarf or light hat for extra warmth, especially in the morning and evening\n\n**Why this works:**\n- The temperature range is cool but not freezing, so layering is key\n- Since it's cloudy but no rain mentioned, you likely won't need an umbrella\n- The jacket will help with the morning chill (7°C) and can be removed if you warm up during the day\n\n**If you have specific plans:**\n- For outdoor activities: Consider adding an extra layer\n- For indoor/office settings: The layered approach allows you to adjust comfortably\n\nWould you like more specific advice based on your planned activities?"
|
|
249
|
+
tool_calls=None
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Abort Fence 机制实践示例
|
|
4
|
+
*
|
|
5
|
+
* 这个示例展示了如何在实际场景中使用 Abort Fence 机制
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createConversation } from '../src/conversation.js';
|
|
9
|
+
import { getConfig } from '../src/config.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 示例 1: 简单的 abort 检查
|
|
13
|
+
*/
|
|
14
|
+
async function example1_SimpleAbortCheck() {
|
|
15
|
+
console.log('\n=== Example 1: Simple Abort Check ===\n');
|
|
16
|
+
|
|
17
|
+
const config = getConfig();
|
|
18
|
+
// 禁用 MCP 以避免初始化延迟
|
|
19
|
+
config.mcp.enabled = false;
|
|
20
|
+
const conversation = await createConversation(config);
|
|
21
|
+
|
|
22
|
+
// 开始一个对话阶段
|
|
23
|
+
const phaseId = conversation.beginPhase();
|
|
24
|
+
console.log(`Started phase ${phaseId}`);
|
|
25
|
+
|
|
26
|
+
// 模拟一些处理
|
|
27
|
+
console.log('Processing...');
|
|
28
|
+
|
|
29
|
+
// 检查是否被 abort
|
|
30
|
+
if (conversation.isAborted(phaseId)) {
|
|
31
|
+
console.log('Phase was aborted!');
|
|
32
|
+
return conversation.createAbortResult();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
console.log('Processing completed successfully');
|
|
36
|
+
return { success: true, data: 'some result' };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 示例 2: 在工具执行中使用 abort 检查
|
|
41
|
+
*/
|
|
42
|
+
async function example2_ToolWithAbortCheck() {
|
|
43
|
+
console.log('\n=== Example 2: Tool With Abort Check ===\n');
|
|
44
|
+
|
|
45
|
+
const config = getConfig();
|
|
46
|
+
config.mcp.enabled = false;
|
|
47
|
+
const conversation = await createConversation(config);
|
|
48
|
+
|
|
49
|
+
// 模拟一个长时间运行的工具
|
|
50
|
+
async function longRunningTool(conversation) {
|
|
51
|
+
const phaseId = conversation.activePhaseId;
|
|
52
|
+
|
|
53
|
+
// 执行前检查
|
|
54
|
+
if (conversation.isAborted(phaseId)) {
|
|
55
|
+
return {
|
|
56
|
+
success: false,
|
|
57
|
+
aborted: true,
|
|
58
|
+
error: 'Tool execution aborted before start'
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
console.log('Tool started...');
|
|
63
|
+
|
|
64
|
+
// 模拟长时间操作
|
|
65
|
+
for (let i = 0; i < 5; i++) {
|
|
66
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
67
|
+
|
|
68
|
+
// 每次迭代都检查 abort
|
|
69
|
+
if (conversation.isAborted(phaseId)) {
|
|
70
|
+
console.log('Tool aborted during execution!');
|
|
71
|
+
return {
|
|
72
|
+
success: false,
|
|
73
|
+
aborted: true,
|
|
74
|
+
error: 'Tool execution aborted by user',
|
|
75
|
+
progress: `${i * 20}%`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log(`Tool progress: ${(i + 1) * 20}%`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
console.log('Tool completed successfully');
|
|
83
|
+
return {
|
|
84
|
+
success: true,
|
|
85
|
+
data: 'tool result'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 场景 1: 正常完成
|
|
90
|
+
console.log('Scenario 1: Normal completion');
|
|
91
|
+
const phase1 = conversation.beginPhase();
|
|
92
|
+
const result1 = await longRunningTool(conversation);
|
|
93
|
+
console.log('Result:', result1);
|
|
94
|
+
|
|
95
|
+
// 场景 2: 中途 abort
|
|
96
|
+
console.log('\nScenario 2: Abort during execution');
|
|
97
|
+
const phase2 = conversation.beginPhase();
|
|
98
|
+
|
|
99
|
+
// 启动工具,但不等待
|
|
100
|
+
const toolPromise = longRunningTool(conversation);
|
|
101
|
+
|
|
102
|
+
// 1.2 秒后 abort
|
|
103
|
+
setTimeout(async () => {
|
|
104
|
+
console.log('\n[User presses Ctrl+C]');
|
|
105
|
+
await conversation.abortCurrentPhase();
|
|
106
|
+
console.log('Abort completed\n');
|
|
107
|
+
}, 1200);
|
|
108
|
+
|
|
109
|
+
const result2 = await toolPromise;
|
|
110
|
+
console.log('Result:', result2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 示例 3: 注册 abort handlers
|
|
115
|
+
*/
|
|
116
|
+
async function example3_AbortHandlers() {
|
|
117
|
+
console.log('\n=== Example 3: Abort Handlers ===\n');
|
|
118
|
+
|
|
119
|
+
const config = getConfig();
|
|
120
|
+
config.mcp.enabled = false;
|
|
121
|
+
const conversation = await createConversation(config);
|
|
122
|
+
|
|
123
|
+
const phaseId = conversation.beginPhase();
|
|
124
|
+
console.log(`Started phase ${phaseId}`);
|
|
125
|
+
|
|
126
|
+
// 注册一些 abort handlers
|
|
127
|
+
conversation.registerAbortHandler('cleanup-file', async () => {
|
|
128
|
+
console.log('🔧 Cleaning up temporary files...');
|
|
129
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
130
|
+
console.log('✓ Temporary files cleaned up');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
conversation.registerAbortHandler('close-connection', async () => {
|
|
134
|
+
console.log('🔧 Closing network connection...');
|
|
135
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
136
|
+
console.log('✓ Connection closed');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
conversation.registerAbortHandler('save-state', async () => {
|
|
140
|
+
console.log('🔧 Saving current state...');
|
|
141
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
142
|
+
console.log('✓ State saved');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
console.log('Handlers registered');
|
|
146
|
+
|
|
147
|
+
// 模拟一些工作
|
|
148
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
149
|
+
|
|
150
|
+
// 执行 abort
|
|
151
|
+
console.log('\n[User presses Ctrl+C]');
|
|
152
|
+
await conversation.abortCurrentPhase();
|
|
153
|
+
console.log('\n✓ All handlers completed');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* 示例 4: Abort 后发起新对话
|
|
158
|
+
*/
|
|
159
|
+
async function example4_NewConversationAfterAbort() {
|
|
160
|
+
console.log('\n=== Example 4: New Conversation After Abort ===\n');
|
|
161
|
+
|
|
162
|
+
const config = getConfig();
|
|
163
|
+
config.mcp.enabled = false;
|
|
164
|
+
const conversation = await createConversation(config);
|
|
165
|
+
|
|
166
|
+
// 第一个对话
|
|
167
|
+
console.log('First conversation:');
|
|
168
|
+
const phase1 = conversation.beginPhase();
|
|
169
|
+
console.log(` Phase ID: ${phase1}`);
|
|
170
|
+
|
|
171
|
+
// Abort 第一个对话
|
|
172
|
+
console.log('\n[User presses Ctrl+C]');
|
|
173
|
+
await conversation.abortCurrentPhase();
|
|
174
|
+
console.log(` Abort fence set to: ${conversation.abortFence}`);
|
|
175
|
+
|
|
176
|
+
// 立即发起新对话
|
|
177
|
+
console.log('\nSecond conversation (immediately after abort):');
|
|
178
|
+
const phase2 = conversation.beginPhase();
|
|
179
|
+
console.log(` Phase ID: ${phase2}`);
|
|
180
|
+
console.log(` Is aborted: ${conversation.isAborted(phase2)}`);
|
|
181
|
+
|
|
182
|
+
if (!conversation.isAborted(phase2)) {
|
|
183
|
+
console.log(' ✓ New conversation is NOT affected by old abort');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 示例 5: 完整的对话流程(带 abort)
|
|
189
|
+
*/
|
|
190
|
+
async function example5_FullConversationFlow() {
|
|
191
|
+
console.log('\n=== Example 5: Full Conversation Flow ===\n');
|
|
192
|
+
|
|
193
|
+
const config = getConfig();
|
|
194
|
+
config.mcp.enabled = false;
|
|
195
|
+
const conversation = await createConversation(config);
|
|
196
|
+
|
|
197
|
+
// 模拟发送消息
|
|
198
|
+
async function sendMessage(text, onProgress) {
|
|
199
|
+
const phaseId = conversation.beginPhase();
|
|
200
|
+
console.log(`\n[Phase ${phaseId}] User: ${text}`);
|
|
201
|
+
|
|
202
|
+
onProgress?.({ type: 'start', phaseId });
|
|
203
|
+
|
|
204
|
+
// 模拟 AI 处理
|
|
205
|
+
const steps = [
|
|
206
|
+
'Analyzing request...',
|
|
207
|
+
'Generating response...',
|
|
208
|
+
'Formatting output...'
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
for (const step of steps) {
|
|
212
|
+
// 检查 abort
|
|
213
|
+
if (conversation.isAborted(phaseId)) {
|
|
214
|
+
console.log(`\n[Phase ${phaseId}] ❌ Aborted`);
|
|
215
|
+
onProgress?.({ type: 'aborted', phaseId });
|
|
216
|
+
return conversation.createAbortResult('aborted_during_processing');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
console.log(`[Phase ${phaseId}] ${step}`);
|
|
220
|
+
onProgress?.({ type: 'progress', step, phaseId });
|
|
221
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log(`\n[Phase ${phaseId}] ✅ Completed`);
|
|
225
|
+
onProgress?.({ type: 'complete', phaseId });
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
content: 'Response from AI',
|
|
229
|
+
usage: { input_tokens: 10, output_tokens: 20 },
|
|
230
|
+
aborted: false
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// 场景 1: 正常完成
|
|
235
|
+
console.log('--- Scenario 1: Normal completion ---');
|
|
236
|
+
const result1 = await sendMessage('Hello', (event) => {
|
|
237
|
+
console.log(` [Event: ${event.type}]`);
|
|
238
|
+
});
|
|
239
|
+
console.log('Result:', result1);
|
|
240
|
+
|
|
241
|
+
// 场景 2: 中途 abort
|
|
242
|
+
console.log('\n--- Scenario 2: Abort during processing ---');
|
|
243
|
+
const messagePromise = sendMessage('Tell me a long story', (event) => {
|
|
244
|
+
console.log(` [Event: ${event.type}]`);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 1.2 秒后 abort
|
|
248
|
+
setTimeout(async () => {
|
|
249
|
+
console.log('\n[User presses Ctrl+C]');
|
|
250
|
+
await conversation.abortCurrentPhase();
|
|
251
|
+
}, 1200);
|
|
252
|
+
|
|
253
|
+
const result2 = await messagePromise;
|
|
254
|
+
console.log('Result:', result2);
|
|
255
|
+
|
|
256
|
+
// 场景 3: Abort 后的新消息
|
|
257
|
+
console.log('\n--- Scenario 3: New message after abort ---');
|
|
258
|
+
const result3 = await sendMessage('How are you?', (event) => {
|
|
259
|
+
console.log(` [Event: ${event.type}]`);
|
|
260
|
+
});
|
|
261
|
+
console.log('Result:', result3);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 主函数
|
|
266
|
+
*/
|
|
267
|
+
async function main() {
|
|
268
|
+
try {
|
|
269
|
+
await example1_SimpleAbortCheck();
|
|
270
|
+
await example2_ToolWithAbortCheck();
|
|
271
|
+
await example3_AbortHandlers();
|
|
272
|
+
await example4_NewConversationAfterAbort();
|
|
273
|
+
await example5_FullConversationFlow();
|
|
274
|
+
|
|
275
|
+
console.log('\n✅ All examples completed successfully!\n');
|
|
276
|
+
} catch (error) {
|
|
277
|
+
console.error('\n❌ Error:', error.message);
|
|
278
|
+
console.error(error.stack);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// 运行示例
|
|
284
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
285
|
+
main();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export {
|
|
289
|
+
example1_SimpleAbortCheck,
|
|
290
|
+
example2_ToolWithAbortCheck,
|
|
291
|
+
example3_AbortHandlers,
|
|
292
|
+
example4_NewConversationAfterAbort,
|
|
293
|
+
example5_FullConversationFlow
|
|
294
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "closer-code",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "AI 编程助理 - 通过对话完成编码、调试和任务规划",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,13 +13,19 @@
|
|
|
13
13
|
"setup": "node src/setup.js",
|
|
14
14
|
"build": "npm run build:main && npm run build:cli && npm run build:bash && npm run build:batch",
|
|
15
15
|
"build:sdk": "esbuild src/ai-client-sdk.js --bundle --platform=node --format=esm --outfile=dist/ai-client-sdk.js --external:@anthropic-ai/sdk && esbuild src/tools-sdk.js --bundle --platform=node --format=esm --outfile=dist/tools-sdk.js --external:@anthropic-ai/sdk && esbuild src/conversation-sdk.js --bundle --platform=node --format=esm --outfile=dist/conversation-sdk.js --external:@anthropic-ai/sdk",
|
|
16
|
-
"build:main": "esbuild src/index.js --bundle --platform=node --format=esm --outfile=dist/index.js --external:ink --external:react --external:react-devtools-core --external:react-is --external:prop-types",
|
|
16
|
+
"build:main": "esbuild src/index.js --bundle --platform=node --format=esm --outfile=dist/index.js --external:ink --external:react --external:react-devtools-core --external:react-is --external:prop-types --external:@openai/agents --external:@openai/agents-core --external:openai --external:zod --external:@modelcontextprotocol/sdk",
|
|
17
17
|
"build:cli": "esbuild src/closer-cli.jsx --bundle --platform=node --format=esm --outfile=dist/closer-cli.js --external:ink --external:react --external:ink-text-input --external:ink-select-input --external:ansi-escapes --external:chalk --external:glob",
|
|
18
18
|
"build:bash": "esbuild src/bash-runner.js --bundle --platform=node --format=esm --outfile=dist/bash-runner.js",
|
|
19
19
|
"build:batch": "esbuild src/batch-cli.js --bundle --platform=node --format=esm --outfile=dist/batch-cli.js --external:glob",
|
|
20
|
+
"minify-build": "npm run minify-build:main && npm run minify-build:cli && npm run minify-build:bash && npm run minify-build:batch",
|
|
21
|
+
"minify-build:main": "esbuild src/index.js --bundle --platform=node --format=esm --outfile=dist/index.js --minify --external:ink --external:react --external:react-devtools-core --external:react-is --external:prop-types --external:@openai/agents --external:@openai/agents-core --external:openai --external:zod --external:@modelcontextprotocol/sdk",
|
|
22
|
+
"minify-build:cli": "esbuild src/closer-cli.jsx --bundle --platform=node --format=esm --outfile=dist/closer-cli.js --minify --external:ink --external:react --external:ink-text-input --external:ink-select-input --external:ansi-escapes --external:chalk --external:glob",
|
|
23
|
+
"minify-build:bash": "esbuild src/bash-runner.js --bundle --platform=node --format=esm --outfile=dist/bash-runner.js --minify",
|
|
24
|
+
"minify-build:batch": "esbuild src/batch-cli.js --bundle --platform=node --format=esm --outfile=dist/batch-cli.js --minify --external:glob",
|
|
20
25
|
"start": "npm run build:main && node dist/index.js",
|
|
21
26
|
"dev": "npm run build -- --watch",
|
|
22
27
|
"test": "node src/test-modules.js",
|
|
28
|
+
"test:mcp": "node test/test-mcp.js",
|
|
23
29
|
"check": "node --check dist/index.js",
|
|
24
30
|
"batch": "npm run build:batch && node dist/batch-cli.js"
|
|
25
31
|
},
|
|
@@ -30,18 +36,26 @@
|
|
|
30
36
|
"programming",
|
|
31
37
|
"automation"
|
|
32
38
|
],
|
|
33
|
-
"
|
|
39
|
+
"repository": "uppet/closer-code",
|
|
40
|
+
"author": {
|
|
41
|
+
"name": "Joyer Huang",
|
|
42
|
+
"url": "https://github.com/uppet"
|
|
43
|
+
},
|
|
34
44
|
"license": "MIT",
|
|
35
45
|
"dependencies": {
|
|
36
46
|
"@anthropic-ai/sdk": "^0.71.2",
|
|
47
|
+
"@modelcontextprotocol/sdk": "^1.25.2",
|
|
48
|
+
"@openai/agents": "^0.4.0",
|
|
37
49
|
"glob": "^10.3.0",
|
|
38
50
|
"ink": "^4.4.1",
|
|
39
51
|
"ink-text-input": "^5.0.1",
|
|
52
|
+
"jsonrepair": "^3.13.2",
|
|
53
|
+
"openai": "^6.16.0",
|
|
40
54
|
"react": "^18.2.0",
|
|
41
55
|
"zod": "^4.3.5"
|
|
42
56
|
},
|
|
43
57
|
"devDependencies": {
|
|
44
|
-
"esbuild": "^0.
|
|
58
|
+
"esbuild": "^0.27.2"
|
|
45
59
|
},
|
|
46
60
|
"engines": {
|
|
47
61
|
"node": ">=18.0.0"
|
package/src/ai-client-legacy.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { safeJSONParse } from './utils/json-repair.js';
|
|
2
|
+
|
|
1
3
|
// OpenAI 客户端
|
|
2
4
|
export class OpenAIClient {
|
|
3
5
|
constructor(config) {
|
|
@@ -75,11 +77,14 @@ export class OpenAIClient {
|
|
|
75
77
|
|
|
76
78
|
if (choice.message.tool_calls) {
|
|
77
79
|
for (const toolCall of choice.message.tool_calls) {
|
|
80
|
+
const input = safeJSONParse(toolCall.function.arguments, {
|
|
81
|
+
fallback: {}
|
|
82
|
+
});
|
|
78
83
|
message.content.push({
|
|
79
84
|
type: 'tool_use',
|
|
80
85
|
id: toolCall.id,
|
|
81
86
|
name: toolCall.function.name,
|
|
82
|
-
input
|
|
87
|
+
input
|
|
83
88
|
});
|
|
84
89
|
}
|
|
85
90
|
}
|