@thincoder/core 0.9.4 → 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.
- package/CHANGELOG.md +63 -0
- package/README.md +1 -1
- package/abort-provenance.mjs +10 -10
- package/advisor/citations.mjs +5 -5
- package/advisor/compaction.mjs +16 -15
- package/advisor/loop.mjs +14 -14
- package/advisor/messages.mjs +6 -6
- package/advisor/project-context.mjs +5 -5
- package/advisor/repos.mjs +3 -3
- package/advisor/run.mjs +23 -13
- package/advisor.mjs +4 -4
- package/agent/assemble.mjs +111 -0
- package/agent/chat-call.mjs +68 -0
- package/agent/child-marks.mjs +2 -2
- package/agent/completion.mjs +9 -2
- package/agent/continue-decision.mjs +25 -0
- package/agent/dispatch-gates.mjs +140 -0
- package/agent/dispatch-run.mjs +167 -0
- package/agent/dispatch.mjs +55 -294
- package/agent/family-tools.mjs +11 -11
- package/agent/helpers.mjs +63 -3
- package/agent/live-beat.mjs +49 -0
- package/agent/post-turn.mjs +4 -8
- package/agent/record-results.mjs +23 -20
- package/agent/relay-prefix.mjs +2 -2
- package/agent/run-stages.mjs +19 -15
- package/agent/run-start.mjs +111 -0
- package/agent/setup-reminders.mjs +9 -9
- package/agent/setup.mjs +32 -4
- package/agent/spawn-child.mjs +9 -9
- package/agent/suspension.mjs +90 -30
- package/agent/timers.mjs +113 -0
- package/agent/turn-loop.mjs +244 -0
- package/agent/write-gate.mjs +103 -6
- package/agent-tools/advisor-async.mjs +11 -11
- package/agent-tools/advisor-settle.mjs +17 -17
- package/agent-tools/advisor.mjs +38 -43
- package/agent-tools/async-discard.mjs +44 -1
- package/agent-tools/async-settle.mjs +23 -20
- package/agent-tools/audit-block.mjs +7 -7
- package/agent-tools/bash-async.mjs +281 -0
- package/agent-tools/batch-lifecycle.mjs +65 -34
- package/agent-tools/batch-paths.mjs +186 -0
- package/agent-tools/batch-skeleton.mjs +19 -1
- package/agent-tools/batch.mjs +22 -119
- package/agent-tools/checkpoint.mjs +121 -0
- package/agent-tools/child-permission.mjs +5 -3
- package/agent-tools/consult.mjs +58 -58
- package/agent-tools/context.mjs +4 -20
- package/agent-tools/design-token.mjs +3 -3
- package/agent-tools/digest-budget.mjs +1 -1
- package/agent-tools/eng.mjs +5 -5
- package/agent-tools/escalate-async.mjs +18 -13
- package/agent-tools/goal.mjs +3 -10
- package/agent-tools/panel-blocks.mjs +3 -3
- package/agent-tools/parent-channel.mjs +3 -13
- package/agent-tools/plan.mjs +3 -4
- package/agent-tools/read-history.mjs +12 -28
- package/agent-tools/recent-changes.mjs +4 -5
- package/agent-tools/review-facts.mjs +1 -1
- package/agent-tools/settings.mjs +47 -20
- package/agent-tools/skill.mjs +3 -5
- package/agent-tools/spawn-gates.mjs +3 -3
- package/agent-tools/subagent-actions-query.mjs +270 -0
- package/agent-tools/subagent-actions.mjs +29 -273
- package/agent-tools/subagent-async.mjs +36 -31
- package/agent-tools/subagent-panel.mjs +38 -9
- package/agent-tools/subagent-run.mjs +44 -29
- package/agent-tools/subagent-scheduler.mjs +29 -28
- package/agent-tools/subagent-spawn.mjs +57 -25
- package/agent-tools/subagent.mjs +46 -66
- package/agent-tools/task.mjs +3 -9
- package/agent-tools/timer.mjs +12 -12
- package/agent-tools/verify.mjs +14 -13
- package/agent-tools.mjs +0 -1
- package/agent.mjs +20 -351
- package/attachments.mjs +125 -0
- package/auto-think.mjs +1 -1
- package/config-presets.mjs +12 -7
- package/config-watch.mjs +83 -0
- package/config.mjs +17 -4
- package/context-degrade.mjs +81 -0
- package/context-echo.mjs +88 -0
- package/context-push.mjs +51 -0
- package/context.mjs +8 -187
- package/conventions.mjs +110 -161
- package/declaration.mjs +194 -0
- package/embedding.mjs +3 -1
- package/env-ref.mjs +81 -0
- package/explore-distill.mjs +2 -3
- package/file-links.mjs +53 -0
- package/file-refs.mjs +103 -0
- package/generate-title.mjs +28 -4
- package/git/checkpoint.mjs +4 -8
- package/history-window.mjs +17 -2
- package/i18n.mjs +2 -0
- package/index-discover.mjs +7 -7
- package/ledger-cmd.mjs +34 -126
- package/ledger-db.mjs +35 -7
- package/ledger-migrate.mjs +299 -0
- package/ledger-tools.mjs +188 -0
- package/ledger.mjs +11 -5
- package/log.mjs +12 -7
- package/manifest-discovery.mjs +131 -0
- package/manifest-schema.mjs +166 -0
- package/manifest.mjs +18 -217
- package/mcp.mjs +4 -1
- package/memory/code-search.mjs +149 -0
- package/memory/code-sync.mjs +89 -230
- package/memory/core.mjs +47 -32
- package/memory/docs.mjs +63 -262
- package/memory/file-list.mjs +89 -0
- package/memory/file-walk.mjs +7 -4
- package/memory/memory-tool.mjs +229 -0
- package/memory/schema.mjs +18 -3
- package/memory/sweep.mjs +289 -0
- package/memory/sync-tail.mjs +81 -0
- package/memory-status.mjs +69 -0
- package/memory.mjs +3 -0
- package/model-specs.mjs +131 -66
- package/notify-policy.mjs +51 -0
- package/package.json +1 -1
- package/peer-claims.mjs +263 -0
- package/peer-domains.mjs +100 -71
- package/peer-instances.mjs +9 -10
- package/process-probe-exec.mjs +187 -0
- package/process-probe.mjs +27 -175
- package/prompt-overlays.mjs +11 -10
- package/prompts/advisor-design.md +2 -2
- package/prompts/advisor-round2.md +1 -1
- package/prompts/advisor-round3.md +1 -1
- package/prompts/common.md +19 -7
- package/prompts/discipline-engineering.md +57 -16
- package/prompts/discipline-normal.md +6 -3
- package/prompts/persona-coder.md +0 -1
- package/prompts/persona-eng-coder.md +1 -5
- package/prompts/persona-eng-designer.md +1 -2
- package/prompts/persona-engineering.md +41 -11
- package/prompts/persona-explore.md +0 -1
- package/prompts/persona-normal.md +10 -1
- package/prompts/persona-plan.md +3 -4
- package/prompts/subagent-base.md +14 -0
- package/provider/core.mjs +11 -48
- package/provider/list-models.mjs +3 -0
- package/provider/responses-request.mjs +237 -0
- package/provider/responses.mjs +6 -228
- package/provider-flows.mjs +248 -0
- package/queued.mjs +90 -0
- package/rules.mjs +209 -2
- package/session-gc.mjs +67 -6
- package/session-guard.mjs +27 -7
- package/session-lifecycle.mjs +90 -18
- package/session-rename.mjs +5 -1
- package/session-slot-claims.mjs +230 -0
- package/session-slot-scan.mjs +299 -0
- package/session-slot-verify.mjs +304 -0
- package/session-slot-write.mjs +74 -8
- package/session-slots-manifest.mjs +163 -236
- package/session-slots.mjs +99 -71
- package/session.mjs +30 -10
- package/shell-candidates.mjs +78 -0
- package/text-budget.mjs +2 -2
- package/think-off.mjs +49 -0
- package/token-ttl.mjs +15 -4
- package/tool-args.mjs +73 -0
- package/tool-docs/advisor.md +9 -0
- package/tool-docs/batch.md +1 -0
- package/tool-docs/code_search.md +1 -0
- package/tool-docs/consult_start.md +5 -0
- package/tool-docs/consult_stop.md +3 -0
- package/tool-docs/context.md +7 -0
- package/tool-docs/doc_search.md +1 -0
- package/tool-docs/edit.md +2 -13
- package/tool-docs/eng.md +1 -0
- package/tool-docs/execute.md +1 -9
- package/tool-docs/git.md +25 -51
- package/tool-docs/goal.md +6 -0
- package/tool-docs/ledger_add.md +1 -0
- package/tool-docs/ledger_close.md +1 -0
- package/tool-docs/ledger_count.md +1 -0
- package/tool-docs/ledger_query.md +1 -0
- package/tool-docs/ledger_update.md +1 -0
- package/tool-docs/main_history.md +1 -0
- package/tool-docs/memory.md +10 -0
- package/tool-docs/notify_parent.md +9 -0
- package/tool-docs/peer_instances.md +1 -0
- package/tool-docs/plan.md +5 -0
- package/tool-docs/process.md +7 -3
- package/tool-docs/question.md +1 -0
- package/tool-docs/read_history.md +7 -0
- package/tool-docs/recent_changes.md +1 -0
- package/tool-docs/repo_outline.md +1 -0
- package/tool-docs/settings.md +6 -0
- package/tool-docs/skill.md +5 -0
- package/tool-docs/subagent.md +31 -0
- package/tool-docs/task.md +7 -0
- package/tool-docs/timer.md +6 -0
- package/tool-docs/verify.md +1 -0
- package/tool-docs/wait_for.md +2 -1
- package/tools/bash.mjs +28 -4
- package/tools/edit-batch.mjs +10 -11
- package/tools/edit-diff.mjs +33 -25
- package/tools/exec-run.mjs +102 -4
- package/tools/execute.mjs +5 -2
- package/tools/file.mjs +11 -12
- package/tools/git-run.mjs +1 -1
- package/tools/git.mjs +0 -24
- package/tools/glob-dialect.mjs +3 -3
- package/tools/index.mjs +2 -1
- package/tools/linter.mjs +2 -1
- package/tools/node-child.mjs +21 -0
- package/tools/ops.mjs +50 -13
- package/tools/patch.mjs +4 -2
- package/tools/repomap.mjs +20 -9
- package/tools/search.mjs +2 -2
- package/tools/shared.mjs +9 -7
- package/tools/web.mjs +5 -2
- package/tools/write-path.mjs +1 -1
- package/traces/trace-store.mjs +17 -14
- package/vision-reader.mjs +76 -0
- package/agent-tools/batch-segment.mjs +0 -18
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* provider-flows.mjs — provider 加 ∕ 删 ∕ 密钥三流程 + 渠道准入探针判据(核件 —— 「桌面处理流 · VSC 对齐」
|
|
3
|
+
* 批 R8 上提产物;2026-09-28)。
|
|
4
|
+
*
|
|
5
|
+
* 上提源 = VSC `thincoder-vscode/src/extension/provider-flows.mjs`(`addProviderFlow:60` ∕
|
|
6
|
+
* `removeProviderFlow:129` ∕ `setKeyFlow:153` ∕ `probeProviderAdmission:32`)——**纯搬 + 转口,零语义改**
|
|
7
|
+
* (三流程步序 ∕ 字段校验 ∕ 拒因 ∕ 探针语义逐字);VSC 自持副本已随本批(parity-b4)迁移改指本档(双写窗口关闭——本档 = 桌面 ∕ VSC 消费单源)。
|
|
8
|
+
*
|
|
9
|
+
* 转口(宿主面 ⇒ 注入缝;本档零宿主依赖、零 `vscode` 引用):
|
|
10
|
+
* - **UI 壳**(VSC = `vscode.window.showQuickPick ∕ showInputBox ∕ showErrorMessage ∕ showWarningMessage ∕
|
|
11
|
+
* showInformationMessage`)⇒ `ui = { pick, input, error, warn, info }`;
|
|
12
|
+
* - **网络探针**(VSC = `probeChannelModels(name, probeTargetFromEntry(entry))`——目标构造随迁 = `probeTargetOf`)
|
|
13
|
+
* ⇒ `deps.probe(name, target)`(缺省 = 核真探 `probeChannelModels`;测试 ∕ 端侧可注入。**缝契约** = 同
|
|
14
|
+
* `probeChannelModels` 形:`{ ok:true, models }` ∕ `{ ok:false, error }`,**并自落账** `recordAdmission`
|
|
15
|
+
* ——未落账 ⇒ 失败分档回落 `classifyProbeFailure` 现算);
|
|
16
|
+
* - **宿主忙证据覆盖**(VSC = `loop-sampler.mjs` `overrideAdmissionIfHostBusy`,F-W19)⇒
|
|
17
|
+
* `deps.hostBusyOverride`(缺省 null = 无宿主证据 —— 核零宿主事件循环观测)。
|
|
18
|
+
* 持久化面 = 核 `config-io.mjs`(`addProviderEntry` ∕ `removeProviderEntry` ∕ `setProviderKey` ∕
|
|
19
|
+
* `resolveProviders`)——本档零副本。
|
|
20
|
+
*
|
|
21
|
+
* M9 语义定死:**准入永不阻断写** —— 探针只出读数 ∕ 失败消息(消息本体逐字长句),绝不抛出 ∕
|
|
22
|
+
* 绝不回滚已完成的保存;失败不缓存(下次配置动作重探)。
|
|
23
|
+
*
|
|
24
|
+
* 消费面:桌面 `thincoder-desktop/src/main/providers.mjs`(四通道壳——判据面:`FORMATS` ∕
|
|
25
|
+
* `customFieldsError` ∕ `probeAdmission`;端侧 reason 词法不动);三流程本体 = 桌面 ∕ VSC(已随本批迁移改指)消费,CLI 待迁。
|
|
26
|
+
*/
|
|
27
|
+
import { PROVIDER_PRESETS } from "./config-presets.mjs"
|
|
28
|
+
import { normalizeProxy } from "./config.mjs"
|
|
29
|
+
import { addProviderEntry, loadRaw, removeProviderEntry, resolveProviders, setProviderKey } from "./config-io.mjs"
|
|
30
|
+
import { admissionOf, channelUnavailableMessage, classifyProbeFailure, probeChannelModels } from "./provider/list-models.mjs"
|
|
31
|
+
|
|
32
|
+
/** 自定形协议域(闭集三协议 —— VSC 同档 `FORMATS` 逐字;端侧形判 ∕ 表单选项集同值域)。 */
|
|
33
|
+
export const FORMATS = Object.freeze(["openai", "anthropic", "google"])
|
|
34
|
+
|
|
35
|
+
/** UI 壳缝校验(缺 ∕ 形违 ⇒ 抛 —— fail-loud:宿主未接线不得静默扮成零动作)。 */
|
|
36
|
+
function requireUi(ui) {
|
|
37
|
+
if (!ui || typeof ui.pick !== "function" || typeof ui.input !== "function"
|
|
38
|
+
|| typeof ui.error !== "function" || typeof ui.warn !== "function" || typeof ui.info !== "function") {
|
|
39
|
+
throw new TypeError("provider-flows: ui seam missing — inject { pick, input, error, warn, info }")
|
|
40
|
+
}
|
|
41
|
+
return ui
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 自定形渠道名校验(VSC `addProviderFlow` 名称步 `validateInput` 逐字):空 ⇒ `Name is required`;
|
|
46
|
+
* 已配 ∕ 撞预设名 ⇒ `Name already in use`;合法 ⇒ `null`。
|
|
47
|
+
* `existing` = 已配渠道名集(VSC 同源 = `resolveProviders().providers` 名集;缺省空集)。
|
|
48
|
+
*/
|
|
49
|
+
export function providerNameError(name, existing = new Set()) {
|
|
50
|
+
const n = String(name ?? "").trim()
|
|
51
|
+
if (!n) return "Name is required"
|
|
52
|
+
if (existing.has(n) || PROVIDER_PRESETS[n]) return "Name already in use"
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 自定形必填字段校验(VSC `addProviderFlow` 自定径输入步步序 = baseURL → model → format;
|
|
58
|
+
* name 步单列见 `providerNameError`)。三拒因串 = 核持久化面**同条件串**逐字(`config-io.mjs`
|
|
59
|
+
* `addProviderEntry`)——VSC 该三步为静默中止位(无消息可搬)⇒ 取同条件核串,保「同条件同词」。
|
|
60
|
+
* `format` 缺省 = `"openai"`(沿端侧形判同式)。
|
|
61
|
+
* @returns {string|null} 首个失败字段的拒因串;全过 ⇒ `null`
|
|
62
|
+
*/
|
|
63
|
+
export function customFieldsError(fields) {
|
|
64
|
+
const baseURL = String(fields?.baseURL ?? "").trim()
|
|
65
|
+
if (!baseURL) return "Base URL is required"
|
|
66
|
+
const model = String(fields?.model ?? "").trim()
|
|
67
|
+
if (!model) return "Model is required"
|
|
68
|
+
const format = fields?.format ?? "openai"
|
|
69
|
+
if (!FORMATS.includes(format)) return `Unknown API format: ${format} (expected ${FORMATS.join("/")})`
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* M9 探针目标构造(上提源 = VSC `provider-flows.mjs:38` 的实参面 `presets.mjs` `probeTargetFromEntry`——
|
|
75
|
+
* 逐条随迁):渠道条目 ⇒ `listModels` 可消费的探针目标。代理链与 chat 请求同规则(per-provider `proxy: true`
|
|
76
|
+
* ∧ 全局 `proxy.model === true` ⇒ 探针也走该代理);apiKey 归一(trim;缺 ⇒ 空串——探针会如实失败 ⇒
|
|
77
|
+
* 渠道标「不可用」——准入判据 M8)。
|
|
78
|
+
*/
|
|
79
|
+
export function probeTargetOf(entry) {
|
|
80
|
+
const raw = loadRaw()
|
|
81
|
+
const proxyCfg = normalizeProxy(raw.proxy)
|
|
82
|
+
const proxyUri = entry?.proxy === true && proxyCfg?.uri && proxyCfg.model === true ? proxyCfg.uri : undefined
|
|
83
|
+
return {
|
|
84
|
+
name: entry?.name,
|
|
85
|
+
baseURL: entry?.baseURL,
|
|
86
|
+
apiKey: typeof entry?.apiKey === "string" ? entry.apiKey.trim() : "",
|
|
87
|
+
format: entry?.format,
|
|
88
|
+
proxyUri,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 探针 + 失败分档(已解析条目的低层面 —— 端侧「读账优先 ∕ 未落账回落现算」单源:
|
|
94
|
+
* `failure` ∈ { timeout, malformed, hostBusy },分类本体住核 `classifyProbeFailure`;网络探针经 `deps.probe`
|
|
95
|
+
* 注入,缺省 = 核真探;**探针目标由本档 `probeTargetOf` 构造**(同 VSC 源实参)。**绝不抛出**(探针族语义)。
|
|
96
|
+
* @returns {{ok:true, models:string[]} | {ok:false, failure:string, error:string}}
|
|
97
|
+
*/
|
|
98
|
+
export async function probeAdmission(name, provider, deps = {}) {
|
|
99
|
+
const probe = typeof deps.probe === "function" ? deps.probe : probeChannelModels
|
|
100
|
+
const r = await probe(name, probeTargetOf(provider))
|
|
101
|
+
if (r?.ok) return { ok: true, models: r.models ?? [] }
|
|
102
|
+
const error = r?.error
|
|
103
|
+
const failure = admissionOf(name)?.failure ?? classifyProbeFailure({ message: error })
|
|
104
|
+
return { ok: false, failure, error }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** M9 渠道准入探(配置写入面):对目标渠道探一次 `GET /models`。
|
|
108
|
+
* 探通 → 渠道可用(探得候选可直接用);探不通 → 记录失败展示态(providerStatus 行 `不可用` + 失败消息本体)
|
|
109
|
+
* 并返回失败消息供调用方提示。**绝不抛出、绝不阻断任何写**;失败不缓存——下次配置动作重探。 */
|
|
110
|
+
export async function probeProviderAdmission(name, deps = {}) {
|
|
111
|
+
if (!name) return { ok: false, error: "provider name is required" }
|
|
112
|
+
try {
|
|
113
|
+
const { providers } = resolveProviders()
|
|
114
|
+
const entry = providers.find((p) => p.name === name)
|
|
115
|
+
if (!entry) return { ok: false, error: `Unknown provider "${name}"` }
|
|
116
|
+
const r = await probeAdmission(name, entry, deps)
|
|
117
|
+
// F-W19:核分类只知超时 ∕ 畸形;宿主忙 = 端侧证据 ⇒ 覆盖落账分类为 `hostBusy`
|
|
118
|
+
// (`reason` 逐字不动;返回面零扩张)——宿主证据经注入缝给(核零宿主事件循环观测)。
|
|
119
|
+
if (!r.ok && typeof deps.hostBusyOverride === "function") deps.hostBusyOverride(name, r.error)
|
|
120
|
+
return r.ok ? { ok: true, models: r.models } : { ok: false, error: r.error }
|
|
121
|
+
} catch (e) {
|
|
122
|
+
return { ok: false, error: channelUnavailableMessage(e) }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** M9:探一次并在失败时界面明示失败消息(消息本体逐字长句)——不阻断已完成的保存。 */
|
|
127
|
+
async function reportAdmission(name, ui, deps) {
|
|
128
|
+
const probe = await probeProviderAdmission(name, deps)
|
|
129
|
+
if (!probe.ok) ui.warn(probe.error)
|
|
130
|
+
return probe
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── Three interactive flows (host UI shell injected) ───
|
|
134
|
+
|
|
135
|
+
/** Add a provider interactively: pick an unused preset or configure custom manually. */
|
|
136
|
+
export async function addProviderFlow(ui, refresh, deps = {}) {
|
|
137
|
+
const host = requireUi(ui)
|
|
138
|
+
let providers
|
|
139
|
+
try {
|
|
140
|
+
({ providers } = resolveProviders())
|
|
141
|
+
} catch (e) {
|
|
142
|
+
host.error(e.message)
|
|
143
|
+
return
|
|
144
|
+
}
|
|
145
|
+
const existing = new Set(providers.map((p) => p.name))
|
|
146
|
+
|
|
147
|
+
const items = Object.entries(PROVIDER_PRESETS)
|
|
148
|
+
.filter(([name]) => !existing.has(name))
|
|
149
|
+
.map(([name, p]) => ({ label: name, description: p.desc, detail: p.model ?? "", kind: "preset", name }))
|
|
150
|
+
items.push({ label: "Custom (manual config)", description: "enter baseURL/model/format", kind: "custom" })
|
|
151
|
+
|
|
152
|
+
const sel = await host.pick(items, {
|
|
153
|
+
placeHolder: "Add provider — select a preset",
|
|
154
|
+
matchOnDescription: true, matchOnDetail: true,
|
|
155
|
+
})
|
|
156
|
+
if (!sel) return
|
|
157
|
+
|
|
158
|
+
if (sel.kind === "custom") {
|
|
159
|
+
const name = (await host.input({
|
|
160
|
+
prompt: "Provider name",
|
|
161
|
+
validateInput: (v) => providerNameError(v, existing),
|
|
162
|
+
}))?.trim()
|
|
163
|
+
if (!name) return
|
|
164
|
+
const baseURL = (await host.input({
|
|
165
|
+
prompt: `Base URL for ${name}`,
|
|
166
|
+
placeHolder: "https://api.example.com/v1",
|
|
167
|
+
}))?.trim()
|
|
168
|
+
if (!baseURL) return
|
|
169
|
+
const model = (await host.input({ prompt: `Model name for ${name}` }))?.trim()
|
|
170
|
+
if (!model) return
|
|
171
|
+
const format = await host.pick(
|
|
172
|
+
FORMATS.map((f) => ({ label: f, description: f === "openai" ? "(default)" : f === "anthropic" ? "Messages API" : "streamGenerateContent" })),
|
|
173
|
+
{ placeHolder: "API format" },
|
|
174
|
+
)
|
|
175
|
+
if (!format) return
|
|
176
|
+
const err = addProviderEntry({ custom: { name, baseURL, model, format: format.label } })
|
|
177
|
+
if (err) { host.error(err); return }
|
|
178
|
+
const key = await host.input({ prompt: `API key for ${name} (leave empty to skip)`, password: true })
|
|
179
|
+
if (key?.trim()) {
|
|
180
|
+
const kerr = setProviderKey(name, key.trim())
|
|
181
|
+
if (kerr) { host.error(kerr); return } // F5b:config 并发被改——放弃 + 提示重试
|
|
182
|
+
}
|
|
183
|
+
await refresh?.()
|
|
184
|
+
await reportAdmission(name, host, deps) // M9:加渠道的配置写入面——探一次 /models(失败标不可用,不阻断保存)
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Preset: entry auto-filled from PROVIDER_PRESETS, then ask for a key
|
|
189
|
+
const err = addProviderEntry({ preset: sel.name })
|
|
190
|
+
if (err) { host.error(err); return }
|
|
191
|
+
const key = await host.input({ prompt: `API key for ${sel.name} (leave empty to skip)`, password: true })
|
|
192
|
+
if (key?.trim()) {
|
|
193
|
+
const kerr = setProviderKey(sel.name, key.trim())
|
|
194
|
+
if (kerr) { host.error(kerr); return } // F5b:冲突放弃提示重试
|
|
195
|
+
}
|
|
196
|
+
await refresh?.()
|
|
197
|
+
await reportAdmission(sel.name, host, deps) // M9:同上
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Remove a provider interactively (active one is not listed, CLI parity). */
|
|
201
|
+
export async function removeProviderFlow(ui, refresh) {
|
|
202
|
+
const host = requireUi(ui)
|
|
203
|
+
let providers, activeProvider
|
|
204
|
+
try {
|
|
205
|
+
({ providers, activeProvider } = resolveProviders())
|
|
206
|
+
} catch (e) {
|
|
207
|
+
host.error(e.message)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
const candidates = providers.filter((p) => p.name !== activeProvider)
|
|
211
|
+
if (candidates.length === 0) {
|
|
212
|
+
host.info("No removable providers — the active provider is kept.")
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
const sel = await host.pick(
|
|
216
|
+
candidates.map((p) => ({ label: p.name, description: p.model ?? "" })),
|
|
217
|
+
{ placeHolder: "Remove provider" },
|
|
218
|
+
)
|
|
219
|
+
if (!sel) return
|
|
220
|
+
const err = removeProviderEntry(sel.label)
|
|
221
|
+
if (err) host.error(err)
|
|
222
|
+
else await refresh?.()
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Set / replace an API key for a configured provider. */
|
|
226
|
+
export async function setKeyFlow(ui, refresh, deps = {}) {
|
|
227
|
+
const host = requireUi(ui)
|
|
228
|
+
let providers
|
|
229
|
+
try {
|
|
230
|
+
({ providers } = resolveProviders())
|
|
231
|
+
} catch (e) {
|
|
232
|
+
host.error(e.message)
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
if (providers.length === 0) return
|
|
236
|
+
const sel = await host.pick(
|
|
237
|
+
providers.map((p) => ({ label: p.name, description: p.apiKey ? "(has key)" : "(no key)" })),
|
|
238
|
+
{ placeHolder: "Set API key for provider" },
|
|
239
|
+
)
|
|
240
|
+
if (!sel) return
|
|
241
|
+
const key = await host.input({ prompt: `API key for ${sel.label}`, password: true })
|
|
242
|
+
if (key?.trim()) {
|
|
243
|
+
const kerr = setProviderKey(sel.label, key.trim())
|
|
244
|
+
if (kerr) { host.error(kerr); return } // F5b:config 并发被改——放弃 + 提示重试
|
|
245
|
+
await refresh?.()
|
|
246
|
+
}
|
|
247
|
+
await reportAdmission(sel.label, host, deps) // M9:设 API key 的配置写入面——探一次 /models(失败标不可用,不阻断保存)
|
|
248
|
+
}
|
package/queued.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queued.mjs — busy 排队消息的合并消费纯函数族(核件——「桌面处理流 · VSC 对齐」批 R3 上提产物)。
|
|
3
|
+
*
|
|
4
|
+
* 机制单源 = `docs/core/design/AGENT-LOOP-ASYNC-POOL.md` §6.8(「合并消费」条 + 「步边界 pickup」块)。
|
|
5
|
+
* 上提源 = VSC `thincoder-vscode/src/extension/queued-merge.mjs:15-36`(容量常量 ∕ 合并串 ∕ 批计划)
|
|
6
|
+
* + `queued-pickup.mjs:49`(`takeQueuedBatchItem` 取项)——**纯搬 + 转口,零语义改**(常量名 / 值 /
|
|
7
|
+
* 形态文案 / 批语义逐字);对拍锚 = CLI 副本 `thincoder-cli/src/tui/queued-merge.mjs`(同名同值)。
|
|
8
|
+
*
|
|
9
|
+
* 消费面:计划面(常量 ∕ 合并串 ∕ 计划)由桌面排队面消费(`thincoder-desktop/src/main/queued-input.mjs`);
|
|
10
|
+
* `takeQueuedBatchItem` = **取项单源**(携图批退化逐条 ∕ slash 单条取——统一语义 = 计划首动作即消费),
|
|
11
|
+
* 消费点在册五处 = VSC 三径(`panel-turn-stages.mjs` 退出兜底 ∕ `suspension.mjs` 取项缝 ∕ 同档退出残余)
|
|
12
|
+
* + 桌面两径(`turn-chain.mjs` 尾径 ∕ `suspension-drive.mjs` 窗输入面)。
|
|
13
|
+
*
|
|
14
|
+
* 留端(载体面不迁——各端 adapter 各持):队容器(按会话键 `Map` ∥ 单容器)· 容量门与满队回执 ·
|
|
15
|
+
* 快照投影 · 步边界守卫与送达面(附件判决 ∕ 回合链出站)· 显示面。
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// ── 合并常量(三端逐字一致):单批 ≤ MAX_MERGE_ITEMS 条且合并注入 ≤ MAX_MERGE_CHARS 字符;
|
|
19
|
+
// 超限截批先行(余下留待下批——不丢不截断单条);单条 > MAX_MERGE_CHARS 直发不进批;
|
|
20
|
+
// /cmd 不进合并(逐条保序即时)。──
|
|
21
|
+
export const MAX_MERGE_ITEMS = 8
|
|
22
|
+
export const MAX_MERGE_CHARS = 2000
|
|
23
|
+
|
|
24
|
+
/** 队容量(条)——**三端同名常量**(无会话载体与会话在飞载体合计口径;满队 = 第 9 条 ⇒
|
|
25
|
+
* 拒收 + 提示 + 不回显——容量门与满队回执留端)。 */
|
|
26
|
+
export const QUEUED_MAX_ITEMS = 8
|
|
27
|
+
|
|
28
|
+
/** 合并注入形态(编号逐条——模型一次处理全部;zh 字面,三端逐字)。 */
|
|
29
|
+
export function formatMergedMessages(items) {
|
|
30
|
+
return `你排队了 ${items.length} 条消息:\n${items.map((m, i) => `${i + 1}. ${m}`).join("\n")}\n——一次处理`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* R15 攒批计划(纯函数——消费点共享同一事实源):按队列顺序产出动作序列(输入 = 文本数组;
|
|
35
|
+
* 调用侧以 `queue.map((q) => q.text)` 取数)。
|
|
36
|
+
* - { kind:"slash", text, count:1 }——/cmd 逐条即时执行(保序——不进合并缓冲);
|
|
37
|
+
* - { kind:"turn", text, count, merged }——文本批次:连续非 / 条目攒批(单批 ≤ MAX_MERGE_ITEMS 条;
|
|
38
|
+
* 合并注入(格式化后)≤ MAX_MERGE_CHARS 字符;count ≥2 → merged 编号注入;count === 1 → 原文
|
|
39
|
+
* 直发(含单条 > MAX_MERGE_CHARS——不进批);超限截批先行——余下条目留待下批(不丢)。
|
|
40
|
+
* 消费者每次取动作 [0] 并按 count 从源队列移除——下轮重新计划余项(边界幂等)。
|
|
41
|
+
*/
|
|
42
|
+
export function planQueuedInput(items) {
|
|
43
|
+
const actions = []
|
|
44
|
+
const n = items.length
|
|
45
|
+
let i = 0
|
|
46
|
+
while (i < n) {
|
|
47
|
+
const cur = String(items[i])
|
|
48
|
+
if (cur.startsWith("/")) { actions.push({ kind: "slash", text: cur, count: 1 }); i++; continue }
|
|
49
|
+
let j = i
|
|
50
|
+
for (;;) {
|
|
51
|
+
if (j - i >= MAX_MERGE_ITEMS || j >= n) break
|
|
52
|
+
const it = String(items[j])
|
|
53
|
+
if (it.startsWith("/")) break
|
|
54
|
+
if (it.length > MAX_MERGE_CHARS) break // 单条超长——不进批(留作直发)
|
|
55
|
+
const cand = items.slice(i, j + 1)
|
|
56
|
+
if (cand.length > 1 && formatMergedMessages(cand).length > MAX_MERGE_CHARS) break // 批总长超限——截批
|
|
57
|
+
j++
|
|
58
|
+
}
|
|
59
|
+
// 超长条目堵头(内环一步未进)→ 该条直发(不进批——也不与前后合并)
|
|
60
|
+
if (j === i) { actions.push({ kind: "turn", text: String(items[i]), count: 1, merged: false }); i++; continue }
|
|
61
|
+
const taken = items.slice(i, j)
|
|
62
|
+
if (taken.length === 1) {
|
|
63
|
+
actions.push({ kind: "turn", text: String(taken[0]), count: 1, merged: false })
|
|
64
|
+
} else {
|
|
65
|
+
actions.push({ kind: "turn", text: formatMergedMessages(taken), count: taken.length, merged: true })
|
|
66
|
+
}
|
|
67
|
+
i = j
|
|
68
|
+
}
|
|
69
|
+
return actions
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** 贴图判定(取项退化与步边界让位共用——图片随条目元数据走降级面,不静默丢)。 */
|
|
73
|
+
const hasImages = (q) => Array.isArray(q?.images) && q.images.length > 0
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 载体条目面取批(纯函数——就地消费;**统一语义 = 计划首动作即消费**):`turn` 首动作 ⇒ 按计划取一批
|
|
77
|
+
* (多条合并 ∕ 超长直发);`slash` 首动作 ⇒ 单条取(count 1 ∕ 原文——保序 ∕ 不合并)。
|
|
78
|
+
* **贴图批退化逐条**(批内任一条目携 `images` ⇒ count = 1——图片随条目元数据走降级面);
|
|
79
|
+
* 无贴图 ⇒ 多条合并为一条(头条目元数据 + 合并文本)。
|
|
80
|
+
* @param {Array} queue 载体条目数组(条目 `{ text, images?, ... }`——就地 `splice` 消费)
|
|
81
|
+
* @returns {{item: object|null, merged: string|null}} 取出的载体条目 + 本批文本(快照 `merged` 源)
|
|
82
|
+
*/
|
|
83
|
+
export function takeQueuedBatchItem(queue) {
|
|
84
|
+
if (!Array.isArray(queue) || queue.length === 0) return { item: null, merged: null }
|
|
85
|
+
const action = planQueuedInput(queue.map((q) => String(q?.text ?? "")))[0]
|
|
86
|
+
const count = queue.slice(0, action.count).some(hasImages) ? 1 : action.count
|
|
87
|
+
const taken = queue.splice(0, count)
|
|
88
|
+
const text = count > 1 ? action.text : String(taken[0]?.text ?? "")
|
|
89
|
+
return { item: { ...taken[0], text }, merged: text }
|
|
90
|
+
}
|
package/rules.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* rules.mjs —
|
|
2
|
+
* rules.mjs — 规则面**单源**(两面):
|
|
3
|
+
* ① stream 规则(`.thincoder/rules/*.md`——两端;`discoverRules` ∕ `mergeFileRules`)
|
|
4
|
+
* ② 作用域规则(`.cursor/rules/*`——三端同面;`loadRules` 读取 ∕ 三分类 ∕ 尾块 ∕ JIT 注入全档单源)
|
|
3
5
|
*
|
|
4
|
-
*
|
|
6
|
+
* 面定义(权威)= `docs/core/design/WORKSPACE.md` §2.3「规则发现面(一线程一语义)」:
|
|
7
|
+
* `.thincoder/rules/*` 恒 = stream 规则;`.cursor/rules/*` 恒 = 作用域规则。
|
|
8
|
+
*
|
|
9
|
+
* Stream rule file format (markdown + YAML frontmatter):
|
|
5
10
|
* ---
|
|
6
11
|
* pattern: "console\\.log"
|
|
7
12
|
* action: abort # "abort"|"warn" (default: "warn")
|
|
@@ -10,10 +15,19 @@
|
|
|
10
15
|
* Use the project logger instead of console.log.
|
|
11
16
|
*
|
|
12
17
|
* The body becomes the rule message; if empty, falls back to `message` frontmatter.
|
|
18
|
+
*
|
|
19
|
+
* 上提史(R10 → B7 3a):`.cursor/rules` **读取面**(`loadRules` + 私有 frontmatter 解析)自 VSC
|
|
20
|
+
* `thincoder-vscode/src/extension/rules.mjs` 纯搬(R10);**三分类 ∕ 尾块文本 ∕ JIT 注入判据**
|
|
21
|
+
* 自 VSC `src/agent/rules-face.mjs` 纯搬 · **glob 匹配**(`matchesGlob`)自 VSC
|
|
22
|
+
* `src/extension/rules.mjs` 纯搬 · **合并**(`mergeFileRules`)自核 `agent/assemble.mjs` 内联块
|
|
23
|
+
* 抽取(B7 3a——均零语义改;VSC 两档退役,desk ∕ CLI 乘核径取得)。尾块 ∕ JIT 两缝同门
|
|
24
|
+
* `depth === 0`(B7 §2 修正轮 1 ④——子代理不携 `.cursor/rules`)。
|
|
13
25
|
*/
|
|
14
26
|
import { existsSync, readFileSync, readdirSync } from "node:fs"
|
|
15
27
|
import { join } from "node:path"
|
|
16
28
|
import { parseFrontmatter } from "./markdown.mjs"
|
|
29
|
+
// #327 触达路径单源谓词 + 写集(JIT 面 `PATH_TOOLS` 构造——B7 3a:与门禁 ∕ 记账同址单源)
|
|
30
|
+
import { FILE_MUTATORS, toolTouchPaths } from "./agent/helpers.mjs"
|
|
17
31
|
|
|
18
32
|
/**
|
|
19
33
|
* Scan `.thincoder/rules/` in the project root for `.md` rule files.
|
|
@@ -51,3 +65,196 @@ export function discoverRules(projectRoot) {
|
|
|
51
65
|
}
|
|
52
66
|
return rules
|
|
53
67
|
}
|
|
68
|
+
|
|
69
|
+
// ─── A 面:stream 规则装配合并(`.thincoder/rules`——B7 3a 自核装配内联块抽取单源)────────
|
|
70
|
+
|
|
71
|
+
/** 装配合并:`.thincoder/rules/` 文件规则**置前**,config 规则按 `pattern` 去重(文件侧优先;
|
|
72
|
+
* 逐字镜像原 CLI `make-agent.mjs:44-48` 语义)。文件规则为空 ⇒ 归一数组原样返回(零拷贝零改)。
|
|
73
|
+
* `discover` 可注入(核装配 `DEFAULT_DEPS.discoverRules` 缝——U79 装配序可机验;VSC 端壳同取本件)。 */
|
|
74
|
+
export function mergeFileRules(configRules, cwd, discover = discoverRules) {
|
|
75
|
+
const rules = Array.isArray(configRules) ? configRules : []
|
|
76
|
+
const fileRules = discover(cwd)
|
|
77
|
+
if (fileRules.length === 0) return rules
|
|
78
|
+
const filePatterns = new Set(fileRules.map((r) => r.pattern))
|
|
79
|
+
return [...fileRules, ...rules.filter((r) => !filePatterns.has(r.pattern))]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ─── B 面:作用域规则读取面(`.cursor/rules`——R10 上提;纯搬自 VSC `src/extension/rules.mjs:28-81`)──
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Load all scoped rules from `.cursor/rules/`.
|
|
86
|
+
* Returns [{ name, content, globs, alwaysApply, description, source }, ...]
|
|
87
|
+
*/
|
|
88
|
+
export function loadRules(cwd) {
|
|
89
|
+
const dir = join(cwd, ".cursor/rules")
|
|
90
|
+
if (!existsSync(dir)) return []
|
|
91
|
+
const results = []
|
|
92
|
+
let entries
|
|
93
|
+
try { entries = readdirSync(dir, { withFileTypes: true }) } catch { return results }
|
|
94
|
+
for (const e of entries) {
|
|
95
|
+
if (!e.isFile() || e.name.startsWith(".") || (!e.name.endsWith(".md") && !e.name.endsWith(".mdc"))) continue
|
|
96
|
+
try {
|
|
97
|
+
const raw = readFileSync(join(dir, e.name), "utf8").trim()
|
|
98
|
+
if (raw.length === 0) continue
|
|
99
|
+
const { content, meta } = parseScopedFrontmatter(raw)
|
|
100
|
+
results.push({
|
|
101
|
+
name: e.name.replace(/\.(md|mdc)$/, ""),
|
|
102
|
+
content,
|
|
103
|
+
globs: meta.globs ? String(meta.globs).split(/[;\n]/).map(s => s.trim()).filter(Boolean) : null,
|
|
104
|
+
alwaysApply: meta.alwaysApply === "true",
|
|
105
|
+
description: meta.description ? String(meta.description) : null,
|
|
106
|
+
source: ".cursor/rules",
|
|
107
|
+
})
|
|
108
|
+
} catch { /* skip unreadable */ }
|
|
109
|
+
}
|
|
110
|
+
return results
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Split frontmatter block from content.
|
|
115
|
+
* Frontmatter starts and ends with `---` on its own line.
|
|
116
|
+
* (私有件——随搬;名改 `parseScopedFrontmatter` = 与 `./markdown.mjs` 的 `parseFrontmatter`
|
|
117
|
+
* 导入同名冲突的机械改名——**体逐字纯搬,零语义改**。)
|
|
118
|
+
*/
|
|
119
|
+
function parseScopedFrontmatter(raw) {
|
|
120
|
+
const m = raw.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/)
|
|
121
|
+
if (!m) return { content: raw, meta: {} }
|
|
122
|
+
|
|
123
|
+
const front = m[1]
|
|
124
|
+
const content = raw.slice(m[0].length).trim()
|
|
125
|
+
const meta = {}
|
|
126
|
+
for (const line of front.split("\n")) {
|
|
127
|
+
const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+?)\s*$/)
|
|
128
|
+
if (kv) {
|
|
129
|
+
const key = kv[1]
|
|
130
|
+
let val = kv[2]
|
|
131
|
+
// Strip surrounding quotes
|
|
132
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
133
|
+
val = val.slice(1, -1)
|
|
134
|
+
}
|
|
135
|
+
meta[key] = val
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { content, meta }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─── B 面判据:三分类 ∕ 尾块 ∕ glob ∕ JIT(B7 3a 自 VSC 端壳上提单源)─────────────────────────
|
|
142
|
+
|
|
143
|
+
/** 三分类(**按序判定·互斥**——权威定义 `docs/core/design/WORKSPACE.md` §2.3):
|
|
144
|
+
* ① `alwaysApply === true` ⇒ **常驻集**(**先判**——`globs` 同在不改分类)
|
|
145
|
+
* ② 有 `globs` ⇒ **作用域集**(命中路径的工具派发前置提醒)
|
|
146
|
+
* ③ 无 `globs` 且无 `description` ⇒ **常驻集**
|
|
147
|
+
* ④ 仅 `description`(Cursor 的 agent-requested 语义)⇒ 两集皆无(本端无该机制——登记)。 */
|
|
148
|
+
export function classifyRules(rules) {
|
|
149
|
+
const always = []
|
|
150
|
+
const scoped = []
|
|
151
|
+
for (const r of rules ?? []) {
|
|
152
|
+
if (r.alwaysApply === true) always.push(r)
|
|
153
|
+
else if (r.globs?.length) scoped.push(r)
|
|
154
|
+
else if (!r.description) always.push(r)
|
|
155
|
+
}
|
|
156
|
+
return { always, scoped }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 目录读点(每 run 一次——装配期):`.cursor/rules` 读取 + 三分类(本函数 = 分类入口单源)。 */
|
|
160
|
+
export function loadScopedRules(cwd) {
|
|
161
|
+
return classifyRules(loadRules(cwd))
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** 常驻集注入块([4] 层尾块——与项目指令块同槽:每 run 重建、不进 history)+ **本 run 唯一
|
|
165
|
+
* 一次目录读取**:结果缓存于 `agent._rules`(`{ always, scoped }`)——作用域集 JIT 判定
|
|
166
|
+
* 只读该缓存(零重读盘)。无命中 ⇒ 空串(零改字符串)。 */
|
|
167
|
+
export function scopedRulesBlock(agent, cwd) {
|
|
168
|
+
const rules = loadScopedRules(cwd)
|
|
169
|
+
if (agent) agent._rules = rules
|
|
170
|
+
if (rules.always.length === 0) return ""
|
|
171
|
+
return `\n\nProject rules (.cursor/rules):\n${rules.always.map((r) => `- ${r.name}: ${r.content}`).join("\n")}`
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** 文件路径 glob 匹配(简单 `*` / `**` / `?` 通配——无完整 glob 库;纯搬自 VSC
|
|
175
|
+
* `src/extension/rules.mjs:37-74`,零语义改)。 */
|
|
176
|
+
export function matchesGlob(filePath, patterns) {
|
|
177
|
+
if (!patterns || patterns.length === 0) return false
|
|
178
|
+
// Normalize to forward slashes
|
|
179
|
+
const normalized = filePath.replace(/\\/g, "/")
|
|
180
|
+
for (const pat of patterns) {
|
|
181
|
+
if (simpleGlobMatch(normalized, pat.replace(/\\/g, "/"))) return true
|
|
182
|
+
}
|
|
183
|
+
return false
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Simple glob match: supports *, **, ? */
|
|
187
|
+
function simpleGlobMatch(path, pattern) {
|
|
188
|
+
// Convert glob pattern to regex
|
|
189
|
+
let regexStr = ""
|
|
190
|
+
let i = 0
|
|
191
|
+
while (i < pattern.length) {
|
|
192
|
+
if (pattern[i] === "*" && pattern[i + 1] === "*") {
|
|
193
|
+
// ** matches anything including /
|
|
194
|
+
regexStr += ".*"
|
|
195
|
+
i += 2
|
|
196
|
+
// Skip trailing /
|
|
197
|
+
if (pattern[i] === "/") i++
|
|
198
|
+
} else if (pattern[i] === "*") {
|
|
199
|
+
regexStr += "[^/]*"
|
|
200
|
+
i++
|
|
201
|
+
} else if (pattern[i] === "?") {
|
|
202
|
+
regexStr += "[^/]"
|
|
203
|
+
i++
|
|
204
|
+
} else {
|
|
205
|
+
// Escape regex special chars
|
|
206
|
+
const c = pattern[i]
|
|
207
|
+
if (".+^${}()|[]\\".includes(c)) regexStr += "\\" + c
|
|
208
|
+
else regexStr += c
|
|
209
|
+
i++
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return new RegExp("^" + regexStr + "$").test(path)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const PATH_TOOLS = new Set([...FILE_MUTATORS, "file_ops", "read", "glob"])
|
|
216
|
+
|
|
217
|
+
/** 路径候选(绝对路径先归一为 cwd 相对——globs 语义 = 项目相对;`read` 可收绝对路径):
|
|
218
|
+
* 前缀比对**大小写不敏感**(VS Code `uri.fsPath` 的 Windows 盘符为小写,模型侧常给大写盘符
|
|
219
|
+
* ——`D:\proj\a.py` vs cwd `d:\proj`);另剥前导 `./`。 */
|
|
220
|
+
function matchCandidates(p, cwd) {
|
|
221
|
+
const norm = String(p).replace(/\\/g, "/").replace(/^\.\//, "")
|
|
222
|
+
const out = [norm]
|
|
223
|
+
const base = String(cwd ?? "").replace(/\\/g, "/").replace(/\/+$/, "")
|
|
224
|
+
const underCwd = base && norm.length > base.length
|
|
225
|
+
&& norm.slice(0, base.length).toLowerCase() === base.toLowerCase() && norm[base.length] === "/"
|
|
226
|
+
if (underCwd) out.push(norm.slice(base.length + 1))
|
|
227
|
+
return out
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** 作用域集 JIT 注入(**派发前**——模型下一轮可见):文件路径类工具(`FILE_MUTATORS` ∪
|
|
231
|
+
* `file_ops` ∪ `read` / `glob`)的触达路径命中**未注入**规则 ⇒ `history` 注入
|
|
232
|
+
* `[System reminder — project rule "<name>" (globs: <g>): <content>]`。语义一句话 =
|
|
233
|
+
* 「Agent 将触碰匹配文件时,该文件作用域的规则先入上下文」。去重键 = `name`;去重域 =
|
|
234
|
+
* 会话(`agent._rulesInjected` 惰性建 Set——顶层 agent 单例;子代理 agent 每 run 新对象
|
|
235
|
+
* ⇒ 去重域 = 该子回合)。路径候选 = #327 单源谓词(`toolTouchPaths`——设计
|
|
236
|
+
* B-4 公式)——`read.filePath` 别名与 `glob.pattern` **不作候选**(登记边界)。
|
|
237
|
+
* @returns {number} 本次注入条数(测试直驱面)。 */
|
|
238
|
+
export function injectScopedRules(agent, history, calls) {
|
|
239
|
+
const scoped = agent?._rules?.scoped ?? []
|
|
240
|
+
if (scoped.length === 0) return 0
|
|
241
|
+
const injected = (agent._rulesInjected ??= new Set())
|
|
242
|
+
let count = 0
|
|
243
|
+
for (const { tool, args } of calls ?? []) {
|
|
244
|
+
if (!PATH_TOOLS.has(tool?.name)) continue
|
|
245
|
+
const raw = toolTouchPaths(tool, args)
|
|
246
|
+
const paths = (raw ?? []).filter((p) => typeof p === "string" && p).flatMap((p) => matchCandidates(p, agent.cwd))
|
|
247
|
+
if (paths.length === 0) continue
|
|
248
|
+
for (const rule of scoped) {
|
|
249
|
+
if (injected.has(rule.name)) continue
|
|
250
|
+
if (!paths.some((p) => matchesGlob(p, rule.globs))) continue
|
|
251
|
+
injected.add(rule.name)
|
|
252
|
+
history.push({
|
|
253
|
+
role: "user",
|
|
254
|
+
content: `[System reminder — project rule "${rule.name}" (globs: ${rule.globs.join("; ")}): ${rule.content}]`,
|
|
255
|
+
})
|
|
256
|
+
count++
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return count
|
|
260
|
+
}
|