dsh-plugin-prompt-tool 0.4.2 → 0.5.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 +125 -222
- package/engine/compaction-epoch.mjs +139 -0
- package/engine/compositions/library/bootstrap-filesystem.yml +19 -0
- package/engine/compositions/library/compaction.yml +52 -0
- package/engine/compositions/library/context-gate.yml +40 -0
- package/engine/compositions/library/custom-bash.yml +14 -0
- package/engine/compositions/library/delegation.yml +85 -0
- package/engine/compositions/library/official-agent-instructions.yml +17 -0
- package/engine/compositions/library/official-persistent-shell.yml +56 -0
- package/engine/compositions/library/official-skill-filesystem-cordis.yml +10 -0
- package/engine/compositions/library/official-tool-bash.yml +6 -0
- package/engine/compositions/library/official-tool-cordis.yml +13 -0
- package/engine/compositions/library/official-tool-presentation.yml +7 -0
- package/engine/compositions/library/official-tool-skill.yml +14 -0
- package/engine/compositions/library/persistent-shell.yml +36 -0
- package/engine/compositions/library/persona.yml +9 -0
- package/engine/compositions/library/planning.yml +41 -0
- package/engine/compositions/library/prompt-config-engine.yml +16 -0
- package/engine/compositions/library/router-first-turn.yml +9 -0
- package/engine/compositions/library/run-code-env.yml +15 -0
- package/engine/compositions/library/skill-filesystem.yml +13 -0
- package/engine/compositions/library/skill-search.yml +14 -0
- package/engine/compositions/library/str-replace-editor.yml +10 -0
- package/engine/compositions/library/tool-ask-user.yml +8 -0
- package/engine/compositions/library/tool-bash.yml +16 -0
- package/engine/compositions/library/tool-bootstrap.yml +16 -0
- package/engine/compositions/library/tool-filter.yml +12 -0
- package/engine/compositions/library/tool-fs-search.yml +18 -0
- package/engine/compositions/library/tool-fs.yml +10 -0
- package/engine/compositions/library/tool-goal.yml +19 -0
- package/engine/compositions/library/tool-jobs.yml +23 -0
- package/engine/compositions/library/tool-pwsh.yml +12 -0
- package/engine/compositions/library/tool-todo.yml +11 -0
- package/engine/compositions/library/tool-web.yml +9 -0
- package/engine/compositions/source/local/context-gate.yml +37 -0
- package/engine/compositions/source/local/custom-bash.yml +11 -0
- package/engine/compositions/source/local/prompt-config-engine.yml +13 -0
- package/engine/compositions/source/local/router-first-turn.yml +5 -0
- package/engine/compositions/source/local/run-code-env.yml +12 -0
- package/engine/compositions/source/local/skill-search.yml +11 -0
- package/engine/compositions/source/local/tool-bootstrap.yml +13 -0
- package/engine/context-gate.mjs +305 -0
- package/{preset → engine}/custom-bash.mjs +243 -243
- package/engine/executor.mjs +271 -0
- package/engine/fillers.mjs +273 -0
- package/engine/layers.mjs +220 -0
- package/engine/prompt-config-engine.mjs +49 -0
- package/{preset → engine}/router-first-turn.mjs +85 -73
- package/engine/run-code-env.mjs +208 -0
- package/engine/schema.mjs +294 -0
- package/{preset → engine}/shared.mjs +54 -0
- package/engine/strategies.mjs +223 -0
- package/{preset → engine}/tool-bootstrap.mjs +350 -282
- package/engine/tool-filter.mjs +80 -0
- package/engine/vendor/yaml/LICENSE +13 -0
- package/engine/vendor/yaml/dist/compose/compose-collection.js +88 -0
- package/engine/vendor/yaml/dist/compose/compose-doc.js +43 -0
- package/engine/vendor/yaml/dist/compose/compose-node.js +109 -0
- package/engine/vendor/yaml/dist/compose/compose-scalar.js +86 -0
- package/engine/vendor/yaml/dist/compose/composer.js +219 -0
- package/engine/vendor/yaml/dist/compose/resolve-block-map.js +115 -0
- package/engine/vendor/yaml/dist/compose/resolve-block-scalar.js +198 -0
- package/engine/vendor/yaml/dist/compose/resolve-block-seq.js +49 -0
- package/engine/vendor/yaml/dist/compose/resolve-end.js +37 -0
- package/engine/vendor/yaml/dist/compose/resolve-flow-collection.js +207 -0
- package/engine/vendor/yaml/dist/compose/resolve-flow-scalar.js +225 -0
- package/engine/vendor/yaml/dist/compose/resolve-props.js +146 -0
- package/engine/vendor/yaml/dist/compose/util-contains-newline.js +34 -0
- package/engine/vendor/yaml/dist/compose/util-empty-scalar-position.js +26 -0
- package/engine/vendor/yaml/dist/compose/util-flow-indent-check.js +15 -0
- package/engine/vendor/yaml/dist/compose/util-map-includes.js +13 -0
- package/engine/vendor/yaml/dist/doc/Document.js +335 -0
- package/engine/vendor/yaml/dist/doc/anchors.js +71 -0
- package/engine/vendor/yaml/dist/doc/applyReviver.js +55 -0
- package/engine/vendor/yaml/dist/doc/createNode.js +88 -0
- package/engine/vendor/yaml/dist/doc/directives.js +176 -0
- package/engine/vendor/yaml/dist/errors.js +57 -0
- package/engine/vendor/yaml/dist/index.js +17 -0
- package/engine/vendor/yaml/dist/log.js +11 -0
- package/engine/vendor/yaml/dist/nodes/Alias.js +116 -0
- package/engine/vendor/yaml/dist/nodes/Collection.js +147 -0
- package/engine/vendor/yaml/dist/nodes/Node.js +38 -0
- package/engine/vendor/yaml/dist/nodes/Pair.js +36 -0
- package/engine/vendor/yaml/dist/nodes/Scalar.js +24 -0
- package/engine/vendor/yaml/dist/nodes/YAMLMap.js +144 -0
- package/engine/vendor/yaml/dist/nodes/YAMLSeq.js +113 -0
- package/engine/vendor/yaml/dist/nodes/addPairToJSMap.js +63 -0
- package/engine/vendor/yaml/dist/nodes/identity.js +36 -0
- package/engine/vendor/yaml/dist/nodes/toJS.js +37 -0
- package/engine/vendor/yaml/dist/parse/cst-scalar.js +214 -0
- package/engine/vendor/yaml/dist/parse/cst-stringify.js +61 -0
- package/engine/vendor/yaml/dist/parse/cst-visit.js +97 -0
- package/engine/vendor/yaml/dist/parse/cst.js +98 -0
- package/engine/vendor/yaml/dist/parse/lexer.js +721 -0
- package/engine/vendor/yaml/dist/parse/line-counter.js +39 -0
- package/engine/vendor/yaml/dist/parse/parser.js +975 -0
- package/engine/vendor/yaml/dist/public-api.js +102 -0
- package/engine/vendor/yaml/dist/schema/Schema.js +37 -0
- package/engine/vendor/yaml/dist/schema/common/map.js +17 -0
- package/engine/vendor/yaml/dist/schema/common/null.js +15 -0
- package/engine/vendor/yaml/dist/schema/common/seq.js +17 -0
- package/engine/vendor/yaml/dist/schema/common/string.js +14 -0
- package/engine/vendor/yaml/dist/schema/core/bool.js +19 -0
- package/engine/vendor/yaml/dist/schema/core/float.js +43 -0
- package/engine/vendor/yaml/dist/schema/core/int.js +38 -0
- package/engine/vendor/yaml/dist/schema/core/schema.js +23 -0
- package/engine/vendor/yaml/dist/schema/json/schema.js +62 -0
- package/engine/vendor/yaml/dist/schema/tags.js +96 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/binary.js +58 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/bool.js +26 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/float.js +46 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/int.js +71 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/merge.js +67 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/omap.js +74 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/pairs.js +78 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/schema.js +39 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/set.js +93 -0
- package/engine/vendor/yaml/dist/schema/yaml-1.1/timestamp.js +101 -0
- package/engine/vendor/yaml/dist/stringify/foldFlowLines.js +146 -0
- package/engine/vendor/yaml/dist/stringify/stringify.js +129 -0
- package/engine/vendor/yaml/dist/stringify/stringifyCollection.js +153 -0
- package/engine/vendor/yaml/dist/stringify/stringifyComment.js +20 -0
- package/engine/vendor/yaml/dist/stringify/stringifyDocument.js +85 -0
- package/engine/vendor/yaml/dist/stringify/stringifyNumber.js +25 -0
- package/engine/vendor/yaml/dist/stringify/stringifyPair.js +150 -0
- package/engine/vendor/yaml/dist/stringify/stringifyString.js +336 -0
- package/engine/vendor/yaml/dist/util.js +11 -0
- package/engine/vendor/yaml/dist/visit.js +233 -0
- package/engine/vendor/yaml/index.js +5 -0
- package/engine/vendor/yaml/package.json +11 -0
- package/lib/client.js +4421 -781
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +504 -51
- package/lib/index.mjs +3871 -731
- package/lib/preset-core.d.mts +7 -37
- package/lib/preset-core.mjs +44 -283
- package/lib/prompt-configs-BcP6miu9.d.mts +102 -0
- package/lib/prompt-configs-k7wuZs0C.mjs +719 -0
- package/package.json +104 -98
- package/preset/anchored/preset.yml +344 -0
- package/preset/creative/preset.yml +62 -0
- package/preset/custom/preset.yml +13 -0
- package/preset/liangshen/preset.yml +66 -0
- package/preset/minimal/preset.yml +41 -0
- package/preset/ptc/preset.yml +50 -0
- package/preset/standard/preset.yml +49 -0
- package/skills/manifest.json +7 -0
- package/skills/sandboxmod/SKILL.md +49 -49
- package/skills/web ui/SKILL.md +42 -0
- package/templates/10-pre-step.yml +28 -0
- package/templates/11-merged-a.yml +11 -0
- package/templates/13-anchor.yml +19 -0
- package/templates/14-first-turn-anchor.yml +27 -0
- package/templates/15-guide-auto.yml +25 -0
- package/templates/16-custom-fallback.yml +21 -0
- package/templates/17-instruction-hint.yml +23 -0
- package/templates/18-placeholder-env-facts.yml +11 -0
- package/templates/19-placeholder-skill-catalog.yml +19 -0
- package/templates/20-system-section.yml +14 -0
- package/templates/30-runtime-context.yml +10 -0
- package/templates/31-runtime-context-placeholder.yml +18 -0
- package/templates/40-agent-request.yml +11 -0
- package/templates/50-llm-stream.yml +9 -0
- package/templates/60-tool-pipeline.yml +12 -0
- package/AGENTS.md +0 -4
- package/plan.md +0 -312
- package/preset/agent.cordis.yml +0 -443
- package/preset/compaction-epoch.mjs +0 -81
- package/preset/context-gate.mjs +0 -165
- package/preset/instruction-hint.mjs +0 -217
- package/preset/near-anchor.mjs +0 -101
- package/preset/preset.yml +0 -3
- package/preset/prompt-injector.mjs +0 -112
- package/preset/router-guide.mjs +0 -79
- package/preset.md +0 -115
- package/upstream/dsh-anchored-standard/LICENSE +0 -22
- package/upstream/dsh-anchored-standard/NOTICE +0 -19
- package/upstream/dsh-anchored-standard/REVISION +0 -1
- package/upstream/dsh-anchored-standard/preset/agent.cordis.yml +0 -440
- package/upstream/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -81
- package/upstream/dsh-anchored-standard/preset/context-gate.mjs +0 -202
- package/upstream/dsh-anchored-standard/preset/custom-bash.mjs +0 -219
- package/upstream/dsh-anchored-standard/preset/dev-tool-search.mjs +0 -131
- package/upstream/dsh-anchored-standard/preset/instruction-hint.mjs +0 -231
- package/upstream/dsh-anchored-standard/preset/preset.yml +0 -3
- package/upstream/dsh-anchored-standard/preset/skill-search.mjs +0 -142
- package/upstream/dsh-anchored-standard/preset/tool-bootstrap.mjs +0 -301
- /package/{preset → engine}/skill-search.mjs +0 -0
package/preset/context-gate.mjs
DELETED
|
@@ -1,165 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* anchored-context-gate — reusable unified injection control for ANY preset.
|
|
3
|
-
*
|
|
4
|
-
* Mount this one plugin to keep a session's first model request free of
|
|
5
|
-
* auto-injected context, whatever its source, and to have every injection
|
|
6
|
-
* return on the second round. It intercepts the harness's two unified
|
|
7
|
-
* injection paths — not a per-source denylist — so it covers sources that do
|
|
8
|
-
* not exist yet:
|
|
9
|
-
*
|
|
10
|
-
* a. RUNTIME CONTEXT (system-prompt/assemble): while the session is
|
|
11
|
-
* unpromoted, the assembly's `contexts` are blanked. That covers the
|
|
12
|
-
* WHOLE `SystemPrompt.context()` family — the sandbox and approval
|
|
13
|
-
* policy snapshots and any third-party context provider — without
|
|
14
|
-
* enumerating them. The loop's own snapshot projection then emits no
|
|
15
|
-
* message during the gate (no snapshot ever existed), and at the first
|
|
16
|
-
* promoted request it emits exactly ONE fresh snapshot: "minimal first
|
|
17
|
-
* round, inject on the second round" falls out of the projection's
|
|
18
|
-
* diffing, with no reinjection logic here.
|
|
19
|
-
*
|
|
20
|
-
* b. STEP MESSAGES (agent/pre-step): the waterfall payload carries the
|
|
21
|
-
* CLAIMED message batch (the inbox messages this step owns). While
|
|
22
|
-
* unpromoted, the gate keeps exactly the claimed messages plus a small
|
|
23
|
-
* kind allowlist, and strips everything any listener appended — skill
|
|
24
|
-
* catalog, AGENTS.md digest, time/tmux context, hooks, unknown
|
|
25
|
-
* third-party plugins — by DEFAULT, regardless of source identity. The
|
|
26
|
-
* default allowlist is `['skill-invocation']`: a user-initiated skill
|
|
27
|
-
* gesture is not an automatic injection, and stripping it would lose the
|
|
28
|
-
* skill content once the gesture scrolls out of the per-step claim.
|
|
29
|
-
* Durable history (compaction summaries included) never passes through
|
|
30
|
-
* this gate: it enters the request via the session surface, not the
|
|
31
|
-
* pre-step waterfall.
|
|
32
|
-
*
|
|
33
|
-
* The phase is the same epoch-aware promotion machine the anchored presets
|
|
34
|
-
* use (see compaction-epoch.mjs): a durable `tool/call` and/or
|
|
35
|
-
* `assistant/message` (per `promoteOn`, default `either`) promotes, and a
|
|
36
|
-
* `compaction/end` boundary demotes again — the first post-compaction request
|
|
37
|
-
* is a "second first request" and is gated the same way. Derived from durable
|
|
38
|
-
* events, so resume and reload preserve it.
|
|
39
|
-
*
|
|
40
|
-
* SUBAGENTS: by default subagents (delegationDepth > 0) skip the gate (their
|
|
41
|
-
* first request already sees full context). `includeSubagents: true` gates
|
|
42
|
-
* them too — their first request is clean and their own first reply or tool
|
|
43
|
-
* call opens the gate — so a delegation cannot reintroduce an uncontrolled
|
|
44
|
-
* first request. Keep this flag in sync with any companion phase plugin
|
|
45
|
-
* (e.g. the tool-bootstrap row).
|
|
46
|
-
*
|
|
47
|
-
* CONFIG:
|
|
48
|
-
* - `promoteOn`: 'either' (default) | 'tool-call' | 'assistant-message'.
|
|
49
|
-
* - `includeSubagents`: boolean, default false.
|
|
50
|
-
* - `enabled`: boolean, default true. `false` disables both interception
|
|
51
|
-
* paths (A/B testing without touching the row set).
|
|
52
|
-
* - `allowKinds`: message `source.kind` names allowed beyond the claimed
|
|
53
|
-
* batch, default ['skill-invocation']. An explicitly empty array keeps
|
|
54
|
-
* ONLY the claimed batch.
|
|
55
|
-
*
|
|
56
|
-
* ROW ORDER: mount this row FIRST in the composition. Waterfall after-next
|
|
57
|
-
* transforms apply in reverse registration order, so registering first (plus
|
|
58
|
-
* the pre-step listener's `prepend: true`) makes the gate the outermost
|
|
59
|
-
* transform — nothing registered later re-injects past it.
|
|
60
|
-
*
|
|
61
|
-
* Robustness: both filters degrade to "keep everything" on their own
|
|
62
|
-
* failures — a gate bug must never eat the user's context — and invalid
|
|
63
|
-
* config fails at apply time, i.e. at preset mount, where it is visible.
|
|
64
|
-
*/
|
|
65
|
-
|
|
66
|
-
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
67
|
-
import { booleanOption, createWarnOnce, parsePromoteOn, validateConfig } from './shared.mjs'
|
|
68
|
-
|
|
69
|
-
/** Cordis plugin name used by loader diagnostics. */
|
|
70
|
-
export const name = 'anchored-context-gate'
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Deliberately NO inject list: the listeners only touch services at event
|
|
74
|
-
* time, and applying without an inject lets this row register before the
|
|
75
|
-
* context-injecting plugins (dsh-agent-instructions, dsh-tool-skill, host
|
|
76
|
-
* plane policy projections) when it sits first in the composition.
|
|
77
|
-
*/
|
|
78
|
-
export const inject = []
|
|
79
|
-
|
|
80
|
-
/** Every config key this plugin accepts — anything else is a typo. */
|
|
81
|
-
const ALLOWED_KEYS = new Set(['promoteOn', 'includeSubagents', 'enabled', 'allowKinds'])
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Message kinds allowed through the pre-step gate beyond the claimed batch.
|
|
85
|
-
* A user-initiated skill gesture is the only default entry: it is not an
|
|
86
|
-
* automatic injection (see the header note).
|
|
87
|
-
*/
|
|
88
|
-
const DEFAULT_ALLOW_KINDS = ['skill-invocation']
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Validate the kind allowlist. An explicitly empty array is meaningful: keep
|
|
93
|
-
* ONLY the claimed batch, stripping even user skill gestures.
|
|
94
|
-
*/
|
|
95
|
-
function allowKindList(value, field) {
|
|
96
|
-
if (value === undefined) return new Set(DEFAULT_ALLOW_KINDS)
|
|
97
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || item.length === 0)) {
|
|
98
|
-
throw new TypeError(`${name}: ${field} must be an array of non-empty strings`)
|
|
99
|
-
}
|
|
100
|
-
return new Set(value)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
/** Register the unified context gate. */
|
|
105
|
-
export function apply(ctx, config) {
|
|
106
|
-
const source = validateConfig(name, config, ALLOWED_KEYS)
|
|
107
|
-
const promoteEvents = parsePromoteOn(name, source.promoteOn)
|
|
108
|
-
const includeSubagents = booleanOption(name, source.includeSubagents, 'includeSubagents', false)
|
|
109
|
-
const enabled = booleanOption(name, source.enabled, 'enabled', true)
|
|
110
|
-
const allowKinds = allowKindList(source.allowKinds, 'allowKinds')
|
|
111
|
-
|
|
112
|
-
const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
|
|
113
|
-
ctx.on('session/event', (session, event) => promotion.observe(session, event))
|
|
114
|
-
|
|
115
|
-
const warnOnce = createWarnOnce(ctx, name)
|
|
116
|
-
|
|
117
|
-
// Path (a): blank the dynamic runtime-context contributions while the
|
|
118
|
-
// session is unpromoted. Covers the whole SystemPrompt.context() family
|
|
119
|
-
// without enumerating it; the loop's snapshot projection then stays silent
|
|
120
|
-
// and diffs exactly ONE fresh snapshot in at the first promoted request.
|
|
121
|
-
ctx.on('system-prompt/assemble', async (_assembly, context, next) => {
|
|
122
|
-
// Downstream errors propagate untouched; only this filter's own logic is guarded.
|
|
123
|
-
const assembled = await next()
|
|
124
|
-
if (enabled === false) return assembled
|
|
125
|
-
try {
|
|
126
|
-
if (promotion.status(context.agent).promoted) return assembled
|
|
127
|
-
if (!Array.isArray(assembled.contexts) || assembled.contexts.length === 0) return assembled
|
|
128
|
-
return { ...assembled, contexts: [] }
|
|
129
|
-
} catch (error) {
|
|
130
|
-
// A gate bug must never break assembly: degrade to the assembled value.
|
|
131
|
-
warnOnce(`${name}: runtime-context suppression failed, keeping contexts: ${String((error && error.message) || error)}`)
|
|
132
|
-
return assembled
|
|
133
|
-
}
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
// Path (b): claimed-baseline deny on the pre-step waterfall. The payload's
|
|
137
|
-
// `messages` is the batch this step CLAIMED from the inbox — the baseline
|
|
138
|
-
// every injection appends to. Keep that baseline plus the kind allowlist,
|
|
139
|
-
// strip every appended message regardless of its source identity.
|
|
140
|
-
ctx.on('agent/pre-step', async ({ agent, messages: claimed }, next) => {
|
|
141
|
-
// Downstream errors propagate untouched; only this filter's own logic is guarded.
|
|
142
|
-
const decision = await next()
|
|
143
|
-
if (decision.kind === 'reject') return decision
|
|
144
|
-
if (enabled === false) return decision
|
|
145
|
-
try {
|
|
146
|
-
if (promotion.status(agent).promoted) return decision
|
|
147
|
-
if (!Array.isArray(decision.messages)) return decision
|
|
148
|
-
if (!Array.isArray(claimed)) return decision
|
|
149
|
-
const baseline = new Set(claimed)
|
|
150
|
-
const baselineIds = new Set(claimed
|
|
151
|
-
.map((message) => message?.id)
|
|
152
|
-
.filter((id) => id !== undefined && id !== null))
|
|
153
|
-
const kept = decision.messages.filter((message) =>
|
|
154
|
-
baseline.has(message)
|
|
155
|
-
|| (message?.id !== undefined && message?.id !== null && baselineIds.has(message.id))
|
|
156
|
-
|| allowKinds.has(message?.source?.kind),
|
|
157
|
-
)
|
|
158
|
-
return kept.length === decision.messages.length ? decision : { ...decision, messages: kept }
|
|
159
|
-
} catch (error) {
|
|
160
|
-
// A gate bug must never eat context: degrade to keeping every message.
|
|
161
|
-
warnOnce(`${name}: pre-step gate failed, keeping injected context: ${String((error && error.message) || error)}`)
|
|
162
|
-
return decision
|
|
163
|
-
}
|
|
164
|
-
}, { prepend: true })
|
|
165
|
-
}
|
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* instruction-hint — replace `dsh-agent-instructions`' full AGENTS.md/CLAUDE.md
|
|
3
|
-
* injection with a minimal "these files exist" hint.
|
|
4
|
-
*
|
|
5
|
-
* WHY: the full workspace-instruction digest is a large injected block. After
|
|
6
|
-
* the anchored bootstrap promotes, we want the model to KNOW the instruction
|
|
7
|
-
* files exist (so it reads them before acting) without dumping their content
|
|
8
|
-
* into every request. The model reads the files itself via the filesystem
|
|
9
|
-
* tools when it needs them.
|
|
10
|
-
*
|
|
11
|
-
* Behavior:
|
|
12
|
-
* - After the session records its first durable promotion signal
|
|
13
|
-
* (`promoteOn`, default `either`), ONE hint message is injected, listing
|
|
14
|
-
* which instruction files were found:
|
|
15
|
-
* - user-global: `$DSH_HOME/AGENTS.md`
|
|
16
|
-
* - project chain: AGENTS.md / CLAUDE.md / AGENTS.local.md / CLAUDE.local.md
|
|
17
|
-
* walking up from the session cwd to the project root (a directory
|
|
18
|
-
* containing `.git`, or the cwd itself).
|
|
19
|
-
* - The hint is ONCE PER SESSION, DERIVED FROM DURABLE EVENTS: the guard
|
|
20
|
-
* scans the session log for an existing `instruction-hint` message (then
|
|
21
|
-
* O(1)), so a process restart — whose in-memory state starts empty —
|
|
22
|
-
* cannot inject a second copy. A duplicate would collide with the first
|
|
23
|
-
* message's deterministic id (`instruction-hint-<sessionId>`) and break
|
|
24
|
-
* history replay.
|
|
25
|
-
* - The hint instructs the model to READ the files before acting when
|
|
26
|
-
* relevant, without embedding their content.
|
|
27
|
-
* - Files are probed via `ctx.fs` (the host filesystem seam); a missing fs
|
|
28
|
-
* service or an unreadable probe degrades to no hint (never throws).
|
|
29
|
-
* - Pre-promotion requests get NO hint (matches the anchored bootstrap).
|
|
30
|
-
* - Subagents skip the phase wait by default (their first request already
|
|
31
|
-
* counts as promoted); `includeSubagents: true` makes a subagent's own
|
|
32
|
-
* first reply or tool call open the hint — which also keeps the injection
|
|
33
|
-
* out of the context gate's stripped first request (the gate strips
|
|
34
|
-
* non-claimed messages while unpromoted).
|
|
35
|
-
*
|
|
36
|
-
* ROW ORDER: this plugin registers its `agent/pre-step` handler with
|
|
37
|
-
* `prepend: true` and after `context-gate`/`tool-bootstrap`, so it runs
|
|
38
|
-
* inside the gate's outermost strip — but it emits AFTER promotion, when the
|
|
39
|
-
* strip is inactive. The hint source kind is `instruction-hint`, which is
|
|
40
|
-
* not in the gate's claimed-baseline allowlist, so the gate can strip it
|
|
41
|
-
* only while the session is unpromoted (never the intended path).
|
|
42
|
-
*/
|
|
43
|
-
|
|
44
|
-
import { readFileSync } from 'node:fs'
|
|
45
|
-
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
46
|
-
import { booleanOption, createWarnOnce, parsePromoteOn, validateConfig } from './shared.mjs'
|
|
47
|
-
|
|
48
|
-
/** Cordis plugin name used by loader diagnostics. */
|
|
49
|
-
export const name = 'instruction-hint'
|
|
50
|
-
|
|
51
|
-
/** Optional prompt-tool hint text (agents-instruction.txt beside this module). */
|
|
52
|
-
function readAgentsInstructionText() {
|
|
53
|
-
try {
|
|
54
|
-
return readFileSync(new URL('./agents-instruction.txt', import.meta.url), 'utf8').trim()
|
|
55
|
-
} catch {
|
|
56
|
-
return ''
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Candidate file names, in probe order, for the project chain and user-global. */
|
|
61
|
-
const PROJECT_CANDIDATES = ['AGENTS.md', 'CLAUDE.md', 'AGENTS.local.md', 'CLAUDE.local.md']
|
|
62
|
-
const USER_GLOBAL_CANDIDATE = 'AGENTS.md'
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
/** Every config key this plugin accepts — anything else is a typo. */
|
|
66
|
-
const ALLOWED_KEYS = new Set(['promoteOn', 'includeSubagents'])
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/** Find the project root: first ancestor containing any root marker (e.g. .git). */
|
|
70
|
-
async function findProjectRoot(fs, cwd, signal) {
|
|
71
|
-
let current = cwd
|
|
72
|
-
for (;;) {
|
|
73
|
-
for (const marker of ['.git', '.hg', '.svn']) {
|
|
74
|
-
try {
|
|
75
|
-
const target = await fs.resolve(joinPath(current, marker), { cwd, signal })
|
|
76
|
-
const info = await fs.stat(target, signal)
|
|
77
|
-
if (info !== undefined) return current
|
|
78
|
-
} catch {
|
|
79
|
-
// Probe failure = marker absent; continue.
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
const parent = parentPath(current)
|
|
83
|
-
if (parent === current || parent.length === 0) return cwd
|
|
84
|
-
current = parent
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** List instruction files present in one directory (project candidates). */
|
|
89
|
-
async function presentInDir(fs, dir, candidates, signal) {
|
|
90
|
-
const found = []
|
|
91
|
-
for (const candidate of candidates) {
|
|
92
|
-
try {
|
|
93
|
-
const target = await fs.resolve(joinPath(dir, candidate), { cwd: dir, signal })
|
|
94
|
-
const info = await fs.stat(target, signal)
|
|
95
|
-
if (info !== undefined && info.type === 'file') found.push(candidate)
|
|
96
|
-
} catch {
|
|
97
|
-
// Absent or unreadable — skip.
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
return found
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/** Join one path segment onto a directory (platform-agnostic string join). */
|
|
104
|
-
function joinPath(dir, segment) {
|
|
105
|
-
if (dir.endsWith('/') || dir.endsWith('\\')) return dir + segment
|
|
106
|
-
const sep = dir.includes('\\') ? '\\' : '/'
|
|
107
|
-
return dir + sep + segment
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/** Parent of an absolute Windows or POSIX path. */
|
|
111
|
-
function parentPath(path) {
|
|
112
|
-
const idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'))
|
|
113
|
-
if (idx <= 0) return path
|
|
114
|
-
const parent = path.slice(0, idx)
|
|
115
|
-
return parent.length === 0 ? path : parent
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Register the post-promotion instruction-hint injector. */
|
|
119
|
-
export function apply(ctx, config) {
|
|
120
|
-
const source = validateConfig(name, config, ALLOWED_KEYS)
|
|
121
|
-
const promoteEvents = parsePromoteOn(name, source.promoteOn)
|
|
122
|
-
const includeSubagents = booleanOption(name, source.includeSubagents, 'includeSubagents', false)
|
|
123
|
-
const agentsInstructionText = readAgentsInstructionText()
|
|
124
|
-
const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
|
|
125
|
-
ctx.on('session/event', (session, event) => promotion.observe(session, event))
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Sessions whose hint is already durable in the event log — the
|
|
129
|
-
* restart-safe replacement for an in-memory "already hinted" set. Seeded by
|
|
130
|
-
* a one-time scan, then maintained incrementally through `session/event`.
|
|
131
|
-
*/
|
|
132
|
-
const hinted = new Map()
|
|
133
|
-
const hintIsDurable = (session) => {
|
|
134
|
-
const known = hinted.get(session.id)
|
|
135
|
-
if (known !== undefined) return known
|
|
136
|
-
const found = (Array.isArray(session.events) ? session.events : []).some((event) =>
|
|
137
|
-
event.type === 'user/message' && event.data?.source?.kind === 'instruction-hint',
|
|
138
|
-
)
|
|
139
|
-
hinted.set(session.id, found)
|
|
140
|
-
return found
|
|
141
|
-
}
|
|
142
|
-
ctx.on('session/event', (session, event) => {
|
|
143
|
-
if (event.type === 'user/message' && event.data?.source?.kind === 'instruction-hint') {
|
|
144
|
-
hinted.set(session.id, true)
|
|
145
|
-
}
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
const warnOnce = createWarnOnce(ctx, name)
|
|
149
|
-
|
|
150
|
-
ctx.on('agent/pre-step', async ({ agent, signal }, next) => {
|
|
151
|
-
const decision = await next()
|
|
152
|
-
try {
|
|
153
|
-
if (promotion.status(agent).promoted !== true) return decision
|
|
154
|
-
const session = agent.session
|
|
155
|
-
if (session === undefined || hintIsDurable(session)) return decision
|
|
156
|
-
hinted.set(session.id, true)
|
|
157
|
-
|
|
158
|
-
if (agentsInstructionText.length > 0) {
|
|
159
|
-
return {
|
|
160
|
-
...decision,
|
|
161
|
-
messages: [...decision.messages, {
|
|
162
|
-
id: `instruction-hint-${session.id}`,
|
|
163
|
-
role: 'user',
|
|
164
|
-
content: [{ type: 'text', text: agentsInstructionText }],
|
|
165
|
-
source: { kind: 'instruction-hint', form: 'hint' },
|
|
166
|
-
}],
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const fs = ctx.get('fs')
|
|
171
|
-
if (fs === undefined) return decision
|
|
172
|
-
const cwd = session.header.cwd ?? process.cwd()
|
|
173
|
-
|
|
174
|
-
const projectFiles = []
|
|
175
|
-
const root = await findProjectRoot(fs, cwd, signal)
|
|
176
|
-
projectFiles.push(...await presentInDir(fs, root, PROJECT_CANDIDATES, signal))
|
|
177
|
-
|
|
178
|
-
const userGlobalFiles = []
|
|
179
|
-
try {
|
|
180
|
-
const dshHome = process.env.DSH_HOME ?? (process.env.USERPROFILE ? `${process.env.USERPROFILE}\\.dsh` : undefined)
|
|
181
|
-
if (dshHome !== undefined) {
|
|
182
|
-
userGlobalFiles.push(...await presentInDir(fs, dshHome, [USER_GLOBAL_CANDIDATE], signal))
|
|
183
|
-
}
|
|
184
|
-
} catch {
|
|
185
|
-
// Unreadable home probe — ignore.
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
const sections = []
|
|
189
|
-
if (projectFiles.length > 0) {
|
|
190
|
-
sections.push(`Workspace instruction files exist: ${projectFiles.join(', ')} (project root: ${root}).`)
|
|
191
|
-
}
|
|
192
|
-
if (userGlobalFiles.length > 0) {
|
|
193
|
-
sections.push(`A user-global instruction file exists: ${USER_GLOBAL_CANDIDATE}.`)
|
|
194
|
-
}
|
|
195
|
-
if (sections.length === 0) return decision
|
|
196
|
-
|
|
197
|
-
const text = [
|
|
198
|
-
...sections,
|
|
199
|
-
'Do NOT assume their content. When a task touches this workspace, read the relevant instruction files first and follow them.',
|
|
200
|
-
].join(' ')
|
|
201
|
-
|
|
202
|
-
return {
|
|
203
|
-
...decision,
|
|
204
|
-
messages: [...decision.messages, {
|
|
205
|
-
id: `instruction-hint-${session.id}`,
|
|
206
|
-
role: 'user',
|
|
207
|
-
content: [{ type: 'text', text }],
|
|
208
|
-
source: { kind: 'instruction-hint', form: 'hint' },
|
|
209
|
-
}],
|
|
210
|
-
}
|
|
211
|
-
} catch (error) {
|
|
212
|
-
// A hint bug must never hurt the session: skip the hint.
|
|
213
|
-
warnOnce(`${name}: hint injection failed, skipping: ${String((error && error.message) || error)}`)
|
|
214
|
-
return decision
|
|
215
|
-
}
|
|
216
|
-
}, { prepend: true })
|
|
217
|
-
}
|
package/preset/near-anchor.mjs
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* near-anchor — prompt-tool 近距离首句锚定(替代独立锚定轮)。
|
|
3
|
-
*
|
|
4
|
-
* 依据 dsh-router-standard P13/P14/P15/P22 与作者勘误:
|
|
5
|
-
* - 行为引导放 system(远距离)会衰减甚至反向;放在真实用户消息之后
|
|
6
|
-
* (近距离)零衰减;
|
|
7
|
-
* - 只要求首句一次:模型自回归续写会维持语域,越锚越牢;
|
|
8
|
-
* - 强制“每个 reasoning 块都以某句开头/回顾开头”是负效果;
|
|
9
|
-
* - 日常任务锚 We(官方后训练压出的高效路径),复杂规划放行 Let
|
|
10
|
-
* (未收敛旧路径携带更深规划能力),不做唯一解。
|
|
11
|
-
*
|
|
12
|
-
* 实现:在首条真实用户消息之后追加一条锚点消息,不拆轮、不挪任务;
|
|
13
|
-
* 幂等性来自持久事件(source.plugin === 'near-anchor')+ 进程内 memo。
|
|
14
|
-
* 自定义开关:config.useCustom 为 true 时固定使用 config.anchorText;
|
|
15
|
-
* false(默认)时忽略 anchorText,按任务与模型自动选择文本。
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { extractText, isDelegated, newMessageId } from './shared.mjs'
|
|
19
|
-
|
|
20
|
-
/** Cordis 插件名,供 loader 诊断使用。 */
|
|
21
|
-
export const name = 'near-anchor'
|
|
22
|
-
|
|
23
|
-
/** 无服务依赖,只监听 pre-step 消息组装。 */
|
|
24
|
-
export const inject = []
|
|
25
|
-
|
|
26
|
-
/** 开发/生成类任务关键词。 */
|
|
27
|
-
const BUILD_RE = /(开发|创建|写一个|生成|从零|做一个|构建|新项目|搭建|实现|做出|上线|落地|脚本|工具|应用|build|create|develop|generate|implement|make a|new project)/i
|
|
28
|
-
/** 复杂规划/修复类任务关键词:放行 Let 深度路径。 */
|
|
29
|
-
const COMPLEX_RE = /(架构|重构|设计|系统|全面|深度|迁移|兼容|审查|architecture|refactor|comprehensive|design|system|migrate|review)/i
|
|
30
|
-
|
|
31
|
-
/** 开发类:we 直接产出并验证。 */
|
|
32
|
-
const ANCHOR_BUILD = "Start your reasoning with the exact sentence: 'We need to build it directly and verify it.'"
|
|
33
|
-
/** 检查/日常类:we 先看代码。 */
|
|
34
|
-
const ANCHOR_INSPECT = "Start your reasoning with the exact sentence: 'We need to inspect the code first.'"
|
|
35
|
-
/** 复杂规划类:放行 Let 深度规划路径。 */
|
|
36
|
-
const ANCHOR_DEEP = "Start your reasoning with the exact sentence: 'Let me think through the design before changing anything.'"
|
|
37
|
-
|
|
38
|
-
/** 按开关与任务选择锚点:useCustom=true 固定用自定义;false 自动选择。 */
|
|
39
|
-
function chooseAnchor(text, customText, useCustom) {
|
|
40
|
-
if (useCustom === true) {
|
|
41
|
-
return typeof customText === 'string' ? customText.trim() : ''
|
|
42
|
-
}
|
|
43
|
-
let anchor
|
|
44
|
-
if (COMPLEX_RE.test(text)) anchor = ANCHOR_DEEP
|
|
45
|
-
else if (BUILD_RE.test(text)) anchor = ANCHOR_BUILD
|
|
46
|
-
else anchor = ANCHOR_INSPECT
|
|
47
|
-
// Flash 主会话的 persona 已由 router-first-turn 注入三锚,这里不再重复。
|
|
48
|
-
return anchor
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function apply(ctx, config) {
|
|
52
|
-
const customText = typeof config.anchorText === 'string' ? config.anchorText : ''
|
|
53
|
-
const useCustom = config.useCustom === true
|
|
54
|
-
|
|
55
|
-
/** 本进程已处理过的会话(真相在持久事件流)。 */
|
|
56
|
-
const handled = new Set()
|
|
57
|
-
|
|
58
|
-
/** 持久事件里是否已有本插件的锚点消息。 */
|
|
59
|
-
const seenAnchor = (session) => session.events.some((event) => {
|
|
60
|
-
const payload = event.data && typeof event.data.message === 'object' && event.data.message !== null
|
|
61
|
-
? event.data.message
|
|
62
|
-
: event.data
|
|
63
|
-
return payload?.source?.plugin === 'near-anchor'
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
67
|
-
const decision = await next()
|
|
68
|
-
if (decision.kind === 'reject') return decision
|
|
69
|
-
if (agent === undefined) return decision
|
|
70
|
-
const session = agent.session
|
|
71
|
-
if (session === undefined || handled.has(session.id) || seenAnchor(session)) return decision
|
|
72
|
-
// 子代理不注入锚点:让 dsh-mnemon 等结构化 worker 按自己的提示词工作。
|
|
73
|
-
if (isDelegated(session)) return decision
|
|
74
|
-
|
|
75
|
-
const messages = Array.isArray(decision.messages) ? decision.messages : []
|
|
76
|
-
// 只锚真实用户消息;插件消息原样保留。
|
|
77
|
-
const userIndex = messages.findIndex((message) => message?.source?.kind === 'user')
|
|
78
|
-
if (userIndex < 0) return decision
|
|
79
|
-
const taskText = extractText(messages[userIndex])
|
|
80
|
-
if (taskText.length === 0) return decision
|
|
81
|
-
|
|
82
|
-
const anchorText = chooseAnchor(taskText, customText, useCustom)
|
|
83
|
-
if (anchorText.length === 0) return decision
|
|
84
|
-
handled.add(session.id)
|
|
85
|
-
|
|
86
|
-
const anchor = {
|
|
87
|
-
id: newMessageId('near-anchor'),
|
|
88
|
-
role: 'user',
|
|
89
|
-
content: [{ type: 'text', text: anchorText }],
|
|
90
|
-
source: {
|
|
91
|
-
kind: 'near-anchor',
|
|
92
|
-
plugin: 'near-anchor',
|
|
93
|
-
form: 'notice',
|
|
94
|
-
summary: 'near-anchor 近距离首句锚点',
|
|
95
|
-
},
|
|
96
|
-
}
|
|
97
|
-
const nextMessages = [...messages]
|
|
98
|
-
nextMessages.splice(userIndex + 1, 0, anchor)
|
|
99
|
-
return { ...decision, messages: nextMessages }
|
|
100
|
-
})
|
|
101
|
-
}
|
package/preset/preset.yml
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* prompt-injector — prompt-tool 的提示词注入层。
|
|
3
|
-
*
|
|
4
|
-
* 职责:会话晋升(首个 tool/call 或 assistant/message,与 promoteOn: either
|
|
5
|
-
* 同定义)后,turn1 reasoning 以 "we" 开头 → 下一轮注入 promptText 一次;
|
|
6
|
-
* we 未确认 → 最多再等一轮,仍无则强制注入(兜底,绝不卡死)。
|
|
7
|
-
*
|
|
8
|
-
* promptText 由宿主在生成 preset 时写入 config(默认等于 preset.md 原文)。
|
|
9
|
-
* AGENTS.md 的注入走 instruction-hint.mjs,不由本模块拼接。
|
|
10
|
-
*
|
|
11
|
-
* 实测依据:
|
|
12
|
-
* - 复杂任务 + 原版 2 工具 + maxTokens 1024 → turn1 reasoning "We need";
|
|
13
|
-
* - 简单任务模型直接调工具干活(无 we 首词),走兜底注入;
|
|
14
|
-
* - 锚定消息("Tools are not open yet")在 2 工具下不产生 we
|
|
15
|
-
* (模型回复 "The user says..." 叙述语气),已废弃。
|
|
16
|
-
*
|
|
17
|
-
* 状态全部从持久 session events 推导(resume/reload 安全):
|
|
18
|
-
* - promoted:events 含 tool/call 或 assistant/message;
|
|
19
|
-
* - 已注入:events 含 source.kind=plugin + source.plugin=prompt-injector 的
|
|
20
|
-
* 消息事件(注入消息本身就是持久事件),进程重启/插件重载后据此跳过;
|
|
21
|
-
* - 内存 Set/Map 只做进程内快路径 memo,不承载跨进程真相。
|
|
22
|
-
* 注入仅一次。
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
26
|
-
import { newMessageId, PROMOTE_EVENTS } from './shared.mjs'
|
|
27
|
-
|
|
28
|
-
/** Cordis plugin name used by loader diagnostics. */
|
|
29
|
-
export const name = 'prompt-injector'
|
|
30
|
-
|
|
31
|
-
/** No inject list: nothing to resolve, only event listeners. */
|
|
32
|
-
export const inject = []
|
|
33
|
-
|
|
34
|
-
export function apply(ctx, config) {
|
|
35
|
-
const promptText = typeof config.promptText === 'string' && config.promptText.length > 0
|
|
36
|
-
? config.promptText
|
|
37
|
-
: undefined
|
|
38
|
-
|
|
39
|
-
/** Sessions whose promptText has been injected (append-only). */
|
|
40
|
-
const injectedPrompt = new Set()
|
|
41
|
-
/** Sessions whose first assistant reasoning has been scanned (memoized). */
|
|
42
|
-
const weScanned = new Map()
|
|
43
|
-
/** Epoch-aware promotion, same definition as tool-bootstrap/context-gate. */
|
|
44
|
-
const promotion = createEpochPromotion(PROMOTE_EVENTS.either, { includeSubagents: false })
|
|
45
|
-
ctx.on('session/event', (session, event) => promotion.observe(session, event))
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* 从持久 session events 推导「是否已经注入过 promptText」。
|
|
49
|
-
* 注入消息本身就是 user/message 持久事件(data 即消息本体),因此跨进程
|
|
50
|
-
* 重启 / 插件热重载后仍能识别;兼容 router-standard 观测到的
|
|
51
|
-
* `data.message` 嵌套形状。内存 Set 只做快路径 memo,真相在事件流。
|
|
52
|
-
*/
|
|
53
|
-
const hasInjectedPrompt = (session) =>
|
|
54
|
-
session.events.some((event) => {
|
|
55
|
-
const payload = event.data && typeof event.data.message === 'object' && event.data.message !== null
|
|
56
|
-
? event.data.message
|
|
57
|
-
: event.data
|
|
58
|
-
const source = payload?.source
|
|
59
|
-
return source?.kind === 'plugin' && source?.plugin === 'prompt-injector'
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/** 第一个 assistant/message(锚定轮)的 reasoning 是否以 "we" 开头。 */
|
|
65
|
-
const isWeAnchored = (agent) => {
|
|
66
|
-
const session = agent.session
|
|
67
|
-
const cached = weScanned.get(session.id)
|
|
68
|
-
if (cached !== undefined) return cached
|
|
69
|
-
const first = session.events.find((event) => event.type === 'assistant/message')
|
|
70
|
-
// 首个 assistant 消息尚未落库时不要缓存 false,等待下一轮再查。
|
|
71
|
-
if (first === undefined) return false
|
|
72
|
-
const content = first.data?.message?.content ?? []
|
|
73
|
-
const reasoning = content.find((block) => block.type === 'reasoning')
|
|
74
|
-
const anchored = reasoning !== undefined && /^we\b/i.test(String(reasoning.text ?? '').trim())
|
|
75
|
-
weScanned.set(session.id, anchored)
|
|
76
|
-
return anchored
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** 已落库的 assistant 回合数(we 未确认时的兜底轮计数)。 */
|
|
80
|
-
const assistantRounds = (agent) =>
|
|
81
|
-
agent.session.events.filter((event) => event.type === 'assistant/message').length
|
|
82
|
-
|
|
83
|
-
// 锚定确认后注入一次 promptText(we 确认立即注入;未确认最多再等一轮)。
|
|
84
|
-
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
85
|
-
const decision = await next()
|
|
86
|
-
if (decision.kind === 'reject') return decision
|
|
87
|
-
if (agent === undefined || promptText === undefined) return decision
|
|
88
|
-
if (!promotion.status(agent).promoted) return decision
|
|
89
|
-
const session = agent.session
|
|
90
|
-
if (session === undefined || injectedPrompt.has(session.id)) return decision
|
|
91
|
-
// 持久事件级幂等:进程重启/插件重载后,已注入消息仍在事件流里,据此跳过。
|
|
92
|
-
if (hasInjectedPrompt(session)) {
|
|
93
|
-
injectedPrompt.add(session.id)
|
|
94
|
-
return decision
|
|
95
|
-
}
|
|
96
|
-
const weOk = isWeAnchored(agent)
|
|
97
|
-
if (!weOk && assistantRounds(agent) <= 1) return decision
|
|
98
|
-
injectedPrompt.add(session.id)
|
|
99
|
-
const message = {
|
|
100
|
-
id: newMessageId('prompt-injector'),
|
|
101
|
-
role: 'user',
|
|
102
|
-
content: [{ type: 'text', text: promptText }],
|
|
103
|
-
source: {
|
|
104
|
-
kind: 'plugin',
|
|
105
|
-
plugin: 'prompt-injector',
|
|
106
|
-
form: 'notice',
|
|
107
|
-
summary: weOk ? 'prompt-tool 提示词(we 锚定确认后注入)' : 'prompt-tool 提示词(we 未确认,兜底注入)',
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
return { ...decision, messages: [message, ...decision.messages] }
|
|
111
|
-
})
|
|
112
|
-
}
|