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.
Files changed (59) hide show
  1. package/CLIENT-CONTRACT.md +78 -13
  2. package/README.md +10 -4
  3. package/client-ui/pc/app.css +14 -0
  4. package/client-ui/pc/js/93-rail.js +3 -1
  5. package/client-ui/shared/conv/03-chat-message.js +20 -3
  6. package/client-ui/shared/conv/07-stream-delegation.js +15 -0
  7. package/client-ui/shared/conv/27-conv-ledger.js +1 -1
  8. package/client-ui/shared/conv/conv.css +17 -0
  9. package/client-ui/shared/conversation.js +209 -39
  10. package/client-ui/shared/transport-http.js +193 -38
  11. package/client-ui-mobile/index.html +733 -107
  12. package/client-ui-mobile/reqs.js +42 -29
  13. package/internal/agent/capability.go +8 -1
  14. package/internal/agent/claude.go +5 -3
  15. package/internal/agent/codebuddy.go +6 -2
  16. package/internal/agent/codex.go +7 -2
  17. package/internal/agent/dsh_sdk.go +8 -0
  18. package/internal/agent/dsh_sdk_test.go +17 -0
  19. package/internal/agent/engine.go +20 -0
  20. package/internal/agent/engine_test.go +52 -0
  21. package/internal/agent/llmengine.go +6 -0
  22. package/internal/agent/llmengine_test.go +7 -0
  23. package/internal/agent/llmhttp.go +6 -0
  24. package/internal/agent/llmhttp_test.go +27 -0
  25. package/internal/agent/llmmodels.go +13 -0
  26. package/internal/agent/permission.go +5 -0
  27. package/internal/cli/ask.go +32 -17
  28. package/internal/cli/requirements.go +2 -0
  29. package/internal/cli/sessionlog.go +5 -0
  30. package/internal/cli/sessionlog_user_test.go +49 -0
  31. package/internal/client/cors_test.go +32 -0
  32. package/internal/client/jobs.go +1160 -0
  33. package/internal/client/jobs_test.go +369 -0
  34. package/internal/client/requirements.go +27 -7
  35. package/internal/client/requirements_test.go +51 -0
  36. package/internal/client/service.go +259 -142
  37. package/internal/client/service_test.go +78 -0
  38. package/internal/requirements/consolidate.go +86 -24
  39. package/internal/requirements/requirements.go +9 -0
  40. package/internal/requirements/requirements_test.go +129 -0
  41. package/internal/requirements/store.go +11 -3
  42. package/internal/session/session.go +15 -1
  43. package/internal/session/workspace_lock.go +165 -0
  44. package/internal/session/workspace_lock_test.go +90 -0
  45. package/internal/suggest/suggest.go +101 -7
  46. package/internal/suggest/suggest_test.go +44 -0
  47. package/npm/apk/magic-agent-mobile-release.apk +0 -0
  48. package/npm/apk/version.json +3 -3
  49. package/npm/dist/darwin-arm64/magic-agent +0 -0
  50. package/npm/dist/darwin-arm64/magic-client +0 -0
  51. package/npm/dist/darwin-x64/magic-agent +0 -0
  52. package/npm/dist/darwin-x64/magic-client +0 -0
  53. package/npm/dist/linux-arm64/magic-agent +0 -0
  54. package/npm/dist/linux-arm64/magic-client +0 -0
  55. package/npm/dist/linux-x64/magic-agent +0 -0
  56. package/npm/dist/linux-x64/magic-client +0 -0
  57. package/npm/dist/win32-x64/magic-agent.exe +0 -0
  58. package/npm/dist/win32-x64/magic-client.exe +0 -0
  59. 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
- engine := agent.Lookup(name)
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
- runner := &agent.Runner{Engine: engine, Timeout: timeout}
186
- resp, err := runner.Run(cctx, agent.Request{
187
- Model: strings.TrimSpace(in.Model),
188
- Tools: agent.ToolsOff,
189
- Reasoning: "off", // 建议要快;思考过程只会拖长等待
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
+ }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.3.0",
3
- "versionCode": 20300,
4
- "builtAt": "2026-10-23T00:29:48.082Z"
2
+ "version": "2.3.9",
3
+ "versionCode": 20309,
4
+ "builtAt": "2026-10-23T11:12:09.067Z"
5
5
  }
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.0",
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/",