magic-agent 2.3.0 → 2.3.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/CLIENT-CONTRACT.md +51 -12
- package/README.md +10 -4
- package/client-ui/pc/app.css +14 -0
- package/client-ui/pc/js/93-rail.js +3 -1
- package/client-ui/shared/conv/03-chat-message.js +20 -3
- package/client-ui/shared/conv/27-conv-ledger.js +1 -1
- package/client-ui/shared/conv/conv.css +17 -0
- package/client-ui/shared/conversation.js +170 -39
- package/client-ui/shared/transport-http.js +183 -38
- package/client-ui-mobile/index.html +370 -68
- package/client-ui-mobile/reqs.js +42 -29
- package/internal/agent/capability.go +8 -1
- package/internal/agent/claude.go +5 -3
- package/internal/agent/codebuddy.go +6 -2
- package/internal/agent/codex.go +7 -2
- package/internal/agent/dsh_sdk.go +8 -0
- package/internal/agent/dsh_sdk_test.go +17 -0
- package/internal/agent/engine.go +20 -0
- package/internal/agent/engine_test.go +52 -0
- package/internal/agent/llmengine.go +14 -0
- package/internal/agent/llmengine_test.go +24 -0
- package/internal/agent/llmhttp.go +6 -0
- package/internal/agent/llmhttp_test.go +27 -0
- package/internal/agent/llmmodels.go +48 -0
- package/internal/agent/permission.go +5 -0
- package/internal/cli/ask.go +32 -17
- package/internal/cli/requirements.go +2 -0
- package/internal/cli/sessionlog.go +5 -0
- package/internal/cli/sessionlog_user_test.go +49 -0
- package/internal/client/requirements.go +27 -7
- package/internal/client/requirements_test.go +51 -0
- package/internal/client/service.go +24 -2
- package/internal/client/service_test.go +35 -0
- package/internal/requirements/consolidate.go +86 -24
- package/internal/requirements/requirements.go +9 -0
- package/internal/requirements/requirements_test.go +129 -0
- package/internal/requirements/store.go +11 -3
- package/internal/suggest/suggest.go +118 -7
- package/internal/suggest/suggest_test.go +50 -0
- package/npm/apk/magic-agent-mobile-release.apk +0 -0
- package/npm/apk/version.json +3 -3
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-arm64/magic-client +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-client +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-client +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-client +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/dist/win32-x64/magic-client.exe +0 -0
- package/package.json +3 -2
package/client-ui-mobile/reqs.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 现在 index 只留两个空容器(#reqs / #reqDetail)+ 一次 mount,本文件自包含:
|
|
6
6
|
* · 一级:空间目录(空间 → 模块,功能点不画);
|
|
7
7
|
* · 二级:点模块进去,**只看这个模块**的需求列表;
|
|
8
|
-
* · 「整理」:POST /desk/requirements/consolidate(全量 / 单空间 /
|
|
8
|
+
* · 「整理」:POST /desk/requirements/consolidate(全量 / 单空间 / 二级页单模块)。
|
|
9
9
|
*
|
|
10
10
|
* 与宿主(index.html)的边界:只通过 mount(host) 注入的能力交互 ——
|
|
11
11
|
* getBase / getToken(切环境后是活的,必须 getter,不能传快照)、
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
var REQ_DATA = null; // 最近一次清单(重画用,避免每次点都重新拉)
|
|
27
27
|
var REQ_BUSY = false; // 「整理」进行中(防重复点击)
|
|
28
28
|
var REQ_BUSY_LABEL = '全部整理'; // 顶栏按钮的进行中文案(重渲染后要能恢复)
|
|
29
|
+
var REQ_BUSY_KEY = ''; // 本次整理的组头(空间 key;空 = 全部整理,所有组头都置忙)
|
|
29
30
|
var REQ_OPEN = {}; // 工作区展开状态(key = 工作区 id),与会话列表的 WS_OPEN 同构
|
|
30
31
|
var REQ_ALIVE = null; // 仍有会话的空间路径集合;null = 拿不到会话清单(不筛)
|
|
31
32
|
var REQ_DETAIL = null; // 二级视图:{key: 工作区 key, mod: 模块名, ws: 工作区对象}
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
'<div class="history__top">' +
|
|
42
43
|
'<button class="topbar__icon" id="btnReqDetailBack" aria-label="返回"><svg width="20" height="20"><use href="#i-back"/></svg></button>' +
|
|
43
44
|
'<span class="history__title" id="reqDetailTitle">需求</span>' +
|
|
44
|
-
'<button class="history__disconnect" id="btnReqDetailSync" type="button">整理</button>' +
|
|
45
|
+
'<button class="history__disconnect" id="btnReqDetailSync" type="button" title="只整理这个模块">整理</button>' +
|
|
45
46
|
'</div>' +
|
|
46
47
|
'<div class="history__list" id="reqDetailList"></div>';
|
|
47
48
|
|
|
@@ -330,17 +331,19 @@
|
|
|
330
331
|
服务端按 state 判断走哪条路:
|
|
331
332
|
· 没初始化过 → 先按空间目录反推需求基线(首次可能跑几分钟,按钮要能等);
|
|
332
333
|
· 已初始化 → 只消费上次整理之后的新会话。
|
|
333
|
-
|
|
334
|
+
入口有三个:
|
|
334
335
|
· 每个空间组头的「整理」——只整理这个空间(用户点哪个就是哪个,不猜);
|
|
335
|
-
·
|
|
336
|
-
|
|
337
|
-
|
|
336
|
+
· 二级页右上角「整理」——只整理当前模块(body 带 module,不推进空间游标);
|
|
337
|
+
· 一级页右上角「全部整理」——按空间逐个串行跑一遍,汇总结果(一键触发全部)。
|
|
338
|
+
三个入口都带「我的 → 默认引擎/模型」,服务端不再自动替换引擎。 */
|
|
339
|
+
function reqPostConsolidate(ws, module) {
|
|
338
340
|
var hdr = { 'Content-Type': 'application/json' };
|
|
339
341
|
if (H.getToken()) hdr['X-Magic-Token'] = H.getToken();
|
|
340
342
|
return fetch(H.getBase() + '/desk/requirements/consolidate', {
|
|
341
343
|
method: 'POST', headers: hdr,
|
|
342
344
|
body: JSON.stringify({
|
|
343
345
|
workspace: ws || '',
|
|
346
|
+
module: module || '',
|
|
344
347
|
engine: H.getRunPref().engine || '',
|
|
345
348
|
model: (H.getRunPref().engine && H.getRunPref().model) || ''
|
|
346
349
|
})
|
|
@@ -354,10 +357,13 @@
|
|
|
354
357
|
});
|
|
355
358
|
}
|
|
356
359
|
|
|
357
|
-
/* 统一置忙:顶部主按钮 +
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
360
|
+
/* 统一置忙:顶部主按钮 + 所有空间组头的「整理」一起禁用(防重复点)。
|
|
361
|
+
只有**真正在整理的那个入口**显示「整理中…」:
|
|
362
|
+
· 点某个空间 / 某个模块的「整理」→ 只有这一组的文案变;
|
|
363
|
+
· 点「全部整理」→ 所有组头一起变(那才是全部在跑)。
|
|
364
|
+
其余按钮只置灰、保持「整理」—— 否则点一个模块会让人以为所有空间都在整理。 */
|
|
365
|
+
function reqSetBusy(on, label, busyKey) {
|
|
366
|
+
if (busyKey === undefined) busyKey = REQ_BUSY_KEY;
|
|
361
367
|
if (on) { if (label !== undefined) REQ_BUSY_LABEL = label; }
|
|
362
368
|
else REQ_BUSY_LABEL = '全部整理';
|
|
363
369
|
var btn = els.sync;
|
|
@@ -372,38 +378,43 @@
|
|
|
372
378
|
}
|
|
373
379
|
Array.prototype.forEach.call(document.querySelectorAll('.wsrow__sync'), function (b) {
|
|
374
380
|
b.disabled = !!on;
|
|
375
|
-
b.textContent =
|
|
381
|
+
if (!on) { b.textContent = '整理'; return; }
|
|
382
|
+
b.textContent = (!busyKey || b.getAttribute('data-ws') === busyKey) ? '整理中…' : '整理';
|
|
376
383
|
});
|
|
377
384
|
}
|
|
378
385
|
|
|
379
|
-
|
|
386
|
+
/* 整理一个空间(module 为空)或一个模块(module 非空)。
|
|
387
|
+
· 空间级:消费该空间游标之后的新会话,可能同时更新多个模块;
|
|
388
|
+
· 模块级:只更新这个模块,**不推进空间游标** —— 别的模块的需求仍留在
|
|
389
|
+
pending,等空间级「整理」/下一轮自动归纳处理,不会被这一下吞掉。
|
|
390
|
+
服务端按 state 判断走哪条路:没初始化过 → 先按空间目录反推基线;
|
|
391
|
+
模块级不会触发首次初始化(目录基线天然是空间级的)。 */
|
|
392
|
+
function consolidateRequirements(target, module) {
|
|
380
393
|
if (REQ_BUSY || !target) return;
|
|
381
394
|
var ws = target.dir || target.id || '';
|
|
382
395
|
if (!ws) return;
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
reqSetBusy(true, firstTime ? '初始化中…' : '整理中…');
|
|
396
|
+
module = String(module || '').trim();
|
|
397
|
+
var firstTime = target.initialized === false && !module;
|
|
386
398
|
var targetKey = target.id || target.dir || '';
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
/* 点的是哪个空间,就只让哪个组头显示「初始化中…」;其它组头保持「整理中…」。 */
|
|
392
|
-
if (groupBtn) groupBtn.textContent = firstTime ? '初始化中…' : '整理中…';
|
|
393
|
-
reqPostConsolidate(ws)
|
|
399
|
+
REQ_BUSY = true;
|
|
400
|
+
REQ_BUSY_KEY = targetKey;
|
|
401
|
+
reqSetBusy(true, firstTime ? '初始化中…' : '整理中…', targetKey);
|
|
402
|
+
reqPostConsolidate(ws, module)
|
|
394
403
|
.then(function (d) {
|
|
404
|
+
var prefix = module ? '模块「' + module + '」' : '';
|
|
395
405
|
if (d.bootstrap) {
|
|
396
406
|
if (d.created || d.updated) toast('已按空间目录生成需求:新建 ' + (d.created || 0) + ' · 更新 ' + (d.updated || 0));
|
|
397
407
|
else toast('已按空间目录初始化:暂未识别到功能点');
|
|
398
408
|
}
|
|
399
|
-
else if (d.noop) toast('没有新的对话需要整理');
|
|
400
|
-
else if (d.updated || d.created) toast('
|
|
401
|
-
else toast('整理完成');
|
|
409
|
+
else if (d.noop) toast(prefix ? prefix + '没有新的对话需要整理' : '没有新的对话需要整理');
|
|
410
|
+
else if (d.updated || d.created) toast((prefix || '整理完成') + ':更新 ' + (d.updated || 0) + ' · 新建 ' + (d.created || 0));
|
|
411
|
+
else toast(prefix ? prefix + '整理完成' : '整理完成');
|
|
402
412
|
loadRequirements(true); // 后台刷新(不闪「读取中」)
|
|
403
413
|
})
|
|
404
414
|
.catch(function (e) { toast('整理失败:' + ((e && e.message) || e)); })
|
|
405
415
|
.then(function () {
|
|
406
416
|
REQ_BUSY = false;
|
|
417
|
+
REQ_BUSY_KEY = '';
|
|
407
418
|
reqSetBusy(false, '全部整理');
|
|
408
419
|
});
|
|
409
420
|
}
|
|
@@ -438,10 +449,12 @@
|
|
|
438
449
|
return;
|
|
439
450
|
}
|
|
440
451
|
REQ_BUSY = true;
|
|
452
|
+
REQ_BUSY_KEY = ''; // 空 = 全部整理:所有组头一起显示「整理中…」
|
|
441
453
|
reqSetBusy(true, '整理中 0/' + list.length);
|
|
442
454
|
var i = 0, ok = 0, fail = 0, created = 0, updated = 0, firstErr = '';
|
|
443
455
|
function done() {
|
|
444
456
|
REQ_BUSY = false;
|
|
457
|
+
REQ_BUSY_KEY = '';
|
|
445
458
|
reqSetBusy(false, '全部整理');
|
|
446
459
|
loadRequirements(true);
|
|
447
460
|
if (fail) {
|
|
@@ -456,7 +469,7 @@
|
|
|
456
469
|
REQ_BUSY_LABEL = '整理中 ' + i + '/' + list.length;
|
|
457
470
|
var btn = els.sync;
|
|
458
471
|
if (btn) btn.textContent = REQ_BUSY_LABEL;
|
|
459
|
-
reqPostConsolidate(w.dir || w.id || '')
|
|
472
|
+
reqPostConsolidate(w.dir || w.id || '', '')
|
|
460
473
|
.then(function (d) {
|
|
461
474
|
ok++;
|
|
462
475
|
created += Number(d.created) || 0;
|
|
@@ -487,13 +500,13 @@
|
|
|
487
500
|
els.sync = root.querySelector('#btnReqSync');
|
|
488
501
|
els.detailBack = detail.querySelector('#btnReqDetailBack');
|
|
489
502
|
els.detailSync = detail.querySelector('#btnReqDetailSync');
|
|
490
|
-
/* 两个动作:全部整理 /
|
|
491
|
-
|
|
503
|
+
/* 两个动作:全部整理 / 返回一级;二级页的「整理」只作用于**当前模块**
|
|
504
|
+
(不是整空间、更不是全部空间 —— 用户点哪个模块,就只整理哪个模块)。 */
|
|
492
505
|
if (els.sync) els.sync.onclick = function () { consolidateAllRequirements(); };
|
|
493
506
|
if (els.detailBack) els.detailBack.onclick = function () { closeReqDetail(); };
|
|
494
507
|
if (els.detailSync) els.detailSync.onclick = function () {
|
|
495
508
|
if (!REQ_DETAIL || !REQ_DETAIL.ws) return;
|
|
496
|
-
consolidateRequirements(REQ_DETAIL.ws);
|
|
509
|
+
consolidateRequirements(REQ_DETAIL.ws, REQ_DETAIL.mod);
|
|
497
510
|
};
|
|
498
511
|
}
|
|
499
512
|
|
|
@@ -149,6 +149,13 @@ const (
|
|
|
149
149
|
FeatureSessionEvents = "feature.session.events"
|
|
150
150
|
// FeatureSessionControl `--stream --control`:stdin NDJSON 控制通道(打断 / 收工 / 回审批)。
|
|
151
151
|
FeatureSessionControl = "feature.session.control"
|
|
152
|
+
// FeatureSessionHosted `--hosted`:这一轮的用户提问由客户端「会话托管」自动发出。
|
|
153
|
+
//
|
|
154
|
+
// 为什么归 core:托管本身是**纯客户端策略**(执行仍走普通 ask),但「这句是托管的」
|
|
155
|
+
// 要写进会话历史,回放时才能把气泡画成「托管发送」而不是「我」—— 历史在 core 手里,
|
|
156
|
+
// 所以标识只能由 core 落。客户端据此决定要不要把 hosted 透传给 core;老 core 没有
|
|
157
|
+
// 这个位时插件忽略该字段(实时气泡仍由客户端自己标),不阻断发送。
|
|
158
|
+
FeatureSessionHosted = "feature.session.hosted"
|
|
152
159
|
// FeatureSessionTeam `--team`:团队模式(主引擎拆任务、子引擎分头执行、主引擎汇总)。
|
|
153
160
|
//
|
|
154
161
|
// 为什么归 core 而不是某个引擎:它是**会话层的一种运行模式**,任何能当主引擎的
|
|
@@ -164,5 +171,5 @@ const (
|
|
|
164
171
|
|
|
165
172
|
// CoreFeatures 返回 core 自身提供的通道能力(字典序,稳定)。
|
|
166
173
|
func CoreFeatures() []string {
|
|
167
|
-
return []string{FeatureRequirements, FeatureSessionControl, FeatureSessionEvents, FeatureSessionTeam}
|
|
174
|
+
return []string{FeatureRequirements, FeatureSessionControl, FeatureSessionEvents, FeatureSessionHosted, FeatureSessionTeam}
|
|
168
175
|
}
|
package/internal/agent/claude.go
CHANGED
|
@@ -238,9 +238,11 @@ func (e *ClaudeEngine) buildArgsBase(req Request) []string {
|
|
|
238
238
|
if m := stripModelPrefix(req.Model); m != "" {
|
|
239
239
|
args = append(args, "--model", m)
|
|
240
240
|
}
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
if
|
|
241
|
+
// 推理 / 思考:建议这类短任务显式关闭(--thinking disabled);
|
|
242
|
+
// 其余调用仍按 --effort 档位。两者互斥,关闭时不传 effort,避免又落回默认高档。
|
|
243
|
+
if req.NoThinking {
|
|
244
|
+
args = append(args, "--thinking", "disabled")
|
|
245
|
+
} else if lv := ReasoningLevel(req.Reasoning); lv != "" {
|
|
244
246
|
if lv == "minimal" {
|
|
245
247
|
lv = "low"
|
|
246
248
|
}
|
|
@@ -375,8 +375,12 @@ func (c codebuddyCore) buildArgsBase(req Request) []string {
|
|
|
375
375
|
args = append(args, "--model", c.model)
|
|
376
376
|
}
|
|
377
377
|
// 推理档位:codebuddy 的 --effort 认 minimal/low/medium/high/xhigh/max。
|
|
378
|
-
|
|
379
|
-
|
|
378
|
+
// NoThinking 走官方 settings alwaysThinkingEnabled=false(见 agentSettingsPayload),
|
|
379
|
+
// 不传 --effort off(该值不在 --effort 合法枚举里)。
|
|
380
|
+
if !req.NoThinking {
|
|
381
|
+
if lv := ReasoningLevel(req.Reasoning); lv != "" {
|
|
382
|
+
args = append(args, "--effort", lv)
|
|
383
|
+
}
|
|
380
384
|
}
|
|
381
385
|
// 统一参数矩阵:`--settings` 只接受一份载荷,故 MaxTokens 的 env 注入与
|
|
382
386
|
// 四档模型的 sandbox / autoMode / permissions 合并进同一个 JSON。
|
package/internal/agent/codex.go
CHANGED
|
@@ -540,8 +540,13 @@ func (e *CodexEngine) buildArgs(req Request, prompt, outFile, schemaFile string)
|
|
|
540
540
|
}
|
|
541
541
|
}
|
|
542
542
|
// 推理档位:`-c` 在 `exec` 与 `exec resume` 下都可用(同 model 覆写),
|
|
543
|
-
// 故不按子命令分流。codex 的取值:minimal/low/medium/high/xhigh/max。
|
|
544
|
-
|
|
543
|
+
// 故不按子命令分流。codex 的取值:none/minimal/low/medium/high/xhigh/max。
|
|
544
|
+
// 建议这类短任务用 none 明确关闭思考(不支持 none 的模型会在服务端报错,
|
|
545
|
+
// 由建议层降级到 llm,不会拖住用户会话)。
|
|
546
|
+
if req.NoThinking {
|
|
547
|
+
args = append(args, "-c", "model_reasoning_effort="+tomlBasicString("none"),
|
|
548
|
+
"-c", "model_reasoning_summary="+tomlBasicString("none"))
|
|
549
|
+
} else if lv := ReasoningLevel(req.Reasoning); lv != "" {
|
|
545
550
|
args = append(args, "-c", "model_reasoning_effort="+tomlBasicString(lv))
|
|
546
551
|
}
|
|
547
552
|
// SystemPrompt → -c developer_instructions=<TOML 字符串>
|
|
@@ -167,6 +167,11 @@ type dshSDKInitParams struct {
|
|
|
167
167
|
Provider string `json:"provider"`
|
|
168
168
|
Model string `json:"model"`
|
|
169
169
|
MaxTokens int `json:"maxTokens,omitempty"`
|
|
170
|
+
// ReasoningEffort 为空 = 沿用模型默认;NoThinking 时传官方值 "off"。
|
|
171
|
+
// 官方依据:dsh-sdk-protocol InitializeParams.reasoningEffort;
|
|
172
|
+
// dsh-llm-deepseek README:"off" 序列化为 thinking.type=disabled。
|
|
173
|
+
// pi-ai 路由需在 profile 声明 reasoningEfforts.off(本机 sdk patch 已加)。
|
|
174
|
+
ReasoningEffort string `json:"reasoningEffort,omitempty"`
|
|
170
175
|
}
|
|
171
176
|
|
|
172
177
|
// dshSDKTextBlock = TextBlock(ContentBlock 的 text 成员)。
|
|
@@ -410,6 +415,9 @@ func dshSDKTurn(ctx context.Context, bin, workspace string, req Request, prompt
|
|
|
410
415
|
|
|
411
416
|
// ① initialize:握手失败 = SDK 通道不可用(旧版 dsh 没有该 profile 时会走到这里)。
|
|
412
417
|
initParams := dshSDKInitParams{Cwd: workspace, Provider: provider, Model: model, MaxTokens: req.MaxTokens}
|
|
418
|
+
if req.NoThinking {
|
|
419
|
+
initParams.ReasoningEffort = "off"
|
|
420
|
+
}
|
|
413
421
|
initID, err := send("initialize", initParams)
|
|
414
422
|
if err != nil {
|
|
415
423
|
return dshSDKTurnResult{}, fmt.Errorf("%w: 写 initialize: %v", errDshSDKUnavailable, err)
|
|
@@ -261,6 +261,23 @@ func TestDshSDKIgnoresOtherSessions(t *testing.T) {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// NoThinking:官方 InitializeParams.reasoningEffort=off,不能只靠隐藏事件。
|
|
265
|
+
func TestDshSDKNoThinkingSendsOff(t *testing.T) {
|
|
266
|
+
dshSDKHome(t, "modelverse", "deepseek-v4.1-flash")
|
|
267
|
+
dir := t.TempDir()
|
|
268
|
+
bin, _, stdinLog := dshSDKStub(t, dir, dshSDKStubOpts{})
|
|
269
|
+
|
|
270
|
+
if _, err := (&DshEngine{BinPath: bin}).Complete(context.Background(), Request{
|
|
271
|
+
Messages: []Message{{Role: "user", Content: "hi"}},
|
|
272
|
+
NoThinking: true,
|
|
273
|
+
}); err != nil {
|
|
274
|
+
t.Fatalf("Complete: %v", err)
|
|
275
|
+
}
|
|
276
|
+
if got := readFileString(t, stdinLog); !strings.Contains(got, `"reasoningEffort":"off"`) {
|
|
277
|
+
t.Errorf("initialize 应带 reasoningEffort=off: %s", got)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
264
281
|
// initialize 被服务端明确拒绝(模型没配)→ **真失败**,不静默回退 headless。
|
|
265
282
|
func TestDshSDKInitializeRejectedIsFatal(t *testing.T) {
|
|
266
283
|
dshSDKHome(t, "modelverse", "no-such-model")
|
package/internal/agent/engine.go
CHANGED
|
@@ -154,6 +154,16 @@ type Request struct {
|
|
|
154
154
|
// trae/llm/openclaw/dsh/arkclaw 无对应参数。
|
|
155
155
|
Reasoning string
|
|
156
156
|
|
|
157
|
+
// NoThinking 请求引擎关闭模型思考:
|
|
158
|
+
// claude --thinking disabled(官方)
|
|
159
|
+
// codex -c model_reasoning_effort="none" + model_reasoning_summary="none"
|
|
160
|
+
// codebuddy settings alwaysThinkingEnabled=false(官方 settings / SDK thinking:disabled)
|
|
161
|
+
// dsh InitializeParams.reasoningEffort="off"(官方 SDK;dsh-llm-deepseek 会发 thinking.type=disabled)
|
|
162
|
+
// llm 选同线 thinking.disabled 变体;没有变体时传 reasoning_effort="none"
|
|
163
|
+
// (CLI 委托路径用 llm 的官方 -o reasoning_effort none)
|
|
164
|
+
// 其余引擎没有已知开关;调用方(建议生成)仍会标记,失败后再降级 llm。
|
|
165
|
+
NoThinking bool
|
|
166
|
+
|
|
157
167
|
// JSONSchema 是可选的结构化输出约束。
|
|
158
168
|
//
|
|
159
169
|
// 各引擎行为:
|
|
@@ -274,6 +284,16 @@ type Response struct {
|
|
|
274
284
|
Attempts int
|
|
275
285
|
}
|
|
276
286
|
|
|
287
|
+
// NoThinkingSupportOf 引擎是否有已知的关闭思考手段。
|
|
288
|
+
// 只列官方文档/参数确认过的;未知引擎返回 false,调用方应提示而不是假装关闭。
|
|
289
|
+
func NoThinkingSupportOf(name string) bool {
|
|
290
|
+
switch name {
|
|
291
|
+
case "claude", "codex", "codebuddy", "codebuddy-ai", "dsh", "llm":
|
|
292
|
+
return true
|
|
293
|
+
}
|
|
294
|
+
return false
|
|
295
|
+
}
|
|
296
|
+
|
|
277
297
|
// Engine 是一个 agent CLI 后端的统一抽象。
|
|
278
298
|
type Engine interface {
|
|
279
299
|
// Name 引擎标识("claude" / "codebuddy" / "trae")。
|
|
@@ -190,6 +190,42 @@ func TestReasoningEffortArgs(t *testing.T) {
|
|
|
190
190
|
t.Errorf("codex model_reasoning_effort 未透传: %v", codexArgs)
|
|
191
191
|
}
|
|
192
192
|
|
|
193
|
+
/* NoThinking(建议生成):每个有原生开关的引擎都要真的关闭,不能只是 minimal。 */
|
|
194
|
+
claudeNo := (&ClaudeEngine{}).buildArgsBase(Request{NoThinking: true, Reasoning: "max"})
|
|
195
|
+
if !hasPair(claudeNo, "--thinking", "disabled") || hasPair(claudeNo, "--effort", "max") {
|
|
196
|
+
t.Errorf("claude NoThinking 应 --thinking disabled 且不传 effort: %v", claudeNo)
|
|
197
|
+
}
|
|
198
|
+
// codebuddy:NoThinking 走官方 settings alwaysThinkingEnabled=false,
|
|
199
|
+
// 不能再传 --effort xhigh(那会把思考档位又打开)。
|
|
200
|
+
cbNo := (&CodeBuddyEngine{}).buildArgsBase(Request{NoThinking: true, Reasoning: "xhigh"})
|
|
201
|
+
if hasPair(cbNo, "--effort", "off") || hasPair(cbNo, "--effort", "xhigh") {
|
|
202
|
+
t.Errorf("codebuddy NoThinking 不应传 --effort: %v", cbNo)
|
|
203
|
+
}
|
|
204
|
+
codexNo := (&CodexEngine{}).buildArgs(Request{NoThinking: true, Reasoning: "max"}, "hi", "", "")
|
|
205
|
+
if !hasPair(codexNo, "-c", `model_reasoning_effort="none"`) ||
|
|
206
|
+
!hasPair(codexNo, "-c", `model_reasoning_summary="none"`) ||
|
|
207
|
+
hasPair(codexNo, "-c", `model_reasoning_effort="max"`) {
|
|
208
|
+
t.Errorf("codex NoThinking 应 none 且不传 max: %v", codexNo)
|
|
209
|
+
}
|
|
210
|
+
// dsh 的 launcher/headless 没有 reasoning 开关:NoThinking 对它保持现状,
|
|
211
|
+
// 不能塞 --effort(实测启动器报 "--profile <name> is required")。
|
|
212
|
+
dshNo := dshHeadlessArgs("headless", "", "hi", false)
|
|
213
|
+
if len(dshNo) < 3 || dshNo[0] != "--profile" || dshNo[1] != "headless" {
|
|
214
|
+
t.Errorf("dsh 参数不应被 NoThinking 改坏: %v", dshNo)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 官方关闭思考能力表:只列有依据的;trae/openclaw/arkclaw 不假装支持。
|
|
218
|
+
for _, e := range []string{"claude", "codex", "codebuddy", "dsh", "llm"} {
|
|
219
|
+
if !NoThinkingSupportOf(e) {
|
|
220
|
+
t.Errorf("NoThinkingSupportOf(%s) 应为 true", e)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for _, e := range []string{"trae", "openclaw", "arkclaw"} {
|
|
224
|
+
if NoThinkingSupportOf(e) {
|
|
225
|
+
t.Errorf("NoThinkingSupportOf(%s) 应为 false", e)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
193
229
|
// off/none/disabled 一律归一为「不传」。
|
|
194
230
|
for _, v := range []string{"off", "none", "disabled", " OFF "} {
|
|
195
231
|
if lv := ReasoningLevel(v); lv != "" {
|
|
@@ -893,6 +929,22 @@ func TestCodeBuddyMaxTokensSettings(t *testing.T) {
|
|
|
893
929
|
}
|
|
894
930
|
}
|
|
895
931
|
|
|
932
|
+
// TestCodeBuddyNoThinkingSettings 官方 settings 键:NoThinking 时 alwaysThinkingEnabled=false。
|
|
933
|
+
func TestCodeBuddyNoThinkingSettings(t *testing.T) {
|
|
934
|
+
dir := t.TempDir()
|
|
935
|
+
w, log := argsCaptureCLI(t, dir, "codebuddy", `echo '{"type":"result","result":"ok"}'`)
|
|
936
|
+
if _, err := w.codebuddy().Complete(context.Background(), Request{
|
|
937
|
+
Messages: []Message{{Role: "user", Content: "hi"}},
|
|
938
|
+
NoThinking: true,
|
|
939
|
+
}); err != nil {
|
|
940
|
+
t.Fatalf("Complete: %v", err)
|
|
941
|
+
}
|
|
942
|
+
data, _ := os.ReadFile(log)
|
|
943
|
+
if !strings.Contains(string(data), `"alwaysThinkingEnabled":false`) {
|
|
944
|
+
t.Errorf("codebuddy 应写官方 settings alwaysThinkingEnabled=false: %q", data)
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
896
948
|
// TestNoSettingsWhenMaxTokensUnset MaxTokens 未设置时不注入 --settings。
|
|
897
949
|
func TestNoSettingsWhenMaxTokensUnset(t *testing.T) {
|
|
898
950
|
cases := []struct {
|
|
@@ -145,6 +145,12 @@ func (e *LLMEngine) buildArgs(req Request, prompt string) []string {
|
|
|
145
145
|
if req.Temperature != nil {
|
|
146
146
|
args = append(args, "-o", "temperature", formatTemperature(*req.Temperature))
|
|
147
147
|
}
|
|
148
|
+
if req.NoThinking {
|
|
149
|
+
// simonw/llm 的官方 model option(llm.datasette.io/en/stable/usage.html:
|
|
150
|
+
// -o/--option key value,OpenAI 系模型暴露 reasoning_effort)。支持该 option
|
|
151
|
+
// 的插件会把它发到端点;不支持时直接报错 —— 显式 --disable-thinking 的预期结果。
|
|
152
|
+
args = append(args, "-o", "reasoning_effort", "none")
|
|
153
|
+
}
|
|
148
154
|
return append(args, appendAttachmentSection(prompt, nonImage))
|
|
149
155
|
}
|
|
150
156
|
|
|
@@ -207,6 +213,11 @@ func (e *LLMEngine) ListModels(ctx context.Context) ([]string, error) {
|
|
|
207
213
|
// --json 输出同 `llm logs --json` 的结构(数组,每项含 response /
|
|
208
214
|
// input_tokens / output_tokens),解析失败时退回纯文本路径,不引入新的失败点。
|
|
209
215
|
func (e *LLMEngine) Complete(ctx context.Context, req Request) (Response, error) {
|
|
216
|
+
// --disable-thinking:优先换成同一条线上的 no-think 变体(MiniMax-M3.1-Flash
|
|
217
|
+
// 这类模型对 thinking.disabled 直接 400,不能靠请求体硬塞)。
|
|
218
|
+
if req.NoThinking {
|
|
219
|
+
req.Model = NoThinkLLMModel(req.Model)
|
|
220
|
+
}
|
|
210
221
|
// 配置文件模型(models.json):命中 id → 直连端点(见 llmhttp.go)。
|
|
211
222
|
// 为什么必须按 id:id 唯一,而真实模型名会重复(MiniMax-M3 与 minimax-nothink
|
|
212
223
|
// 真实名相同,只差 extraBody)—— 用名字做参数就无法区分。
|
|
@@ -317,6 +328,9 @@ func parseLLMJSONEntry(raw string) (llmJSONEntry, bool) {
|
|
|
317
328
|
// Stream 实现 Streamer:llm prompt 默认流式,逐行读纯文本 stdout,
|
|
318
329
|
// thinkSplitter 把标签块路由到 thinking 通道。
|
|
319
330
|
func (e *LLMEngine) Stream(ctx context.Context, req Request, onEvent func(StreamEvent)) (StreamResult, error) {
|
|
331
|
+
if req.NoThinking {
|
|
332
|
+
req.Model = NoThinkLLMModel(req.Model)
|
|
333
|
+
}
|
|
320
334
|
// 配置文件模型:命中 id → 直连 SSE(见 llmhttp.go / completeConfigured 上方注释)
|
|
321
335
|
if m, ok := ResolveLLMConfigModel(req.Model); ok {
|
|
322
336
|
return e.streamConfigured(ctx, m, req, onEvent)
|
|
@@ -104,6 +104,13 @@ func TestLLMBuildArgsModelOptions(t *testing.T) {
|
|
|
104
104
|
t.Errorf("buildArgs = %q, want %q (温度 0 必须透传)", got, want)
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// NoThinking:llm 官方 model option;只在 CLI 委托路径出现
|
|
108
|
+
got = e.buildArgs(Request{NoThinking: true}, "hi")
|
|
109
|
+
want = []string{"prompt", "-o", "reasoning_effort", "none", "hi"}
|
|
110
|
+
if strings.Join(got, "\x00") != strings.Join(want, "\x00") {
|
|
111
|
+
t.Errorf("buildArgs = %q, want %q", got, want)
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
// 都不给
|
|
108
115
|
got = e.buildArgs(Request{Model: "minimax-m3"}, "hi")
|
|
109
116
|
for _, a := range got {
|
|
@@ -549,3 +556,20 @@ func TestLLMDetectMissingCli(t *testing.T) {
|
|
|
549
556
|
t.Errorf("note = %q, want the missing path", note)
|
|
550
557
|
}
|
|
551
558
|
}
|
|
559
|
+
|
|
560
|
+
func TestNoThinkLLMModelPicksAnyVariant(t *testing.T) {
|
|
561
|
+
dir := t.TempDir()
|
|
562
|
+
p := filepath.Join(dir, "models.json")
|
|
563
|
+
raw := `[
|
|
564
|
+
{"id":"MiniMax-M3.1-Flash","model":"MiniMax-M3.1-Flash","url":"http://x/v1","apiKey":"k"},
|
|
565
|
+
{"id":"minimax-nothink","model":"MiniMax-M3","url":"http://x/v1","apiKey":"k",
|
|
566
|
+
"extraBody":{"thinking":{"type":"disabled"}}}
|
|
567
|
+
]`
|
|
568
|
+
if err := os.WriteFile(p, []byte(raw), 0o600); err != nil {
|
|
569
|
+
t.Fatal(err)
|
|
570
|
+
}
|
|
571
|
+
t.Setenv("MAGIC_AGENT_MODELS", p)
|
|
572
|
+
if got := NoThinkLLMModel("MiniMax-M3.1-Flash"); got != "minimax-nothink" {
|
|
573
|
+
t.Fatalf("NoThinkLLMModel = %q, want minimax-nothink", got)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
@@ -92,6 +92,12 @@ func buildConfiguredBody(m LLMConfigModel, req Request, stream bool) (map[string
|
|
|
92
92
|
} else if m.Temperature != nil {
|
|
93
93
|
body["temperature"] = *m.Temperature
|
|
94
94
|
}
|
|
95
|
+
if req.NoThinking && !LLMModelDisablesThinking(m) {
|
|
96
|
+
// 没有同线 no-think 变体时,用 llm/OpenAI 兼容端点认的官方参数。
|
|
97
|
+
// 若端点不支持该参数会返回 400 —— 这是显式 --disable-thinking 的预期失败,
|
|
98
|
+
// 不静默退回「假装关闭」。
|
|
99
|
+
body["reasoning_effort"] = "none"
|
|
100
|
+
}
|
|
95
101
|
return body, nil
|
|
96
102
|
}
|
|
97
103
|
|
|
@@ -274,6 +274,33 @@ func TestUnknownModelFallsBackToCLI(t *testing.T) {
|
|
|
274
274
|
}
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
func TestBuildConfiguredBodyNoThinking(t *testing.T) {
|
|
278
|
+
m := LLMConfigModel{ID: "x", Model: "x", URL: "http://x/v1"}
|
|
279
|
+
body, err := buildConfiguredBody(m, Request{
|
|
280
|
+
Messages: []Message{{Role: "user", Content: "hi"}},
|
|
281
|
+
NoThinking: true,
|
|
282
|
+
}, false)
|
|
283
|
+
if err != nil {
|
|
284
|
+
t.Fatal(err)
|
|
285
|
+
}
|
|
286
|
+
if body["reasoning_effort"] != "none" {
|
|
287
|
+
t.Errorf("没有 no-think 变体时应传 reasoning_effort=none: %#v", body["reasoning_effort"])
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
noThink := LLMConfigModel{ID: "x-nothink", Model: "x", URL: "http://x/v1",
|
|
291
|
+
ExtraBody: map[string]any{"thinking": map[string]any{"type": "disabled"}}}
|
|
292
|
+
body, err = buildConfiguredBody(noThink, Request{
|
|
293
|
+
Messages: []Message{{Role: "user", Content: "hi"}},
|
|
294
|
+
NoThinking: true,
|
|
295
|
+
}, false)
|
|
296
|
+
if err != nil {
|
|
297
|
+
t.Fatal(err)
|
|
298
|
+
}
|
|
299
|
+
if _, ok := body["reasoning_effort"]; ok {
|
|
300
|
+
t.Errorf("已有 thinking.disabled 时不应再塞 reasoning_effort: %#v", body)
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
277
304
|
func TestExpandEnvKeep(t *testing.T) {
|
|
278
305
|
t.Setenv("LLM_TEST_KEY", "secret")
|
|
279
306
|
cases := map[string]string{
|
|
@@ -76,6 +76,54 @@ func (m LLMConfigModel) Endpoint() string {
|
|
|
76
76
|
return strings.TrimRight(u, "/") + "/chat/completions"
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// LLMModelDisablesThinking 判断 models.json 条目是否声明了不思考变体
|
|
80
|
+
// (extraBody.thinking.type == "disabled")。
|
|
81
|
+
func LLMModelDisablesThinking(m LLMConfigModel) bool {
|
|
82
|
+
if strings.TrimSpace(m.ID) == "" || m.ExtraBody == nil {
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
th, ok := m.ExtraBody["thinking"].(map[string]any)
|
|
86
|
+
if !ok {
|
|
87
|
+
return false
|
|
88
|
+
}
|
|
89
|
+
return strings.EqualFold(strings.TrimSpace(fmt.Sprint(th["type"])), "disabled")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// NoThinkLLMModel 给 --disable-thinking 选 no-think 变体:
|
|
93
|
+
// 用户已选 no-think → 原样;否则先找 WireModel 相同、声明 thinking.disabled 的条目,
|
|
94
|
+
// 再退到任意 no-think 条目(M3.1-Flash 只能换模型才能关思考)。都没有则保留原模型
|
|
95
|
+
// (由调用方决定是否再传 reasoning_effort=none)。
|
|
96
|
+
func NoThinkLLMModel(selected string) string {
|
|
97
|
+
m, ok := ResolveLLMConfigModel(selected)
|
|
98
|
+
if !ok {
|
|
99
|
+
return strings.TrimSpace(selected)
|
|
100
|
+
}
|
|
101
|
+
if LLMModelDisablesThinking(m) {
|
|
102
|
+
return m.ID
|
|
103
|
+
}
|
|
104
|
+
list, err := LoadLLMConfigModels()
|
|
105
|
+
if err != nil {
|
|
106
|
+
return m.ID
|
|
107
|
+
}
|
|
108
|
+
// 先找同一条线上的变体(不换真实模型),再退到任意 no-think 变体 ——
|
|
109
|
+
// 显式 --disable-thinking 的目标是「不思考」,MiniMax-M3.1-Flash 这类模型
|
|
110
|
+
// 对 thinking.disabled / reasoning_effort=none 直接 400,只能换到 no-think 条目。
|
|
111
|
+
for _, alt := range list {
|
|
112
|
+
if alt.ID == m.ID || alt.WireModel() != m.WireModel() {
|
|
113
|
+
continue
|
|
114
|
+
}
|
|
115
|
+
if LLMModelDisablesThinking(alt) {
|
|
116
|
+
return alt.ID
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for _, alt := range list {
|
|
120
|
+
if alt.ID != m.ID && LLMModelDisablesThinking(alt) {
|
|
121
|
+
return alt.ID
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return m.ID
|
|
125
|
+
}
|
|
126
|
+
|
|
79
127
|
// LLMModelsPath 返回 models.json 的路径($MAGIC_AGENT_MODELS 优先)。
|
|
80
128
|
func LLMModelsPath() string {
|
|
81
129
|
if p := strings.TrimSpace(os.Getenv("MAGIC_AGENT_MODELS")); p != "" {
|
|
@@ -383,6 +383,11 @@ func agentSettingsPayload(req Request, engine string) (string, bool) {
|
|
|
383
383
|
if env := maxTokensEnv(req.MaxTokens); env != nil {
|
|
384
384
|
settings["env"] = env
|
|
385
385
|
}
|
|
386
|
+
// CodeBuddy 官方 settings 键:始终开启思考。NoThinking 时显式关掉;
|
|
387
|
+
// 这是 codebuddy CLI 能对模型请求施加的官方开关(SDK 另有 thinking.type=disabled)。
|
|
388
|
+
if req.NoThinking {
|
|
389
|
+
settings["alwaysThinkingEnabled"] = false
|
|
390
|
+
}
|
|
386
391
|
|
|
387
392
|
// 沙箱 / autoMode / 权限规则只在真会调用工具时注入。
|
|
388
393
|
if !toolsIsOff(req) {
|