dsh-plugin-prompt-tool 0.1.4 → 0.2.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 +75 -171
- package/lib/client.js +118 -11
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +16 -2
- package/lib/index.mjs +399 -101
- package/lib/preset-core.d.mts +12 -1
- package/lib/preset-core.mjs +94 -21
- package/package.json +5 -6
- package/plan.md +292 -265
- package/preset/near-anchor.mjs +117 -0
- package/preset/router-first-turn.mjs +59 -0
- package/{vendor → upstream}/dsh-anchored-standard/NOTICE +5 -0
- package/upstream/dsh-anchored-standard/REVISION +1 -0
- package/{vendor → upstream}/dsh-anchored-standard/preset/agent.cordis.yml +53 -24
- package/upstream/dsh-anchored-standard/preset/context-gate.mjs +202 -0
- package/{vendor → upstream}/dsh-anchored-standard/preset/dev-tool-search.mjs +10 -8
- package/{vendor → upstream}/dsh-anchored-standard/preset/instruction-hint.mjs +66 -13
- package/{vendor → upstream}/dsh-anchored-standard/preset/tool-bootstrap.mjs +41 -78
- package/preset/turn-anchor.mjs +0 -83
- /package/{vendor → upstream}/dsh-anchored-standard/LICENSE +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/compaction-epoch.mjs +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/custom-bash.mjs +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/preset.yml +0 -0
- /package/{vendor → upstream}/dsh-anchored-standard/preset/skill-search.mjs +0 -0
|
@@ -10,24 +10,35 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Behavior:
|
|
12
12
|
* - After the session records its first durable promotion signal
|
|
13
|
-
* (`promoteOn`, default `either`), ONE hint message is injected
|
|
14
|
-
*
|
|
15
|
-
* files were found:
|
|
13
|
+
* (`promoteOn`, default `either`), ONE hint message is injected, listing
|
|
14
|
+
* which instruction files were found:
|
|
16
15
|
* - user-global: `$DSH_HOME/AGENTS.md`
|
|
17
16
|
* - project chain: AGENTS.md / CLAUDE.md / AGENTS.local.md / CLAUDE.local.md
|
|
18
17
|
* walking up from the session cwd to the project root (a directory
|
|
19
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.
|
|
20
25
|
* - The hint instructs the model to READ the files before acting when
|
|
21
26
|
* relevant, without embedding their content.
|
|
22
27
|
* - Files are probed via `ctx.fs` (the host filesystem seam); a missing fs
|
|
23
28
|
* service or an unreadable probe degrades to no hint (never throws).
|
|
24
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).
|
|
25
35
|
*
|
|
26
36
|
* ROW ORDER: this plugin registers its `agent/pre-step` handler with
|
|
27
|
-
* `prepend: true` and after `tool-bootstrap`, so it runs
|
|
28
|
-
*
|
|
29
|
-
* is inactive. The hint source kind is `instruction-hint`, which is
|
|
30
|
-
*
|
|
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).
|
|
31
42
|
*/
|
|
32
43
|
|
|
33
44
|
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
@@ -52,6 +63,18 @@ function parsePromoteOn(value) {
|
|
|
52
63
|
throw new TypeError(`${name}: promoteOn must be one of "tool-call", "assistant-message", "either"; got ${JSON.stringify(value)}`)
|
|
53
64
|
}
|
|
54
65
|
|
|
66
|
+
/** Every config key this plugin accepts — anything else is a typo. */
|
|
67
|
+
const ALLOWED_KEYS = new Set(['promoteOn', 'includeSubagents'])
|
|
68
|
+
|
|
69
|
+
/** Validate an optional boolean flag with a default. */
|
|
70
|
+
function booleanOption(value, field, fallback) {
|
|
71
|
+
if (value === undefined) return fallback
|
|
72
|
+
if (typeof value !== 'boolean') {
|
|
73
|
+
throw new TypeError(`${name}: ${field} must be a boolean`)
|
|
74
|
+
}
|
|
75
|
+
return value
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
/** Find the project root: first ancestor containing any root marker (e.g. .git). */
|
|
56
79
|
async function findProjectRoot(fs, cwd, signal) {
|
|
57
80
|
let current = cwd
|
|
@@ -103,12 +126,42 @@ function parentPath(path) {
|
|
|
103
126
|
|
|
104
127
|
/** Register the post-promotion instruction-hint injector. */
|
|
105
128
|
export function apply(ctx, config) {
|
|
106
|
-
const
|
|
107
|
-
|
|
129
|
+
const source = config === undefined ? {} : config
|
|
130
|
+
if (typeof source !== 'object' || source === null || Array.isArray(source)) {
|
|
131
|
+
throw new TypeError(`${name}: config must be an object`)
|
|
132
|
+
}
|
|
133
|
+
const unknown = Object.keys(source).filter((key) => !ALLOWED_KEYS.has(key))
|
|
134
|
+
if (unknown.length > 0) {
|
|
135
|
+
throw new TypeError(
|
|
136
|
+
`${name}: unknown config key(s) ${unknown.join(', ')} — allowed keys: ${[...ALLOWED_KEYS].sort().join(', ')}`,
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
const promoteEvents = parsePromoteOn(source.promoteOn)
|
|
140
|
+
const includeSubagents = booleanOption(source.includeSubagents, 'includeSubagents', false)
|
|
141
|
+
const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
|
|
108
142
|
ctx.on('session/event', (session, event) => promotion.observe(session, event))
|
|
109
143
|
|
|
110
|
-
/**
|
|
111
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Sessions whose hint is already durable in the event log — the
|
|
146
|
+
* restart-safe replacement for an in-memory "already hinted" set. Seeded by
|
|
147
|
+
* a one-time scan, then maintained incrementally through `session/event`.
|
|
148
|
+
*/
|
|
149
|
+
const hinted = new Map()
|
|
150
|
+
const hintIsDurable = (session) => {
|
|
151
|
+
const known = hinted.get(session.id)
|
|
152
|
+
if (known !== undefined) return known
|
|
153
|
+
const found = (Array.isArray(session.events) ? session.events : []).some((event) =>
|
|
154
|
+
event.type === 'user/message' && event.data?.source?.kind === 'instruction-hint',
|
|
155
|
+
)
|
|
156
|
+
hinted.set(session.id, found)
|
|
157
|
+
return found
|
|
158
|
+
}
|
|
159
|
+
ctx.on('session/event', (session, event) => {
|
|
160
|
+
if (event.type === 'user/message' && event.data?.source?.kind === 'instruction-hint') {
|
|
161
|
+
hinted.set(session.id, true)
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
|
|
112
165
|
let warned = false
|
|
113
166
|
const warnOnce = (message) => {
|
|
114
167
|
if (warned) return
|
|
@@ -125,8 +178,8 @@ export function apply(ctx, config) {
|
|
|
125
178
|
try {
|
|
126
179
|
if (promotion.status(agent).promoted !== true) return decision
|
|
127
180
|
const session = agent.session
|
|
128
|
-
if (session === undefined ||
|
|
129
|
-
hinted.
|
|
181
|
+
if (session === undefined || hintIsDurable(session)) return decision
|
|
182
|
+
hinted.set(session.id, true)
|
|
130
183
|
|
|
131
184
|
const fs = ctx.get('fs')
|
|
132
185
|
if (fs === undefined) return decision
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Anchored tool bootstrap — keep the FIRST model request on the Minimal
|
|
3
|
-
* preset's REAL tool schema (persistent `bash` + `str_replace_editor`),
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* preset's REAL tool schema (persistent `bash` + `str_replace_editor`), then
|
|
4
|
+
* narrow the catalog to a minimal RESIDENT set once the session has produced
|
|
5
|
+
* its first durable promotion signal. Injected-context control lives in the
|
|
6
|
+
* companion `context-gate` plugin, not here.
|
|
7
7
|
*
|
|
8
8
|
* The phase is derived from durable session events, so resume and reload
|
|
9
9
|
* preserve it. By default (`promoteOn: 'either'`) a session promotes after the
|
|
@@ -39,18 +39,19 @@
|
|
|
39
39
|
* promotion — the next request's seed proposal carries the previous
|
|
40
40
|
* header's maxTokens forward, so the release must be explicit.
|
|
41
41
|
*
|
|
42
|
-
* 3. Injected
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
42
|
+
* 3. Injected context is NOT this plugin's concern: the companion
|
|
43
|
+
* `context-gate` plugin (shared/context-gate.mjs, mounted as the FIRST
|
|
44
|
+
* row) owns the unified injection control — runtime-context suppression
|
|
45
|
+
* on the assembly path and a claimed-baseline deny on the pre-step
|
|
46
|
+
* waterfall, both keyed to the same epoch-aware promotion phase. Mount it
|
|
47
|
+
* separately for context control alone; this file narrows only the tool
|
|
48
|
+
* catalog (plus the optional output cap below).
|
|
49
|
+
*
|
|
50
|
+
* SUBAGENTS: by default subagents (delegationDepth > 0) are always promoted
|
|
51
|
+
* (resident catalog from their first request). `includeSubagents: true`
|
|
52
|
+
* makes them follow the same bootstrap phase — their first request also sees
|
|
53
|
+
* the bootstrap pair, and their own first reply or tool call promotes them.
|
|
54
|
+
* Keep this flag in sync with the context-gate row's flag.
|
|
54
55
|
*
|
|
55
56
|
* POST-PROMOTION RESIDENT SET (local addition, user-measured): the promoted
|
|
56
57
|
* phase does NOT dump the whole Standard catalog at once — that dump pulls
|
|
@@ -76,15 +77,14 @@
|
|
|
76
77
|
* Robustness:
|
|
77
78
|
* - Promotion decisions are memoized per session id for this process; the
|
|
78
79
|
* durable event scan runs once per session per process, then O(1).
|
|
79
|
-
* - Subagents (delegationDepth > 0) are always promoted (resident catalog)
|
|
80
|
+
* - Subagents (delegationDepth > 0) are always promoted (resident catalog)
|
|
81
|
+
* unless `includeSubagents: true`.
|
|
80
82
|
* - A missing bootstrap tool degrades to the full catalog with a one-time
|
|
81
83
|
* warning instead of throwing, so a composition drift can never brick
|
|
82
84
|
* every request of a session.
|
|
83
|
-
* -
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
* `suppressedContextSources`, non-positive `bootstrapMaxTokens`) fails at
|
|
87
|
-
* apply time, i.e. at preset mount, where it is visible and fixable.
|
|
85
|
+
* - Invalid config (bad tool lists, unknown `promoteOn`, malformed flags,
|
|
86
|
+
* non-positive `bootstrapMaxTokens`) fails at apply time, i.e. at preset
|
|
87
|
+
* mount, where it is visible and fixable.
|
|
88
88
|
*/
|
|
89
89
|
|
|
90
90
|
import { createEpochPromotion } from './compaction-epoch.mjs'
|
|
@@ -94,14 +94,11 @@ export const name = 'anchored-tool-bootstrap'
|
|
|
94
94
|
|
|
95
95
|
/**
|
|
96
96
|
* Deliberately NO inject list: the listeners only touch services at event
|
|
97
|
-
* time.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* messages after the strip. The pre-step listener additionally registers with
|
|
103
|
-
* `prepend: true` so the strip stays the outermost transform even against
|
|
104
|
-
* host-plane listeners and future row reordering.
|
|
97
|
+
* time. Keep this row right AFTER the context-gate row in agent.cordis.yml:
|
|
98
|
+
* waterfall after-next transforms apply in reverse registration order, so the
|
|
99
|
+
* tool filter here must register before any plugin that touches the same
|
|
100
|
+
* assembly. The optional budget listener registers with `prepend: true` so a
|
|
101
|
+
* later listener can never override the first-round cap after we set it.
|
|
105
102
|
*/
|
|
106
103
|
export const inject = []
|
|
107
104
|
|
|
@@ -113,15 +110,16 @@ const PROMOTE_EVENTS = {
|
|
|
113
110
|
}
|
|
114
111
|
|
|
115
112
|
/** Every config key this plugin accepts — anything else is a typo. */
|
|
116
|
-
const ALLOWED_KEYS = new Set(['bootstrapTools', 'promoteOn', 'bootstrapMaxTokens', '
|
|
113
|
+
const ALLOWED_KEYS = new Set(['bootstrapTools', 'promoteOn', 'bootstrapMaxTokens', 'compactionTools', 'includeSubagents'])
|
|
117
114
|
|
|
118
|
-
/**
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
115
|
+
/** Validate an optional boolean flag with a default. */
|
|
116
|
+
function booleanOption(value, field, fallback) {
|
|
117
|
+
if (value === undefined) return fallback
|
|
118
|
+
if (typeof value !== 'boolean') {
|
|
119
|
+
throw new TypeError(`${name}: ${field} must be a boolean`)
|
|
120
|
+
}
|
|
121
|
+
return value
|
|
122
|
+
}
|
|
125
123
|
|
|
126
124
|
/**
|
|
127
125
|
* The default first-request catalog: the OFFICIAL Minimal preset's exact tool
|
|
@@ -152,19 +150,6 @@ function parsePromoteOn(value) {
|
|
|
152
150
|
throw new TypeError(`${name}: promoteOn must be one of "tool-call", "assistant-message", "either"; got ${JSON.stringify(value)}`)
|
|
153
151
|
}
|
|
154
152
|
|
|
155
|
-
/**
|
|
156
|
-
* Validate the suppressed context sources. Unlike the bootstrap tool lists,
|
|
157
|
-
* an explicitly empty array is meaningful: it disables the context filter
|
|
158
|
-
* while keeping the tool bootstrap.
|
|
159
|
-
*/
|
|
160
|
-
function sourceList(value, field, fallback) {
|
|
161
|
-
if (value === undefined) return new Set(fallback)
|
|
162
|
-
if (!Array.isArray(value) || value.some((item) => typeof item !== 'string' || item.length === 0)) {
|
|
163
|
-
throw new TypeError(`${name}: ${field} must be an array of non-empty strings`)
|
|
164
|
-
}
|
|
165
|
-
return new Set(value)
|
|
166
|
-
}
|
|
167
|
-
|
|
168
153
|
/**
|
|
169
154
|
* Validate the optional first-request output cap. `undefined` means NO cap:
|
|
170
155
|
* the Minimal tool schema anchors at the adapter-default maxTokens, and the
|
|
@@ -194,13 +179,13 @@ export function apply(ctx, config) {
|
|
|
194
179
|
const bootstrapTools = stringList(source.bootstrapTools, 'bootstrapTools')
|
|
195
180
|
const promoteEvents = parsePromoteOn(source.promoteOn)
|
|
196
181
|
const bootstrapMaxTokens = optionalPositiveInt(source.bootstrapMaxTokens, 'bootstrapMaxTokens')
|
|
197
|
-
const
|
|
182
|
+
const includeSubagents = booleanOption(source.includeSubagents, 'includeSubagents', false)
|
|
198
183
|
// Core work set exposed after a compaction, before re-promotion. Empty
|
|
199
184
|
// means "no compaction recovery catalog": the session stays on the
|
|
200
185
|
// bootstrap pair until a new promotion signal.
|
|
201
186
|
const compactionTools = stringListOrEmpty(source.compactionTools, 'compactionTools')
|
|
202
187
|
|
|
203
|
-
const promotion = createEpochPromotion(promoteEvents)
|
|
188
|
+
const promotion = createEpochPromotion(promoteEvents, { includeSubagents })
|
|
204
189
|
ctx.on('session/event', (session, event) => promotion.observe(session, event))
|
|
205
190
|
|
|
206
191
|
let warned = false
|
|
@@ -270,7 +255,9 @@ export function apply(ctx, config) {
|
|
|
270
255
|
return keepTools(assembled, keep, false)
|
|
271
256
|
}
|
|
272
257
|
// Controlled phase: the bootstrap pair; after a compaction, plus the
|
|
273
|
-
// compaction work set so mid-task work can continue.
|
|
258
|
+
// compaction work set so mid-task work can continue. Context control is
|
|
259
|
+
// NOT here: the companion `context-gate` plugin owns it (see the header
|
|
260
|
+
// note), so this filter touches only the tool catalog.
|
|
274
261
|
const { boundary } = status
|
|
275
262
|
const keep = new Set(bootstrapTools)
|
|
276
263
|
if (boundary >= 0) for (const toolName of compactionTools) keep.add(toolName)
|
|
@@ -311,28 +298,4 @@ export function apply(ctx, config) {
|
|
|
311
298
|
}
|
|
312
299
|
}, { prepend: true })
|
|
313
300
|
}
|
|
314
|
-
|
|
315
|
-
// Strip first-step injected reminders (skill catalog, AGENTS.md) during
|
|
316
|
-
// bootstrap. Because this listener is the first registered (see the inject
|
|
317
|
-
// note, the row order in agent.cordis.yml, and `prepend` below), the strip
|
|
318
|
-
// is the final waterfall transform and actually removes what later
|
|
319
|
-
// listeners inject.
|
|
320
|
-
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
321
|
-
// Downstream errors propagate untouched; only this filter's own logic is guarded.
|
|
322
|
-
const decision = await next()
|
|
323
|
-
if (decision.kind === 'reject') return decision
|
|
324
|
-
try {
|
|
325
|
-
if (promotion.status(agent).promoted || suppressedSources.size === 0) return decision
|
|
326
|
-
if (!Array.isArray(decision.messages)) return decision
|
|
327
|
-
const kept = decision.messages.filter((message) => {
|
|
328
|
-
const kind = message?.source?.kind
|
|
329
|
-
return typeof kind !== 'string' || !suppressedSources.has(kind)
|
|
330
|
-
})
|
|
331
|
-
return kept.length === decision.messages.length ? decision : { ...decision, messages: kept }
|
|
332
|
-
} catch (error) {
|
|
333
|
-
// A filter bug must never eat context: degrade to keeping every message.
|
|
334
|
-
warnOnce(`${name}: pre-step context filter failed, keeping injected context: ${String((error && error.message) || error)}`)
|
|
335
|
-
return decision
|
|
336
|
-
}
|
|
337
|
-
}, { prepend: true })
|
|
338
301
|
}
|
package/preset/turn-anchor.mjs
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* turn-anchor — prompt-tool 可选附加件:首轮独立锚定轮。
|
|
3
|
-
*
|
|
4
|
-
* 启用后,会话首个真实用户消息不直接发给模型:先把任务原样挪进
|
|
5
|
-
* `agent.inbox` 的 `next-step`(持久事件),首步只发一条 anchorText 作为
|
|
6
|
-
* 独立输入。模型先回应锚定句(实测首词稳定 "We need…"),driver 随后在同
|
|
7
|
-
* 一轮内自动消费 next-step 中的真实任务继续执行——因此用户只发一次消息,
|
|
8
|
-
* 但模型看到的首轮是锚定句。
|
|
9
|
-
*
|
|
10
|
-
* 状态从持久 session events 推导(resume/reload 安全):
|
|
11
|
-
* - 已处理/已拆轮:events 含 source.plugin=turn-anchor 的消息,或会话已
|
|
12
|
-
* 晋升(tool/call | assistant/message);
|
|
13
|
-
* - 任务在 next-step inbox 中持久化,进程重启后由 driver 继续消费。
|
|
14
|
-
* 失败兜底:inbox.append 抛错时原样返回决策,绝不吞掉用户任务。
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/** Cordis plugin name used by loader diagnostics. */
|
|
18
|
-
export const name = 'turn-anchor'
|
|
19
|
-
|
|
20
|
-
/** No inject list: nothing to resolve, only event listeners. */
|
|
21
|
-
export const inject = []
|
|
22
|
-
|
|
23
|
-
/** 消息 id:优先 crypto.randomUUID,旧运行时回退到随机串。 */
|
|
24
|
-
function newMessageId() {
|
|
25
|
-
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
26
|
-
? crypto.randomUUID()
|
|
27
|
-
: `turn-anchor-${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function apply(ctx, config) {
|
|
31
|
-
const anchorText = typeof config.anchorText === 'string' && config.anchorText.length > 0
|
|
32
|
-
? config.anchorText
|
|
33
|
-
: undefined
|
|
34
|
-
if (anchorText === undefined) return
|
|
35
|
-
|
|
36
|
-
/** Sessions already handled in this process (memo; truth stays in events/inbox). */
|
|
37
|
-
const handled = new Set()
|
|
38
|
-
|
|
39
|
-
/** 是否已经拆过轮:锚定句消息是持久 user/message,兼容 data.message 嵌套形状。 */
|
|
40
|
-
const seenAnchor = (session) => session.events.some((event) => {
|
|
41
|
-
const payload = event.data && typeof event.data.message === 'object' && event.data.message !== null
|
|
42
|
-
? event.data.message
|
|
43
|
-
: event.data
|
|
44
|
-
return payload?.source?.plugin === 'turn-anchor'
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
ctx.on('agent/pre-step', async ({ agent }, next) => {
|
|
48
|
-
const decision = await next()
|
|
49
|
-
if (decision.kind === 'reject') return decision
|
|
50
|
-
if (agent === undefined) return decision
|
|
51
|
-
const session = agent.session
|
|
52
|
-
if (session === undefined || handled.has(session.id)) return decision
|
|
53
|
-
|
|
54
|
-
const events = session.events
|
|
55
|
-
const promoted = events.some((event) => event.type === 'tool/call' || event.type === 'assistant/message')
|
|
56
|
-
if (promoted || seenAnchor(session)) {
|
|
57
|
-
handled.add(session.id)
|
|
58
|
-
return decision
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// 首步里的真实用户消息(source.kind === 'user')才是任务;插件消息原样保留。
|
|
62
|
-
const tasks = decision.messages.filter((message) => message?.source?.kind === 'user')
|
|
63
|
-
if (tasks.length === 0) return decision
|
|
64
|
-
|
|
65
|
-
// 任务挪进 next-step:driver 在本轮锚定步结束后自动 claim 并继续执行。
|
|
66
|
-
try {
|
|
67
|
-
for (const task of tasks) agent.inbox.append('next-step', task)
|
|
68
|
-
} catch {
|
|
69
|
-
// 入队失败(id 冲突/坏状态)→ 不拆轮,原样发送,绝不丢任务。
|
|
70
|
-
return decision
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
handled.add(session.id)
|
|
74
|
-
const anchor = {
|
|
75
|
-
id: newMessageId(),
|
|
76
|
-
role: 'user',
|
|
77
|
-
content: [{ type: 'text', text: anchorText }],
|
|
78
|
-
source: { kind: 'plugin', plugin: 'turn-anchor', form: 'notice', summary: 'turn-anchor 首轮独立锚定句' },
|
|
79
|
-
}
|
|
80
|
-
const kept = decision.messages.filter((message) => message?.source?.kind !== 'user')
|
|
81
|
-
return { ...decision, messages: [anchor, ...kept] }
|
|
82
|
-
})
|
|
83
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|