magic-agent 1.0.0
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/LICENSE +21 -0
- package/README.md +392 -0
- package/cmd/magic-agent/main.go +12 -0
- package/cmd/magic-client/main.go +144 -0
- package/go.mod +11 -0
- package/go.sum +12 -0
- package/internal/agent/arkclaw.go +587 -0
- package/internal/agent/arkclaw_stream.go +312 -0
- package/internal/agent/arkclaw_stream_test.go +428 -0
- package/internal/agent/arkclaw_test.go +827 -0
- package/internal/agent/ask.go +694 -0
- package/internal/agent/ask_test.go +587 -0
- package/internal/agent/attachments.go +200 -0
- package/internal/agent/attachments_test.go +481 -0
- package/internal/agent/capability.go +154 -0
- package/internal/agent/capability_test.go +190 -0
- package/internal/agent/claude.go +406 -0
- package/internal/agent/codebuddy.go +1044 -0
- package/internal/agent/codebuddy_account_test.go +128 -0
- package/internal/agent/codebuddy_gateway.go +479 -0
- package/internal/agent/codebuddy_gateway_stream.go +358 -0
- package/internal/agent/codebuddy_gateway_test.go +673 -0
- package/internal/agent/codex.go +599 -0
- package/internal/agent/codex_home.go +206 -0
- package/internal/agent/codex_home_test.go +130 -0
- package/internal/agent/codex_stream_test.go +97 -0
- package/internal/agent/codex_test.go +280 -0
- package/internal/agent/custom_models.go +197 -0
- package/internal/agent/custom_models_test.go +283 -0
- package/internal/agent/dsh.go +754 -0
- package/internal/agent/dsh_sdk.go +679 -0
- package/internal/agent/dsh_sdk_test.go +726 -0
- package/internal/agent/dsh_test.go +759 -0
- package/internal/agent/engine.go +469 -0
- package/internal/agent/engine_base.go +497 -0
- package/internal/agent/engine_base_test.go +308 -0
- package/internal/agent/engine_test.go +858 -0
- package/internal/agent/env.go +88 -0
- package/internal/agent/env_test.go +106 -0
- package/internal/agent/jsonextract.go +128 -0
- package/internal/agent/jsonextract_test.go +110 -0
- package/internal/agent/keepalive_test.go +216 -0
- package/internal/agent/llmengine.go +470 -0
- package/internal/agent/llmengine_test.go +551 -0
- package/internal/agent/llmhttp.go +437 -0
- package/internal/agent/llmhttp_test.go +304 -0
- package/internal/agent/llmmodels.go +205 -0
- package/internal/agent/login.go +27 -0
- package/internal/agent/main_test.go +23 -0
- package/internal/agent/models.go +686 -0
- package/internal/agent/models_test.go +877 -0
- package/internal/agent/openclaw.go +543 -0
- package/internal/agent/openclaw_acp.go +949 -0
- package/internal/agent/openclaw_acp_test.go +406 -0
- package/internal/agent/openclaw_test.go +451 -0
- package/internal/agent/output.go +191 -0
- package/internal/agent/permission.go +362 -0
- package/internal/agent/permission_test.go +526 -0
- package/internal/agent/prompt.go +65 -0
- package/internal/agent/runcmd.go +259 -0
- package/internal/agent/runcmd_test.go +81 -0
- package/internal/agent/runcmd_unix.go +56 -0
- package/internal/agent/runcmd_windows.go +66 -0
- package/internal/agent/runner.go +180 -0
- package/internal/agent/runner_test.go +263 -0
- package/internal/agent/spawnhook_test.go +96 -0
- package/internal/agent/stream.go +882 -0
- package/internal/agent/stream_test.go +565 -0
- package/internal/agent/streamjson.go +185 -0
- package/internal/agent/tags.go +52 -0
- package/internal/agent/toolkind.go +96 -0
- package/internal/agent/toolkind_test.go +70 -0
- package/internal/agent/tools_suffix_test.go +261 -0
- package/internal/agent/trae.go +342 -0
- package/internal/agent/workspace_test.go +166 -0
- package/internal/boundary/boundary_test.go +165 -0
- package/internal/cli/ask.go +1113 -0
- package/internal/cli/ask_test.go +1088 -0
- package/internal/cli/attach_test.go +239 -0
- package/internal/cli/contract_test.go +157 -0
- package/internal/cli/control.go +149 -0
- package/internal/cli/control_test.go +192 -0
- package/internal/cli/engines_test.go +205 -0
- package/internal/cli/events.go +198 -0
- package/internal/cli/events_test.go +166 -0
- package/internal/cli/keepalive_test.go +391 -0
- package/internal/cli/login.go +78 -0
- package/internal/cli/permission_test.go +295 -0
- package/internal/cli/prompt_source_test.go +222 -0
- package/internal/cli/root.go +752 -0
- package/internal/cli/session.go +486 -0
- package/internal/cli/session_test.go +202 -0
- package/internal/cli/sessionlog.go +325 -0
- package/internal/cli/sessionlog_command_test.go +204 -0
- package/internal/cli/sessionlog_test.go +262 -0
- package/internal/cli/sessionlog_user_test.go +129 -0
- package/internal/cli/toolkind_wire_test.go +84 -0
- package/internal/cli/workspace_test.go +188 -0
- package/internal/client/cors_test.go +136 -0
- package/internal/client/lan.go +68 -0
- package/internal/client/lan_test.go +53 -0
- package/internal/client/pair.go +163 -0
- package/internal/client/pair_test.go +109 -0
- package/internal/client/profile.go +184 -0
- package/internal/client/service.go +888 -0
- package/internal/client/service_test.go +654 -0
- package/internal/client/session_events_test.go +186 -0
- package/internal/client/session_messages_test.go +150 -0
- package/internal/config/config.go +439 -0
- package/internal/config/config_test.go +448 -0
- package/internal/session/session.go +447 -0
- package/internal/session/session_append.go +241 -0
- package/internal/session/session_append_test.go +161 -0
- package/internal/session/session_kill_unix_test.go +90 -0
- package/internal/session/session_mark_test.go +95 -0
- package/internal/session/session_test.go +237 -0
- package/internal/session/sessionlog.go +128 -0
- package/internal/session/sessionlog_read_test.go +157 -0
- package/npm/dist/darwin-arm64/magic-agent +0 -0
- package/npm/dist/darwin-x64/magic-agent +0 -0
- package/npm/dist/linux-arm64/magic-agent +0 -0
- package/npm/dist/linux-x64/magic-agent +0 -0
- package/npm/dist/win32-x64/magic-agent.exe +0 -0
- package/npm/install.js +121 -0
- package/npm/lib/platform.js +78 -0
- package/package.json +36 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
package agent
|
|
2
|
+
|
|
3
|
+
// dsh 的 SDK 传输(`dsh --profile sdk`):换行分帧的 JSON-RPC 2.0 over stdio。
|
|
4
|
+
//
|
|
5
|
+
// 为什么需要它:headless profile 没有工具调用通道 —— 官方定位就是「reasoning 走 stderr、
|
|
6
|
+
// 最终正文走 stdout、然后退出」,stdout 里只有最终助手消息、stderr 里只有推理增量,
|
|
7
|
+
// **工具调用不出现在任何 CLI 流上**(实测:模型用了 bash/glob/read,CLI 只字未提)。
|
|
8
|
+
// 工具调用只写进会话日志($DSH_HOME/sessions/…/session.v3.jsonl.zstd,zstd 压缩)。
|
|
9
|
+
//
|
|
10
|
+
// 官方内置的 `sdk` profile 则把**运行时内每个会话事件**实时推给客户端:
|
|
11
|
+
//
|
|
12
|
+
// initialize {cwd, provider, model, maxTokens?} → {serverInfo}
|
|
13
|
+
// session/prompt {sessionId, contentBlocks} → {messageId}(持久入队回执)
|
|
14
|
+
// shutdown {} → {}
|
|
15
|
+
// ← session.event {sessionId, event} 每个会话事件,不过滤
|
|
16
|
+
// ← session.status {sessionId, status: running|idle}
|
|
17
|
+
//
|
|
18
|
+
// session.event 的 event.type 就是会话日志里那些类型(@deepseek-ai/dsh-session 的
|
|
19
|
+
// SessionEventMap),其中:
|
|
20
|
+
//
|
|
21
|
+
// assistant/message {turn, step, message:{content:[…]}} content 里混有
|
|
22
|
+
// reasoning / text 块 → KindThinking / KindText
|
|
23
|
+
// tool/call {turn, step, callId, name, arguments} → KindToolUse
|
|
24
|
+
// tool/result {turn, step, message:{source:{kind:"tool",callId}, content:[…]}}
|
|
25
|
+
// → KindToolResult
|
|
26
|
+
// turn/end {turn, reason} 一轮结束
|
|
27
|
+
//
|
|
28
|
+
// 相比 headless 的净收益:① 工具调用可上报;② 正文按 step 实时到达(不再等 turn 结束);
|
|
29
|
+
// ③ initialize 可指定 provider/model 与 maxTokens;④ 一轮有明确的 turn/end 边界;
|
|
30
|
+
// ⑤ **同一进程内可续接同一会话**(见下)。
|
|
31
|
+
//
|
|
32
|
+
// 会话续接(两种情形必须分清):
|
|
33
|
+
//
|
|
34
|
+
// 同一进程内续接:**支持**。对同一个 sessionId 连续 session/prompt 就是官方推荐的续接
|
|
35
|
+
// 方式 —— 官方 Python SDK 文档原文「reuse a harness, home, and id only to continue
|
|
36
|
+
// the same durable conversation」。本引擎把它接在 Request.Append 上:常驻会话
|
|
37
|
+
// (CLI 的 --keep-alive + --append)期间同一个 runtime 进程活着,每条追加都排进同一
|
|
38
|
+
// 会话,于是拿到真正的多轮上下文(探针实测第二轮能复述第一轮让记的数字)。
|
|
39
|
+
// 跨进程续接:**做不到**。服务端的 getOrCreateSession 只在**本进程的** sessions 表里
|
|
40
|
+
// 查,未命中就走 createSession → `ctx.agents.create`(源码 lib/index.js),拿一个已
|
|
41
|
+
// 存在的持久 id 会撞 SessionAlreadyExistsError(实测 -32603
|
|
42
|
+
// `session "x" already exists`)。harness 核心其实有 `agents.resume`
|
|
43
|
+
// (dsh-agent 类型声明:「Load a persisted session and resume an agent on it」),
|
|
44
|
+
// 但 SDK 协议只暴露 initialize / session/prompt / shutdown 三个方法,没把它放出来
|
|
45
|
+
// —— 只有 Web/TUI 那条 host API 用得上 resume。故 magic-agent 的 `-s <id>` 对 dsh
|
|
46
|
+
// 仍是显式报错,并指向 `--append`(见 dsh.go 的 errDshNoSessionResume)。
|
|
47
|
+
//
|
|
48
|
+
// 依据:@deepseek-ai/dsh-sdk-protocol 的 README.zh.md / lib/types/types.d.ts,
|
|
49
|
+
// @deepseek-ai/dsh-sdk-jsonrpc-server 的 README.zh.md 与 lib/index.js,
|
|
50
|
+
// @deepseek-ai/dsh-llm 的 ContentBlock、@deepseek-ai/dsh-session 的 SessionEventMap,
|
|
51
|
+
// 以及官方 Python SDK 文档(docs/user/guide/python-sdk.md)。
|
|
52
|
+
//(serverInfo.name 固定为 deepseek-harness-sdk-runtime,实测 version 0.0.1)。
|
|
53
|
+
// 协议自述「无协议版本协商、预发布、无兼容承诺」,故本实现只依赖上面这几个具名帧,
|
|
54
|
+
// 未知 event.type 一律忽略(前向兼容)。
|
|
55
|
+
|
|
56
|
+
import (
|
|
57
|
+
"bufio"
|
|
58
|
+
"context"
|
|
59
|
+
"encoding/json"
|
|
60
|
+
"errors"
|
|
61
|
+
"fmt"
|
|
62
|
+
"io"
|
|
63
|
+
"os"
|
|
64
|
+
"path/filepath"
|
|
65
|
+
"strings"
|
|
66
|
+
"sync"
|
|
67
|
+
"time"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// dshSDKProfile sdk profile 名(启动器 app 参数)。
|
|
71
|
+
const dshSDKProfile = "sdk"
|
|
72
|
+
|
|
73
|
+
// errDshSDKUnavailable 表示 SDK 通道不可用(profile 起不来 / 握手握手不上),
|
|
74
|
+
// 调用方据此回退到 headless,而不是把整次调用判死。
|
|
75
|
+
var errDshSDKUnavailable = errors.New("dsh SDK profile unavailable")
|
|
76
|
+
|
|
77
|
+
// errDshSDKClosed SDK 子进程在响应到达前就退出了(profile 不存在 / 启动即崩)。
|
|
78
|
+
var errDshSDKClosed = errors.New("SDK 进程提前退出")
|
|
79
|
+
|
|
80
|
+
// dshRPCErrFrame JSON-RPC 错误响应(服务端明确拒绝:模型没配、参数非法…)。
|
|
81
|
+
//
|
|
82
|
+
// 与 errDshSDKUnavailable 严格区分:这是**这一轮真的失败了**(例如 initialize 报
|
|
83
|
+
// `provider "x" has no configured model "y"`),回退 headless 只会把错误吞掉、
|
|
84
|
+
// 让用户拿到一个「看起来成功但换了模型」的结果。
|
|
85
|
+
type dshRPCErrFrame struct {
|
|
86
|
+
Code int
|
|
87
|
+
Message string
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func (e *dshRPCErrFrame) Error() string {
|
|
91
|
+
return fmt.Sprintf("JSON-RPC %d: %s", e.Code, e.Message)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// dshSDKInitTimeout 握手超时:initialize 要解析 provider/model 路由,比普通请求慢。
|
|
95
|
+
const dshSDKInitTimeout = 30 * time.Second
|
|
96
|
+
|
|
97
|
+
// dshSDKTurnResult 一轮 SDK turn 的汇总。
|
|
98
|
+
type dshSDKTurnResult struct {
|
|
99
|
+
Text string
|
|
100
|
+
Thinking string
|
|
101
|
+
Tools []ToolCall
|
|
102
|
+
// Provider / Model 本轮实际用的路由与模型(initialize 的回显,非猜测)。
|
|
103
|
+
Provider string
|
|
104
|
+
Model string
|
|
105
|
+
// SessionID 本轮的 SDK 会话 id(客户端持有)。
|
|
106
|
+
//
|
|
107
|
+
// 用途:常驻会话里 `magic-agent --append <session_id>` 能用它定位到这条会话
|
|
108
|
+
//(会话登记表按 run_id 或 session_id 查找)。
|
|
109
|
+
// ⚠️ 它**不能**当 `-s` 用:跨进程续接拿不到(见本文件头部的「会话续接」说明)。
|
|
110
|
+
SessionID string
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// dshResolveRoute 决定 initialize 的 (provider, model)。
|
|
114
|
+
//
|
|
115
|
+
// 取值优先级:req.Model(非空)→ settings.yaml 的 agent-default-model。
|
|
116
|
+
// 形态兼容三种写法(ListModels 报的就是第一种):
|
|
117
|
+
//
|
|
118
|
+
// route/model → (route, model) ← ListModels 的输出形态
|
|
119
|
+
// dsh/route/model → 先剥引擎前缀再按上面拆
|
|
120
|
+
// model → (配置里的 provider, model) 裸 id 只能在配置路由内解释
|
|
121
|
+
//
|
|
122
|
+
// 两者都拿不到时返回空串,由调用方判为「SDK 通道不可用」并回退 headless
|
|
123
|
+
// (initialize 的 provider/model 是必填项,没有默认值)。
|
|
124
|
+
func dshResolveRoute(reqModel string) (string, string) {
|
|
125
|
+
defProvider, defModel := DshConfiguredRoute()
|
|
126
|
+
m := strings.TrimSpace(reqModel)
|
|
127
|
+
if m == "" {
|
|
128
|
+
return defProvider, defModel
|
|
129
|
+
}
|
|
130
|
+
m = strings.TrimPrefix(m, "dsh/") // 先剥可能的引擎前缀
|
|
131
|
+
if p, id, ok := strings.Cut(m, "/"); ok && p != "" && id != "" {
|
|
132
|
+
return p, id
|
|
133
|
+
}
|
|
134
|
+
// 裸 id:只能在配置层的路由内解释(同名模型可能出现在多条路由上)。
|
|
135
|
+
return defProvider, m
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── 协议载荷(只声明本实现用到的字段)────────────────────────────
|
|
139
|
+
//
|
|
140
|
+
// 字段名与 dsh-sdk-protocol / dsh-llm / dsh-session 的 d.ts 逐一对齐,
|
|
141
|
+
// 不按实测样本猜:tool/call 的 callId、tool/result 的 message.source.callId、
|
|
142
|
+
// assistant/message 的 message.content[] 都是官方具名结构。
|
|
143
|
+
|
|
144
|
+
type dshRPCRequest struct {
|
|
145
|
+
JSONRPC string `json:"jsonrpc"`
|
|
146
|
+
ID int `json:"id"`
|
|
147
|
+
Method string `json:"method"`
|
|
148
|
+
Params any `json:"params,omitempty"`
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
type dshRPCError struct {
|
|
152
|
+
Code int `json:"code"`
|
|
153
|
+
Message string `json:"message"`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
type dshRPCInbound struct {
|
|
157
|
+
ID *int `json:"id"`
|
|
158
|
+
Method string `json:"method"`
|
|
159
|
+
Params json.RawMessage `json:"params"`
|
|
160
|
+
Error *dshRPCError `json:"error"`
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// dshSDKInitParams = InitializeParams。provider/model 必填,maxTokens 可选
|
|
164
|
+
// (「限制 SDK 创建的 agent 及其进程内后代的每次对话模型输出」—— 这是 headless
|
|
165
|
+
// 完全没有的能力,见 dsh.go 的参数矩阵)。
|
|
166
|
+
type dshSDKInitParams struct {
|
|
167
|
+
Cwd string `json:"cwd"`
|
|
168
|
+
Provider string `json:"provider"`
|
|
169
|
+
Model string `json:"model"`
|
|
170
|
+
MaxTokens int `json:"maxTokens,omitempty"`
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// dshSDKTextBlock = TextBlock(ContentBlock 的 text 成员)。
|
|
174
|
+
type dshSDKTextBlock struct {
|
|
175
|
+
Type string `json:"type"`
|
|
176
|
+
Text string `json:"text"`
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
type dshSDKPromptParams struct {
|
|
180
|
+
SessionID string `json:"sessionId"`
|
|
181
|
+
ContentBlocks []dshSDKTextBlock `json:"contentBlocks"`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// dshSDKSessionEvent = SessionEventNotification。
|
|
185
|
+
type dshSDKSessionEvent struct {
|
|
186
|
+
SessionID string `json:"sessionId"`
|
|
187
|
+
Event struct {
|
|
188
|
+
Type string `json:"type"`
|
|
189
|
+
Data json.RawMessage `json:"data"`
|
|
190
|
+
} `json:"event"`
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// dshSDKStatusEvent = SessionStatusNotification。
|
|
194
|
+
type dshSDKStatusEvent struct {
|
|
195
|
+
SessionID string `json:"sessionId"`
|
|
196
|
+
Status string `json:"status"`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// assistant/message:message.content[] 里混有 reasoning / text / tool-call 块。
|
|
200
|
+
// turn / step 用来把正文**按轮分桶**(常驻会话里多轮的事件可能交错到达)。
|
|
201
|
+
type dshSDKAssistantMessage struct {
|
|
202
|
+
Turn int `json:"turn"`
|
|
203
|
+
Step int `json:"step"`
|
|
204
|
+
Message struct {
|
|
205
|
+
Content []struct {
|
|
206
|
+
Type string `json:"type"`
|
|
207
|
+
Text string `json:"text"`
|
|
208
|
+
} `json:"content"`
|
|
209
|
+
} `json:"message"`
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// turn/end:一轮结束(reason.kind 如 completed / max-tokens / error)。
|
|
213
|
+
type dshSDKTurnEnd struct {
|
|
214
|
+
Turn int `json:"turn"`
|
|
215
|
+
Reason struct {
|
|
216
|
+
Kind string `json:"kind"`
|
|
217
|
+
} `json:"reason"`
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// tool/call:arguments 是模型原样产出的 JSON **字符串**(不是对象)。
|
|
221
|
+
type dshSDKToolCall struct {
|
|
222
|
+
CallID string `json:"callId"`
|
|
223
|
+
Name string `json:"name"`
|
|
224
|
+
Arguments string `json:"arguments"`
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// tool/result:ToolResultMessage = {source:{kind:"tool",callId}, content:[…]},
|
|
228
|
+
// 其中 content[0] 是 ToolResultBlock{type:"tool-result", toolCallId, content:[TextBlock…]}。
|
|
229
|
+
// 这里按「两级都可能带文本」来取,避免把结构差异变成空结果。
|
|
230
|
+
type dshSDKToolResult struct {
|
|
231
|
+
Message struct {
|
|
232
|
+
Source struct {
|
|
233
|
+
CallID string `json:"callId"`
|
|
234
|
+
} `json:"source"`
|
|
235
|
+
Content []dshSDKToolResultBlock `json:"content"`
|
|
236
|
+
} `json:"message"`
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
type dshSDKToolResultBlock struct {
|
|
240
|
+
Type string `json:"type"`
|
|
241
|
+
Text string `json:"text"`
|
|
242
|
+
IsError bool `json:"isError"`
|
|
243
|
+
Content []struct {
|
|
244
|
+
Type string `json:"type"`
|
|
245
|
+
Text string `json:"text"`
|
|
246
|
+
} `json:"content"`
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// dshSDKTurn 跑一轮 SDK turn。
|
|
250
|
+
//
|
|
251
|
+
// onEvent 为 nil 时只收集不回调(非流式路径复用同一实现)。返回 errDshSDKUnavailable
|
|
252
|
+
// 表示 SDK 通道本身不可用(调用方回退 headless);其余错误是这一轮真的失败了。
|
|
253
|
+
func dshSDKTurn(ctx context.Context, bin, workspace string, req Request, prompt string, onEvent func(StreamEvent)) (dshSDKTurnResult, error) {
|
|
254
|
+
provider, model := dshResolveRoute(req.Model)
|
|
255
|
+
if provider == "" || model == "" {
|
|
256
|
+
return dshSDKTurnResult{}, fmt.Errorf(
|
|
257
|
+
"%w: 读不到 $DSH_HOME/settings.yaml 的 agent-default-model(provider/model 是 initialize 的必填项)",
|
|
258
|
+
errDshSDKUnavailable)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
cmd := newStreamCmdIn(workspace, bin, []string{"--profile", dshSDKProfile})
|
|
262
|
+
|
|
263
|
+
/* 三条管道都自建(不直挂 bytes.Buffer / StdinPipe 的 stdout 侧):
|
|
264
|
+
沿用 runcmd.go / stream.go 的同一条经验 —— Go 只在目标是 *os.File 时才不做内部
|
|
265
|
+
拷贝 goroutine,而 cmd.Wait() 会等那些 goroutine 结束;只要 dsh 的 node worker
|
|
266
|
+
还握着写端,Wait 就永远不返回(症状:引擎已死、magic-agent 挂着不退)。 */
|
|
267
|
+
stdoutR, stdoutW, err := os.Pipe()
|
|
268
|
+
if err != nil {
|
|
269
|
+
return dshSDKTurnResult{}, fmt.Errorf("%w: stdout pipe: %v", errDshSDKUnavailable, err)
|
|
270
|
+
}
|
|
271
|
+
stderrR, stderrW, err := os.Pipe()
|
|
272
|
+
if err != nil {
|
|
273
|
+
stdoutR.Close()
|
|
274
|
+
stdoutW.Close()
|
|
275
|
+
return dshSDKTurnResult{}, fmt.Errorf("%w: stderr pipe: %v", errDshSDKUnavailable, err)
|
|
276
|
+
}
|
|
277
|
+
stdin, err := cmd.StdinPipe()
|
|
278
|
+
if err != nil {
|
|
279
|
+
stdoutR.Close()
|
|
280
|
+
stdoutW.Close()
|
|
281
|
+
stderrR.Close()
|
|
282
|
+
stderrW.Close()
|
|
283
|
+
return dshSDKTurnResult{}, fmt.Errorf("%w: stdin pipe: %v", errDshSDKUnavailable, err)
|
|
284
|
+
}
|
|
285
|
+
cmd.Stdout = stdoutW
|
|
286
|
+
cmd.Stderr = stderrW
|
|
287
|
+
|
|
288
|
+
if err := cmd.Start(); err != nil {
|
|
289
|
+
stdoutR.Close()
|
|
290
|
+
stdoutW.Close()
|
|
291
|
+
stderrR.Close()
|
|
292
|
+
stderrW.Close()
|
|
293
|
+
stdin.Close()
|
|
294
|
+
return dshSDKTurnResult{}, fmt.Errorf("%w: start: %v", errDshSDKUnavailable, err)
|
|
295
|
+
}
|
|
296
|
+
notifySpawn(ctx, cmd.Process.Pid) // 会话登记表据此记住引擎子进程 pid(--stop 靠它)
|
|
297
|
+
stdoutW.Close() // 父进程不再持有写端;EOF 只取决于子进程一侧
|
|
298
|
+
stderrW.Close()
|
|
299
|
+
|
|
300
|
+
// stderr 尾部留 32KB 做错误摘要(与其它流式路径同一个缓冲类型)。
|
|
301
|
+
var stderrBuf streamStderrBuf
|
|
302
|
+
go func() { defer stderrR.Close(); _, _ = io.Copy(&stderrBuf, stderrR) }()
|
|
303
|
+
|
|
304
|
+
// turnDone 本轮收尾信号:收尾后读线程**继续把 stdout 读干净**(否则子进程写满管道
|
|
305
|
+
// 会卡住、只能等 stop 的超时杀组),但不再往 inbound 投递。
|
|
306
|
+
// 幂等 —— turn/end、session.status:idle、以及 stop()(ctx 取消/报错退出)都可能先到。
|
|
307
|
+
turnDone := make(chan struct{})
|
|
308
|
+
var closeTurn sync.Once
|
|
309
|
+
markTurnDone := func() { closeTurn.Do(func() { close(turnDone) }) }
|
|
310
|
+
|
|
311
|
+
// 读线程:逐行解 JSON-RPC 帧投进 channel;主循环只做 select,避免与 ctx 取消竞争。
|
|
312
|
+
inbound := make(chan dshRPCInbound, 64)
|
|
313
|
+
go func() {
|
|
314
|
+
defer close(inbound)
|
|
315
|
+
defer stdoutR.Close()
|
|
316
|
+
sc := bufio.NewScanner(stdoutR)
|
|
317
|
+
sc.Buffer(make([]byte, 0, 64*1024), 8*1024*1024) // tool/result 可能很大
|
|
318
|
+
for sc.Scan() {
|
|
319
|
+
line := strings.TrimSpace(sc.Text())
|
|
320
|
+
if line == "" {
|
|
321
|
+
continue
|
|
322
|
+
}
|
|
323
|
+
var msg dshRPCInbound
|
|
324
|
+
if err := json.Unmarshal([]byte(line), &msg); err != nil {
|
|
325
|
+
continue // 格式错误的行按协议要求忽略
|
|
326
|
+
}
|
|
327
|
+
select {
|
|
328
|
+
case inbound <- msg:
|
|
329
|
+
case <-turnDone:
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}()
|
|
333
|
+
|
|
334
|
+
// procExit 是进程退出的广播(close 后所有接收方立即返回),供 stop 与主循环共用。
|
|
335
|
+
procExit := make(chan struct{})
|
|
336
|
+
var exitErr error
|
|
337
|
+
go func() {
|
|
338
|
+
exitErr = cmd.Wait()
|
|
339
|
+
close(procExit)
|
|
340
|
+
}()
|
|
341
|
+
|
|
342
|
+
// stop 幂等收尾:先让读线程转入「只读不投递」(否则它会卡在投递上、stdout 不再被
|
|
343
|
+
// drain),再关 stdin 让 runtime 自己退(协议唯一的「结束」手段),5s 不退就杀整个
|
|
344
|
+
// 进程组(dsh 会起 node worker,只杀父进程会留孤儿)。
|
|
345
|
+
var stopOnce sync.Once
|
|
346
|
+
stop := func() {
|
|
347
|
+
stopOnce.Do(func() {
|
|
348
|
+
markTurnDone()
|
|
349
|
+
_ = stdin.Close()
|
|
350
|
+
select {
|
|
351
|
+
case <-procExit:
|
|
352
|
+
case <-time.After(5 * time.Second):
|
|
353
|
+
killProcessGroup(cmd)
|
|
354
|
+
select {
|
|
355
|
+
case <-procExit:
|
|
356
|
+
case <-time.After(5 * time.Second):
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
})
|
|
360
|
+
}
|
|
361
|
+
defer stop()
|
|
362
|
+
|
|
363
|
+
nextID := 0
|
|
364
|
+
send := func(method string, params any) (int, error) {
|
|
365
|
+
nextID++
|
|
366
|
+
b, err := json.Marshal(dshRPCRequest{JSONRPC: "2.0", ID: nextID, Method: method, Params: params})
|
|
367
|
+
if err != nil {
|
|
368
|
+
return nextID, err
|
|
369
|
+
}
|
|
370
|
+
_, err = stdin.Write(append(b, '\n'))
|
|
371
|
+
return nextID, err
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ① initialize:握手失败 = SDK 通道不可用(旧版 dsh 没有该 profile 时会走到这里)。
|
|
375
|
+
initParams := dshSDKInitParams{Cwd: workspace, Provider: provider, Model: model, MaxTokens: req.MaxTokens}
|
|
376
|
+
initID, err := send("initialize", initParams)
|
|
377
|
+
if err != nil {
|
|
378
|
+
return dshSDKTurnResult{}, fmt.Errorf("%w: 写 initialize: %v", errDshSDKUnavailable, err)
|
|
379
|
+
}
|
|
380
|
+
initCtx, cancelInit := context.WithTimeout(ctx, dshSDKInitTimeout)
|
|
381
|
+
defer cancelInit()
|
|
382
|
+
if err := waitForResponse(initCtx, inbound, procExit, initID); err != nil {
|
|
383
|
+
var rpcErr *dshRPCErrFrame
|
|
384
|
+
if errors.As(err, &rpcErr) {
|
|
385
|
+
// 服务端明确拒绝(模型没配 / 参数非法):这是真失败,不回退。
|
|
386
|
+
return dshSDKTurnResult{}, fmt.Errorf(
|
|
387
|
+
"dsh: initialize 被拒绝(provider=%s model=%s):%s;"+
|
|
388
|
+
"可用的 provider/model 见 --engines 的 models 字段(只有 settings.yaml 里声明过的模型才会被路由接受)",
|
|
389
|
+
provider, model, rpcErr.Message)
|
|
390
|
+
}
|
|
391
|
+
return dshSDKTurnResult{}, fmt.Errorf("%w: initialize: %v%s", errDshSDKUnavailable, err, dshStderrTail(stderrBuf.String()))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// ② session/prompt:入队即返回 messageId,事件随后异步到达。
|
|
395
|
+
//
|
|
396
|
+
// 常驻会话(req.Append 非 nil):同一进程内对同一 sessionId 继续 prompt 就是官方
|
|
397
|
+
// 支持的续接方式 —— 官方 Python SDK 文档原文「reuse a harness, home, and id only to
|
|
398
|
+
// continue the same durable conversation」,本引擎探针也验证了第二轮能复述第一轮让
|
|
399
|
+
// 记的数字。所以收到一条追加就**立刻**再排一条(服务器明确支持排队:「同一会话上的
|
|
400
|
+
// 独立请求可以继续排入更多工作」),任务跑着的时候追加也能被接住,不必等当前轮结束。
|
|
401
|
+
//
|
|
402
|
+
// ⚠️ 跨进程续接做不到:服务端 createSession 走的是 `ctx.agents.create`(源码
|
|
403
|
+
// lib/index.js 的 getOrCreateSession / createSession),拿一个已存在的持久 id 会撞
|
|
404
|
+
// SessionAlreadyExistsError(实测 -32603 `session "x" already exists`)。harness 核心
|
|
405
|
+
// 有 `agents.resume`(dsh-agent 类型声明:「Load a persisted session and resume an
|
|
406
|
+
// agent on it」),但 SDK 协议只有 initialize / session/prompt / shutdown 三个方法,
|
|
407
|
+
// 没有暴露它 —— 只有 Web/TUI 那条 host API 用得上 resume。
|
|
408
|
+
sessionID := newDshSessionID()
|
|
409
|
+
out := dshSDKTurnResult{Provider: provider, Model: model, SessionID: sessionID}
|
|
410
|
+
callIndex := map[string]int{}
|
|
411
|
+
promptIDs := map[int]bool{} // 已发出的 prompt 请求 id(用来把它的错误响应挑出来)
|
|
412
|
+
// 正文按 **turn 号分桶**:常驻会话里多轮的事件可能交错到达(第二轮的 prompt 刚排进去,
|
|
413
|
+
// 第一轮的事件还在路上),用单一缓冲区会把两轮正文粘成一段。
|
|
414
|
+
textByTurn := map[int]string{}
|
|
415
|
+
lastTurn := 0
|
|
416
|
+
roundsPending := 0 // 已排入但还没收到 turn/end 的轮数
|
|
417
|
+
roundEnded := false // 当前轮是否已经收到 turn/end(session.status:idle 兜底要用)
|
|
418
|
+
appendCh := req.Append
|
|
419
|
+
appendClosed := appendCh == nil
|
|
420
|
+
|
|
421
|
+
emit := func(ev StreamEvent) {
|
|
422
|
+
if onEvent != nil {
|
|
423
|
+
onEvent(ev)
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
sendPrompt := func(text string) error {
|
|
427
|
+
id, serr := send("session/prompt", dshSDKPromptParams{
|
|
428
|
+
SessionID: sessionID,
|
|
429
|
+
ContentBlocks: []dshSDKTextBlock{{Type: "text", Text: text}},
|
|
430
|
+
})
|
|
431
|
+
if serr != nil {
|
|
432
|
+
return serr
|
|
433
|
+
}
|
|
434
|
+
promptIDs[id] = true
|
|
435
|
+
roundsPending++
|
|
436
|
+
roundEnded = false
|
|
437
|
+
return nil
|
|
438
|
+
}
|
|
439
|
+
// finish 收尾:先让读线程转入「只读不投递」,再发 shutdown(协议唯一的「结束」
|
|
440
|
+
// 手段,没有 cancel 方法)。幂等 —— turn/end、session.status:idle、追加通道关闭
|
|
441
|
+
// 三条收尾路径都可能走到。
|
|
442
|
+
finish := func() (dshSDKTurnResult, error) {
|
|
443
|
+
markTurnDone()
|
|
444
|
+
_, _ = send("shutdown", nil)
|
|
445
|
+
return out, nil
|
|
446
|
+
}
|
|
447
|
+
// roundDone 一轮结束:取该轮(turn 号)的正文、报一条 turn_end,并在「追加窗口已关
|
|
448
|
+
// 且没有在跑的轮次」时收尾(常驻会话里 turn/end 只是轮次边界,不是会话终点)。
|
|
449
|
+
roundDone := func(turn int) (dshSDKTurnResult, bool, error) {
|
|
450
|
+
if roundsPending > 0 {
|
|
451
|
+
roundsPending--
|
|
452
|
+
}
|
|
453
|
+
roundEnded = true
|
|
454
|
+
if turn == 0 {
|
|
455
|
+
turn = lastTurn // 载荷没给 turn 号时的兜底
|
|
456
|
+
}
|
|
457
|
+
roundText := textByTurn[turn]
|
|
458
|
+
delete(textByTurn, turn)
|
|
459
|
+
out.Text = roundText
|
|
460
|
+
// 带 SessionID:常驻会话里宿主需要在「这一轮刚做完」时就把会话锚点绑上
|
|
461
|
+
//(与 claude/codebuddy 的 turn_end 同一取舍,见 stream.go 的 StreamEvent.SessionID)。
|
|
462
|
+
emit(StreamEvent{Kind: KindTurnEnd, Text: roundText, SessionID: sessionID})
|
|
463
|
+
if appendClosed && roundsPending == 0 {
|
|
464
|
+
res, err := finish()
|
|
465
|
+
return res, true, err
|
|
466
|
+
}
|
|
467
|
+
return dshSDKTurnResult{}, false, nil
|
|
468
|
+
}
|
|
469
|
+
if err := sendPrompt(prompt); err != nil {
|
|
470
|
+
return dshSDKTurnResult{}, fmt.Errorf("dsh sdk: 写 session/prompt: %v", err)
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ③ 读事件直到收尾(或进程退出 / ctx 取消)。
|
|
474
|
+
for {
|
|
475
|
+
select {
|
|
476
|
+
case <-ctx.Done():
|
|
477
|
+
return out, fmt.Errorf("dsh sdk: %w", ctx.Err())
|
|
478
|
+
case <-procExit:
|
|
479
|
+
// 进程结束:已经拿到正文就正常收尾,否则报错(stderr 尾部通常就是原因)。
|
|
480
|
+
if out.Text == "" {
|
|
481
|
+
return out, fmt.Errorf("dsh sdk: 进程提前退出(%v)%s", exitErr, dshStderrTail(stderrBuf.String()))
|
|
482
|
+
}
|
|
483
|
+
return out, nil
|
|
484
|
+
case text, ok := <-appendCh:
|
|
485
|
+
// 追加通道(常驻会话)。通道关闭 = 调用方不再追加 → 等在跑的轮次收完就收尾。
|
|
486
|
+
if !ok {
|
|
487
|
+
appendClosed = true
|
|
488
|
+
appendCh = nil // nil channel 在 select 里永久阻塞,不再进这个分支
|
|
489
|
+
if roundsPending == 0 {
|
|
490
|
+
return finish()
|
|
491
|
+
}
|
|
492
|
+
continue
|
|
493
|
+
}
|
|
494
|
+
if strings.TrimSpace(text) == "" {
|
|
495
|
+
continue
|
|
496
|
+
}
|
|
497
|
+
if err := sendPrompt(text); err != nil {
|
|
498
|
+
return out, fmt.Errorf("dsh sdk: 写 session/prompt(追加): %v", err)
|
|
499
|
+
}
|
|
500
|
+
case msg, ok := <-inbound:
|
|
501
|
+
if !ok {
|
|
502
|
+
if out.Text == "" {
|
|
503
|
+
return out, fmt.Errorf("dsh sdk: 进程提前退出%s", dshStderrTail(stderrBuf.String()))
|
|
504
|
+
}
|
|
505
|
+
return out, nil
|
|
506
|
+
}
|
|
507
|
+
// 响应帧:prompt 被拒(内容非法等)时立刻失败,别干等到 ctx 超时。
|
|
508
|
+
if msg.ID != nil {
|
|
509
|
+
if promptIDs[*msg.ID] && msg.Error != nil {
|
|
510
|
+
return out, fmt.Errorf("dsh sdk: session/prompt 被拒绝:%s",
|
|
511
|
+
(&dshRPCErrFrame{Code: msg.Error.Code, Message: msg.Error.Message}).Error())
|
|
512
|
+
}
|
|
513
|
+
continue
|
|
514
|
+
}
|
|
515
|
+
switch msg.Method {
|
|
516
|
+
case "session.event":
|
|
517
|
+
var n dshSDKSessionEvent
|
|
518
|
+
if err := json.Unmarshal(msg.Params, &n); err != nil {
|
|
519
|
+
continue
|
|
520
|
+
}
|
|
521
|
+
// session.event 覆盖「运行时内每个会话」,不止 SDK 建的这一个
|
|
522
|
+
//(协议原文)→ 必须按 sessionId 过滤,否则并行的 TUI/Web 会话
|
|
523
|
+
// 会把它们的工具调用混进本轮结果。
|
|
524
|
+
if n.SessionID != sessionID {
|
|
525
|
+
continue
|
|
526
|
+
}
|
|
527
|
+
switch n.Event.Type {
|
|
528
|
+
case "assistant/message":
|
|
529
|
+
var am dshSDKAssistantMessage
|
|
530
|
+
if err := json.Unmarshal(n.Event.Data, &am); err != nil {
|
|
531
|
+
continue
|
|
532
|
+
}
|
|
533
|
+
for _, blk := range am.Message.Content {
|
|
534
|
+
if blk.Text == "" {
|
|
535
|
+
continue
|
|
536
|
+
}
|
|
537
|
+
turn := am.Turn
|
|
538
|
+
if turn == 0 {
|
|
539
|
+
turn = lastTurn // 载荷没给 turn 号时的兜底
|
|
540
|
+
}
|
|
541
|
+
lastTurn = turn
|
|
542
|
+
switch blk.Type {
|
|
543
|
+
case "text":
|
|
544
|
+
textByTurn[turn] += blk.Text
|
|
545
|
+
out.Text = textByTurn[turn]
|
|
546
|
+
emit(StreamEvent{Kind: KindText, Text: blk.Text})
|
|
547
|
+
case "reasoning":
|
|
548
|
+
// 推理跨轮累加(与 claude/codebuddy 的 acc.Thinking 同语义)。
|
|
549
|
+
out.Thinking += blk.Text
|
|
550
|
+
emit(StreamEvent{Kind: KindThinking, Text: blk.Text})
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
case "tool/call":
|
|
554
|
+
var tc dshSDKToolCall
|
|
555
|
+
if err := json.Unmarshal(n.Event.Data, &tc); err != nil {
|
|
556
|
+
continue
|
|
557
|
+
}
|
|
558
|
+
if tc.Name == "" && tc.CallID == "" {
|
|
559
|
+
continue
|
|
560
|
+
}
|
|
561
|
+
emit(StreamEvent{Kind: KindToolUse, Text: tc.Arguments, Name: tc.Name, ID: tc.CallID})
|
|
562
|
+
callIndex[tc.CallID] = len(out.Tools)
|
|
563
|
+
out.Tools = append(out.Tools, ToolCall{Name: tc.Name, ID: tc.CallID, Args: tc.Arguments})
|
|
564
|
+
case "tool/result":
|
|
565
|
+
var tr dshSDKToolResult
|
|
566
|
+
if err := json.Unmarshal(n.Event.Data, &tr); err != nil {
|
|
567
|
+
continue
|
|
568
|
+
}
|
|
569
|
+
text := dshToolResultText(tr)
|
|
570
|
+
emit(StreamEvent{Kind: KindToolResult, Text: text, ID: tr.Message.Source.CallID})
|
|
571
|
+
if i, ok := callIndex[tr.Message.Source.CallID]; ok && out.Tools[i].Result == "" {
|
|
572
|
+
out.Tools[i].Result = text
|
|
573
|
+
}
|
|
574
|
+
case "turn/end":
|
|
575
|
+
var te dshSDKTurnEnd
|
|
576
|
+
_ = json.Unmarshal(n.Event.Data, &te)
|
|
577
|
+
res, done, err := roundDone(te.Turn)
|
|
578
|
+
if done {
|
|
579
|
+
return res, err
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
// 其余 event.type(turn/start、step/*、request/*、session/title…)忽略。
|
|
583
|
+
case "session.status":
|
|
584
|
+
var st dshSDKStatusEvent
|
|
585
|
+
// 兜底:某些轮次可能只给 status 不给 turn/end(同样按 sessionId 过滤)。
|
|
586
|
+
//
|
|
587
|
+
// 三个条件缺一不可:
|
|
588
|
+
// ① 本轮还没收到 turn/end(roundEnded=false)—— 否则这条 idle 只是
|
|
589
|
+
// 一轮结束后的状态回落,当收尾信号会把常驻会话在轮次之间掐掉
|
|
590
|
+
// (实测踩过:追加通道已关 + 第二轮已排入时,第一轮尾部的 idle
|
|
591
|
+
// 把整个会话结束了,第二轮的事件再也没被消费);
|
|
592
|
+
// ② 已经拿到正文;
|
|
593
|
+
// ③ 追加窗口已关 —— 窗口开着时的 idle 只是「两轮之间」。
|
|
594
|
+
if err := json.Unmarshal(msg.Params, &st); err == nil &&
|
|
595
|
+
st.SessionID == sessionID && st.Status == "idle" &&
|
|
596
|
+
!roundEnded && out.Text != "" && appendClosed {
|
|
597
|
+
res, done, err := roundDone(lastTurn)
|
|
598
|
+
if done {
|
|
599
|
+
return res, err
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// waitForResponse 等到 id 对应的响应帧;错误响应转成 *dshRPCErrFrame。
|
|
608
|
+
func waitForResponse(ctx context.Context, inbound <-chan dshRPCInbound, procExit <-chan struct{}, id int) error {
|
|
609
|
+
for {
|
|
610
|
+
select {
|
|
611
|
+
case <-ctx.Done():
|
|
612
|
+
return ctx.Err()
|
|
613
|
+
case <-procExit:
|
|
614
|
+
return errDshSDKClosed
|
|
615
|
+
case msg, ok := <-inbound:
|
|
616
|
+
if !ok {
|
|
617
|
+
return errDshSDKClosed
|
|
618
|
+
}
|
|
619
|
+
if msg.ID == nil || *msg.ID != id {
|
|
620
|
+
continue // 握手期间的通知先丢弃(此时还没有会话)
|
|
621
|
+
}
|
|
622
|
+
if msg.Error != nil {
|
|
623
|
+
return &dshRPCErrFrame{Code: msg.Error.Code, Message: msg.Error.Message}
|
|
624
|
+
}
|
|
625
|
+
return nil
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// dshToolResultText 从 tool/result 载荷里取结果正文。
|
|
631
|
+
//
|
|
632
|
+
// 取两级:ToolResultBlock 自带的 text(少见)与它嵌套的 content[](TextBlock,常态)。
|
|
633
|
+
// 没有任何文本产出的工具(例如只改了文件)返回空串。
|
|
634
|
+
func dshToolResultText(tr dshSDKToolResult) string {
|
|
635
|
+
var b strings.Builder
|
|
636
|
+
for _, blk := range tr.Message.Content {
|
|
637
|
+
if blk.Type == "text" && blk.Text != "" {
|
|
638
|
+
b.WriteString(blk.Text)
|
|
639
|
+
}
|
|
640
|
+
for _, inner := range blk.Content {
|
|
641
|
+
if inner.Type == "text" && inner.Text != "" {
|
|
642
|
+
b.WriteString(inner.Text)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return b.String()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// dshStderrTail 摘 stderr 末尾一行做错误补充(进程起不来时这里就是原因)。
|
|
650
|
+
func dshStderrTail(s string) string {
|
|
651
|
+
s = strings.TrimSpace(s)
|
|
652
|
+
if s == "" {
|
|
653
|
+
return ""
|
|
654
|
+
}
|
|
655
|
+
lines := strings.Split(s, "\n")
|
|
656
|
+
return ": " + strings.TrimSpace(lines[len(lines)-1])
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// DshConfiguredRoute 读 $DSH_HOME/settings.yaml 里 agent-default-model 的 (provider, model)。
|
|
660
|
+
// SDK 的 initialize 两者都是必填;读不到就只能回退 headless。
|
|
661
|
+
func DshConfiguredRoute() (string, string) {
|
|
662
|
+
home := DshHome()
|
|
663
|
+
if home == "" {
|
|
664
|
+
return "", ""
|
|
665
|
+
}
|
|
666
|
+
data, err := os.ReadFile(filepath.Join(home, "settings.yaml"))
|
|
667
|
+
if err != nil {
|
|
668
|
+
return "", ""
|
|
669
|
+
}
|
|
670
|
+
return parseDshDefaultModelRoute(string(data))
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// newDshSessionID 生成 SDK 侧会话 id。
|
|
674
|
+
//
|
|
675
|
+
// 必须**每次都是新 id**:session/prompt 的语义是「未知 id 惰性建 agent+session 对」,
|
|
676
|
+
// 已存在的 id 会被 -32603 `session "x" already exists` 拒掉(协议无 resume 方法)。
|
|
677
|
+
func newDshSessionID() string {
|
|
678
|
+
return fmt.Sprintf("magic-agent-%d", time.Now().UnixNano())
|
|
679
|
+
}
|