magic-agent 2.3.0 → 2.3.9
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 +78 -13
- 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/07-stream-delegation.js +15 -0
- 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 +209 -39
- package/client-ui/shared/transport-http.js +193 -38
- package/client-ui-mobile/index.html +733 -107
- 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 +6 -0
- package/internal/agent/llmengine_test.go +7 -0
- package/internal/agent/llmhttp.go +6 -0
- package/internal/agent/llmhttp_test.go +27 -0
- package/internal/agent/llmmodels.go +13 -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/cors_test.go +32 -0
- package/internal/client/jobs.go +1160 -0
- package/internal/client/jobs_test.go +369 -0
- package/internal/client/requirements.go +27 -7
- package/internal/client/requirements_test.go +51 -0
- package/internal/client/service.go +259 -142
- package/internal/client/service_test.go +78 -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/session/session.go +15 -1
- package/internal/session/workspace_lock.go +165 -0
- package/internal/session/workspace_lock_test.go +90 -0
- package/internal/suggest/suggest.go +101 -7
- package/internal/suggest/suggest_test.go +44 -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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
package session
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"os"
|
|
5
|
+
"path/filepath"
|
|
6
|
+
"strconv"
|
|
7
|
+
"testing"
|
|
8
|
+
"time"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
// 工作空间串行锁:同一 workspace 的第二轮必须等第一轮 Finish 之后才能 Begin。
|
|
12
|
+
func TestWorkspaceLockSerializesRuns(t *testing.T) {
|
|
13
|
+
t.Setenv(EnvDir, t.TempDir())
|
|
14
|
+
t.Setenv(EnvWorkspaceLock, "1")
|
|
15
|
+
t.Setenv(EnvWorkspaceLockWait, "5s")
|
|
16
|
+
ws := t.TempDir()
|
|
17
|
+
|
|
18
|
+
h1, err := Begin(BeginOptions{NoPersist: true, Workspace: ws, Engine: "claude"})
|
|
19
|
+
if err != nil {
|
|
20
|
+
t.Fatalf("第一轮 Begin 失败:%v", err)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
got := make(chan *Handle, 1)
|
|
24
|
+
go func() {
|
|
25
|
+
h2, err := Begin(BeginOptions{NoPersist: true, Workspace: ws, Engine: "claude"})
|
|
26
|
+
if err != nil {
|
|
27
|
+
got <- nil
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
got <- h2
|
|
31
|
+
}()
|
|
32
|
+
|
|
33
|
+
select {
|
|
34
|
+
case h := <-got:
|
|
35
|
+
if h != nil {
|
|
36
|
+
h.Finish("", StateDone)
|
|
37
|
+
}
|
|
38
|
+
t.Fatal("第一轮还没结束,第二轮不应拿到锁")
|
|
39
|
+
case <-time.After(400 * time.Millisecond):
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
h1.Finish("", StateDone)
|
|
43
|
+
select {
|
|
44
|
+
case h2 := <-got:
|
|
45
|
+
if h2 == nil {
|
|
46
|
+
t.Fatal("第一轮结束后,第二轮应拿到锁")
|
|
47
|
+
}
|
|
48
|
+
h2.Finish("", StateDone)
|
|
49
|
+
case <-time.After(4 * time.Second):
|
|
50
|
+
t.Fatal("第一轮结束后,第二轮仍未拿到锁")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 持锁进程已不在(崩溃 / 被 kill):下一次 Begin 要能回收陈旧锁。
|
|
55
|
+
func TestWorkspaceLockReapsDeadOwner(t *testing.T) {
|
|
56
|
+
dir := t.TempDir()
|
|
57
|
+
t.Setenv(EnvDir, dir)
|
|
58
|
+
t.Setenv(EnvWorkspaceLock, "1")
|
|
59
|
+
t.Setenv(EnvWorkspaceLockWait, "2s")
|
|
60
|
+
ws := t.TempDir()
|
|
61
|
+
|
|
62
|
+
_, name := workspaceLockKey(ws)
|
|
63
|
+
lockDir := filepath.Join(dir, "locks")
|
|
64
|
+
if err := os.MkdirAll(lockDir, 0o700); err != nil {
|
|
65
|
+
t.Fatal(err)
|
|
66
|
+
}
|
|
67
|
+
// 一个几乎不可能存活、但格式合法的 pid。
|
|
68
|
+
if err := os.WriteFile(filepath.Join(lockDir, name+".lock"), []byte("999999\n"+strconv.FormatInt(time.Now().UnixNano(), 10)+"\n"), 0o600); err != nil {
|
|
69
|
+
t.Fatal(err)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
done := make(chan *Handle, 1)
|
|
73
|
+
go func() {
|
|
74
|
+
h, err := Begin(BeginOptions{NoPersist: true, Workspace: ws})
|
|
75
|
+
if err != nil {
|
|
76
|
+
done <- nil
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
done <- h
|
|
80
|
+
}()
|
|
81
|
+
select {
|
|
82
|
+
case h := <-done:
|
|
83
|
+
if h == nil {
|
|
84
|
+
t.Fatal("陈旧锁应被回收,第二轮应拿到锁")
|
|
85
|
+
}
|
|
86
|
+
h.Finish("", StateDone)
|
|
87
|
+
case <-time.After(3 * time.Second):
|
|
88
|
+
t.Fatal("陈旧锁未被回收:第二轮一直等")
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -162,6 +162,13 @@ func Parse(raw string, count int) []string {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
// Generate 跑一次旁路生成。失败时返回空建议 + error(调用方如实降级:不画按钮)。
|
|
165
|
+
//
|
|
166
|
+
// 2026-10-23 用户定调:「建议的生成要走引擎的不启用工具 json 返回的方式,提高速度」。
|
|
167
|
+
// 这里的主路径就是如此:当前引擎(in.Engine)+ ToolsOff + JSONSchema + NoThinking,
|
|
168
|
+
// 各引擎都通过 NoThinking 走官方关闭参数;失败/超时再降级到 llm 的 no-think 变体
|
|
169
|
+
// (纯文本、无工具、1-2s),保证界面不会跟着主引擎的 CLI 一起卡到 20s。
|
|
170
|
+
// `ToolsOff` / `JSONSchema` / `NoThinking` 不是可选项 —— 少任何一条都会把建议变成
|
|
171
|
+
// 一次带工具痕迹(或默认高档思考)的完整 agent 轮次。
|
|
165
172
|
func Generate(ctx context.Context, in Request) (Result, error) {
|
|
166
173
|
reply := strings.TrimSpace(in.Reply)
|
|
167
174
|
if reply == "" {
|
|
@@ -171,8 +178,7 @@ func Generate(ctx context.Context, in Request) (Result, error) {
|
|
|
171
178
|
if name == "" {
|
|
172
179
|
name = DefaultEngine
|
|
173
180
|
}
|
|
174
|
-
|
|
175
|
-
if engine == nil {
|
|
181
|
+
if agent.Lookup(name) == nil {
|
|
176
182
|
return Result{}, fmt.Errorf("建议生成:未知引擎 %q", name)
|
|
177
183
|
}
|
|
178
184
|
timeout := in.Timeout
|
|
@@ -182,11 +188,99 @@ func Generate(ctx context.Context, in Request) (Result, error) {
|
|
|
182
188
|
cctx, cancel := context.WithTimeout(ctx, timeout)
|
|
183
189
|
defer cancel()
|
|
184
190
|
start := time.Now()
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
|
|
192
|
+
/* 建议一律不思考;所有引擎都尽量走官方开关:
|
|
193
|
+
llm 传 reasoning_effort=none,claude --thinking disabled,
|
|
194
|
+
codex effort=none,codebuddy settings alwaysThinkingEnabled=false,
|
|
195
|
+
dsh InitializeParams.reasoningEffort=off。没有可靠开关的引擎(trae/openclaw/arkclaw)
|
|
196
|
+
仍会被 NoThinking 标记,失败后再降级 llm;不在这里预先改道。 */
|
|
197
|
+
/* 主引擎:优先当前引擎(用户要的“引擎 JSON 返回”)。claude / codex 给 10s ——
|
|
198
|
+
这是「CLI 启动 + 一次短 JSON」的实测上限;超过 10s 多半是凭据/主会话占用,
|
|
199
|
+
继续等只会把界面的 20s 预算吃光。之后才降级到 llm(1-2s),并清掉当前引擎的 model。 */
|
|
200
|
+
primaryBudget := timeout
|
|
201
|
+
if name != DefaultEngine && primaryBudget > 10*time.Second {
|
|
202
|
+
primaryBudget = 10 * time.Second
|
|
203
|
+
}
|
|
204
|
+
res, err := tryGenerate(cctx, in, name, primaryBudget, start)
|
|
205
|
+
if err == nil {
|
|
206
|
+
return res, nil
|
|
207
|
+
}
|
|
208
|
+
if name == DefaultEngine {
|
|
209
|
+
return Result{}, err
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/* 降级:给 llm 留一段独立预算(整体仍受 timeout 约束)。主引擎的错误不丢,
|
|
213
|
+
只有 fallback 也失败时才报它 —— 至少让调用方知道不是“没有建议”,是真失败。 */
|
|
214
|
+
remain := timeout - time.Since(start)
|
|
215
|
+
if remain < 2*time.Second {
|
|
216
|
+
remain = 2 * time.Second
|
|
217
|
+
}
|
|
218
|
+
if remain > 8*time.Second {
|
|
219
|
+
remain = 8 * time.Second
|
|
220
|
+
}
|
|
221
|
+
fallbackIn := in
|
|
222
|
+
fallbackIn.Model = "" // model 是当前引擎的,不能拿去喂 llm
|
|
223
|
+
fallback, ferr := tryGenerate(cctx, fallbackIn, DefaultEngine, remain, start)
|
|
224
|
+
if ferr != nil {
|
|
225
|
+
return Result{}, err
|
|
226
|
+
}
|
|
227
|
+
return fallback, nil
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// fastLLMModel 找 models.json 里第一条「不思考」的变体(extraBody.thinking.type=disabled)。
|
|
231
|
+
// 仅用于**没有显式选模型**的建议默认值:M3.1-Flash 明确拒绝 reasoning_effort=none,
|
|
232
|
+
// 默认走首条会直接 400。显式选中的模型只传参数,不换模型。
|
|
233
|
+
func fastLLMModel() string {
|
|
234
|
+
list, err := agent.LoadLLMConfigModels()
|
|
235
|
+
if err != nil {
|
|
236
|
+
return ""
|
|
237
|
+
}
|
|
238
|
+
for _, m := range list {
|
|
239
|
+
if llmModelDisablesThinking(m) {
|
|
240
|
+
return m.ID
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return ""
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// llmModelDisablesThinking 判断 models.json 条目是否声明了不思考变体。
|
|
247
|
+
func llmModelDisablesThinking(m agent.LLMConfigModel) bool {
|
|
248
|
+
if strings.TrimSpace(m.ID) == "" || m.ExtraBody == nil {
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
th, ok := m.ExtraBody["thinking"].(map[string]any)
|
|
252
|
+
if !ok {
|
|
253
|
+
return false
|
|
254
|
+
}
|
|
255
|
+
return strings.EqualFold(strings.TrimSpace(fmt.Sprint(th["type"])), "disabled")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// tryGenerate 用指定引擎跑一次 ToolsOff + JSONSchema 的短调用。
|
|
259
|
+
func tryGenerate(ctx context.Context, in Request, name string, budget time.Duration, start time.Time) (Result, error) {
|
|
260
|
+
engine := agent.Lookup(name)
|
|
261
|
+
if engine == nil {
|
|
262
|
+
return Result{}, fmt.Errorf("建议生成:未知引擎 %q", name)
|
|
263
|
+
}
|
|
264
|
+
reply := strings.TrimSpace(in.Reply)
|
|
265
|
+
attemptCtx, cancel := context.WithTimeout(ctx, budget)
|
|
266
|
+
defer cancel()
|
|
267
|
+
/* 建议是纯文本短任务,**一律不思考**。Reasoning 只作为没有 NoThinking 通道的
|
|
268
|
+
引擎的兜底(minimal 仍比默认高档轻);能真正关闭的引擎看 NoThinking。 */
|
|
269
|
+
reasoning := "minimal"
|
|
270
|
+
model := strings.TrimSpace(in.Model)
|
|
271
|
+
if name == DefaultEngine && model == "" {
|
|
272
|
+
// 仅默认值:显式选了模型就只传 reasoning_effort=none,不换模型。
|
|
273
|
+
model = fastLLMModel()
|
|
274
|
+
}
|
|
275
|
+
runner := &agent.Runner{Engine: engine, Timeout: budget}
|
|
276
|
+
resp, err := runner.Run(attemptCtx, agent.Request{
|
|
277
|
+
Model: model,
|
|
278
|
+
Tools: agent.ToolsOff,
|
|
279
|
+
/* NoThinking:claude → --thinking disabled,codex → effort none,
|
|
280
|
+
codebuddy → settings:false,dsh → initialize.reasoningEffort=off,
|
|
281
|
+
llm → reasoning_effort=none(不自动换模型)。 */
|
|
282
|
+
NoThinking: true,
|
|
283
|
+
Reasoning: reasoning,
|
|
190
284
|
MaxTokens: 300,
|
|
191
285
|
Messages: []agent.Message{{Role: "user", Content: Prompt(in.User, reply, in.Count)}},
|
|
192
286
|
JSONSchema: Schema(),
|
|
@@ -2,6 +2,8 @@ package suggest
|
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
4
|
"context"
|
|
5
|
+
"os"
|
|
6
|
+
"path/filepath"
|
|
5
7
|
"strings"
|
|
6
8
|
"testing"
|
|
7
9
|
|
|
@@ -130,6 +132,12 @@ func TestGenerateUsesToolsOffAndNoSession(t *testing.T) {
|
|
|
130
132
|
if f.got.JSONSchema == nil || len(f.got.JSONSchema.Required) != 1 {
|
|
131
133
|
t.Fatalf("缺少结构化输出契约")
|
|
132
134
|
}
|
|
135
|
+
if f.got.Reasoning != "minimal" {
|
|
136
|
+
t.Fatalf("建议调用必须用 minimal 兜底档:%q", f.got.Reasoning)
|
|
137
|
+
}
|
|
138
|
+
if !f.got.NoThinking {
|
|
139
|
+
t.Fatalf("建议调用必须显式 NoThinking(claude/codebuddy/codex/dsh 各自关闭思考)")
|
|
140
|
+
}
|
|
133
141
|
}
|
|
134
142
|
|
|
135
143
|
func TestDropCreativeRestarts(t *testing.T) {
|
|
@@ -216,3 +224,39 @@ func TestGenerateEmptyReplyShortCircuits(t *testing.T) {
|
|
|
216
224
|
t.Fatalf("空正文不该调引擎:%#v %#v", res.Suggestions, f.got.Messages)
|
|
217
225
|
}
|
|
218
226
|
}
|
|
227
|
+
|
|
228
|
+
func TestEverySuggestionEngineUsesNoThinking(t *testing.T) {
|
|
229
|
+
// 官方文档确认 codebuddy 的 alwaysThinkingEnabled / dsh 的 reasoningEffort=off
|
|
230
|
+
// 都能关思考;建议链路不再把它们预先改道到 llm,而是让引擎自己按 NoThinking 执行。
|
|
231
|
+
for _, name := range []string{"llm", "claude", "codex", "codebuddy", "dsh"} {
|
|
232
|
+
f := &fakeEngine{name: name, text: `{"suggestions":["继续"]}`}
|
|
233
|
+
agent.Register(f)
|
|
234
|
+
res, err := Generate(context.Background(), Request{Reply: "要我继续吗?", Engine: name})
|
|
235
|
+
if err != nil {
|
|
236
|
+
t.Fatalf("%s: Generate: %v", name, err)
|
|
237
|
+
}
|
|
238
|
+
if !f.got.NoThinking || len(f.got.Messages) == 0 {
|
|
239
|
+
t.Fatalf("%s 必须真的执行且带 NoThinking:%#v", name, f.got)
|
|
240
|
+
}
|
|
241
|
+
if res.Engine != name {
|
|
242
|
+
t.Fatalf("%s: 不应预先改道,got engine %q", name, res.Engine)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
func TestFastLLMModelPicksNoThinkVariant(t *testing.T) {
|
|
248
|
+
dir := t.TempDir()
|
|
249
|
+
p := filepath.Join(dir, "models.json")
|
|
250
|
+
raw := `[
|
|
251
|
+
{"id":"slow","model":"MiniMax-M3.1-Flash","url":"http://x/v1","apiKey":"k"},
|
|
252
|
+
{"id":"fast-nothink","model":"MiniMax-M3","url":"http://x/v1","apiKey":"k",
|
|
253
|
+
"extraBody":{"thinking":{"type":"disabled"}}}
|
|
254
|
+
]`
|
|
255
|
+
if err := os.WriteFile(p, []byte(raw), 0o600); err != nil {
|
|
256
|
+
t.Fatal(err)
|
|
257
|
+
}
|
|
258
|
+
t.Setenv("MAGIC_AGENT_MODELS", p)
|
|
259
|
+
if got := fastLLMModel(); got != "fast-nothink" {
|
|
260
|
+
t.Fatalf("fastLLMModel = %q, want fast-nothink", got)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
Binary file
|
package/npm/apk/version.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "magic-agent",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.9",
|
|
4
4
|
"description": "专业的 agent CLI 代理工具 - 统一 claude / codebuddy / trae / llm / codex / openclaw 引擎",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"bin": {
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"check:conv-host": "node tests/check-conv-shared-host.mjs",
|
|
20
20
|
"check:conv-mobile": "node tests/check-conv-mobile-host.mjs",
|
|
21
21
|
"check:conv-comp": "node tests/check-conv-component.mjs",
|
|
22
|
-
"check:conv-queue": "node tests/check-conv-queue.mjs"
|
|
22
|
+
"check:conv-queue": "node tests/check-conv-queue.mjs",
|
|
23
|
+
"check:follow": "node tests/check-follow-retry.mjs"
|
|
23
24
|
},
|
|
24
25
|
"files": [
|
|
25
26
|
"npm/bin/",
|