dsh-plugin-prompt-tool 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -233
- package/engine/actions/append-context.mjs +3 -24
- package/engine/actions/assembly.mjs +70 -94
- package/engine/actions/catalog.mjs +65 -28
- package/engine/actions/content.mjs +8 -152
- package/engine/actions/examples.mjs +8 -4
- package/engine/actions/inbox-prepend.mjs +4 -5
- package/engine/actions/index.mjs +17 -9
- package/engine/actions/inject-text.mjs +24 -4
- package/engine/actions/pre-step-filter.mjs +3 -4
- package/engine/actions/registration.mjs +20 -28
- package/engine/actions/request-params.mjs +21 -1
- package/engine/actions/shared.mjs +22 -13
- package/engine/actions/stream-output.mjs +26 -0
- package/engine/actions/tool-gate.mjs +31 -0
- package/engine/actions/tool-result.mjs +36 -0
- package/engine/actions/tool-visibility.mjs +248 -0
- package/engine/agent-capability.mjs +67 -0
- package/engine/anchor-match.mjs +14 -21
- package/engine/audience.d.mts +2 -0
- package/engine/audience.mjs +102 -0
- package/engine/branch.d.mts +4 -0
- package/engine/branch.mjs +61 -18
- package/engine/compaction-epoch.d.mts +25 -0
- package/engine/compaction-epoch.mjs +27 -4
- package/engine/compositions/source/local/filesystem-editor.yml +3 -3
- package/engine/compositions/source/local/instruction-hint.yml +2 -4
- package/engine/compositions/source/local/module-file-tools.yml +2 -0
- package/engine/compositions/source/local/module-import-tools.yml +4 -0
- package/engine/compositions/source/local/module-manifest-tools.yml +2 -0
- package/engine/compositions/source/local/module-rule-tools.yml +5 -0
- package/engine/compositions/source/local/module-variable-tools.yml +5 -0
- package/engine/compositions/source/local/present-as.yml +26 -0
- package/engine/compositions/source/local/tool-config-engine.yml +2 -3
- package/engine/compositions/source/local/tool-search.yml +11 -0
- package/engine/condition-facts.mjs +84 -0
- package/engine/conditions/anchor.mjs +63 -23
- package/engine/conditions/availability.mjs +1 -1
- package/engine/conditions/composite.mjs +3 -3
- package/engine/conditions/count.mjs +42 -11
- package/engine/conditions/evaluation.mjs +74 -19
- package/engine/conditions/examples.mjs +2 -1
- package/engine/conditions/index.d.mts +15 -0
- package/engine/conditions/index.mjs +23 -9
- package/engine/conditions/operators.mjs +4 -0
- package/engine/conditions/phase.mjs +9 -3
- package/engine/conditions/preset.mjs +1 -1
- package/engine/conditions/session.mjs +8 -2
- package/engine/conditions/subject.mjs +144 -65
- package/engine/conditions/text.mjs +26 -8
- package/engine/conditions/variable.mjs +83 -0
- package/engine/context-activation.d.mts +11 -0
- package/engine/{st-world-book.mjs → context-activation.mjs} +104 -64
- package/engine/delivery.d.mts +2 -0
- package/engine/delivery.mjs +93 -0
- package/engine/discovery.d.mts +38 -0
- package/engine/discovery.mjs +146 -0
- package/engine/env-safety.mjs +14 -0
- package/engine/executor.d.mts +28 -0
- package/engine/executor.mjs +290 -205
- package/engine/fillers.mjs +39 -8
- package/engine/fixed-registration.mjs +12 -0
- package/engine/history.mjs +71 -0
- package/engine/instruction-blocks.d.mts +13 -0
- package/engine/instruction-blocks.mjs +21 -0
- package/engine/instruction-files.d.mts +5 -0
- package/engine/instruction-files.mjs +97 -0
- package/engine/instruction-hint.mjs +69 -103
- package/engine/interpolate.d.mts +7 -0
- package/engine/interpolate.mjs +44 -13
- package/engine/layers.mjs +163 -227
- package/engine/machine-paths.mjs +40 -0
- package/engine/module-file-tools.mjs +8 -0
- package/engine/module-import-tools.mjs +10 -0
- package/engine/module-manifest-tools.mjs +8 -0
- package/engine/module-rule-tools.mjs +10 -0
- package/engine/module-variable-tools.mjs +10 -0
- package/engine/order.d.mts +14 -0
- package/engine/order.mjs +0 -3
- package/engine/path-safety.d.mts +2 -0
- package/engine/path-safety.mjs +58 -0
- package/engine/pre-step-source.d.mts +69 -0
- package/engine/pre-step-source.mjs +103 -0
- package/engine/present-as.mjs +77 -0
- package/engine/promotion-pool.d.mts +9 -0
- package/engine/promotion-pool.mjs +69 -0
- package/engine/record.mjs +2 -0
- package/engine/rule-engine.mjs +3 -2
- package/engine/rule-facts.d.mts +11 -0
- package/engine/rule-facts.mjs +60 -0
- package/engine/rule-runtime.d.mts +46 -0
- package/engine/rule-runtime.mjs +98 -13
- package/engine/rule-spec.d.mts +106 -0
- package/engine/rule-spec.mjs +301 -27
- package/engine/run-code-env.mjs +73 -19
- package/engine/schema.mjs +165 -243
- package/engine/sdk-strip.mjs +2 -2
- package/engine/service-names.d.mts +10 -0
- package/engine/service-names.mjs +10 -0
- package/engine/shared.d.mts +21 -0
- package/engine/shared.mjs +46 -13
- package/engine/skill-search.mjs +125 -53
- package/engine/source-kind.d.mts +9 -0
- package/engine/source-kind.mjs +16 -0
- package/engine/strategy-ids.mjs +5 -0
- package/engine/template-evaluation.d.mts +16 -0
- package/engine/template-evaluation.mjs +178 -0
- package/engine/template-frame.d.mts +16 -0
- package/engine/template-frame.mjs +69 -0
- package/engine/tool-config-engine.mjs +38 -34
- package/engine/tool-definition.d.mts +4 -0
- package/engine/tool-definition.mjs +5 -1
- package/engine/tool-git-bash.mjs +5 -1
- package/engine/tool-scope.mjs +22 -0
- package/engine/{dev-tool-search.mjs → tool-search.mjs} +63 -65
- package/engine/trigger.mjs +15 -270
- package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/engine/vendor/yaml/dist/nodes/Alias.js +25 -23
- package/engine/vendor/yaml/package.json +1 -1
- package/lib/client.js +8312 -6724
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +16 -2120
- package/lib/index.mjs +26914 -5816
- package/modules/ponytail/module.yml +83 -203
- package/modules/ptc-mode/module.yml +45 -0
- package/modules/soul/assets/core-hard.md +6 -0
- package/modules/soul/assets/criteria.md +12 -0
- package/modules/soul/assets/echo.md +12 -0
- package/modules/soul/assets/layers.md +11 -0
- package/modules/soul/assets/ledger.md +9 -0
- package/modules/soul/assets/manifest.yml +20 -0
- package/modules/soul/assets/reference/00-layers.md +27 -0
- package/modules/soul/assets/reference/01-criteria.md +32 -0
- package/modules/soul/assets/reference/02-echo.md +158 -0
- package/modules/soul/assets/reference/03-vital.md +183 -0
- package/modules/soul/assets/reference/04-manifest.json +106 -0
- package/modules/soul/assets/reference/05-doctor.md +67 -0
- package/modules/soul/assets/reference/06-reference.md +189 -0
- package/modules/soul/assets/reference/07-bundle.json +431 -0
- package/modules/soul/assets/reference/08-/350/270/251/345/235/221/345/256/236/345/275/225.md +59 -0
- package/modules/soul/assets/reference/09-dev-workflow.md +109 -0
- package/modules/soul/assets/reference/10-echo-anatomy.md +100 -0
- package/modules/soul/assets/reference/11-self.md +80 -0
- package/modules/soul/assets/reference/12-ledger.md +159 -0
- package/modules/soul/assets/reference/README.md +10 -0
- package/modules/soul/assets/templates/agents-template.md +6 -0
- package/modules/soul/assets/vital.md +9 -0
- package/modules/soul/module.yml +311 -0
- package/modules/superpowers/module.yml +122 -0
- package/modules/tool-surface/module.yml +51 -47
- package/package.json +37 -40
- package/skills/dsh-module/SKILL.md +67 -147
- package/skills/dsh-module/reference.md +113 -86
- package/templates/10-pre-step.yml +2 -3
- package/templates/14-first-turn-anchor.yml +10 -14
- package/templates/15-guide-auto.yml +32 -28
- package/templates/16-custom-fallback.yml +33 -21
- package/templates/20-system-section.yml +1 -4
- package/templates/50-llm-stream.yml +4 -10
- package/templates/60-tool-pipeline.yml +12 -10
- package/templates/63-append-context.yml +4 -6
- package/templates/65-turn-stop.yml +2 -2
- package/templates/67-inbox-prepend.yml +2 -2
- package/templates/70-subagent-maintenance.yml +9 -10
- package/templates/75-subagent-end.yml +2 -2
- package/templates/80-tool-surface.yml +41 -57
- package/templates/tools/40-delegate-module-import.yml +22 -0
- package/templates/tools/50-delegate-module-rules.yml +14 -0
- package/templates/tools/60-delegate-module-variables.yml +17 -0
- package/templates/tools/70-delegate-module-rules-edit.yml +31 -0
- package/engine/actions/decision.mjs +0 -54
- package/engine/actions/guard.mjs +0 -80
- package/engine/actions/sdk-strip.mjs +0 -30
- package/engine/character-tools.mjs +0 -8
- package/engine/classify-task.mjs +0 -53
- package/engine/compositions/source/local/character-tools.yml +0 -5
- package/engine/compositions/source/local/dev-tool-search.yml +0 -11
- package/engine/compositions/source/local/session-var-tools.yml +0 -5
- package/engine/compositions/source/local/subagent-tool-policy.yml +0 -21
- package/engine/compositions/source/local/world-book-tools.yml +0 -5
- package/engine/condition.mjs +0 -96
- package/engine/declared-triggers.mjs +0 -93
- package/engine/session-var-tools.mjs +0 -8
- package/engine/session-vars.mjs +0 -54
- package/engine/st-macros.d.mts +0 -22
- package/engine/st-macros.mjs +0 -208
- package/engine/st-render.mjs +0 -109
- package/engine/st-world-book.d.mts +0 -10
- package/engine/subagent-tool-policy-core.mjs +0 -400
- package/engine/subagent-tool-policy.mjs +0 -352
- package/engine/trigger-spec.mjs +0 -173
- package/engine/world-book-tools.mjs +0 -8
- package/lib/migrate-rules.d.mts +0 -1
- package/lib/migrate-rules.mjs +0 -610
- package/lib/module-package-DqJZaE71.mjs +0 -2
- package/lib/module-package-q2gDRu9H.mjs +0 -2869
- package/lib/rule-builder-DMrJten6.mjs +0 -14767
- package/templates/61-deny-tools-presentation.yml +0 -27
- package/templates/62-guard-deny-tools.yml +0 -20
- package/templates/policies/legacy-defaults.yml +0 -34
- package/templates/tools/40-delegate-character.yml +0 -15
- package/templates/tools/50-delegate-worldbook.yml +0 -15
- package/templates/tools/60-delegate-session-var.yml +0 -15
- package/templates/tools/70-delegate-worldbook-upsert.yml +0 -36
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { TemplateSnapshot } from './template-evaluation.mjs'
|
|
2
|
+
export interface TemplateVariableStore {
|
|
3
|
+
read(moduleId: string): { module: Record<string, string>; global: Record<string, string>; enabled?: boolean; revisions: { module: string; global: string | null; settings?: string } }
|
|
4
|
+
commit(moduleId: string, request: { scope: 'global' | 'module'; changes: Record<string, string>; expectedRevisions: { module?: string; global?: string | null; settings?: string } }): { publicationPending?: boolean }
|
|
5
|
+
publish(moduleId: string): void
|
|
6
|
+
}
|
|
7
|
+
export interface TemplateConfig {
|
|
8
|
+
sourceModuleId: string
|
|
9
|
+
ruleId?: string
|
|
10
|
+
id: string
|
|
11
|
+
texts: string[]
|
|
12
|
+
variables?: TemplateSnapshot['module']
|
|
13
|
+
variableStore?: TemplateVariableStore
|
|
14
|
+
}
|
|
15
|
+
export function templateExecutionKey(agent: unknown, signal?: AbortSignal): string | undefined
|
|
16
|
+
export function renderTemplate(config: TemplateConfig, context?: { agent?: unknown; signal?: AbortSignal }, options?: { variableStore?: TemplateVariableStore; enabled?: boolean; warn?: (message: string) => void }): string
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/** 执行凭据独立于规则 mount;变量真值始终归显式宿主 adapter。 */
|
|
2
|
+
import { historyEvents } from './history.mjs'
|
|
3
|
+
import { evaluateTemplate } from './template-evaluation.mjs'
|
|
4
|
+
import { interpolateVariables, stripUnresolvedRefs } from './interpolate.mjs'
|
|
5
|
+
|
|
6
|
+
const frames = new WeakMap()
|
|
7
|
+
const MAX_CARDS = 4096
|
|
8
|
+
|
|
9
|
+
/** 官方循环先 assemble、再 pre-step、最后打开 step/start;两出口共享已提交边界。 */
|
|
10
|
+
export function templateExecutionKey(agent, signal) {
|
|
11
|
+
if (agent?.status !== 'running' || !signal || signal.aborted) return undefined
|
|
12
|
+
const events = historyEvents(agent.session)
|
|
13
|
+
const turn = events.findLast(event => event.type === 'turn/start' || event.type === 'turn/end')
|
|
14
|
+
if (turn?.type !== 'turn/start' || !Number.isSafeInteger(turn.seq)) return undefined
|
|
15
|
+
const step = events.findLast(event => event.type === 'step/start' || event.type === 'step/end')
|
|
16
|
+
if (step?.type === 'step/start' && step.seq > turn.seq) return undefined
|
|
17
|
+
return `${turn.seq}:${step?.seq ?? -1}`
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function renderTemplate(config, context = {}, { variableStore = config.variableStore, enabled = true, warn = () => {} } = {}) {
|
|
21
|
+
const text = config.texts.join('\n\n'), agent = context.agent, signal = context.signal
|
|
22
|
+
signal?.throwIfAborted()
|
|
23
|
+
if (!enabled) return stripUnresolvedRefs(interpolateVariables(text, {}, agent?.session, undefined, config.id)).text
|
|
24
|
+
const execution = templateExecutionKey(agent, signal)
|
|
25
|
+
let frame
|
|
26
|
+
if (execution !== undefined) {
|
|
27
|
+
frame = frames.get(agent)
|
|
28
|
+
if (frame?.execution !== execution) { frame = { execution, cards: new Map() }; frames.set(agent, frame) }
|
|
29
|
+
}
|
|
30
|
+
const identity = JSON.stringify([config.sourceModuleId, config.ruleId, config.id])
|
|
31
|
+
const previous = frame?.cards.get(identity)
|
|
32
|
+
const publish = entry => {
|
|
33
|
+
if (!entry.pending) return
|
|
34
|
+
try {
|
|
35
|
+
if (!variableStore?.publish) throw new Error('Variable publication adapter unavailable')
|
|
36
|
+
variableStore.publish(config.sourceModuleId)
|
|
37
|
+
entry.pending = false
|
|
38
|
+
} catch (error) { warn(`Template ${config.id}: variables saved; publication pending: ${String(error?.message ?? error)}`) }
|
|
39
|
+
}
|
|
40
|
+
if (previous) { publish(previous); return previous.text }
|
|
41
|
+
if (frame?.cards.size >= MAX_CARDS) throw new RangeError('Template execution card limit exceeded')
|
|
42
|
+
const samples = new Map()
|
|
43
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
44
|
+
const snapshot = variableStore?.read(config.sourceModuleId) ?? { module: config.variables ?? {}, global: {} }
|
|
45
|
+
if (snapshot.enabled === false) return stripUnresolvedRefs(interpolateVariables(text, {}, agent?.session, undefined, config.id)).text
|
|
46
|
+
const result = evaluateTemplate(text, { module: snapshot.module, global: snapshot.global, session: agent?.session,
|
|
47
|
+
sourceId: `${config.sourceModuleId ?? ''}:${config.id}`, readOnly: execution === undefined, samples })
|
|
48
|
+
const entry = { text: stripUnresolvedRefs(result.text).text, pending: false }
|
|
49
|
+
if (result.changes) {
|
|
50
|
+
if (!variableStore?.commit) throw new Error('Persistent template writes require a variable adapter')
|
|
51
|
+
signal?.throwIfAborted()
|
|
52
|
+
const domains = new Set([...result.reads, result.changes.scope])
|
|
53
|
+
const expectedRevisions = Object.fromEntries([...domains].map(domain => [domain, snapshot.revisions[domain]]))
|
|
54
|
+
if (snapshot.revisions.settings !== undefined) expectedRevisions.settings = snapshot.revisions.settings
|
|
55
|
+
try {
|
|
56
|
+
const saved = variableStore.commit(config.sourceModuleId, { scope: result.changes.scope, changes: result.changes.values, expectedRevisions })
|
|
57
|
+
entry.pending = saved.publicationPending === true
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error?.persisted === true) entry.pending = true
|
|
60
|
+
else if (error?.status === 409 && attempt < 2) continue
|
|
61
|
+
else throw error
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
frame?.cards.set(identity, entry)
|
|
65
|
+
if (entry.pending) warn(`Template ${config.id}: variables saved; publication pending`)
|
|
66
|
+
return entry.text
|
|
67
|
+
}
|
|
68
|
+
throw new Error('Template variable commit conflict')
|
|
69
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* delegate — 委托已注册工具(经 ctx.tools.execute 的 nested dispatch 走
|
|
15
15
|
* 完整官方管线:参数/输出校验、allow/deny/ask、pre/execute/post
|
|
16
16
|
* waterfall、timeout、nested token 与 durable result)
|
|
17
|
-
* fs — 文件操作(read/write/append/list/delete
|
|
17
|
+
* fs — 文件操作(read/write/append/list/delete,真实路径限定 cwd 内)
|
|
18
18
|
* ask-user — approval 通道询问用户(结果文本化)
|
|
19
19
|
*
|
|
20
20
|
* 安全:行 config.requireApproval = [kind...] 时该执行器先过 approval 门
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
* 工具注册经 disposer 契约(keepDisposer),插件卸载自动撤销。
|
|
23
23
|
*
|
|
24
24
|
* ── 能力提供者边界(T4)────────────────────────────────────────────────────
|
|
25
|
-
* 本模块是**能力提供者**,不是触发器。分类判据(与组合源 tool-config-engine.yml
|
|
26
|
-
*
|
|
25
|
+
* 本模块是**能力提供者**,不是触发器。分类判据(与组合源 tool-config-engine.yml
|
|
26
|
+
* 同一份措辞):
|
|
27
27
|
* - 会给模型提供可调用能力(注册工具 / 域 / 服务)的 → 能力提供者;
|
|
28
28
|
* - 干预流程(改提示词、改装配、裁决、追加)的 → 声明式触发器。
|
|
29
29
|
* 因此本模块**不接入** engine/trigger.mjs:它不订阅装配 waterfall,也没有
|
|
@@ -34,10 +34,10 @@
|
|
|
34
34
|
* - 配置声明走 fields.mjs 的 `defineConfig`(未知键在挂载期 fail loud);
|
|
35
35
|
* - 注册走 disposer 契约:单次注册用 shared.keepDisposer(register() 返回的正是
|
|
36
36
|
* 它自己的 effect disposer);需要整组原子回滚或定向撤销句柄时才用 ctx.effect;
|
|
37
|
-
* - 降级告警统一 `${name}: <what>; <fallback>`
|
|
37
|
+
* - 降级告警统一 `${name}: <what>; <fallback>` 一个前缀格式。
|
|
38
38
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
39
|
+
* 能力提供:把 module.yml 的 customTools 段注册进官方 tools registry;模型可见工具名
|
|
40
|
+
* 在装配期决定,引擎侧无法静态枚举(本模块不导出 provider 登记)。
|
|
41
41
|
*
|
|
42
42
|
* 文件归属校正(PLAN T4):第三个指定提供者 str-replace-editor 由
|
|
43
43
|
* engine/compositions/source/local/filesystem-editor.yml:20 引用**官方**包
|
|
@@ -46,11 +46,12 @@
|
|
|
46
46
|
*/
|
|
47
47
|
|
|
48
48
|
import { readFileSync, readdirSync, writeFileSync, appendFileSync, rmSync, statSync } from 'node:fs'
|
|
49
|
-
import { execFile } from 'node:child_process'
|
|
49
|
+
import { execFile, execFileSync } from 'node:child_process'
|
|
50
50
|
import { isAbsolute, join, resolve, sep } from 'node:path'
|
|
51
51
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
52
52
|
import { parse as parseYaml } from './vendor/yaml/index.js'
|
|
53
53
|
import { importHostPackage } from './host-package.mjs'
|
|
54
|
+
import { resolveRealWithin } from './path-safety.mjs'
|
|
54
55
|
import { validateDefinition } from './tool-definition.mjs'
|
|
55
56
|
import { defineConfig, passthrough } from './fields.mjs'
|
|
56
57
|
import { keepDisposer } from './shared.mjs'
|
|
@@ -68,6 +69,24 @@ const ENV_ALLOWLIST = [
|
|
|
68
69
|
'USERPROFILE', 'HOME', 'DSH_HOME', 'DSH_WORKSPACE', 'DSH_SESSION_ID',
|
|
69
70
|
]
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* shell 输出上限(stdout/stderr 各自):超限时 execFile 以
|
|
74
|
+
* ERR_CHILD_PROCESS_STDIO_MAXBUFFER 失败并保留截断输出,错误文本说明原因。
|
|
75
|
+
*/
|
|
76
|
+
const SHELL_MAX_OUTPUT_BYTES = 1024 * 1024
|
|
77
|
+
|
|
78
|
+
/** 超时尽量结束整棵子进程树;Windows 用 taskkill /T,失败再退回 child.kill。 */
|
|
79
|
+
function killShellTree(child) {
|
|
80
|
+
if (child === undefined || child.pid === undefined) return
|
|
81
|
+
if (process.platform === 'win32') {
|
|
82
|
+
try {
|
|
83
|
+
execFileSync('taskkill', ['/pid', String(child.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true })
|
|
84
|
+
return
|
|
85
|
+
} catch { /* taskkill 不可用时退回单进程 kill */ }
|
|
86
|
+
}
|
|
87
|
+
try { child.kill() } catch { /* 已退出 */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
/** 插值 {{args.x.y}} → 参数深取值(缺失保留字面)。 */
|
|
72
91
|
function interpolateArgs(text, args) {
|
|
73
92
|
return String(text ?? '').replace(/\{\{args\.([A-Za-z0-9_.]+)\}\}/g, (_whole, path) => {
|
|
@@ -163,16 +182,6 @@ async function approvalGate(ctx, exec, kind, reason) {
|
|
|
163
182
|
return { ok: false, message: `approval for tool ${exec.name} was ${outcome}` }
|
|
164
183
|
}
|
|
165
184
|
|
|
166
|
-
/** cwd 内路径限定(防越界;resolve 后必须位于 cwd 之下)。 */
|
|
167
|
-
function withinCwd(cwd, target) {
|
|
168
|
-
const resolved = resolve(cwd, target)
|
|
169
|
-
const root = resolve(cwd)
|
|
170
|
-
if (resolved !== root && !resolved.startsWith(root + sep)) {
|
|
171
|
-
throw new Error(`path ${JSON.stringify(target)} escapes the workspace`)
|
|
172
|
-
}
|
|
173
|
-
return resolved
|
|
174
|
-
}
|
|
175
|
-
|
|
176
185
|
/** delegate args 映射:完整引用({{args.x}})透传原始类型(数组/数字/布尔不字符串化);
|
|
177
186
|
* 部分引用(前缀-{{args.x}})插值为字符串;非插值值原样。 */
|
|
178
187
|
function mapDelegateArgs(execArgs, args) {
|
|
@@ -231,18 +240,25 @@ function createExecute(ctx, def, requireApproval) {
|
|
|
231
240
|
const env = { ...Object.fromEntries(ENV_ALLOWLIST.filter((key) => process.env[key] !== undefined).map((key) => [key, process.env[key]])), ...(exec.env ?? {}) }
|
|
232
241
|
const output = await new Promise((resolvePromise) => {
|
|
233
242
|
let killTimer
|
|
234
|
-
|
|
243
|
+
let timedOut = false
|
|
244
|
+
const child = execFile(shell, argv, { cwd, env, windowsHide: true, maxBuffer: SHELL_MAX_OUTPUT_BYTES }, (error, stdout, stderr) => {
|
|
235
245
|
clearTimeout(killTimer)
|
|
246
|
+
// 超时被杀与输出超限都不是普通退出码:error 文本必须能区分二者(不新增输出字段)。
|
|
247
|
+
const reason = error === null ? undefined
|
|
248
|
+
: timedOut ? `shell command timed out after ${def.timeoutMs} ms and was killed`
|
|
249
|
+
: error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER'
|
|
250
|
+
? `shell command output exceeded ${SHELL_MAX_OUTPUT_BYTES} bytes (truncated)`
|
|
251
|
+
: String(error.message)
|
|
236
252
|
resolvePromise({
|
|
237
253
|
exitCode: error === null ? 0 : (typeof error.code === 'number' ? error.code : 1),
|
|
238
254
|
stdout: String(stdout ?? ''),
|
|
239
255
|
stderr: String(stderr ?? ''),
|
|
240
|
-
...(
|
|
256
|
+
...(reason === undefined ? {} : { error: reason }),
|
|
241
257
|
})
|
|
242
258
|
})
|
|
243
259
|
const timeout = def.timeoutMs
|
|
244
260
|
if (timeout !== undefined && Number.isSafeInteger(timeout) && timeout > 0) {
|
|
245
|
-
killTimer = setTimeout(() => child
|
|
261
|
+
killTimer = setTimeout(() => { timedOut = true; killShellTree(child) }, timeout)
|
|
246
262
|
}
|
|
247
263
|
})
|
|
248
264
|
return output
|
|
@@ -292,7 +308,8 @@ function createExecute(ctx, def, requireApproval) {
|
|
|
292
308
|
if (exec.kind === 'fs') {
|
|
293
309
|
const cwd = run.agent?.session?.header?.cwd ?? process.cwd()
|
|
294
310
|
try {
|
|
295
|
-
|
|
311
|
+
// 词法 resolve 挡不住 symlink/junction:统一用真实路径判定,不存在的目标按父目录解析。
|
|
312
|
+
const file = resolveRealWithin(resolve(cwd, interpolateArgs(exec.path, args)), [cwd])
|
|
296
313
|
const action = interpolateArgs(exec.action, args)
|
|
297
314
|
if (action === 'read') {
|
|
298
315
|
const raw = readFileSync(file, 'utf8')
|
|
@@ -423,19 +440,6 @@ export const configContract = {
|
|
|
423
440
|
},
|
|
424
441
|
}
|
|
425
442
|
|
|
426
|
-
/**
|
|
427
|
-
* 能力提供者登记(T4 边界守卫的数据源)。
|
|
428
|
-
* `provides.kind === 'dynamic'`:模型可见的工具名由 module.yml 的 customTools 段在
|
|
429
|
-
* 装配期决定,引擎侧无法静态枚举——守卫只断言登记形状、
|
|
430
|
-
* 注册通道,以及「提供者不导出触发器声明」这条互斥。
|
|
431
|
-
*/
|
|
432
|
-
export const engineProvider = {
|
|
433
|
-
kind: 'provider',
|
|
434
|
-
moduleId: name,
|
|
435
|
-
registers: 'tools',
|
|
436
|
-
provides: { kind: 'dynamic', source: 'module.yml#customTools → tools' },
|
|
437
|
-
}
|
|
438
|
-
|
|
439
443
|
/** 插件入口:优先注册内联 tools,缺字段时兼容 configsDir。 */
|
|
440
444
|
export function apply(ctx, config) {
|
|
441
445
|
const source = configContract.parse(config, name)
|
|
@@ -62,7 +62,7 @@ export function validateDefinition(def) {
|
|
|
62
62
|
throw new TypeError(`tool ${def.id}: description is required`)
|
|
63
63
|
}
|
|
64
64
|
if (def.scope !== undefined) {
|
|
65
|
-
throw new TypeError(`tool ${def.id}: customTools.scope is not supported (
|
|
65
|
+
throw new TypeError(`tool ${def.id}: customTools.scope is not supported (a custom tool carries no per-tool scope)`)
|
|
66
66
|
}
|
|
67
67
|
if (def.parameters !== undefined) {
|
|
68
68
|
validateJsonSchemaNode(def.parameters, `tool ${def.id}: parameters`)
|
|
@@ -88,6 +88,10 @@ export function validateDefinition(def) {
|
|
|
88
88
|
if (exec.kind === 'delegate' && (typeof exec.tool !== 'string' || exec.tool.length === 0)) {
|
|
89
89
|
throw new TypeError(`tool ${def.id}: execute.tool is required for delegate`)
|
|
90
90
|
}
|
|
91
|
+
if (exec.kind === 'delegate' && ['character_import', 'character_list', 'character_apply', 'character_remove', 'character_delete',
|
|
92
|
+
'world_book_list', 'world_book_upsert', 'world_book_delete', 'world_book_read_memory', 'session_var'].includes(exec.tool)) {
|
|
93
|
+
throw new TypeError(`tool ${def.id}: delegate ${exec.tool} 已退役;请重新导入或显式转换为模块工具`)
|
|
94
|
+
}
|
|
91
95
|
if (exec.kind === 'fs' && (typeof exec.action !== 'string'
|
|
92
96
|
|| (!['read', 'write', 'append', 'list', 'delete'].includes(exec.action) && !exec.action.includes('{{args.')))) {
|
|
93
97
|
throw new TypeError(`tool ${def.id}: execute.action must be read/write/append/list/delete (or {{args.*}} template) for fs`)
|
package/engine/tool-git-bash.mjs
CHANGED
|
@@ -133,12 +133,16 @@ const commandSchema = {
|
|
|
133
133
|
type: 'string',
|
|
134
134
|
description: 'The bash command to execute (`bash -c` string domain).',
|
|
135
135
|
},
|
|
136
|
+
description: {
|
|
137
|
+
type: 'string',
|
|
138
|
+
description: 'Clear, concise description of what this command does, 5-10 words (shown in the UI).',
|
|
139
|
+
},
|
|
136
140
|
workdir: {
|
|
137
141
|
type: 'string',
|
|
138
142
|
description: 'Optional working directory; defaults to the session cwd. On Windows, Git Bash paths like /e/foo are accepted and converted to E:\\foo.',
|
|
139
143
|
},
|
|
140
144
|
},
|
|
141
|
-
required: ['command'],
|
|
145
|
+
required: ['command', 'description'],
|
|
142
146
|
additionalProperties: false,
|
|
143
147
|
}
|
|
144
148
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tool-scope — 工具面的作用域快照,避免各消费方把全局视图当成模型可见面。
|
|
3
|
+
*
|
|
4
|
+
* 返回两个字段,各自只服务一个调用方:
|
|
5
|
+
* - visible:带 agent 的 `tools.schemas(agent)`,包含预设/祖先/本层工具;模型可见面。
|
|
6
|
+
* - globalNames:不带 scope 的 `tools.schemas()` 名字集合;只供 `restrict` 过滤已知名。
|
|
7
|
+
*
|
|
8
|
+
* 全局原始 schema 与 visible-only 差集不外露;等真实诊断消费方出现再加字段。
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const schemaList = (tools, scope) => {
|
|
12
|
+
if (typeof tools?.schemas !== 'function') return []
|
|
13
|
+
const schemas = tools.schemas(scope)
|
|
14
|
+
return Array.isArray(schemas) ? schemas : []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function toolScopeSnapshot(tools, agent) {
|
|
18
|
+
const global = schemaList(tools)
|
|
19
|
+
const visible = agent === undefined ? global : schemaList(tools, agent)
|
|
20
|
+
const globalNames = new Set(global.map((schema) => schema?.name).filter((name) => typeof name === 'string'))
|
|
21
|
+
return { visible, globalNames }
|
|
22
|
+
}
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
*
|
|
7
7
|
* 两个动作合一:
|
|
8
8
|
* - `query` 关键词搜索**完整装配目录**(只回名字 + 一行截断描述,不回 parameters);
|
|
9
|
-
* - `toolNames`
|
|
10
|
-
* `
|
|
9
|
+
* - `toolNames` 按**精确名**解锁。解锁名写进本次调用的持久参数(顶层 `tool/call`;
|
|
10
|
+
* PTC 嵌套为 `tool/ptc-dispatch`),发现消费方的 allowExtra 从那里读回 → 下次请求起
|
|
11
|
+
* 生效,且跨压缩保留。
|
|
11
12
|
*
|
|
12
13
|
* **能力摘要来自运行时截获,不手工维护**(2026-10-05 用户指出):空查询时按前缀把
|
|
13
14
|
* `ctx.tools.schemas()` 的可见目录折叠成分组摘要(`mcp__github__* ×46`,实测 433 字符
|
|
@@ -15,17 +16,22 @@
|
|
|
15
16
|
* 名字写错还会让模型照着错名字解锁(那份实现甚至需要一条专门测试来防错名)。
|
|
16
17
|
* 官方注册表只投影 name/description(无分组字段),所以分组只能按前缀在本地折叠。
|
|
17
18
|
*
|
|
18
|
-
* 与原 `
|
|
19
|
+
* 与原 `tool-search.mjs` 的两处修正照搬:
|
|
19
20
|
* 1. 匹配是**打分**而非 AND 过滤——长自然语言查询("file edit write replace")在
|
|
20
21
|
* AND 口径下命中零个,等于白搜。现在精确名优先,其次按命中 token 数排序。
|
|
21
22
|
* 2. 描述里显式教解锁路径("搜索为空 ≠ 工具不存在")——实测模型只会搜、不会传
|
|
22
23
|
* toolNames,于是收窄后永远拿不到工具。
|
|
23
24
|
*/
|
|
24
|
-
export const name = '
|
|
25
|
+
export const name = 'tool-search'
|
|
25
26
|
|
|
26
27
|
/** 工具注册表必须先于本工具存在(同 skill-search 的 inject 纪律)。 */
|
|
27
28
|
export const inject = ['tools']
|
|
28
29
|
|
|
30
|
+
import { toJsonSchema } from './shared.mjs'
|
|
31
|
+
import { renderRows, rankRows, scoreRows, queryTokens } from './discovery.mjs'
|
|
32
|
+
import { toolScopeSnapshot } from './tool-scope.mjs'
|
|
33
|
+
import { DISCOVERY_SERVICE } from './service-names.mjs'
|
|
34
|
+
|
|
29
35
|
/** 单次搜索结果上限:再多就该让模型收窄关键词。 */
|
|
30
36
|
export const MAX_RESULTS = 25
|
|
31
37
|
|
|
@@ -36,31 +42,17 @@ const DESC_LIMIT = 90
|
|
|
36
42
|
const MAX_GROUPS = 24
|
|
37
43
|
|
|
38
44
|
/** 常驻核心集:由工具面收窄模板声明,这里只在描述里如实告知模型。 */
|
|
39
|
-
const RESIDENT = ['pwsh', 'read', 'write', 'edit', 'glob', 'grep', 'todo_write', 'skill_search', 'skill_load']
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
function toJsonSchema(spec) {
|
|
43
|
-
const properties = {}
|
|
44
|
-
const required = []
|
|
45
|
-
for (const [key, meta] of Object.entries(spec || {})) {
|
|
46
|
-
const prop = { type: meta.type }
|
|
47
|
-
if (meta.description) prop.description = meta.description
|
|
48
|
-
if (meta.items) prop.items = meta.items
|
|
49
|
-
properties[key] = prop
|
|
50
|
-
if (meta.required) required.push(key)
|
|
51
|
-
}
|
|
52
|
-
return { type: 'object', properties, required, additionalProperties: false }
|
|
53
|
-
}
|
|
45
|
+
const RESIDENT = ['bash', 'pwsh', 'read', 'write', 'edit', 'glob', 'grep', 'todo_write', 'skill_search', 'skill_load']
|
|
46
|
+
|
|
47
|
+
/** 参数 schema 由 `shared.mjs` 的统一实现生成(与 `skill-search` 同一份)。 */
|
|
54
48
|
|
|
55
49
|
/**
|
|
56
|
-
* 能力索引:常驻核心集(pwsh / read / write / edit / glob / grep / todo_write /
|
|
50
|
+
* 能力索引:常驻核心集(bash / pwsh / read / write / edit / glob / grep / todo_write /
|
|
57
51
|
* skill_search / skill_load)覆盖不到的能力。名字全部取自实测工具面。
|
|
58
52
|
*/
|
|
59
53
|
|
|
60
|
-
/**
|
|
61
|
-
export
|
|
62
|
-
return (typeof query === 'string' ? query : '').toLowerCase().split(/[^\p{L}\p{N}_-]+/u).filter(Boolean)
|
|
63
|
-
}
|
|
54
|
+
/** 查询分词由发现引擎统一实现(与技能面同一口径)。 */
|
|
55
|
+
export { queryTokens }
|
|
64
56
|
|
|
65
57
|
/**
|
|
66
58
|
* 一个工具名的分组键:MCP 工具折到服务级(`mcp__github__*`),其余折到首个下划线段
|
|
@@ -102,17 +94,10 @@ export function summarizeCatalog(schemas) {
|
|
|
102
94
|
* `wanted` 为空时返回空数组——调用方据此区分「没给查询」与「没搜到」。
|
|
103
95
|
*/
|
|
104
96
|
export function scoreTools(schemas, wanted) {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const haystack = `${name} ${schema?.description ?? ''}`.toLowerCase()
|
|
110
|
-
let score = 0
|
|
111
|
-
for (const token of wanted) if (haystack.includes(token)) score += 1
|
|
112
|
-
return { schema, name, score, exact: wanted.includes(name.toLowerCase()) }
|
|
113
|
-
})
|
|
114
|
-
.filter((entry) => entry.name.length > 0 && (entry.exact || entry.score >= 1))
|
|
115
|
-
.sort((a, b) => (b.exact - a.exact) || (b.score - a.score) || a.name.localeCompare(b.name))
|
|
97
|
+
return rankRows(scoreRows(schemas, wanted, {
|
|
98
|
+
nameOf: (schema) => schema?.name,
|
|
99
|
+
haystackOf: (schema) => `${schema?.name ?? ''} ${schema?.description ?? ''}`,
|
|
100
|
+
})).map((row) => ({ ...row, schema: row.item }))
|
|
116
101
|
}
|
|
117
102
|
|
|
118
103
|
/** 解锁名单归一:只留非空字符串,去重保序。 */
|
|
@@ -129,46 +114,38 @@ export function unlockNames(value) {
|
|
|
129
114
|
|
|
130
115
|
/** 把目录搜索结果渲染成给模型的文本(带截断提示与解锁引导)。 */
|
|
131
116
|
export function renderMatches(scored, query) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
lines.push(`Matching tools (${matches.length}${scored.length > MAX_RESULTS ? ` of ${scored.length}` : ''}):`)
|
|
141
|
-
for (const { schema, name, exact } of matches) {
|
|
142
|
-
const desc = String(schema?.description ?? '').split('\n')[0].slice(0, DESC_LIMIT)
|
|
143
|
-
lines.push(`- ${name}${exact ? ' (exact)' : ''}: ${desc}`)
|
|
144
|
-
}
|
|
145
|
-
if (scored.length > MAX_RESULTS) {
|
|
146
|
-
lines.push(`(truncated at ${MAX_RESULTS} — add tokens to narrow the query)`)
|
|
147
|
-
}
|
|
148
|
-
lines.push('Unlock with dev_tool_search({"toolNames": ["<exact name>"]}).')
|
|
149
|
-
return lines
|
|
117
|
+
return renderRows(scored, {
|
|
118
|
+
query,
|
|
119
|
+
limit: MAX_RESULTS,
|
|
120
|
+
noun: 'tools',
|
|
121
|
+
emptyText: `No tools match "${query}". An empty result only means no tool matched your keywords — if you need a specific tool, unlock it directly by exact name, e.g. tool_search({"toolNames":["web_search"]}).`,
|
|
122
|
+
lineOf: ({ schema, name, exact }) => `- ${name}${exact ? ' (exact)' : ''}: ${String(schema?.description ?? '').split('\n')[0].slice(0, DESC_LIMIT)}`,
|
|
123
|
+
footer: 'Unlock with tool_search({"toolNames": ["<exact name>"]}).',
|
|
124
|
+
})
|
|
150
125
|
}
|
|
151
126
|
|
|
152
|
-
/** 注册模型可见的 `
|
|
153
|
-
export function
|
|
154
|
-
|
|
155
|
-
|
|
127
|
+
/** 注册模型可见的 `tool_search`。 */
|
|
128
|
+
export function createToolSearchDefinition(ctx, options = {}) {
|
|
129
|
+
const resident = options.resident ?? RESIDENT
|
|
130
|
+
const unlockKey = options.unlockKey ?? 'toolNames'
|
|
131
|
+
return {
|
|
132
|
+
name: options.tool ?? 'tool_search',
|
|
156
133
|
description: [
|
|
157
134
|
'Discover and unlock tools that are NOT currently available.',
|
|
158
135
|
'',
|
|
159
|
-
`This session starts with a minimal resident set: ${
|
|
136
|
+
`This session starts with a minimal resident set: ${resident.join(', ')}. Everything else is unlocked on demand through this tool.`,
|
|
160
137
|
'',
|
|
161
138
|
'Usage:',
|
|
162
139
|
'- `query` — search the catalog by keyword (matches tool names AND descriptions, 1-2 short words work best).',
|
|
163
140
|
'- call with NO arguments — list the catalog as group counts (`<prefix>_*(count) ...`), so you can see what exists before searching.',
|
|
164
|
-
|
|
141
|
+
`- \`${unlockKey}\` — unlock exact names. Unlocked tools appear from the NEXT request on and stay unlocked for the session.`,
|
|
165
142
|
'',
|
|
166
|
-
|
|
167
|
-
'IMPORTANT: an empty search result does NOT mean the tool does not exist
|
|
143
|
+
`Example: ${options.tool ?? 'tool_search'}({"query":"pull request","${unlockKey}":["mcp__github__create_pull_request"]}) — search AND unlock in one call.`,
|
|
144
|
+
'IMPORTANT: if the task needs a capability outside that resident set — web search, subagents, task boards, MCP servers, anything not listed above — search for it here and unlock it. Do NOT make do with a resident tool when the tool you actually need is merely locked; an empty search result does NOT mean the tool does not exist, it only means no tool matched your keywords. Call with no arguments to see the catalog groups, then unlock by exact name.',
|
|
168
145
|
].join('\n'),
|
|
169
146
|
parameters: toJsonSchema({
|
|
170
147
|
query: { type: 'string', required: false, description: 'search keywords (e.g. "web", "subagent", "github")' },
|
|
171
|
-
|
|
148
|
+
[unlockKey]: {
|
|
172
149
|
type: 'array',
|
|
173
150
|
required: false,
|
|
174
151
|
items: { type: 'string' },
|
|
@@ -181,7 +158,7 @@ export function apply(ctx) {
|
|
|
181
158
|
},
|
|
182
159
|
async execute(args, exec) {
|
|
183
160
|
const query = typeof args?.query === 'string' ? args.query.trim() : ''
|
|
184
|
-
const unlock = unlockNames(args?.
|
|
161
|
+
const unlock = unlockNames(args?.[unlockKey])
|
|
185
162
|
const lines = []
|
|
186
163
|
// 解锁动作本身不需要任何运行时服务:引擎的 allowFrom 只读本工具的持久调用参数,
|
|
187
164
|
// 所以即便目录不可用(无 tools 服务/无 agent),解锁也必须照样回报成功。
|
|
@@ -191,7 +168,7 @@ export function apply(ctx) {
|
|
|
191
168
|
try {
|
|
192
169
|
// 执行中的 agent 就是观察作用域:preset 工具注册在 agent scope 层,
|
|
193
170
|
// 不带 scope 的 schemas() 只看全局层,会漏掉它们。
|
|
194
|
-
schemas = ctx.tools
|
|
171
|
+
schemas = toolScopeSnapshot(ctx.tools, exec?.agent).visible
|
|
195
172
|
} catch (error) {
|
|
196
173
|
return { text: [...lines, `catalog search unavailable: ${String(error?.message ?? error)}`].join('\n') }
|
|
197
174
|
}
|
|
@@ -209,5 +186,26 @@ export function apply(ctx) {
|
|
|
209
186
|
lines.push(...renderMatches(scoreTools(schemas, queryTokens(query)), query))
|
|
210
187
|
return { text: lines.join('\n') }
|
|
211
188
|
},
|
|
212
|
-
}
|
|
189
|
+
}
|
|
213
190
|
}
|
|
191
|
+
|
|
192
|
+
/** 本引擎向发现注册表提供的元数据:工具名与「解锁名单从哪个参数回收」。 */
|
|
193
|
+
export const discoveryAdapter = Object.freeze({
|
|
194
|
+
kind: 'tools',
|
|
195
|
+
tool: 'tool_search',
|
|
196
|
+
unlock: Object.freeze({ tool: 'tool_search', key: 'toolNames' }),
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 组合源入口:注册工具(行语义不变),并向发现注册表登记元数据。
|
|
201
|
+
* 注册表由宿主 root provide;引擎单独使用时它不存在,跳过登记即可。
|
|
202
|
+
*/
|
|
203
|
+
export function apply(ctx, _config, options = {}) {
|
|
204
|
+
const registered = ctx.tools.register(createToolSearchDefinition(ctx))
|
|
205
|
+
const registry = typeof ctx.get === 'function' ? ctx.get(DISCOVERY_SERVICE) : undefined
|
|
206
|
+
const disposeAdapter = registry === undefined ? undefined : registry.register(options.agent, 'tools', discoveryAdapter)
|
|
207
|
+
return () => {
|
|
208
|
+
if (typeof disposeAdapter === 'function') disposeAdapter()
|
|
209
|
+
if (typeof registered === 'function') registered()
|
|
210
|
+
}
|
|
211
|
+
}
|