dsh-plugin-prompt-tool 0.6.2 → 0.7.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/README.md +11 -9
- package/engine/anchor-match.mjs +38 -12
- package/engine/anchor-turn.mjs +68 -0
- package/engine/classify-task.mjs +35 -0
- package/engine/code-presentation.mjs +103 -0
- package/engine/compositions/library/anchor-turn.yml +10 -0
- package/engine/compositions/library/bootstrap-filesystem.yml +1 -1
- package/engine/compositions/library/code-presentation.yml +8 -0
- package/engine/compositions/library/command-goal.yml +11 -0
- package/engine/compositions/library/compaction.yml +1 -1
- package/engine/compositions/library/context-gate.yml +1 -2
- package/engine/compositions/library/cot-drip.yml +11 -0
- package/engine/compositions/library/custom-bash.yml +1 -1
- package/engine/compositions/library/delegation.yml +11 -7
- package/engine/compositions/library/deliberation-gate.yml +11 -0
- package/engine/compositions/library/persistent-shell.yml +25 -2
- package/engine/compositions/library/persona.yml +6 -2
- package/engine/compositions/library/planning.yml +1 -1
- package/engine/compositions/library/prompt-config-engine.yml +1 -1
- package/engine/compositions/library/run-code-env.yml +1 -1
- package/engine/compositions/library/skill-filesystem.yml +1 -1
- package/engine/compositions/library/skill-search.yml +1 -1
- package/engine/compositions/library/str-replace-editor.yml +2 -2
- package/engine/compositions/library/tool-ask-user.yml +1 -1
- package/engine/compositions/library/tool-bash.yml +1 -1
- package/engine/compositions/library/tool-bootstrap.yml +1 -3
- package/engine/compositions/library/tool-config-engine.yml +9 -0
- package/engine/compositions/library/tool-filter.yml +3 -7
- package/engine/compositions/library/tool-fs-search.yml +1 -1
- package/engine/compositions/library/tool-fs.yml +1 -1
- package/engine/compositions/library/tool-goal.yml +1 -9
- package/engine/compositions/library/tool-jobs.yml +1 -1
- package/engine/compositions/library/tool-pwsh.yml +1 -1
- package/engine/compositions/library/tool-todo.yml +1 -1
- package/engine/compositions/library/tool-web.yml +2 -2
- package/engine/compositions/source/local/code-presentation.yml +8 -0
- package/engine/compositions/source/local/context-gate.yml +0 -1
- package/engine/compositions/source/local/str-replace-editor.yml +10 -0
- package/engine/compositions/source/local/tool-bootstrap.yml +1 -3
- package/engine/compositions/source/local/tool-config-engine.yml +9 -0
- package/engine/compositions/source/local/tool-filter.yml +8 -0
- package/engine/cot-drip.mjs +137 -0
- package/engine/custom-bash.mjs +73 -21
- package/engine/deliberation-gate.mjs +127 -0
- package/engine/layers.mjs +223 -198
- package/engine/run-code-env.mjs +8 -2
- package/engine/schema.mjs +1 -1
- package/engine/strategies.mjs +25 -14
- package/engine/tool-bootstrap.mjs +185 -59
- package/engine/tool-config-engine.mjs +390 -0
- package/lib/client.js +3840 -2311
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +186 -101
- package/lib/index.mjs +1012 -413
- package/lib/preset-core.d.mts +1 -1
- package/lib/preset-core.mjs +29 -3
- package/lib/{prompt-configs-BgFw_bFf.mjs → prompt-configs-BnQKRHVb.mjs} +211 -136
- package/lib/prompt-configs-CN0sJTUL.d.mts +193 -0
- package/package.json +121 -105
- package/preset/anchored/preset.yml +262 -308
- package/preset/creative/preset.yml +34 -32
- package/preset/custom/preset.yml +0 -1
- package/preset/liangshen/preset.yml +53 -50
- package/preset/minimal/preset.yml +18 -17
- package/preset/ptc/preset.yml +35 -33
- package/preset/standard/preset.yml +34 -32
- package/templates/10-pre-step.yml +18 -15
- package/templates/14-first-turn-anchor.yml +13 -13
- package/templates/15-guide-auto.yml +13 -13
- package/templates/16-custom-fallback.yml +19 -18
- package/templates/18-placeholder-env-facts.yml +13 -2
- package/templates/30-runtime-context.yml +3 -0
- package/templates/40-agent-request.yml +9 -9
- package/templates/60-tool-pipeline.yml +9 -8
- package/templates/tools/10-shell-command.yml +20 -0
- package/templates/tools/20-http-fetch.yml +24 -0
- package/templates/tools/30-fs-read.yml +20 -0
- package/templates/tools/40-delegate-character.yml +15 -0
- package/templates/tools/50-delegate-worldbook.yml +15 -0
- package/templates/tools/60-delegate-session-var.yml +15 -0
- package/templates/tools/70-delegate-worldbook-upsert.yml +36 -0
- package/lib/prompt-configs-B4vH09wx.d.mts +0 -100
- package/templates/11-merged-a.yml +0 -11
- package/templates/13-anchor.yml +0 -19
- package/templates/17-instruction-hint.yml +0 -23
- package/templates/19-placeholder-skill-catalog.yml +0 -19
- package/templates/31-runtime-context-placeholder.yml +0 -18
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# module: tool-bootstrap
|
|
2
|
-
# source:
|
|
2
|
+
# source: engine/compositions/source/local/tool-bootstrap.yml (本地附加,非官方模块)
|
|
3
3
|
|
|
4
4
|
- id: tool-bootstrap
|
|
5
5
|
name: ./engine/tool-bootstrap.mjs
|
|
@@ -7,8 +7,6 @@
|
|
|
7
7
|
bootstrapTools: [bash, str_replace_editor]
|
|
8
8
|
promoteOn: either
|
|
9
9
|
includeSubagents: false
|
|
10
|
-
usePtcMode: __usePtcMode__
|
|
11
|
-
__bootstrapMaxTokens__
|
|
12
10
|
# Post-compaction core work set: the model is mid-task and needs to keep
|
|
13
11
|
# working, but faces a small catalog instead of the full Standard set.
|
|
14
12
|
compactionTools: [read, write, edit, glob, grep, todo_write, ask_user_question]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# module: tool-config-engine
|
|
2
|
+
# source: 本地新增(自定义工具引擎:preset.yml customTools 段 → 渲染 custom-tools/ → 运行时注册)
|
|
3
|
+
# 工具定义在预设目录 custom-tools/*.yml;执行器 shell/http/delegate/fs/ask-user。
|
|
4
|
+
# requireApproval 数组可列出需 approval 门的执行器(默认空 = 不要求)。
|
|
5
|
+
|
|
6
|
+
- id: tool-config-engine
|
|
7
|
+
name: ./engine/tool-config-engine.mjs
|
|
8
|
+
config:
|
|
9
|
+
configsDir: ../custom-tools
|
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
# module: tool-filter
|
|
2
|
-
# source:
|
|
2
|
+
# source: engine/compositions/source/local/tool-filter.yml (本地附加,非官方模块)
|
|
3
3
|
# 主会话常驻工具白名单/黑名单:作用于任意注册工具(含第三方插件工具)。
|
|
4
|
-
#
|
|
5
|
-
#
|
|
4
|
+
# config 由参数桥按 params.toolFilterAllow/Deny/Subagents 合并;
|
|
5
|
+
# 未声明 = 不过滤(官方默认),见 tool-filter.mjs。
|
|
6
6
|
|
|
7
7
|
- id: tool-filter
|
|
8
8
|
name: ./engine/tool-filter.mjs
|
|
9
|
-
config:
|
|
10
|
-
allow: __mainToolFilterAllow__
|
|
11
|
-
deny: __mainToolFilterDeny__
|
|
12
|
-
includeSubagents: __mainToolFilterSubagents__
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# module: tool-fs-search
|
|
2
|
-
# source:
|
|
2
|
+
# source: D:\AI\GitHub\deepseek-harness/packages/preset/agent-presets/presets/standard/agent.cordis.yml
|
|
3
3
|
# local patches: 0
|
|
4
4
|
|
|
5
5
|
- id: tool-fs-search
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# module: tool-fs
|
|
2
|
-
# source:
|
|
2
|
+
# source: D:\AI\GitHub\deepseek-harness/packages/preset/agent-presets/presets/standard/agent.cordis.yml
|
|
3
3
|
# local patches: 0
|
|
4
4
|
|
|
5
5
|
# ── filesystem ──────────────────────────────────────────────────────────────
|
|
@@ -1,15 +1,7 @@
|
|
|
1
1
|
# module: tool-goal
|
|
2
|
-
# source:
|
|
2
|
+
# source: D:\AI\GitHub\deepseek-harness/packages/preset/agent-presets/presets/standard/agent.cordis.yml
|
|
3
3
|
# local patches: 0
|
|
4
4
|
|
|
5
|
-
# ── goals ───────────────────────────────────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
# Only the model-facing tool. The goal SERVICE, its session driver, and the
|
|
8
|
-
# `/goal` command stay on the host plane: the Gateway serves the goal domain as
|
|
9
|
-
# Remote endpoints whose receiver comes from a generated descriptor, so it
|
|
10
|
-
# resolves `goals` on the host and an entry-local realm here would hide it. The
|
|
11
|
-
# registry is keyed by session anyway, so one host instance serves every
|
|
12
|
-
# session. What a preset chooses is whether its agent can call the goal tool.
|
|
13
5
|
- id: tool-goal
|
|
14
6
|
name: '@deepseek-ai/dsh-tool-goal'
|
|
15
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# module: tool-jobs
|
|
2
|
-
# source:
|
|
2
|
+
# source: D:\AI\GitHub\deepseek-harness/packages/preset/agent-presets/presets/standard/agent.cordis.yml
|
|
3
3
|
# local patches: 0
|
|
4
4
|
|
|
5
5
|
# ── background jobs ────────────────────────────────────────────────────────
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# module: tool-pwsh
|
|
2
|
-
# source:
|
|
2
|
+
# source: D:\AI\GitHub\deepseek-harness/packages/preset/agent-presets/presets/standard/agent.cordis.yml
|
|
3
3
|
# local patches: 0
|
|
4
4
|
|
|
5
5
|
- id: tool-pwsh
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
# module: tool-todo
|
|
2
|
-
# source:
|
|
2
|
+
# source: D:\AI\GitHub\deepseek-harness/packages/preset/agent-presets/presets/standard/agent.cordis.yml
|
|
3
3
|
# local patches: 0
|
|
4
4
|
|
|
5
5
|
- id: tool-todo
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# module: tool-web
|
|
2
|
-
# source:
|
|
2
|
+
# source: D:\AI\GitHub\deepseek-harness/packages/preset/agent-presets/presets/standard/agent.cordis.yml
|
|
3
3
|
# local patches: 0
|
|
4
4
|
|
|
5
5
|
- id: tool-web
|
|
6
6
|
name: '@deepseek-ai/dsh-tool-web'
|
|
7
7
|
config:
|
|
8
|
-
fetch:
|
|
8
|
+
fetch: true
|
|
9
9
|
searchTimeoutMs: 60000
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# module: code-presentation
|
|
2
|
+
# source: engine/compositions/source/local/code-presentation.yml (本地附加,非官方模块)
|
|
3
|
+
# 晋升后 Code Mode (PTC) wire 呈现(从 tool-bootstrap 拆出,与目录窄化解耦):
|
|
4
|
+
# "只要 PTC 不要 bootstrap" = modules 只挂本行;PTC+bootstrap = 同时挂两行。
|
|
5
|
+
# usePtcMode 默认 false(opt-in);params.usePtcMode 显式覆盖(参数桥)。
|
|
6
|
+
|
|
7
|
+
- id: code-presentation
|
|
8
|
+
name: ./engine/code-presentation.mjs
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# module: str-replace-editor
|
|
2
|
+
# source: engine/compositions/source/local/str-replace-editor.yml (本地附加,非官方模块)
|
|
3
|
+
# 官方 minimal 内置工具(filesystem group 内的 str-replace-editor 行)拆出为独立模块:
|
|
4
|
+
# 复用宿主 sandbox 的 ctx.fs,不挂 fs-local 裸文件系统。
|
|
5
|
+
# maxOutputChars 参数化:params.strReplaceEditorMaxOutputChars(默认官方值 16000)。
|
|
6
|
+
|
|
7
|
+
- id: str-replace-editor
|
|
8
|
+
name: '@deepseek-ai/dsh-tool-str-replace-editor'
|
|
9
|
+
config:
|
|
10
|
+
maxOutputChars: 16000
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
config:
|
|
4
4
|
bootstrapTools: [bash, str_replace_editor]
|
|
5
5
|
promoteOn: either
|
|
6
|
-
includeSubagents:
|
|
7
|
-
usePtcMode: __usePtcMode__
|
|
8
|
-
__bootstrapMaxTokens__
|
|
6
|
+
includeSubagents: false
|
|
9
7
|
# Post-compaction core work set: the model is mid-task and needs to keep
|
|
10
8
|
# working, but faces a small catalog instead of the full Standard set.
|
|
11
9
|
compactionTools: [read, write, edit, glob, grep, todo_write, ask_user_question]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# module: tool-config-engine
|
|
2
|
+
# source: 本地新增(自定义工具引擎:preset.yml customTools 段 → 渲染 custom-tools/ → 运行时注册)
|
|
3
|
+
# 工具定义在预设目录 custom-tools/*.yml;执行器 shell/http/delegate/fs/ask-user。
|
|
4
|
+
# requireApproval 数组可列出需 approval 门的执行器(默认空 = 不要求)。
|
|
5
|
+
|
|
6
|
+
- id: tool-config-engine
|
|
7
|
+
name: ./engine/tool-config-engine.mjs
|
|
8
|
+
config:
|
|
9
|
+
configsDir: ../custom-tools
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# module: tool-filter
|
|
2
|
+
# source: engine/compositions/source/local/tool-filter.yml (本地附加,非官方模块)
|
|
3
|
+
# 主会话常驻工具白名单/黑名单:作用于任意注册工具(含第三方插件工具)。
|
|
4
|
+
# config 由参数桥按 params.toolFilterAllow/Deny/Subagents 合并;
|
|
5
|
+
# 未声明 = 不过滤(官方默认),见 tool-filter.mjs。
|
|
6
|
+
|
|
7
|
+
- id: tool-filter
|
|
8
|
+
name: ./engine/tool-filter.mjs
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cot-drip — 执行期深思维持节拍(上游 dsh-anchored-standard 移植, MIT)。
|
|
3
|
+
*
|
|
4
|
+
* 锚定模式在每轮开场有深思考,但长工具循环中深思会衰减(后期步骤退化
|
|
5
|
+
* 回 "Let me…")。本模块每 N 次工具结果向对话滴入一条短 user 提醒——
|
|
6
|
+
* 永不阻塞、永不报错、不碰工具目录:
|
|
7
|
+
*
|
|
8
|
+
* `tools/post-execute` → { kind: 'accept', additionalContexts: [notice] }
|
|
9
|
+
*
|
|
10
|
+
* harness 把 additionalContexts 作为 durable user 消息追加在整批工具结果
|
|
11
|
+
* 之后,落在下一请求的规划节拍位(与 Code Mode 嵌套上下文同形状)。
|
|
12
|
+
* 模型读到后以一句 "We …" 重申剩余目标并继续。
|
|
13
|
+
*
|
|
14
|
+
* 节奏刻意温和:默认 every: 4 次结果、每轮最多 maxPerTurn: 1 条;
|
|
15
|
+
* every: 0 禁用。计数在 await 前同步自增(并行调用无法竞态越过节奏);
|
|
16
|
+
* 轮边界由 durable turn/start + assistant/chunk 双路跟踪(无轮号时降级
|
|
17
|
+
* session 全局);子代理默认不滴(brief 即计划)。
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { booleanOption, validateConfig } from './shared.mjs'
|
|
21
|
+
|
|
22
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
23
|
+
export const name = 'cot-drip'
|
|
24
|
+
|
|
25
|
+
/** 默认提醒文本——一条规划节拍,措辞维持 "We" 语态。 */
|
|
26
|
+
export const DRIP_TEXT = [
|
|
27
|
+
'Progress check: before the next action, restate in one "We …" sentence what remains of the goal and why the next step is the right one.',
|
|
28
|
+
].join(' ')
|
|
29
|
+
|
|
30
|
+
/** Every config key this plugin accepts — anything else is a typo. */
|
|
31
|
+
const ALLOWED_KEYS = new Set(['enabled', 'every', 'maxPerTurn', 'includeSubagents', 'text'])
|
|
32
|
+
|
|
33
|
+
function parseCounter(value, field, fallback, minimum) {
|
|
34
|
+
if (value === undefined) return fallback
|
|
35
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
36
|
+
throw new TypeError(`${name}: ${field} must be an integer >= ${minimum}; got ${JSON.stringify(value)}`)
|
|
37
|
+
}
|
|
38
|
+
return value
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 注册执行后深思滴入。 */
|
|
42
|
+
export function apply(ctx, config) {
|
|
43
|
+
const source = validateConfig(name, config, ALLOWED_KEYS)
|
|
44
|
+
if (source.enabled === false) return
|
|
45
|
+
const every = parseCounter(source.every, 'every', 4, 0)
|
|
46
|
+
const maxPerTurn = parseCounter(source.maxPerTurn, 'maxPerTurn', 1, 1)
|
|
47
|
+
const includeSubagents = booleanOption(name, source.includeSubagents, 'includeSubagents', false)
|
|
48
|
+
const text = typeof source.text === 'string' && source.text.length > 0 ? source.text : DRIP_TEXT
|
|
49
|
+
|
|
50
|
+
/** sessionId -> { results, drips, lastTurn } — 每轮计数。 */
|
|
51
|
+
const state = new Map()
|
|
52
|
+
|
|
53
|
+
const countersOf = (sessionId) => {
|
|
54
|
+
let entry = state.get(sessionId)
|
|
55
|
+
if (entry === undefined) {
|
|
56
|
+
entry = { results: 0, drips: 0, lastTurn: undefined }
|
|
57
|
+
state.set(sessionId, entry)
|
|
58
|
+
}
|
|
59
|
+
return entry
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let warned = false
|
|
63
|
+
const warnOnce = (message) => {
|
|
64
|
+
if (warned) return
|
|
65
|
+
warned = true
|
|
66
|
+
try {
|
|
67
|
+
ctx.logger.warn(message)
|
|
68
|
+
} catch {
|
|
69
|
+
// Logger unavailable — the guard exists only to avoid spamming.
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 轮跟踪:turn/start 重置计数;无可用轮号时用 assistant/chunk 的新轮。
|
|
74
|
+
ctx.on('session/event', (session, event) => {
|
|
75
|
+
if (session === undefined || session.id === undefined) return
|
|
76
|
+
if (event.type === 'turn/start') {
|
|
77
|
+
const turn = event.data?.turn
|
|
78
|
+
const entry = countersOf(session.id)
|
|
79
|
+
if (entry.lastTurn !== turn) {
|
|
80
|
+
entry.lastTurn = typeof turn === 'number' ? turn : entry.lastTurn
|
|
81
|
+
entry.results = 0
|
|
82
|
+
entry.drips = 0
|
|
83
|
+
}
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
if (event.type === 'assistant/chunk') {
|
|
87
|
+
const turn = event.data?.turn
|
|
88
|
+
if (typeof turn !== 'number') return
|
|
89
|
+
const entry = countersOf(session.id)
|
|
90
|
+
if (entry.lastTurn === undefined || turn > entry.lastTurn) {
|
|
91
|
+
entry.lastTurn = turn
|
|
92
|
+
entry.results = 0
|
|
93
|
+
entry.drips = 0
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
ctx.on('tools/post-execute', async (exec, result, next) => {
|
|
99
|
+
// await 前同步计数,并行调用不能竞态越过节奏。
|
|
100
|
+
const session = exec?.agent?.session
|
|
101
|
+
const eligible = session !== undefined
|
|
102
|
+
&& session.id !== undefined
|
|
103
|
+
&& (includeSubagents || (session.header?.delegationDepth ?? 0) === 0)
|
|
104
|
+
const entry = eligible ? countersOf(session.id) : undefined
|
|
105
|
+
if (entry !== undefined) entry.results += 1
|
|
106
|
+
const due = entry !== undefined
|
|
107
|
+
&& every > 0
|
|
108
|
+
&& entry.results % every === 0
|
|
109
|
+
&& entry.drips < maxPerTurn
|
|
110
|
+
|
|
111
|
+
const decision = await next()
|
|
112
|
+
try {
|
|
113
|
+
if (!due || decision?.kind !== 'accept') return decision
|
|
114
|
+
entry.drips += 1
|
|
115
|
+
const notice = {
|
|
116
|
+
id: typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
117
|
+
? crypto.randomUUID()
|
|
118
|
+
: `cot-drip-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
119
|
+
role: 'user',
|
|
120
|
+
content: [{ type: 'text', text }],
|
|
121
|
+
source: {
|
|
122
|
+
kind: 'plugin',
|
|
123
|
+
plugin: name,
|
|
124
|
+
form: 'notice',
|
|
125
|
+
summary: 'deliberation maintenance beat',
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
...decision,
|
|
130
|
+
additionalContexts: [...(decision.additionalContexts ?? []), notice],
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
warnOnce(`${name}: drip injection failed, keeping the plain result: ${String((error && error.message) || error)}`)
|
|
134
|
+
return decision
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
}
|
package/engine/custom-bash.mjs
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
import { access } from 'node:fs/promises'
|
|
45
45
|
import { dirname, join } from 'node:path'
|
|
46
|
+
import { validateConfig } from './shared.mjs'
|
|
46
47
|
|
|
47
48
|
/** Cordis plugin name used by loader diagnostics. */
|
|
48
49
|
export const name = 'custom-bash'
|
|
@@ -53,6 +54,9 @@ export const inject = ['subprocess', 'tools']
|
|
|
53
54
|
const DEFAULT_TIMEOUT_MS = 120000
|
|
54
55
|
const DEFAULT_MAX_OUTPUT_BYTES = 64000
|
|
55
56
|
|
|
57
|
+
/** Every config key this plugin accepts — anything else is a typo. */
|
|
58
|
+
const ALLOWED_KEYS = new Set(['bashPath', 'timeoutMs', 'maxOutputBytes'])
|
|
59
|
+
|
|
56
60
|
/**
|
|
57
61
|
* Git Bash candidate paths, in probe order (see the header): the `git`
|
|
58
62
|
* executable's install root first, then the well-known env-derived roots.
|
|
@@ -81,6 +85,24 @@ export function bashCandidates(env, gitExe) {
|
|
|
81
85
|
return [...new Set(candidates)]
|
|
82
86
|
}
|
|
83
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Git Bash / MSYS drive path (`/e/foo`) → Windows path (`E:\foo`).
|
|
90
|
+
* Unix paths like `/usr/bin` or `/tmp` are left unchanged. Exported for tests.
|
|
91
|
+
* (上游 anchored-standard 移植, issue #72)
|
|
92
|
+
*/
|
|
93
|
+
export function normalizeGitBashWorkdir(workdir, platform = process.platform) {
|
|
94
|
+
if (typeof workdir !== 'string' || workdir.length === 0) return workdir
|
|
95
|
+
if (platform !== 'win32') return workdir
|
|
96
|
+
const match = workdir.match(/^\/([a-zA-Z])(?:\/(.*))?$/)
|
|
97
|
+
if (!match) return workdir
|
|
98
|
+
const rest = match[2] ? match[2].replace(/\//g, '\\') : ''
|
|
99
|
+
return rest ? `${match[1].toUpperCase()}:\\${rest}` : `${match[1].toUpperCase()}:\\`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isWorkdirEnoent(error) {
|
|
103
|
+
return /\bENOENT\b/.test(String((error && error.message) || error || ''))
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
/** Tool parameter schema for the model-facing command. */
|
|
85
107
|
const commandSchema = {
|
|
86
108
|
type: 'object',
|
|
@@ -91,7 +113,7 @@ const commandSchema = {
|
|
|
91
113
|
},
|
|
92
114
|
workdir: {
|
|
93
115
|
type: 'string',
|
|
94
|
-
description: 'Optional working directory; defaults to the session cwd.',
|
|
116
|
+
description: 'Optional working directory; defaults to the session cwd. On Windows, Git Bash paths like /e/foo are accepted and converted to E:\\foo.',
|
|
95
117
|
},
|
|
96
118
|
},
|
|
97
119
|
required: ['command'],
|
|
@@ -100,9 +122,10 @@ const commandSchema = {
|
|
|
100
122
|
|
|
101
123
|
/** Register the model-facing `bash` tool. */
|
|
102
124
|
export function apply(ctx, config) {
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
const
|
|
125
|
+
const source = validateConfig(name, config, ALLOWED_KEYS)
|
|
126
|
+
const explicitBashPath = typeof source.bashPath === 'string' && source.bashPath.length > 0 ? source.bashPath : undefined
|
|
127
|
+
const timeoutMs = Number.isSafeInteger(source.timeoutMs) && source.timeoutMs > 0 ? source.timeoutMs : DEFAULT_TIMEOUT_MS
|
|
128
|
+
const maxOutputBytes = Number.isSafeInteger(source.maxOutputBytes) && source.maxOutputBytes > 0 ? source.maxOutputBytes : DEFAULT_MAX_OUTPUT_BYTES
|
|
106
129
|
|
|
107
130
|
// The inferred executable is memoized per plugin instance: candidate probing
|
|
108
131
|
// walks the filesystem, and the answer cannot change within a mount. A
|
|
@@ -159,6 +182,7 @@ export function apply(ctx, config) {
|
|
|
159
182
|
"* To inspect a particular line range of a file, e.g. lines 10-25, try 'sed -n 10,25p /path/to/the/file'.",
|
|
160
183
|
'* Please avoid commands that may produce a very large amount of output.',
|
|
161
184
|
'* NOTE: runs without OS sandbox confinement on Windows (no landlock); treat output as untrusted.',
|
|
185
|
+
'* workdir accepts a Windows path or a Git Bash drive path (/e/foo).',
|
|
162
186
|
].join('\n'),
|
|
163
187
|
parameters: commandSchema,
|
|
164
188
|
output: {
|
|
@@ -174,9 +198,24 @@ export function apply(ctx, config) {
|
|
|
174
198
|
},
|
|
175
199
|
async execute(args, exec) {
|
|
176
200
|
const shell = await resolveShell(exec?.signal)
|
|
177
|
-
const
|
|
201
|
+
const sessionCwd = exec?.agent?.session?.header?.cwd
|
|
202
|
+
const requested = typeof args.workdir === 'string' && args.workdir.length > 0
|
|
178
203
|
? args.workdir
|
|
179
|
-
:
|
|
204
|
+
: sessionCwd
|
|
205
|
+
const workdir = normalizeGitBashWorkdir(requested)
|
|
206
|
+
|
|
207
|
+
// spawnOnce:单次 spawn 封装(ENOENT 回退重试复用同一配置)。
|
|
208
|
+
const spawnOnce = (cwd) => ctx.subprocess.spawn({
|
|
209
|
+
argv: [shell, '-c', args.command],
|
|
210
|
+
...cwd !== undefined ? { cwd } : {},
|
|
211
|
+
stdio: {
|
|
212
|
+
stdin: 'ignore',
|
|
213
|
+
stdout: { maxBytes: maxOutputBytes },
|
|
214
|
+
stderr: { maxBytes: maxOutputBytes },
|
|
215
|
+
},
|
|
216
|
+
signal: abort.signal,
|
|
217
|
+
graceMs: 3000,
|
|
218
|
+
})
|
|
180
219
|
|
|
181
220
|
// timeoutMs is a foreground deadline. subprocess only reacts to an abort
|
|
182
221
|
// signal, so we own the classification here: a timeout aborts the tree
|
|
@@ -191,25 +230,35 @@ export function apply(ctx, config) {
|
|
|
191
230
|
if (exec?.signal?.aborted) onExecAbort()
|
|
192
231
|
else exec?.signal?.addEventListener('abort', onExecAbort, { once: true })
|
|
193
232
|
|
|
233
|
+
let handle
|
|
194
234
|
let outcome
|
|
235
|
+
let usedFallback = false
|
|
195
236
|
try {
|
|
196
|
-
const handle = ctx.subprocess.spawn({
|
|
197
|
-
argv: [shell, '-c', args.command],
|
|
198
|
-
...workdir !== undefined ? { cwd: workdir } : {},
|
|
199
|
-
stdio: {
|
|
200
|
-
stdin: 'ignore',
|
|
201
|
-
stdout: { maxBytes: maxOutputBytes },
|
|
202
|
-
stderr: { maxBytes: maxOutputBytes },
|
|
203
|
-
},
|
|
204
|
-
signal: abort.signal,
|
|
205
|
-
graceMs: 3000,
|
|
206
|
-
})
|
|
207
237
|
try {
|
|
238
|
+
handle = spawnOnce(workdir)
|
|
208
239
|
outcome = await handle.done
|
|
209
240
|
} catch (error) {
|
|
210
|
-
//
|
|
211
|
-
//
|
|
212
|
-
|
|
241
|
+
// workdir 归一化后仍 ENOENT(如 /e/foo 目录不存在)→ 回退会话 cwd 重试
|
|
242
|
+
// (上游 anchored-standard issue #72)。
|
|
243
|
+
const fallback = sessionCwd !== undefined ? normalizeGitBashWorkdir(sessionCwd) : undefined
|
|
244
|
+
if (
|
|
245
|
+
requested !== undefined
|
|
246
|
+
&& fallback !== undefined
|
|
247
|
+
&& fallback !== workdir
|
|
248
|
+
&& isWorkdirEnoent(error)
|
|
249
|
+
) {
|
|
250
|
+
usedFallback = true
|
|
251
|
+
try {
|
|
252
|
+
handle = spawnOnce(fallback)
|
|
253
|
+
outcome = await handle.done
|
|
254
|
+
} catch (retryError) {
|
|
255
|
+
throw new Error(`bash spawn failed: ${String(retryError)}`)
|
|
256
|
+
}
|
|
257
|
+
} else {
|
|
258
|
+
// A spawn-level failure (bad executable, EPERM) surfaces as a throw,
|
|
259
|
+
// which the runtime turns into an isError result.
|
|
260
|
+
throw new Error(`bash spawn failed: ${String(error)}`)
|
|
261
|
+
}
|
|
213
262
|
}
|
|
214
263
|
let stdout = ''
|
|
215
264
|
let stderr = ''
|
|
@@ -219,8 +268,11 @@ export function apply(ctx, config) {
|
|
|
219
268
|
} catch {
|
|
220
269
|
// Collected readers may be unavailable on some backends; tolerate.
|
|
221
270
|
}
|
|
271
|
+
const note = usedFallback
|
|
272
|
+
? `[custom-bash] workdir ${requested} was unusable (ENOENT); fell back to session cwd\n`
|
|
273
|
+
: ''
|
|
222
274
|
const text = [stdout, stderr].filter((part) => part.length > 0).join('\n')
|
|
223
|
-
const tail = text.length > 0 ? text : `exit code: ${outcome.exitCode} (no output)`
|
|
275
|
+
const tail = note + (text.length > 0 ? text : `exit code: ${outcome.exitCode} (no output)`)
|
|
224
276
|
if (timedOut) {
|
|
225
277
|
throw new Error(`bash timed out after ${timeoutMs}ms${tail ? `\n${tail}` : ''}`)
|
|
226
278
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* deliberation-gate — 轨迹深度门(上游 dsh-anchored-standard 移植, MIT)。
|
|
3
|
+
*
|
|
4
|
+
* 完整目录保持可见可调(会话观感正常),但首工具调用前深思不足就拒绝
|
|
5
|
+
* 一次。以 durable `assistant/chunk` 的 text/reasoning delta 累计长度作为
|
|
6
|
+
* "深思深度"代理(adapter 无 logprobs,观测层只有流式 chunk)。
|
|
7
|
+
*
|
|
8
|
+
* 行为:
|
|
9
|
+
* - session/event 累计每轮流式长度;tools/pre-execute 时当前轮深度
|
|
10
|
+
* < minChars(默认 400)→ { kind:'deny', reason: gateText }(规划式
|
|
11
|
+
* 提示,措辞明示"非工具失败"),每轮最多 maxGatesPerTurn 次(默认 1)。
|
|
12
|
+
* - retry 把强制深思带进历史;无流式文本的轮 = 深度 0,恰好门一次
|
|
13
|
+
* (fail-safe 向更多深思)。
|
|
14
|
+
* - 冷启动从 durable log 扫描(重启保持深度);turn map 修剪
|
|
15
|
+
* (MAX_TRACKED_TURNS=8);deny 前无 await(并行调用无法竞态越过预算)。
|
|
16
|
+
* - 子代理默认不门控(brief 即计划);includeSubagents: true 同门控。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { booleanOption, validateConfig } from './shared.mjs'
|
|
20
|
+
|
|
21
|
+
/** Cordis plugin name used by loader diagnostics. */
|
|
22
|
+
export const name = 'deliberation-gate'
|
|
23
|
+
|
|
24
|
+
/** 默认深思下限(每轮首工具调用前,字符数)。 */
|
|
25
|
+
export const DEFAULT_MIN_CHARS = 400
|
|
26
|
+
|
|
27
|
+
/** 默认门控指令(拒绝回传的规划提示)。 */
|
|
28
|
+
export const GATE_TEXT = [
|
|
29
|
+
'Deliberation gate: this turn has not shown its reasoning yet.',
|
|
30
|
+
'Before retrying this tool call, write out your full reasoning in your reply — start with "We", restate the goal, weigh the approaches, and lay out the concrete steps and risks — then issue the tool call again.',
|
|
31
|
+
'This message is a planning prompt, not a tool failure.',
|
|
32
|
+
].join(' ')
|
|
33
|
+
|
|
34
|
+
/** 每会话最多保留的轮次深度状态。 */
|
|
35
|
+
const MAX_TRACKED_TURNS = 8
|
|
36
|
+
|
|
37
|
+
/** Every config key this plugin accepts — anything else is a typo. */
|
|
38
|
+
const ALLOWED_KEYS = new Set(['enabled', 'minChars', 'maxGatesPerTurn', 'includeSubagents', 'gateText'])
|
|
39
|
+
|
|
40
|
+
function parseCounter(value, field, fallback, minimum) {
|
|
41
|
+
if (value === undefined) return fallback
|
|
42
|
+
if (!Number.isInteger(value) || value < minimum) {
|
|
43
|
+
throw new TypeError(`${name}: ${field} must be an integer >= ${minimum}; got ${JSON.stringify(value)}`)
|
|
44
|
+
}
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 注册轨迹深度门。 */
|
|
49
|
+
export function apply(ctx, config) {
|
|
50
|
+
const source = validateConfig(name, config, ALLOWED_KEYS)
|
|
51
|
+
if (source.enabled === false) return
|
|
52
|
+
const minChars = parseCounter(source.minChars, 'minChars', DEFAULT_MIN_CHARS, 0)
|
|
53
|
+
const maxGatesPerTurn = parseCounter(source.maxGatesPerTurn, 'maxGatesPerTurn', 1, 1)
|
|
54
|
+
const includeSubagents = booleanOption(name, source.includeSubagents, 'includeSubagents', false)
|
|
55
|
+
const gateText = typeof source.gateText === 'string' && source.gateText.length > 0 ? source.gateText : GATE_TEXT
|
|
56
|
+
|
|
57
|
+
/** sessionId -> { turns: Map<turn, { chars, gates }>, lastTurn } */
|
|
58
|
+
const state = new Map()
|
|
59
|
+
|
|
60
|
+
/** 实时路径:每条流式 chunk 扩展轮条目。 */
|
|
61
|
+
const observeChunk = (sessionId, turn, textLength) => {
|
|
62
|
+
let entry = state.get(sessionId)
|
|
63
|
+
if (entry === undefined) {
|
|
64
|
+
entry = { turns: new Map(), lastTurn: turn }
|
|
65
|
+
state.set(sessionId, entry)
|
|
66
|
+
}
|
|
67
|
+
let turnEntry = entry.turns.get(turn)
|
|
68
|
+
if (turnEntry === undefined) {
|
|
69
|
+
// 修剪旧轮,长会话不累积状态。
|
|
70
|
+
if (entry.turns.size >= MAX_TRACKED_TURNS) {
|
|
71
|
+
const oldest = [...entry.turns.keys()].sort((a, b) => a - b).slice(0, entry.turns.size - MAX_TRACKED_TURNS + 1)
|
|
72
|
+
for (const key of oldest) entry.turns.delete(key)
|
|
73
|
+
}
|
|
74
|
+
turnEntry = { chars: 0, gates: 0 }
|
|
75
|
+
entry.turns.set(turn, turnEntry)
|
|
76
|
+
}
|
|
77
|
+
turnEntry.chars += textLength
|
|
78
|
+
if (turn > entry.lastTurn) entry.lastTurn = turn
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** 会话深度状态;首见冷扫 durable log(重启保持已流式深度)。 */
|
|
82
|
+
const depthOf = (session) => {
|
|
83
|
+
let entry = state.get(session.id)
|
|
84
|
+
if (entry === undefined) {
|
|
85
|
+
entry = { turns: new Map(), lastTurn: -1 }
|
|
86
|
+
if (Array.isArray(session.events)) {
|
|
87
|
+
for (const event of session.events) {
|
|
88
|
+
if (event.type !== 'assistant/chunk') continue
|
|
89
|
+
const turn = event.data?.turn
|
|
90
|
+
if (typeof turn !== 'number' || !Number.isFinite(turn)) continue
|
|
91
|
+
const text = event.data?.chunk?.text
|
|
92
|
+
observeChunk(session.id, turn, typeof text === 'string' ? text.length : 0)
|
|
93
|
+
}
|
|
94
|
+
entry = state.get(session.id) ?? entry
|
|
95
|
+
}
|
|
96
|
+
if (entry.turns.size === 0) {
|
|
97
|
+
// 无任何流式文本:深度视为 0 于哨兵轮,会话恰好门一次后放行。
|
|
98
|
+
entry.turns.set(entry.lastTurn, { chars: 0, gates: 0 })
|
|
99
|
+
}
|
|
100
|
+
state.set(session.id, entry)
|
|
101
|
+
}
|
|
102
|
+
return entry
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 深度代理:累计 durable assistant/chunk 的流式长度。
|
|
106
|
+
ctx.on('session/event', (session, event) => {
|
|
107
|
+
if (event.type !== 'assistant/chunk') return
|
|
108
|
+
const turn = event.data?.turn
|
|
109
|
+
if (typeof turn !== 'number' || !Number.isFinite(turn)) return
|
|
110
|
+
const text = event.data?.chunk?.text
|
|
111
|
+
observeChunk(session.id, turn, typeof text === 'string' ? text.length : 0)
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
// 门:同步决策,当前轮累计深度 < minChars 时最多 deny maxGatesPerTurn 次。
|
|
115
|
+
ctx.on('tools/pre-execute', (exec, next) => {
|
|
116
|
+
const session = exec?.agent?.session
|
|
117
|
+
if (session === undefined || session.id === undefined) return next()
|
|
118
|
+
if (!includeSubagents && (session.header?.delegationDepth ?? 0) > 0) return next()
|
|
119
|
+
const entry = depthOf(session)
|
|
120
|
+
const turnEntry = entry.turns.get(entry.lastTurn)
|
|
121
|
+
if (turnEntry === undefined) return next()
|
|
122
|
+
if (turnEntry.gates >= maxGatesPerTurn) return next()
|
|
123
|
+
if (turnEntry.chars >= minChars) return next()
|
|
124
|
+
turnEntry.gates += 1
|
|
125
|
+
return { kind: 'deny', reason: gateText }
|
|
126
|
+
})
|
|
127
|
+
}
|