@thincoder/core 0.9.5 → 0.10.1

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 (215) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +1 -1
  3. package/abort-provenance.mjs +10 -10
  4. package/advisor/citations.mjs +5 -5
  5. package/advisor/compaction.mjs +16 -15
  6. package/advisor/loop.mjs +12 -12
  7. package/advisor/messages.mjs +6 -6
  8. package/advisor/project-context.mjs +5 -5
  9. package/advisor/repos.mjs +3 -3
  10. package/advisor/run.mjs +22 -12
  11. package/advisor.mjs +4 -4
  12. package/agent/assemble.mjs +111 -0
  13. package/agent/chat-call.mjs +68 -0
  14. package/agent/child-marks.mjs +2 -2
  15. package/agent/completion.mjs +9 -2
  16. package/agent/continue-decision.mjs +25 -0
  17. package/agent/dispatch-gates.mjs +140 -0
  18. package/agent/dispatch-run.mjs +167 -0
  19. package/agent/dispatch.mjs +54 -296
  20. package/agent/family-tools.mjs +5 -5
  21. package/agent/helpers.mjs +53 -3
  22. package/agent/live-beat.mjs +49 -0
  23. package/agent/post-turn.mjs +4 -8
  24. package/agent/record-results.mjs +23 -20
  25. package/agent/relay-prefix.mjs +2 -2
  26. package/agent/run-stages.mjs +15 -11
  27. package/agent/run-start.mjs +111 -0
  28. package/agent/setup-reminders.mjs +9 -9
  29. package/agent/setup.mjs +32 -4
  30. package/agent/spawn-child.mjs +9 -9
  31. package/agent/suspension.mjs +89 -29
  32. package/agent/timers.mjs +113 -0
  33. package/agent/turn-loop.mjs +244 -0
  34. package/agent/write-gate.mjs +103 -6
  35. package/agent-tools/advisor-async.mjs +9 -9
  36. package/agent-tools/advisor-settle.mjs +12 -12
  37. package/agent-tools/advisor.mjs +36 -41
  38. package/agent-tools/async-discard.mjs +44 -1
  39. package/agent-tools/async-settle.mjs +21 -18
  40. package/agent-tools/audit-block.mjs +7 -7
  41. package/agent-tools/bash-async.mjs +281 -0
  42. package/agent-tools/batch-lifecycle.mjs +57 -18
  43. package/agent-tools/batch-paths.mjs +52 -8
  44. package/agent-tools/batch-skeleton.mjs +19 -1
  45. package/agent-tools/batch.mjs +12 -65
  46. package/agent-tools/checkpoint.mjs +121 -0
  47. package/agent-tools/child-permission.mjs +5 -3
  48. package/agent-tools/consult.mjs +56 -56
  49. package/agent-tools/context.mjs +4 -20
  50. package/agent-tools/design-token.mjs +3 -3
  51. package/agent-tools/digest-budget.mjs +1 -1
  52. package/agent-tools/eng.mjs +5 -5
  53. package/agent-tools/escalate-async.mjs +16 -11
  54. package/agent-tools/goal.mjs +3 -10
  55. package/agent-tools/panel-blocks.mjs +2 -2
  56. package/agent-tools/parent-channel.mjs +3 -13
  57. package/agent-tools/plan.mjs +3 -4
  58. package/agent-tools/read-history.mjs +12 -28
  59. package/agent-tools/recent-changes.mjs +4 -5
  60. package/agent-tools/review-facts.mjs +1 -1
  61. package/agent-tools/settings.mjs +47 -20
  62. package/agent-tools/skill.mjs +3 -5
  63. package/agent-tools/spawn-gates.mjs +3 -3
  64. package/agent-tools/subagent-actions-query.mjs +270 -0
  65. package/agent-tools/subagent-actions.mjs +21 -265
  66. package/agent-tools/subagent-async.mjs +25 -20
  67. package/agent-tools/subagent-panel.mjs +33 -4
  68. package/agent-tools/subagent-run.mjs +36 -21
  69. package/agent-tools/subagent-scheduler.mjs +28 -27
  70. package/agent-tools/subagent-spawn.mjs +45 -14
  71. package/agent-tools/subagent.mjs +33 -53
  72. package/agent-tools/task.mjs +3 -9
  73. package/agent-tools/timer.mjs +12 -12
  74. package/agent-tools/verify.mjs +14 -13
  75. package/agent-tools.mjs +0 -1
  76. package/agent.mjs +18 -355
  77. package/attachments.mjs +125 -0
  78. package/auto-think.mjs +1 -1
  79. package/config-presets.mjs +11 -6
  80. package/config-watch.mjs +83 -0
  81. package/config.mjs +17 -4
  82. package/context-degrade.mjs +81 -0
  83. package/context-echo.mjs +88 -0
  84. package/context-push.mjs +51 -0
  85. package/context.mjs +8 -187
  86. package/conventions.mjs +110 -161
  87. package/declaration.mjs +194 -0
  88. package/embedding.mjs +3 -1
  89. package/env-ref.mjs +81 -0
  90. package/explore-distill.mjs +2 -3
  91. package/file-links.mjs +53 -0
  92. package/file-refs.mjs +103 -0
  93. package/generate-title.mjs +28 -4
  94. package/history-window.mjs +17 -2
  95. package/i18n.mjs +2 -0
  96. package/index-discover.mjs +7 -7
  97. package/ledger-cmd.mjs +34 -126
  98. package/ledger-db.mjs +10 -2
  99. package/ledger-tools.mjs +188 -0
  100. package/ledger.mjs +9 -4
  101. package/log.mjs +12 -7
  102. package/manifest-discovery.mjs +131 -0
  103. package/manifest-schema.mjs +166 -0
  104. package/manifest.mjs +18 -217
  105. package/mcp.mjs +4 -1
  106. package/memory/code-search.mjs +149 -0
  107. package/memory/code-sync.mjs +89 -230
  108. package/memory/core.mjs +47 -32
  109. package/memory/docs.mjs +63 -262
  110. package/memory/file-list.mjs +89 -0
  111. package/memory/file-walk.mjs +7 -4
  112. package/memory/memory-tool.mjs +229 -0
  113. package/memory/schema.mjs +18 -3
  114. package/memory/sweep.mjs +289 -0
  115. package/memory/sync-tail.mjs +81 -0
  116. package/memory-status.mjs +69 -0
  117. package/memory.mjs +3 -0
  118. package/model-specs.mjs +16 -7
  119. package/notify-policy.mjs +51 -0
  120. package/package.json +1 -1
  121. package/peer-claims.mjs +2 -2
  122. package/peer-domains.mjs +7 -11
  123. package/peer-instances.mjs +9 -10
  124. package/process-probe-exec.mjs +187 -0
  125. package/process-probe.mjs +27 -175
  126. package/prompt-overlays.mjs +11 -10
  127. package/prompts/advisor-design.md +2 -2
  128. package/prompts/advisor-round2.md +1 -1
  129. package/prompts/advisor-round3.md +1 -1
  130. package/prompts/common.md +16 -8
  131. package/prompts/discipline-engineering.md +57 -16
  132. package/prompts/discipline-normal.md +6 -3
  133. package/prompts/persona-coder.md +0 -1
  134. package/prompts/persona-eng-coder.md +1 -5
  135. package/prompts/persona-eng-designer.md +2 -3
  136. package/prompts/persona-engineering.md +32 -11
  137. package/prompts/persona-explore.md +0 -1
  138. package/prompts/persona-normal.md +1 -1
  139. package/prompts/persona-plan.md +3 -4
  140. package/prompts/subagent-base.md +14 -0
  141. package/provider/core.mjs +11 -48
  142. package/provider/list-models.mjs +3 -0
  143. package/provider/responses-request.mjs +237 -0
  144. package/provider/responses.mjs +6 -228
  145. package/provider-flows.mjs +248 -0
  146. package/queued.mjs +90 -0
  147. package/rules.mjs +209 -2
  148. package/session-guard.mjs +27 -7
  149. package/session-lifecycle.mjs +90 -18
  150. package/session-rename.mjs +5 -1
  151. package/session-slot-claims.mjs +230 -0
  152. package/session-slot-scan.mjs +299 -0
  153. package/session-slot-verify.mjs +304 -0
  154. package/session-slot-write.mjs +74 -8
  155. package/session-slots-manifest.mjs +163 -241
  156. package/session-slots.mjs +99 -71
  157. package/session.mjs +30 -10
  158. package/shell-candidates.mjs +78 -0
  159. package/text-budget.mjs +2 -2
  160. package/think-off.mjs +49 -0
  161. package/token-ttl.mjs +15 -4
  162. package/tool-args.mjs +73 -0
  163. package/tool-docs/advisor.md +9 -0
  164. package/tool-docs/batch.md +1 -0
  165. package/tool-docs/code_search.md +1 -0
  166. package/tool-docs/consult_start.md +5 -0
  167. package/tool-docs/consult_stop.md +3 -0
  168. package/tool-docs/context.md +7 -0
  169. package/tool-docs/doc_search.md +1 -0
  170. package/tool-docs/edit.md +2 -13
  171. package/tool-docs/eng.md +1 -0
  172. package/tool-docs/execute.md +1 -9
  173. package/tool-docs/git.md +25 -51
  174. package/tool-docs/goal.md +6 -0
  175. package/tool-docs/ledger_add.md +1 -0
  176. package/tool-docs/ledger_close.md +1 -0
  177. package/tool-docs/ledger_count.md +1 -0
  178. package/tool-docs/ledger_query.md +1 -0
  179. package/tool-docs/ledger_update.md +1 -0
  180. package/tool-docs/main_history.md +1 -0
  181. package/tool-docs/memory.md +10 -0
  182. package/tool-docs/notify_parent.md +9 -0
  183. package/tool-docs/peer_instances.md +1 -0
  184. package/tool-docs/plan.md +5 -0
  185. package/tool-docs/process.md +7 -3
  186. package/tool-docs/read_history.md +7 -0
  187. package/tool-docs/recent_changes.md +1 -0
  188. package/tool-docs/repo_outline.md +1 -0
  189. package/tool-docs/settings.md +6 -0
  190. package/tool-docs/skill.md +5 -0
  191. package/tool-docs/subagent.md +31 -0
  192. package/tool-docs/task.md +7 -0
  193. package/tool-docs/timer.md +6 -0
  194. package/tool-docs/verify.md +1 -0
  195. package/tool-docs/wait_for.md +2 -1
  196. package/tools/bash.mjs +28 -4
  197. package/tools/edit-batch.mjs +4 -4
  198. package/tools/edit-diff.mjs +4 -23
  199. package/tools/exec-run.mjs +102 -4
  200. package/tools/execute.mjs +5 -2
  201. package/tools/file.mjs +4 -9
  202. package/tools/git-run.mjs +1 -1
  203. package/tools/git.mjs +0 -24
  204. package/tools/index.mjs +2 -1
  205. package/tools/linter.mjs +2 -1
  206. package/tools/node-child.mjs +21 -0
  207. package/tools/ops.mjs +50 -13
  208. package/tools/patch.mjs +4 -2
  209. package/tools/repomap.mjs +20 -9
  210. package/tools/shared.mjs +8 -6
  211. package/tools/web.mjs +5 -2
  212. package/tools/write-path.mjs +1 -1
  213. package/traces/trace-store.mjs +17 -14
  214. package/vision-reader.mjs +76 -0
  215. package/agent-tools/batch-segment.mjs +0 -18
@@ -0,0 +1,237 @@
1
+ /**
2
+ * provider/responses-request.mjs — Responses **请求构造面**出档(2026-09-29 structure-split-2 · 台账 #620):自
3
+ * `provider/responses.mjs` 全量迁出〔原 `:22-245`〕——host 白名单判定(`isStatefulHost` ∕ `isStoreRequiredHost` ∕
4
+ * `isNonStatefulHost`)· `chainKey` · `builtinToolsFor` · `toItems` / `toTools` / `normalizeUsage` · `buildBody`;
5
+ * **结构拆分零语义**(面不变 ∕ 判据不变,只换宿主档;切点 ∕ 缝单源 = 批档 `docs/batches/2026-09-29-structure-split-2.md` §2.2-B)。
6
+ *
7
+ * 缝 = 同名再出口:宿主 `provider/responses.mjs` 转口 `buildBody` / `isStoreRequiredHost` / `builtinToolsFor` 三名
8
+ * 既有导出(消费者 import 面零改);`normalizeUsage` 被宿主 `parseStream` 消费 ⇒ 随迁导出(宿主 import)。
9
+ * 零环:本档不引宿主(方向单行);档外依赖 = `../config.mjs`(`specForModel` ∥ `isBailianHost`)。
10
+ * 链语义(双轨 ∕ 白名单 ∕ store 必开 ∥ 链重置)详述见宿主档头;本档 = 请求侧构造 ∥ 链状态机决策。
11
+ */
12
+ import { specForModel, isBailianHost } from "../config.mjs"
13
+
14
+ /** 白名单:已实证 previous_response_id 的官方端(2026-08-31 真机验证:
15
+ * 百炼 store:true 全链路 ✅;GLM(open.bigmodel.cn/api/v1)store:true 全链路 ✅)。 */
16
+ function isStatefulHost(baseURL) {
17
+ try {
18
+ const host = new URL(baseURL).hostname
19
+ return /(^|\.)openai\.com$/.test(host) || isBailianHost(baseURL) || /(^|\.)bigmodel\.cn$/.test(host)
20
+ } catch {
21
+ return false
22
+ }
23
+ }
24
+
25
+ /** store 必开 host(链保留依赖 store:true——真机:百炼 store:false → 链 400;GLM 同)。
26
+ * OpenAI 官方 store:false 链仍可用,不在内。 */
27
+ export function isStoreRequiredHost(baseURL) {
28
+ try {
29
+ const host = new URL(baseURL).hostname
30
+ return isBailianHost(baseURL) || /(^|\.)bigmodel\.cn$/.test(host)
31
+ } catch {
32
+ return false
33
+ }
34
+ }
35
+
36
+ /** 灰名单:格式完整但链未证实/不支持——显式全量 + 一次性 warning(不靠服务端报错)。
37
+ * 2026-08-31 真机后仅剩 DeepSeek(官方明确 previous_response_id 不支持且参数静默忽略)。 */
38
+ function isNonStatefulHost(baseURL) {
39
+ try {
40
+ const host = new URL(baseURL).hostname
41
+ return /(^|\.)deepseek\.com$/.test(host)
42
+ } catch {
43
+ return false
44
+ }
45
+ }
46
+
47
+ /** 链 key:system 部分 + 最后一条 user 消息(turn 内不变、跨 turn 变、压缩后变)。 */
48
+ function chainKey(messages) {
49
+ let sig = "s:"
50
+ let lastUser = ""
51
+ for (const m of messages ?? []) {
52
+ if (m.role === "system") sig += (typeof m.content === "string" ? m.content : "") + "\u0001"
53
+ else if (m.role === "user") lastUser = typeof m.content === "string" ? m.content : ""
54
+ }
55
+ return sig + "\u0002u:" + lastUser
56
+ }
57
+
58
+ /** 内置工具声明(2026-08-31 用户拍板"内置工具还是要用",一期 web_search)。
59
+ * 按 host 映射默认集;provider.builtinTools === false 关闭、数组显式覆盖。
60
+ * 注意:内置工具由**服务端执行**——绕过我们的工具权限门/审计(产品决策,用户拍板)。 */
61
+ export function builtinToolsFor(baseURL, providerBuiltin) {
62
+ if (providerBuiltin === false) return []
63
+ if (Array.isArray(providerBuiltin)) return providerBuiltin
64
+ try {
65
+ const host = new URL(baseURL).hostname
66
+ if (/(^|\.)openai\.com$/.test(host) || isBailianHost(baseURL) || /(^|\.)deepseek\.com$/.test(host)) {
67
+ return [{ type: "web_search" }]
68
+ }
69
+ } catch { /* fallthrough */ }
70
+ return []
71
+ }
72
+
73
+ /** OpenAI Chat 消息 → Responses input items。system 提升为 instructions(不进 input)。
74
+ * 内置工具结果(web_search_call 本地化 tool 消息)→ 原样 web_search_call item 回传。 */
75
+ function toItems(messages, { instructions } = {}) {
76
+ const items = []
77
+ for (const m of messages ?? []) {
78
+ if (m.role === "system") continue
79
+ if (typeof m.tool_call_id === "string" && m.tool_call_id.startsWith("web_search_call_") && typeof m.content === "string") {
80
+ // 内置工具结果本地化消息 → 原样回传(DeepSeek 官方:web_search_call 原样回传即可,
81
+ // 服务端自动恢复搜索结果)。id 用 content 里的原始服务端 id(msg_xxx),前缀只是本地锚点。
82
+ let query = ""
83
+ let srcs = []
84
+ let wsId = m.tool_call_id.slice("web_search_call_".length)
85
+ try {
86
+ const parsed = JSON.parse(m.content)
87
+ query = parsed.query ?? ""
88
+ srcs = parsed.sources ?? []
89
+ if (parsed.id) wsId = parsed.id
90
+ } catch { /* content 非 JSON(纯展示)→ query 缺省 */ }
91
+ items.push({ type: "web_search_call", id: wsId, status: "completed", action: { query, type: "search", sources: srcs } })
92
+ continue
93
+ }
94
+ if (m.role === "user") {
95
+ const content = m.content
96
+ if (Array.isArray(content)) {
97
+ items.push({
98
+ role: "user",
99
+ content: content
100
+ .map((p) => (p?.type === "image_url"
101
+ ? { type: "input_image", image_url: p.image_url?.url }
102
+ : p?.type === "text" || typeof p === "string"
103
+ ? { type: "input_text", text: typeof p === "string" ? p : p.text }
104
+ : null))
105
+ .filter(Boolean),
106
+ })
107
+ } else {
108
+ items.push({ role: "user", content: [{ type: "input_text", text: String(content ?? "") }] })
109
+ }
110
+ } else if (m.role === "assistant") {
111
+ const tcList = m.tool_calls ?? []
112
+ items.push({ role: "assistant", content: [{ type: "output_text", text: String(m.content ?? "") }] })
113
+ for (const tc of tcList) {
114
+ items.push({
115
+ type: "function_call",
116
+ call_id: tc.id ?? "",
117
+ name: tc.function?.name ?? "",
118
+ arguments: tc.function?.arguments ?? "{}",
119
+ })
120
+ }
121
+ } else if (m.role === "tool" || m.role === "function") {
122
+ items.push({
123
+ type: "function_call_output",
124
+ call_id: m.tool_call_id ?? m.name ?? "",
125
+ output: typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""),
126
+ })
127
+ }
128
+ }
129
+ return { items, instructions: instructions ?? (messages ?? []).find((m) => m.role === "system")?.content ?? "" }
130
+ }
131
+
132
+ /** OpenAI 工具 schema → Responses 扁平 tools。 */
133
+ function toTools(tools) {
134
+ return (tools ?? []).map((t) => ({
135
+ type: "function",
136
+ name: t.function?.name ?? t.name,
137
+ description: t.function?.description ?? t.description,
138
+ parameters: t.function?.parameters ?? t.parameters ?? { type: "object", properties: {} },
139
+ }))
140
+ }
141
+
142
+ /** Responses usage → 内部 cache 字段形状。 */
143
+ export function normalizeUsage(usage) {
144
+ if (!usage) return null
145
+ return {
146
+ prompt_tokens: usage.input_tokens ?? 0,
147
+ completion_tokens: usage.output_tokens ?? 0,
148
+ total_tokens: usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0),
149
+ prompt_cache_hit_tokens: usage.input_tokens_details?.cached_tokens ?? 0,
150
+ prompt_cache_miss_tokens: Math.max(0, (usage.input_tokens ?? 0) - (usage.input_tokens_details?.cached_tokens ?? 0)),
151
+ }
152
+ }
153
+
154
+ /**
155
+ * 组装请求体 + 链状态机决策。
156
+ * @returns {{ body, previousResponseId, warnings, newChain: {id,key}|null }}
157
+ */
158
+ export function buildBody(provider, messages, tools, opts = {}) {
159
+ const { instructions: extracted, items } = toItems(messages)
160
+ const toolsFlat = tools?.length ? toTools(tools) : undefined
161
+ const spec = specForModel(provider.model)
162
+ const warnings = []
163
+ const wantStateful = provider.stateful !== false && opts.stateful !== false
164
+ const hostStateful = isStatefulHost(provider.baseURL)
165
+ const hostNonStateful = isNonStatefulHost(provider.baseURL)
166
+
167
+ let chain = provider._responsesChain ?? null
168
+ const key = chainKey(messages)
169
+
170
+ if (!wantStateful) {
171
+ chain = null // stateful:false 显式覆盖:残留链(同 session 开过)必须作废
172
+ } else if (hostNonStateful && !opts.forceStateful) {
173
+ // 灰名单:链不支持/未证实(DeepSeek 静默忽略 → 无声丢上下文)——显式全量 + 一次警告
174
+ if (wantStateful) {
175
+ warnings.push({ name: "responses-stateful-unsupported", message: "endpoint 未实证支持 previous_response_id;已发送全量上下文(可 provider.stateful=false 关闭此消息)" })
176
+ }
177
+ chain = null
178
+ } else if (chain && chain.key !== key) {
179
+ chain = null // 跨 turn/压缩/换模型:链失效,全量重建
180
+ } else if (chain && !hostStateful && !opts.forceStateful) {
181
+ chain = null // 非白名单 host 且无显式 forceStateful:不冒险开链
182
+ }
183
+ // 2026-08-31 真机冒烟:百炼/GLM 开链 = 云端留存 7 天——首次知情警告(不刷屏)
184
+ if (wantStateful && hostStateful && isStoreRequiredHost(provider.baseURL) && !provider._responsesStoreWarned) {
185
+ provider._responsesStoreWarned = true
186
+ warnings.push({ name: "responses-store-retention", message: "链生效需要 store:true——对话将在云端留存 7 天(provider.stateful=false 可退出)" })
187
+ }
188
+
189
+ const body = {
190
+ model: provider.model,
191
+ input: items, // 占位:chain 有效时下方替换为增量
192
+ stream: true,
193
+ // 2026-08-31 真机冒烟实锤:百炼/GLM 链要求 R1 store:true(store:false → 链 400
194
+ // Not found);OpenAI 官方 store:false 链仍可用。开链时 = 对话在云端留存 7 天
195
+ // (警告上报);灰名单全量 store:false。
196
+ store: wantStateful && hostStateful && isStoreRequiredHost(provider.baseURL),
197
+ ...(extracted ? { instructions: extracted } : {}),
198
+ ...(toolsFlat ? { tools: toolsFlat } : {}),
199
+ ...(spec.maxOutput || provider.maxTokens ? { max_output_tokens: provider.maxTokens ?? spec.maxOutput } : {}),
200
+ }
201
+ // 内置工具声明追加(2026-08-31 用户拍板):web_search 与本地 function 工具共存
202
+ const builtin = builtinToolsFor(provider.baseURL, provider.builtinTools)
203
+ if (builtin.length) body.tools = [...(toolsFlat ?? []), ...builtin]
204
+ if (provider.temperature != null) {
205
+ let t = provider.temperature
206
+ if (spec.tempRange) {
207
+ t = Math.min(spec.tempRange[1], Math.max(spec.tempRange[0], t))
208
+ t = Math.round(t * 100) / 100 // 与 core.mjs 同语义(round3 #7)
209
+ }
210
+ body.temperature = t
211
+ }
212
+ if (provider.reasoningEffort) body.reasoning = { effort: provider.reasoningEffort }
213
+ if (opts.toolChoice !== undefined) body.tool_choice = opts.toolChoice
214
+
215
+ // 链模式:turn 内增量 = 上一链轮未发送的 function_call_output(工具结果)。
216
+ // 注意:assistant 的 function_call item 与服务端链输出重复(服务端自动含上轮 output),
217
+ // 增量只发工具结果即可;新 user 消息/压缩/换模型已由 chainKey 挡掉 → 全量。
218
+ const outputs = items.filter((i) => i.type === "function_call_output")
219
+ let previousResponseId = null
220
+ if (chain && chain.id) {
221
+ const newOutputs = outputs.slice(chain.outputSent ?? 0)
222
+ if (newOutputs.length === 0) {
223
+ chain = null // 无新增(重复调用/异常重试):退化为全量,正确性优先
224
+ } else {
225
+ body.input = newOutputs
226
+ previousResponseId = chain.id
227
+ }
228
+ } else {
229
+ body.input = items
230
+ }
231
+
232
+ const newChain = chain
233
+ ? { ...chain, key, outputSent: outputs.length }
234
+ : (wantStateful && (hostStateful || opts.forceStateful) ? { id: null, key, outputSent: outputs.length } : null)
235
+
236
+ return { body, previousResponseId, warnings, newChain }
237
+ }
@@ -11,239 +11,17 @@
11
11
  * 事件流规范:流以 response.completed / response.incomplete / response.failed 结束,
12
12
  * 没有 "data: [DONE]"。
13
13
  */
14
- import { specForModel, isBailianHost } from "../config.mjs"
15
14
  import { effectiveFetchTimeoutMs } from "./core.mjs"
16
15
  import { proxyFetch } from "../proxy.mjs"
17
16
  import { requestWithRetry } from "./retry.mjs"
18
17
  import { rateGate, recordRate, estimateRequestTokens } from "./rate.mjs"
18
+ import { buildBody, normalizeUsage } from "./responses-request.mjs"
19
+ // 同名 re-export(2026-09-29 structure-split-2 · 台账 #620——缝 = 同名再出口):请求构造面出档 `provider/responses-request.mjs`;
20
+ // `buildBody` / `isStoreRequiredHost` / `builtinToolsFor` 三名既有导出经本档转口——消费者 import 面零改。
21
+ export { buildBody, isStoreRequiredHost, builtinToolsFor } from "./responses-request.mjs"
19
22
 
20
23
  // 2026-09-01:FETCH_TIMEOUT_MS 常量退役(绝对墙钟废除)——经 core.mjs effectiveFetchTimeoutMs 共用
21
24
 
22
- /** 白名单:已实证 previous_response_id 的官方端(2026-08-31 真机验证:
23
- * 百炼 store:true 全链路 ✅;GLM(open.bigmodel.cn/api/v1)store:true 全链路 ✅)。 */
24
- function isStatefulHost(baseURL) {
25
- try {
26
- const host = new URL(baseURL).hostname
27
- return /(^|\.)openai\.com$/.test(host) || isBailianHost(baseURL) || /(^|\.)bigmodel\.cn$/.test(host)
28
- } catch {
29
- return false
30
- }
31
- }
32
-
33
- /** store 必开 host(链保留依赖 store:true——真机:百炼 store:false → 链 400;GLM 同)。
34
- * OpenAI 官方 store:false 链仍可用,不在内。 */
35
- export function isStoreRequiredHost(baseURL) {
36
- try {
37
- const host = new URL(baseURL).hostname
38
- return isBailianHost(baseURL) || /(^|\.)bigmodel\.cn$/.test(host)
39
- } catch {
40
- return false
41
- }
42
- }
43
-
44
- /** 灰名单:格式完整但链未证实/不支持——显式全量 + 一次性 warning(不靠服务端报错)。
45
- * 2026-08-31 真机后仅剩 DeepSeek(官方明确 previous_response_id 不支持且参数静默忽略)。 */
46
- function isNonStatefulHost(baseURL) {
47
- try {
48
- const host = new URL(baseURL).hostname
49
- return /(^|\.)deepseek\.com$/.test(host)
50
- } catch {
51
- return false
52
- }
53
- }
54
-
55
- /** 链 key:system 部分 + 最后一条 user 消息(turn 内不变、跨 turn 变、压缩后变)。 */
56
- function chainKey(messages) {
57
- let sig = "s:"
58
- let lastUser = ""
59
- for (const m of messages ?? []) {
60
- if (m.role === "system") sig += (typeof m.content === "string" ? m.content : "") + "\u0001"
61
- else if (m.role === "user") lastUser = typeof m.content === "string" ? m.content : ""
62
- }
63
- return sig + "\u0002u:" + lastUser
64
- }
65
-
66
- /** 内置工具声明(2026-08-31 用户拍板"内置工具还是要用",一期 web_search)。
67
- * 按 host 映射默认集;provider.builtinTools === false 关闭、数组显式覆盖。
68
- * 注意:内置工具由**服务端执行**——绕过我们的工具权限门/审计(产品决策,用户拍板)。 */
69
- export function builtinToolsFor(baseURL, providerBuiltin) {
70
- if (providerBuiltin === false) return []
71
- if (Array.isArray(providerBuiltin)) return providerBuiltin
72
- try {
73
- const host = new URL(baseURL).hostname
74
- if (/(^|\.)openai\.com$/.test(host) || isBailianHost(baseURL) || /(^|\.)deepseek\.com$/.test(host)) {
75
- return [{ type: "web_search" }]
76
- }
77
- } catch { /* fallthrough */ }
78
- return []
79
- }
80
-
81
- /** OpenAI Chat 消息 → Responses input items。system 提升为 instructions(不进 input)。
82
- * 内置工具结果(web_search_call 本地化 tool 消息)→ 原样 web_search_call item 回传。 */
83
- function toItems(messages, { instructions } = {}) {
84
- const items = []
85
- for (const m of messages ?? []) {
86
- if (m.role === "system") continue
87
- if (typeof m.tool_call_id === "string" && m.tool_call_id.startsWith("web_search_call_") && typeof m.content === "string") {
88
- // 内置工具结果本地化消息 → 原样回传(DeepSeek 官方:web_search_call 原样回传即可,
89
- // 服务端自动恢复搜索结果)。id 用 content 里的原始服务端 id(msg_xxx),前缀只是本地锚点。
90
- let query = ""
91
- let srcs = []
92
- let wsId = m.tool_call_id.slice("web_search_call_".length)
93
- try {
94
- const parsed = JSON.parse(m.content)
95
- query = parsed.query ?? ""
96
- srcs = parsed.sources ?? []
97
- if (parsed.id) wsId = parsed.id
98
- } catch { /* content 非 JSON(纯展示)→ query 缺省 */ }
99
- items.push({ type: "web_search_call", id: wsId, status: "completed", action: { query, type: "search", sources: srcs } })
100
- continue
101
- }
102
- if (m.role === "user") {
103
- const content = m.content
104
- if (Array.isArray(content)) {
105
- items.push({
106
- role: "user",
107
- content: content
108
- .map((p) => (p?.type === "image_url"
109
- ? { type: "input_image", image_url: p.image_url?.url }
110
- : p?.type === "text" || typeof p === "string"
111
- ? { type: "input_text", text: typeof p === "string" ? p : p.text }
112
- : null))
113
- .filter(Boolean),
114
- })
115
- } else {
116
- items.push({ role: "user", content: [{ type: "input_text", text: String(content ?? "") }] })
117
- }
118
- } else if (m.role === "assistant") {
119
- const tcList = m.tool_calls ?? []
120
- items.push({ role: "assistant", content: [{ type: "output_text", text: String(m.content ?? "") }] })
121
- for (const tc of tcList) {
122
- items.push({
123
- type: "function_call",
124
- call_id: tc.id ?? "",
125
- name: tc.function?.name ?? "",
126
- arguments: tc.function?.arguments ?? "{}",
127
- })
128
- }
129
- } else if (m.role === "tool" || m.role === "function") {
130
- items.push({
131
- type: "function_call_output",
132
- call_id: m.tool_call_id ?? m.name ?? "",
133
- output: typeof m.content === "string" ? m.content : JSON.stringify(m.content ?? ""),
134
- })
135
- }
136
- }
137
- return { items, instructions: instructions ?? (messages ?? []).find((m) => m.role === "system")?.content ?? "" }
138
- }
139
-
140
- /** OpenAI 工具 schema → Responses 扁平 tools。 */
141
- function toTools(tools) {
142
- return (tools ?? []).map((t) => ({
143
- type: "function",
144
- name: t.function?.name ?? t.name,
145
- description: t.function?.description ?? t.description,
146
- parameters: t.function?.parameters ?? t.parameters ?? { type: "object", properties: {} },
147
- }))
148
- }
149
-
150
- /** Responses usage → 内部 cache 字段形状。 */
151
- function normalizeUsage(usage) {
152
- if (!usage) return null
153
- return {
154
- prompt_tokens: usage.input_tokens ?? 0,
155
- completion_tokens: usage.output_tokens ?? 0,
156
- total_tokens: usage.total_tokens ?? (usage.input_tokens ?? 0) + (usage.output_tokens ?? 0),
157
- prompt_cache_hit_tokens: usage.input_tokens_details?.cached_tokens ?? 0,
158
- prompt_cache_miss_tokens: Math.max(0, (usage.input_tokens ?? 0) - (usage.input_tokens_details?.cached_tokens ?? 0)),
159
- }
160
- }
161
-
162
- /**
163
- * 组装请求体 + 链状态机决策。
164
- * @returns {{ body, previousResponseId, warnings, newChain: {id,key}|null }}
165
- */
166
- export function buildBody(provider, messages, tools, opts = {}) {
167
- const { instructions: extracted, items } = toItems(messages)
168
- const toolsFlat = tools?.length ? toTools(tools) : undefined
169
- const spec = specForModel(provider.model)
170
- const warnings = []
171
- const wantStateful = provider.stateful !== false && opts.stateful !== false
172
- const hostStateful = isStatefulHost(provider.baseURL)
173
- const hostNonStateful = isNonStatefulHost(provider.baseURL)
174
-
175
- let chain = provider._responsesChain ?? null
176
- const key = chainKey(messages)
177
-
178
- if (!wantStateful) {
179
- chain = null // stateful:false 显式覆盖:残留链(同 session 开过)必须作废
180
- } else if (hostNonStateful && !opts.forceStateful) {
181
- // 灰名单:链不支持/未证实(DeepSeek 静默忽略 → 无声丢上下文)——显式全量 + 一次警告
182
- if (wantStateful) {
183
- warnings.push({ name: "responses-stateful-unsupported", message: "endpoint 未实证支持 previous_response_id;已发送全量上下文(可 provider.stateful=false 关闭此消息)" })
184
- }
185
- chain = null
186
- } else if (chain && chain.key !== key) {
187
- chain = null // 跨 turn/压缩/换模型:链失效,全量重建
188
- } else if (chain && !hostStateful && !opts.forceStateful) {
189
- chain = null // 非白名单 host 且无显式 forceStateful:不冒险开链
190
- }
191
- // 2026-08-31 真机冒烟:百炼/GLM 开链 = 云端留存 7 天——首次知情警告(不刷屏)
192
- if (wantStateful && hostStateful && isStoreRequiredHost(provider.baseURL) && !provider._responsesStoreWarned) {
193
- provider._responsesStoreWarned = true
194
- warnings.push({ name: "responses-store-retention", message: "链生效需要 store:true——对话将在云端留存 7 天(provider.stateful=false 可退出)" })
195
- }
196
-
197
- const body = {
198
- model: provider.model,
199
- input: items, // 占位:chain 有效时下方替换为增量
200
- stream: true,
201
- // 2026-08-31 真机冒烟实锤:百炼/GLM 链要求 R1 store:true(store:false → 链 400
202
- // Not found);OpenAI 官方 store:false 链仍可用。开链时 = 对话在云端留存 7 天
203
- // (警告上报);灰名单全量 store:false。
204
- store: wantStateful && hostStateful && isStoreRequiredHost(provider.baseURL),
205
- ...(extracted ? { instructions: extracted } : {}),
206
- ...(toolsFlat ? { tools: toolsFlat } : {}),
207
- ...(spec.maxOutput || provider.maxTokens ? { max_output_tokens: provider.maxTokens ?? spec.maxOutput } : {}),
208
- }
209
- // 内置工具声明追加(2026-08-31 用户拍板):web_search 与本地 function 工具共存
210
- const builtin = builtinToolsFor(provider.baseURL, provider.builtinTools)
211
- if (builtin.length) body.tools = [...(toolsFlat ?? []), ...builtin]
212
- if (provider.temperature != null) {
213
- let t = provider.temperature
214
- if (spec.tempRange) {
215
- t = Math.min(spec.tempRange[1], Math.max(spec.tempRange[0], t))
216
- t = Math.round(t * 100) / 100 // 与 core.mjs 同语义(round3 #7)
217
- }
218
- body.temperature = t
219
- }
220
- if (provider.reasoningEffort) body.reasoning = { effort: provider.reasoningEffort }
221
- if (opts.toolChoice !== undefined) body.tool_choice = opts.toolChoice
222
-
223
- // 链模式:turn 内增量 = 上一链轮未发送的 function_call_output(工具结果)。
224
- // 注意:assistant 的 function_call item 与服务端链输出重复(服务端自动含上轮 output),
225
- // 增量只发工具结果即可;新 user 消息/压缩/换模型已由 chainKey 挡掉 → 全量。
226
- const outputs = items.filter((i) => i.type === "function_call_output")
227
- let previousResponseId = null
228
- if (chain && chain.id) {
229
- const newOutputs = outputs.slice(chain.outputSent ?? 0)
230
- if (newOutputs.length === 0) {
231
- chain = null // 无新增(重复调用/异常重试):退化为全量,正确性优先
232
- } else {
233
- body.input = newOutputs
234
- previousResponseId = chain.id
235
- }
236
- } else {
237
- body.input = items
238
- }
239
-
240
- const newChain = chain
241
- ? { ...chain, key, outputSent: outputs.length }
242
- : (wantStateful && (hostStateful || opts.forceStateful) ? { id: null, key, outputSent: outputs.length } : null)
243
-
244
- return { body, previousResponseId, warnings, newChain }
245
- }
246
-
247
25
  /** 链失效回退:404/无效 id → 返回 null 表示"应全量重发";其他失败抛错。 */
248
26
  export function isChainInvalidError(status) {
249
27
  return status === 404 || status === 400
@@ -292,7 +70,7 @@ export async function parseStream(response, { onToken, onReasoning, signal }) {
292
70
  onCompleted: seal,
293
71
  onIncomplete: (resp) => {
294
72
  seal(resp)
295
- // 非长度原因(content_filter 等)不能报成 "length"——agent 层 322 行按原因给用户提示
73
+ // 非长度原因(content_filter 等)不能报成 "length"——agent 层按原因给用户提示(`agent/run-stages.mjs:43`)
296
74
  result.finishReason = resp?.incomplete_details?.reason === "content_filter" ? "content_filter" : "length"
297
75
  },
298
76
  onFailed: (resp) => {
@@ -442,7 +220,7 @@ export async function chat(provider, { messages, tools, onToken, onReasoning, on
442
220
  } catch (chainErr) {
443
221
  if (!isChainInvalidError(chainErr?.status)) throw chainErr
444
222
  // 链失效(404/400):先清残留链再重建——D6 语义是真·全量重发(body.input=items)。
445
- // 不清链会走 buildBody 217-224 的增量分支:body.input = 裸 function_call_output 且
223
+ // 不清链会走 buildBody 的增量分支(`responses-request.mjs:220-227`):body.input = 裸 function_call_output 且
446
224
  // previousResponseId 未随 fullBody 带走 → 服务端 call_id 无归属 → 二次 400(2026-08-31 评审 #1)
447
225
  provider._responsesChain = null
448
226
  const fresh = buildBody(provider, messages, tools, { toolChoice, stateful, forceStateful: true })