@make-u-free/migi 0.4.5 → 0.4.7

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 (3) hide show
  1. package/bin/migi.js +18 -10
  2. package/package.json +1 -1
  3. package/src/agent.js +52 -11
package/bin/migi.js CHANGED
@@ -87,10 +87,18 @@ async function readChatInput() {
87
87
  let curLine = 0
88
88
  let drawnLines = 0
89
89
  let cursorLine = 0 // カーソルの物理行(drawn area 先頭からの offset)
90
+ let drawPending = false
90
91
 
91
92
  emitKeypressEvents(process.stdin)
92
93
  if (process.stdin.isTTY) process.stdin.setRawMode(true)
93
94
 
95
+ // ペースト等の連続入力をまとめて1回の描画にするためのデバウンス
96
+ function scheduleDraw() {
97
+ if (drawPending) return
98
+ drawPending = true
99
+ setImmediate(() => { drawPending = false; draw() })
100
+ }
101
+
94
102
  function draw() {
95
103
  const w = process.stdout.columns || 80
96
104
  const newLines = [
@@ -136,11 +144,12 @@ async function readChatInput() {
136
144
 
137
145
  const onKey = (str, key) => {
138
146
  if (!key) {
139
- if (str) { lines[curLine] += str; draw() }
147
+ if (str) { lines[curLine] += str; scheduleDraw() }
140
148
  return
141
149
  }
142
150
 
143
151
  if (key.ctrl && key.name === 'c') {
152
+ if (drawPending) { drawPending = false; draw() } // カーソル位置を確定させてから終了
144
153
  process.stdout.write(`\x1b[${drawnLines - 1 - curLine}B\n`)
145
154
  process.stdin.removeListener('keypress', onKey)
146
155
  if (process.stdin.isTTY) process.stdin.setRawMode(false)
@@ -153,9 +162,10 @@ async function readChatInput() {
153
162
  if (key.meta || key.shift) {
154
163
  lines.splice(curLine + 1, 0, '')
155
164
  curLine++
156
- draw()
165
+ scheduleDraw()
157
166
  } else {
158
- // Enter → 送信
167
+ // Enter → 送信(保留中の描画があれば先に確定)
168
+ if (drawPending) { drawPending = false; draw() }
159
169
  const content = lines.join('\n').trim()
160
170
  if (!content) return
161
171
  process.stdout.write(`\x1b[${drawnLines - 1 - curLine}B\n`)
@@ -169,18 +179,18 @@ async function readChatInput() {
169
179
  if (key.name === 'backspace') {
170
180
  if (lines[curLine].length > 0) {
171
181
  lines[curLine] = lines[curLine].slice(0, -1)
172
- draw()
182
+ scheduleDraw()
173
183
  } else if (curLine > 0) {
174
184
  lines.splice(curLine, 1)
175
185
  curLine--
176
- draw()
186
+ scheduleDraw()
177
187
  }
178
188
  return
179
189
  }
180
190
 
181
191
  if (str && !key.ctrl && !key.meta) {
182
192
  lines[curLine] += str
183
- draw()
193
+ scheduleDraw()
184
194
  }
185
195
  }
186
196
 
@@ -239,8 +249,7 @@ async function prompt() {
239
249
  console.log('\n' + sepWithLabel(chalk.bold.cyan(agentName) + chalk.dim(` [スキル: ${parsed.name}]`)))
240
250
  const expanded = expandSkill(skill.content, parsed.args)
241
251
  try {
242
- const reply = await agent.chat(expanded)
243
- console.log('\n' + reply + '\n')
252
+ await agent.chat(expanded)
244
253
  } catch (err) {
245
254
  console.error(chalk.red('\n エラー: ' + err.message + '\n'))
246
255
  }
@@ -255,8 +264,7 @@ async function prompt() {
255
264
  // --- 通常チャット ---
256
265
  console.log('\n' + sepWithLabel(chalk.bold.cyan(agentName)))
257
266
  try {
258
- const reply = await agent.chat(input)
259
- console.log('\n' + reply + '\n')
267
+ await agent.chat(input)
260
268
  } catch (err) {
261
269
  console.error(chalk.red('\n エラー: ' + err.message + '\n'))
262
270
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@make-u-free/migi",
3
- "version": "0.4.5",
3
+ "version": "0.4.7",
4
4
  "description": "Your AI right-hand agent. Works anywhere, with any LLM API.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/agent.js CHANGED
@@ -97,28 +97,69 @@ ${userNameLine}
97
97
 
98
98
  while (true) {
99
99
  spinner.start('考え中…')
100
- const response = await this.client.chat.completions.create({
100
+
101
+ const stream = await this.client.chat.completions.create({
101
102
  model: this.model,
102
103
  messages,
103
104
  tools: this.tools,
104
- tool_choice: 'auto'
105
+ tool_choice: 'auto',
106
+ stream: true
105
107
  })
106
- spinner.stop()
107
108
 
108
- const choice = response.choices[0]
109
- messages.push(choice.message)
110
- this.history.push(choice.message)
109
+ let content = ''
110
+ const tcMap = {} // tool_calls をインデックスで蓄積
111
+ let finishReason = null
112
+ let streaming = false // 最初のコンテンツが届いたか
113
+
114
+ for await (const chunk of stream) {
115
+ const choice = chunk.choices[0]
116
+ if (!choice) continue
117
+ const delta = choice.delta
118
+ if (choice.finish_reason) finishReason = choice.finish_reason
119
+
120
+ // テキストチャンク
121
+ if (delta?.content) {
122
+ if (!streaming) {
123
+ spinner.stop()
124
+ process.stdout.write('\n')
125
+ streaming = true
126
+ }
127
+ content += delta.content
128
+ process.stdout.write(delta.content)
129
+ }
130
+
131
+ // tool_calls チャンク(引数はストリームで分割されて届く)
132
+ if (delta?.tool_calls) {
133
+ for (const tc of delta.tool_calls) {
134
+ if (!tcMap[tc.index]) tcMap[tc.index] = { id: '', type: 'function', function: { name: '', arguments: '' } }
135
+ if (tc.id) tcMap[tc.index].id += tc.id
136
+ if (tc.function?.name) tcMap[tc.index].function.name += tc.function.name
137
+ if (tc.function?.arguments) tcMap[tc.index].function.arguments += tc.function.arguments
138
+ }
139
+ }
140
+ }
141
+
142
+ spinner.stop()
111
143
 
112
144
  // 通常の返答
113
- if (choice.finish_reason === 'stop') {
114
- return choice.message.content
145
+ if (finishReason === 'stop') {
146
+ process.stdout.write('\n\n')
147
+ const assistantMsg = { role: 'assistant', content }
148
+ messages.push(assistantMsg)
149
+ this.history.push(assistantMsg)
150
+ return content
115
151
  }
116
152
 
117
153
  // ツール呼び出し
118
- if (choice.finish_reason === 'tool_calls') {
119
- const toolResults = []
154
+ if (finishReason === 'tool_calls') {
155
+ if (streaming) process.stdout.write('\n')
156
+ const toolCalls = Object.values(tcMap)
157
+ const assistantMsg = { role: 'assistant', content: content || null, tool_calls: toolCalls }
158
+ messages.push(assistantMsg)
159
+ this.history.push(assistantMsg)
120
160
 
121
- for (const toolCall of choice.message.tool_calls) {
161
+ const toolResults = []
162
+ for (const toolCall of toolCalls) {
122
163
  const args = JSON.parse(toolCall.function.arguments)
123
164
  const name = toolCall.function.name
124
165