@theowlops/boombot 1.1.0 → 1.1.2
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/dist/cli/commands/gateway.js +4 -4
- package/dist/cli/commands/setup.js +60 -46
- package/dist/gateway/config.js +11 -3
- package/dist/gateway/turns/session-adapter.js +4 -0
- package/dist/node_modules/@boombot/core/agent-core/turn-loop.js +75 -7
- package/dist/node_modules/@boombot/core/agent-core/types.d.ts +3 -0
- package/dist/node_modules/@boombot/protocol/gateway-protocol/commands.d.ts +185 -185
- package/dist/package.json +1 -1
- package/dist/web/assets/ChatView-CP16lYju.css +1 -0
- package/dist/web/assets/{ChatView-BZrSD-64.js → ChatView-DwrZXRTf.js} +15 -15
- package/dist/web/assets/{MemoryView-BfMq_6CD.js → MemoryView-GytJpeXY.js} +1 -1
- package/dist/web/assets/{SchedulesView-B22QmPR-.js → SchedulesView-CgZHa7Zc.js} +1 -1
- package/dist/web/assets/{SessionsView-DS0-aH2a.js → SessionsView-DIaw3vEm.js} +1 -1
- package/dist/web/assets/{TasksView-ey0FLsW8.js → TasksView-BIccQf95.js} +1 -1
- package/dist/web/assets/{Textarea-DN3Zybl1.js → Textarea-DjaaMDYP.js} +1 -1
- package/dist/web/assets/{Tooltip-MzV2Gqnc.js → Tooltip-DSXSVOvn.js} +1 -1
- package/dist/web/assets/{browser-Bnt73-Ub.js → browser-BfCCGa0f.js} +1 -1
- package/dist/web/assets/{index-CuPR73Tn.js → index-7FrWWPBR.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/ChatView-Cahbjez6.css +0 -1
|
@@ -97,11 +97,11 @@ function gatewayHostPort() {
|
|
|
97
97
|
try {
|
|
98
98
|
const config = loadConfig(resolveConfigPath());
|
|
99
99
|
const host = process.env['BOOMBOT_HOST']?.trim() || config?.host || '127.0.0.1';
|
|
100
|
-
const port = Number.parseInt(process.env['BOOMBOT_PORT']?.trim() || String(config?.port ??
|
|
101
|
-
return { host, port: Number.isInteger(port) && port > 0 && port <= 65535 ? port :
|
|
100
|
+
const port = Number.parseInt(process.env['BOOMBOT_PORT']?.trim() || String(config?.port ?? 8788), 10);
|
|
101
|
+
return { host, port: Number.isInteger(port) && port > 0 && port <= 65535 ? port : 8788 };
|
|
102
102
|
}
|
|
103
103
|
catch {
|
|
104
|
-
return { host: '127.0.0.1', port:
|
|
104
|
+
return { host: '127.0.0.1', port: 8788 };
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
/** Public URL for the browser: 0.0.0.0/:: mean "this machine". Exported for tests. */
|
|
@@ -142,7 +142,7 @@ export function initializeGateway(configPath = resolveConfigPath()) {
|
|
|
142
142
|
ensureMasterKey(masterKeyPath);
|
|
143
143
|
}
|
|
144
144
|
const { apiKeyHash } = ensureApiKey(resolveApiKeyPath(configPath));
|
|
145
|
-
saveConfig({ host: '127.0.0.1', port:
|
|
145
|
+
saveConfig({ host: '127.0.0.1', port: 8788, dataDir, apiKeyHash }, configPath, 'wx');
|
|
146
146
|
return true;
|
|
147
147
|
}
|
|
148
148
|
/**
|
|
@@ -129,59 +129,69 @@ export function browserHost(host) {
|
|
|
129
129
|
return host === '0.0.0.0' || host === '::' ? '127.0.0.1' : host;
|
|
130
130
|
}
|
|
131
131
|
export async function runSetup() {
|
|
132
|
-
|
|
132
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
133
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
134
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
135
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
136
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
137
|
+
console.log(`
|
|
138
|
+
${cyan('╭───────────────────────────────────────────────╮')}
|
|
139
|
+
${cyan('│')} ${bold('🦉 BoomBot — First-Run Setup Wizard')} ${cyan('│')}
|
|
140
|
+
${cyan('│')} ${dim('Thiết lập nhanh Gateway, API Key & LLM')} ${cyan('│')}
|
|
141
|
+
${cyan('╰───────────────────────────────────────────────╯')}
|
|
142
|
+
`);
|
|
133
143
|
const existing = loadConfig();
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
144
|
+
console.log(`${bold('1. Cấu hình mạng & đường dẫn (Network & Paths)')}`);
|
|
145
|
+
const host = await askDefault(` ${cyan('•')} Gateway host`, existing?.host ?? '127.0.0.1');
|
|
146
|
+
const defaultPort = existing?.port ?? 8788;
|
|
147
|
+
const port = parsePort(await ask(` ${cyan('•')} Gateway port [${defaultPort}]: `), defaultPort);
|
|
148
|
+
const dataDir = await askDefault(` ${cyan('•')} Thư mục dữ liệu (Data directory)`, existing?.dataDir ?? '~/.boombot/data');
|
|
138
149
|
// API key — kept at <config-dir>/api.key (0600) so it stays recoverable; the
|
|
139
150
|
// config only ever holds its sha256. An existing key is reused, never rotated.
|
|
151
|
+
console.log(`\n${bold('2. Khóa bảo mật Gateway (Security Keys)')}`);
|
|
140
152
|
const { apiKey, apiKeyHash, keyPath, created, fromEnv } = ensureApiKey();
|
|
141
153
|
if (fromEnv) {
|
|
142
|
-
console.log('
|
|
154
|
+
console.log(` ${green('✔')} Sử dụng API key từ biến môi trường BOOMBOT_API_KEY`);
|
|
143
155
|
}
|
|
144
156
|
else if (created) {
|
|
145
|
-
console.log('
|
|
146
|
-
console.log('
|
|
147
|
-
console.log(
|
|
148
|
-
console.log(`
|
|
149
|
-
console.log('there and signs your browser in automatically, so you can ignore this');
|
|
150
|
-
console.log('notice. Only the sha256 hash is written to config.json.');
|
|
151
|
-
console.log('────────────────────────────────────────\n');
|
|
157
|
+
console.log(` ${yellow('┌─ Gateway API Key của bạn (Lưu vào nơi an toàn) ───────────────┐')}`);
|
|
158
|
+
console.log(` ${yellow('│')} ${bold(apiKey)}`);
|
|
159
|
+
console.log(` ${yellow('└───────────────────────────────────────────────────────────────┘')}`);
|
|
160
|
+
console.log(` ${dim(`Đã lưu tại: ${keyPath} (tự động đăng nhập web)`)}`);
|
|
152
161
|
}
|
|
153
162
|
else {
|
|
154
|
-
console.log(
|
|
155
|
-
console.log('(delete that file and re-run setup to rotate it)\n');
|
|
163
|
+
console.log(` ${green('✔')} Giữ API key hiện có tại: ${dim(keyPath)}`);
|
|
156
164
|
}
|
|
157
|
-
// Master key — encrypts provider API keys at rest.
|
|
158
|
-
// as BOOMBOT_MASTER_KEY; `boombot gateway` loads this file automatically.
|
|
165
|
+
// Master key — encrypts provider API keys at rest.
|
|
159
166
|
const masterKeyPath = resolveMasterKeyPath();
|
|
160
167
|
const masterKey = process.env['BOOMBOT_MASTER_KEY'] || ensureMasterKey(masterKeyPath);
|
|
161
|
-
console.log(`Master key
|
|
162
|
-
console.log('(mode 0600 — it decrypts your provider keys, keep it safe)\n');
|
|
168
|
+
console.log(` ${green('✔')} Master key mã hóa AES-256: ${dim(masterKeyPath)}`);
|
|
163
169
|
// Telegram (optional).
|
|
170
|
+
console.log(`\n${bold('3. Kênh kết nối Chat (Messaging Channels)')}`);
|
|
164
171
|
let telegram = existing?.telegram;
|
|
165
|
-
if (await askYesNo('
|
|
166
|
-
const botToken = await readSecret('
|
|
167
|
-
const allowlist = parseAllowlist(await ask('
|
|
172
|
+
if (await askYesNo(` ${cyan('?')} Cấu hình bot Telegram?`, false)) {
|
|
173
|
+
const botToken = await readSecret(' Token bot Telegram: ');
|
|
174
|
+
const allowlist = parseAllowlist(await ask(' Chat IDs được phép (phân cách bằng dấu phẩy, để trống = tất cả): '));
|
|
168
175
|
telegram = { botToken: botToken.trim(), allowlistChatIds: allowlist };
|
|
176
|
+
console.log(` ${green('✔')} Đã cấu hình Telegram.`);
|
|
169
177
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
else {
|
|
179
|
+
console.log(` ${dim('Bỏ qua kết nối Telegram (có thể cấu hình sau trên Web UI).')}`);
|
|
180
|
+
}
|
|
181
|
+
// Default provider → the real encrypted store (<dataDir>/providers.json)
|
|
182
|
+
console.log(`\n${bold('4. Nhà cung cấp AI (LLM Provider)')}`);
|
|
183
|
+
if (await askYesNo(` ${cyan('?')} Thiết lập nhà cung cấp LLM mặc định?`, true)) {
|
|
184
|
+
console.log(` ${dim(`Các loại hỗ trợ: ${PROVIDER_KINDS.join(', ')}`)}`);
|
|
175
185
|
const kind = await askProviderKind();
|
|
176
186
|
if (kind !== null) {
|
|
177
|
-
const model = await askRequired('
|
|
187
|
+
const model = await askRequired(' Model mặc định (vd: gpt-4o, claude-3-7-sonnet): ');
|
|
178
188
|
const needsBaseUrl = kind === 'openai-compatible' || kind === 'local';
|
|
179
189
|
const baseUrl = needsBaseUrl
|
|
180
|
-
? await askRequired('Base URL (
|
|
181
|
-
: (await ask('Base URL (
|
|
190
|
+
? await askRequired(' Base URL (bắt buộc cho loại này): ')
|
|
191
|
+
: (await ask(' Base URL (để trống = mặc định): ')).trim();
|
|
182
192
|
const providerApiKey = kind === 'local'
|
|
183
|
-
? (await readSecret('Provider API key (
|
|
184
|
-
: await readRequiredSecret('Provider API key: ');
|
|
193
|
+
? (await readSecret(' Provider API key (để trống nếu không có): ')).trim()
|
|
194
|
+
: await readRequiredSecret(' Provider API key: ');
|
|
185
195
|
const service = createProviderService({
|
|
186
196
|
storagePath: path.join(expandTilde(dataDir), 'providers.json'),
|
|
187
197
|
masterKey,
|
|
@@ -195,11 +205,13 @@ export async function runSetup() {
|
|
|
195
205
|
...(providerApiKey !== '' ? { apiKey: providerApiKey } : {}),
|
|
196
206
|
active: true,
|
|
197
207
|
});
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
console.log(`\nSaved default provider '${saved.name}' (${saved.kind})${keyNote}.`);
|
|
208
|
+
const keyNote = providerApiKey !== '' ? ' (đã mã hóa an toàn)' : '';
|
|
209
|
+
console.log(` ${green('✔')} Đã lưu provider '${saved.name}' [${saved.kind}]${keyNote}`);
|
|
201
210
|
}
|
|
202
211
|
}
|
|
212
|
+
else {
|
|
213
|
+
console.log(` ${dim('Bỏ qua cấu hình LLM (bạn có thể thêm trực tiếp trên Web UI).')}`);
|
|
214
|
+
}
|
|
203
215
|
const config = {
|
|
204
216
|
...existing,
|
|
205
217
|
host,
|
|
@@ -210,21 +222,23 @@ export async function runSetup() {
|
|
|
210
222
|
};
|
|
211
223
|
const problems = validateConfig(config);
|
|
212
224
|
if (problems.length > 0) {
|
|
213
|
-
console.error(`\
|
|
225
|
+
console.error(`\n❌ Cấu hình không hợp lệ: ${problems.join('; ')}`);
|
|
214
226
|
process.exitCode = 1;
|
|
215
227
|
return;
|
|
216
228
|
}
|
|
217
229
|
const configPath = resolveConfigPath();
|
|
218
230
|
saveConfig(config, configPath);
|
|
219
|
-
const permsNote = process.platform === 'win32'
|
|
220
|
-
? 'permissions NOT enforced on Windows (chmod is a no-op) — see SECURITY.md §5'
|
|
221
|
-
: 'mode 0600 — secrets protected';
|
|
222
|
-
console.log(`\nSaved config to ${configPath} (${permsNote}).`);
|
|
223
231
|
const url = `http://${browserHost(host)}:${port}`;
|
|
224
|
-
console.log(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
232
|
+
console.log(`
|
|
233
|
+
${green('╭───────────────────────────────────────────────╮')}
|
|
234
|
+
${green('│')} ${bold('🎉 CÀI ĐẶT HOÀN TẤT THÀNH CÔNG!')} ${green('│')}
|
|
235
|
+
${green('╰───────────────────────────────────────────────╯')}
|
|
236
|
+
|
|
237
|
+
${bold('Tệp cấu hình:')} ${dim(configPath)}
|
|
238
|
+
${bold('Giao diện Web:')} ${cyan(url)}
|
|
239
|
+
|
|
240
|
+
${bold('Bắt đầu sử dụng:')}
|
|
241
|
+
${cyan('1.')} ${bold('boombot')} Khởi động Gateway và mở Web UI
|
|
242
|
+
${cyan('2.')} ${bold('boombot doctor')} Kiểm tra toàn diện tình trạng hệ thống
|
|
243
|
+
`);
|
|
230
244
|
}
|
package/dist/gateway/config.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
1
2
|
import { join } from 'node:path';
|
|
2
3
|
import { hashApiKey } from './auth.js';
|
|
3
4
|
import { coerceChannelSection, defaultConfigDir, defaultConfigPath, readConfigObject, resolveConfigFilePath, } from './config-format.js';
|
|
4
5
|
const DEFAULT_HOST = '127.0.0.1';
|
|
5
|
-
const DEFAULT_PORT =
|
|
6
|
+
const DEFAULT_PORT = 8788;
|
|
6
7
|
/**
|
|
7
8
|
* Default config path (`~/.boombot/config.yml`). The real definition lives in
|
|
8
9
|
* `config-format.ts`, next to the file probe order; re-exported here so old
|
|
@@ -397,8 +398,15 @@ export function loadConfig(env = process.env) {
|
|
|
397
398
|
const fileHost = typeof file.host === 'string' && file.host.length > 0 ? file.host : undefined;
|
|
398
399
|
const host = get('BOOMBOT_HOST') ?? fileHost ?? DEFAULT_HOST;
|
|
399
400
|
const port = parsePort(get('BOOMBOT_PORT') ?? file.port);
|
|
400
|
-
const fileDataDir = typeof file.dataDir === 'string' && file.dataDir.length > 0
|
|
401
|
-
|
|
401
|
+
const fileDataDir = typeof file.dataDir === 'string' && file.dataDir.length > 0
|
|
402
|
+
? (file.dataDir.startsWith('~/') || file.dataDir.startsWith('~\\')
|
|
403
|
+
? join(homedir(), file.dataDir.slice(2))
|
|
404
|
+
: file.dataDir)
|
|
405
|
+
: undefined;
|
|
406
|
+
const rawDataDir = get('BOOMBOT_DATA_DIR') ?? fileDataDir ?? defaultDataDir(env);
|
|
407
|
+
const dataDir = rawDataDir.startsWith('~/') || rawDataDir.startsWith('~\\')
|
|
408
|
+
? join(homedir(), rawDataDir.slice(2))
|
|
409
|
+
: rawDataDir;
|
|
402
410
|
const apiKeyHash = resolveApiKeyHash(file.apiKeyHash, get('BOOMBOT_API_KEY'));
|
|
403
411
|
const channels = mergeChannelEnv(parseChannels(file.channels, configPath), get);
|
|
404
412
|
const reasoningGuard = {
|
|
@@ -84,6 +84,7 @@ function toChatMessage(m) {
|
|
|
84
84
|
...(metadata?.toolCalls !== undefined ? { toolCalls: metadata.toolCalls } : {}),
|
|
85
85
|
...(metadata?.toolCallId !== undefined ? { toolCallId: metadata.toolCallId } : {}),
|
|
86
86
|
...(metadata?.toolName !== undefined ? { name: metadata.toolName } : {}),
|
|
87
|
+
...(metadata?.reasoning !== undefined ? { reasoning: metadata.reasoning } : {}),
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
/**
|
|
@@ -105,6 +106,9 @@ function toSessionMetadata(message) {
|
|
|
105
106
|
if (message.name !== undefined && message.name !== '') {
|
|
106
107
|
metadata.toolName = message.name;
|
|
107
108
|
}
|
|
109
|
+
if (message.reasoning !== undefined && message.reasoning !== '') {
|
|
110
|
+
metadata.reasoning = message.reasoning;
|
|
111
|
+
}
|
|
108
112
|
return Object.keys(metadata).length > 0 ? metadata : undefined;
|
|
109
113
|
}
|
|
110
114
|
function boundedContext(messages, alreadyOmitted = 0) {
|
|
@@ -174,6 +174,7 @@ async function modelRound(deps, messages, tools, emit, signal) {
|
|
|
174
174
|
checkCancelled(signal);
|
|
175
175
|
let text = '';
|
|
176
176
|
let reasoning = '';
|
|
177
|
+
let inThinkTag = false;
|
|
177
178
|
const usage = { inputTokens: 0, outputTokens: 0 };
|
|
178
179
|
const accumulators = new Map();
|
|
179
180
|
const order = [];
|
|
@@ -188,8 +189,43 @@ async function modelRound(deps, messages, tools, emit, signal) {
|
|
|
188
189
|
}
|
|
189
190
|
}
|
|
190
191
|
if (chunk.delta) {
|
|
191
|
-
|
|
192
|
-
|
|
192
|
+
let remaining = chunk.delta;
|
|
193
|
+
while (remaining.length > 0) {
|
|
194
|
+
if (inThinkTag) {
|
|
195
|
+
const closeIdx = remaining.indexOf('</think>');
|
|
196
|
+
if (closeIdx !== -1) {
|
|
197
|
+
const thinkPart = remaining.slice(0, closeIdx);
|
|
198
|
+
remaining = remaining.slice(closeIdx + '</think>'.length);
|
|
199
|
+
inThinkTag = false;
|
|
200
|
+
if (thinkPart) {
|
|
201
|
+
reasoning += thinkPart;
|
|
202
|
+
emit({ type: 'reasoning.delta', text: thinkPart });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
reasoning += remaining;
|
|
207
|
+
emit({ type: 'reasoning.delta', text: remaining });
|
|
208
|
+
remaining = '';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const openIdx = remaining.indexOf('<think>');
|
|
213
|
+
if (openIdx !== -1) {
|
|
214
|
+
const textPart = remaining.slice(0, openIdx);
|
|
215
|
+
remaining = remaining.slice(openIdx + '<think>'.length);
|
|
216
|
+
inThinkTag = true;
|
|
217
|
+
if (textPart) {
|
|
218
|
+
text += textPart;
|
|
219
|
+
emit({ type: 'text.delta', text: textPart });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
text += remaining;
|
|
224
|
+
emit({ type: 'text.delta', text: remaining });
|
|
225
|
+
remaining = '';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
193
229
|
}
|
|
194
230
|
if (chunk.reasoningDelta) {
|
|
195
231
|
reasoning += chunk.reasoningDelta;
|
|
@@ -244,15 +280,15 @@ async function modelRound(deps, messages, tools, emit, signal) {
|
|
|
244
280
|
arguments: parsed,
|
|
245
281
|
});
|
|
246
282
|
}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const finalText = text.trim() ? text : (calls.length ? '' : reasoning.trim());
|
|
283
|
+
const finalText = text.trim();
|
|
284
|
+
const trimmedReasoning = reasoning.trim();
|
|
250
285
|
const assistantMessage = {
|
|
251
286
|
role: 'assistant',
|
|
252
287
|
content: finalText,
|
|
288
|
+
...(trimmedReasoning ? { reasoning: trimmedReasoning } : {}),
|
|
253
289
|
...(calls.length > 0 ? { toolCalls: calls } : {}),
|
|
254
290
|
};
|
|
255
|
-
return { text: finalText, toolCalls: calls, assistantMessage, usage };
|
|
291
|
+
return { text: finalText, reasoning: trimmedReasoning || undefined, toolCalls: calls, assistantMessage, usage };
|
|
256
292
|
}
|
|
257
293
|
async function executeToolBatch(deps, batch, emit, signal) {
|
|
258
294
|
const results = [];
|
|
@@ -431,6 +467,7 @@ export async function runTurn(deps, context, onEvent, signal) {
|
|
|
431
467
|
let emptyRecoveryPending = false;
|
|
432
468
|
let emptyRecoveryUsed = false;
|
|
433
469
|
let contextRetryUsed = false;
|
|
470
|
+
let lastRoundReasoning;
|
|
434
471
|
while (rounds < maxRounds) {
|
|
435
472
|
checkCancelled(signal);
|
|
436
473
|
rounds++;
|
|
@@ -440,7 +477,10 @@ export async function runTurn(deps, context, onEvent, signal) {
|
|
|
440
477
|
const requestMessages = () => {
|
|
441
478
|
const prepared = [...modelMessages];
|
|
442
479
|
if (recoverEmpty) {
|
|
443
|
-
|
|
480
|
+
const nudge = lastRoundReasoning
|
|
481
|
+
? 'Bạn đã hoàn thành phần suy luận nhưng chưa sinh nội dung câu trả lời cho người dùng. Hãy đưa ra câu trả lời trực tiếp và chi tiết cho người dùng ngay bây giờ bằng tiếng Việt, không lặp lại phần suy luận và không gọi công cụ.'
|
|
482
|
+
: EMPTY_RESPONSE_NUDGE;
|
|
483
|
+
prepared.push({ role: 'user', content: `[Lưu ý hệ thống: ${nudge}]` });
|
|
444
484
|
}
|
|
445
485
|
if (answerOnly) {
|
|
446
486
|
prepared.push({
|
|
@@ -469,8 +509,36 @@ export async function runTurn(deps, context, onEvent, signal) {
|
|
|
469
509
|
if (!emptyRecoveryUsed && rounds < maxRounds) {
|
|
470
510
|
emptyRecoveryUsed = true;
|
|
471
511
|
emptyRecoveryPending = true;
|
|
512
|
+
lastRoundReasoning = model.reasoning;
|
|
472
513
|
continue;
|
|
473
514
|
}
|
|
515
|
+
// Force one final synthesis round without tools if reasoning exists
|
|
516
|
+
if (model.reasoning && rounds < maxRounds) {
|
|
517
|
+
try {
|
|
518
|
+
const forceMessages = [
|
|
519
|
+
...modelMessages,
|
|
520
|
+
{
|
|
521
|
+
role: 'user',
|
|
522
|
+
content: 'Hãy tổng hợp và đưa ra câu trả lời trực tiếp, đầy đủ cho người dùng ngay bây giờ dựa trên các suy luận ở trên bằng tiếng Việt. Không gọi thêm công cụ.',
|
|
523
|
+
},
|
|
524
|
+
];
|
|
525
|
+
const finalModel = await execPhase('modelRound', () => modelRound(deps, forceMessages, [], emit, signal));
|
|
526
|
+
const finalText = finalModel.text.trim();
|
|
527
|
+
if (finalText) {
|
|
528
|
+
const finalMsg = {
|
|
529
|
+
role: 'assistant',
|
|
530
|
+
content: finalText,
|
|
531
|
+
...(model.reasoning ? { reasoning: model.reasoning } : {}),
|
|
532
|
+
};
|
|
533
|
+
messages.push(finalMsg);
|
|
534
|
+
modelMessages.push(finalMsg);
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch {
|
|
539
|
+
// fall through to error
|
|
540
|
+
}
|
|
541
|
+
}
|
|
474
542
|
const result = await finish('error', 'model returned an empty response');
|
|
475
543
|
emit({ type: 'turn.end', result });
|
|
476
544
|
return result;
|
|
@@ -21,6 +21,8 @@ export type MessageRole = 'system' | 'user' | 'assistant' | 'tool';
|
|
|
21
21
|
export interface ChatMessage {
|
|
22
22
|
readonly role: MessageRole;
|
|
23
23
|
readonly content: string;
|
|
24
|
+
/** Optional reasoning/thinking output from reasoning models or thinking blocks. */
|
|
25
|
+
readonly reasoning?: string;
|
|
24
26
|
/** Optional image references for multimodal user messages. */
|
|
25
27
|
readonly images?: readonly {
|
|
26
28
|
readonly url: string;
|
|
@@ -123,6 +125,7 @@ export type EventHandler = (event: AgentEvent) => void;
|
|
|
123
125
|
/** Phase payloads consumed by gateway middleware after Python returns them. */
|
|
124
126
|
export interface ModelRoundResult {
|
|
125
127
|
readonly text: string;
|
|
128
|
+
readonly reasoning?: string;
|
|
126
129
|
readonly toolCalls: readonly ToolCall[];
|
|
127
130
|
readonly assistantMessage: ChatMessage;
|
|
128
131
|
}
|