dsh-loop-continue 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -0
- package/README.md +51 -3
- package/cordis.patch.yml +11 -2
- package/lib/index.d.ts +6 -3
- package/lib/index.js +173 -37
- package/package.json +13 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Judge route leaked a literal `null` provider.** With `judgeProvider` absent
|
|
8
|
+
the guard tested `!== undefined`, so a `null` in the profile config was passed
|
|
9
|
+
straight to the LLM service as `{ provider: null, model: null }`. The service
|
|
10
|
+
reports an unknown provider through an error *finish chunk* rather than a
|
|
11
|
+
throw, so the call failed silently, the judge read an empty answer, and every
|
|
12
|
+
turn logged `verdict=false`. The guard now treats `null` and unset alike and
|
|
13
|
+
follows the conversation's own provider/model.
|
|
14
|
+
- **A judge failure was invisible.** An error finish chunk is now surfaced as
|
|
15
|
+
`judge failed: ...` instead of being parsed as a `false` verdict.
|
|
16
|
+
- **Steering could burn the whole per-turn budget on the same refusal.** The
|
|
17
|
+
guard now records the session-log position at each steer and checks whether
|
|
18
|
+
any step in between actually called a tool. A steer the model ignored ends the
|
|
19
|
+
turn instead of spending the remaining budget.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- **Configuration is resolved per evaluation** rather than frozen at mount, so
|
|
24
|
+
a change reaches the next turn-stopping check without a restart.
|
|
25
|
+
- **A Settings section is installed** (namespace `loop-continue`) for live
|
|
26
|
+
reconfiguration from the harness Settings UI.
|
|
27
|
+
- `judgeProvider`/`judgeModel` at `null` or unset both mean "follow the
|
|
28
|
+
conversation's route"; the judge no longer derives a route from the global
|
|
29
|
+
default-model setting.
|
|
30
|
+
|
|
31
|
+
## 0.1.0
|
|
32
|
+
|
|
33
|
+
Initial release: continue a turn whose trailing text promised an action that no
|
|
34
|
+
tool call performed.
|
package/README.md
CHANGED
|
@@ -18,9 +18,9 @@ more step; `false` (or an unparseable answer) lets the turn close.
|
|
|
18
18
|
|------------------|---------|------------------------------------------------|
|
|
19
19
|
| `maxContinuations` | 10 | hard cap on steering per turn (no infinite loop) |
|
|
20
20
|
| `maxSteps` | 10 | newest steps shown to the judge |
|
|
21
|
-
| `maxTailChars` | 2000 |
|
|
22
|
-
| `judgeProvider` | null | override provider; null =
|
|
23
|
-
| `judgeModel` | null | override model; null =
|
|
21
|
+
| `maxTailChars` | 2000 | trailing-text budget split across the first and last halves |
|
|
22
|
+
| `judgeProvider` | null | override provider; null/unset = derive from the active default model |
|
|
23
|
+
| `judgeModel` | null | override model; null/unset = derive from the active default model |
|
|
24
24
|
| `judgeMaxTokens` | 64 | judge output cap |
|
|
25
25
|
| `judgeTemperature` | 0 | judge sampling temperature |
|
|
26
26
|
| `steerText` | built-in | message that resumes the turn |
|
|
@@ -35,3 +35,51 @@ No model call runs unless the turn *both*:
|
|
|
35
35
|
|
|
36
36
|
This keeps the extra judge call off ordinary finished turns and only spends it
|
|
37
37
|
where the model plausibly dropped a pending action.
|
|
38
|
+
|
|
39
|
+
## Stopping early
|
|
40
|
+
|
|
41
|
+
Steering is not free: each continuation costs a judge round-trip plus one more
|
|
42
|
+
agent step. So the guard also watches whether a steer *worked*.
|
|
43
|
+
|
|
44
|
+
When it steers, it records how far the session log had grown. The next time the
|
|
45
|
+
same turn stops, it checks whether any assistant step in between actually called
|
|
46
|
+
a tool. If the model only narrated again, the steer was ignored and the turn is
|
|
47
|
+
closed instead of spending the remaining budget on the same refusal. A model
|
|
48
|
+
that answers the steer with a real tool call keeps its normal budget.
|
|
49
|
+
|
|
50
|
+
## Configuring the guard
|
|
51
|
+
|
|
52
|
+
The guard resolves its config on every evaluation rather than freezing it at
|
|
53
|
+
mount, so a change reaches the very next turn-stopping check. Two ways to set
|
|
54
|
+
values:
|
|
55
|
+
|
|
56
|
+
- the harness **Settings UI** (the plugin installs a `loop-continue` section), or
|
|
57
|
+
- the profile's own `cordis.patch.yml`.
|
|
58
|
+
|
|
59
|
+
Prefer leaving `judgeProvider`/`judgeModel` unset. The guard then derives a
|
|
60
|
+
registered route from the active default model, which always resolves; a
|
|
61
|
+
hand-written route can name a provider the LLM service does not have registered,
|
|
62
|
+
and the judge call then fails.
|
|
63
|
+
|
|
64
|
+
## Hot mount
|
|
65
|
+
|
|
66
|
+
`cordis.patch.yml` ships a **plain insert** — only `id` + `name`, no `config`
|
|
67
|
+
and no `!!js` expressions — so a market hot-mount can add or remove this plugin
|
|
68
|
+
as a minimal row. No user-specific endpoint, provider, or key is baked into the
|
|
69
|
+
patch: policy is resolved at runtime as described above.
|
|
70
|
+
|
|
71
|
+
A patch-layer change is replayed in full on every reload
|
|
72
|
+
(`applyEntryPatches` clones the entry list before applying), so a row adds and
|
|
73
|
+
removes cleanly and never accumulates.
|
|
74
|
+
|
|
75
|
+
Whether a patch edit takes effect *without a restart* depends on the host:
|
|
76
|
+
|
|
77
|
+
- Under the CLI (`dsh profile`), `runProfile` installs an HMR service and
|
|
78
|
+
registers the profile and user patch files with it, so patch edits are applied
|
|
79
|
+
live.
|
|
80
|
+
- Under **DSH Desktop 2.0.3** that path is not taken — the desktop shell
|
|
81
|
+
composes the profile itself (`dsh-app-boot` helpers) and never loads
|
|
82
|
+
`cordis-plugin-hmr`, so a `cordis.patch.yml` edit needs a profile restart.
|
|
83
|
+
|
|
84
|
+
Edits to `lib/*.js` always need a restart: the dsh HMR service is created with
|
|
85
|
+
`root: []`, so no source directory is watched for module replacement.
|
package/cordis.patch.yml
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
# dsh bundle patch: inserts this plugin into a profile's layer stack.
|
|
2
|
-
#
|
|
3
|
-
#
|
|
2
|
+
#
|
|
3
|
+
# This is a PLAIN insert -- only `id` + `name`, no `config`, no `!!js`
|
|
4
|
+
# expressions -- so the market's hot-mount parser can activate it live without
|
|
5
|
+
# a profile restart. `applyEntryPatches` structuredClones the entry list and
|
|
6
|
+
# replays the whole patch stack on every reload, so a minimal row adds and
|
|
7
|
+
# removes cleanly.
|
|
8
|
+
#
|
|
9
|
+
# Policy is NOT frozen here. The guard resolves its config on every evaluation
|
|
10
|
+
# (and accepts a live settings source), so values set in the profile's own
|
|
11
|
+
# cordis.patch.yml or in the harness Settings UI apply to the next turn without
|
|
12
|
+
# a restart.
|
|
4
13
|
- insert:
|
|
5
14
|
- id: loop-continue
|
|
6
15
|
name: 'dsh-loop-continue'
|
package/lib/index.d.ts
CHANGED
|
@@ -17,11 +17,11 @@ export interface Config {
|
|
|
17
17
|
maxContinuations?: number
|
|
18
18
|
/** Newest steps shown to the judge. */
|
|
19
19
|
maxSteps?: number
|
|
20
|
-
/**
|
|
20
|
+
/** Total trailing-text characters handed to the judge, split across the first and last halves. */
|
|
21
21
|
maxTailChars?: number
|
|
22
|
-
/** Override provider; null
|
|
22
|
+
/** Override provider; null/unset derives from the active default model. */
|
|
23
23
|
judgeProvider?: string | null
|
|
24
|
-
/** Override model; null
|
|
24
|
+
/** Override model; null/unset derives from the active default model. */
|
|
25
25
|
judgeModel?: string | null
|
|
26
26
|
/** Judge output cap. */
|
|
27
27
|
judgeMaxTokens?: number
|
|
@@ -73,3 +73,6 @@ export declare function parseVerdict(text: string): boolean
|
|
|
73
73
|
|
|
74
74
|
/** Register the turn-stopping guard on a Cordis context. */
|
|
75
75
|
export declare function apply(ctx: unknown, config: Config): void
|
|
76
|
+
|
|
77
|
+
/** Settings namespace carrying this plugin's policy; drives live reconfiguration. */
|
|
78
|
+
export declare const SETTINGS_NAMESPACE: 'loop-continue'
|
package/lib/index.js
CHANGED
|
@@ -33,7 +33,7 @@ const DEFAULT_MAX_CONTINUATIONS = 10
|
|
|
33
33
|
/** How many most-recent steps of the current turn the summary shows. */
|
|
34
34
|
const DEFAULT_MAX_STEPS = 10
|
|
35
35
|
|
|
36
|
-
/**
|
|
36
|
+
/** Total characters of trailing text handed to the judge, split across the first and last halves. */
|
|
37
37
|
const DEFAULT_MAX_TAIL_CHARS = 2000
|
|
38
38
|
|
|
39
39
|
export const Config = z.object({
|
|
@@ -119,16 +119,40 @@ export function summarizeTurn(events, turn, maxSteps) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
-
// The
|
|
123
|
-
// human
|
|
122
|
+
// The judge needs the task this turn is answering. Walk the preceding
|
|
123
|
+
// human turns in reverse, skipping bare acknowledgements ("continue",
|
|
124
|
+
// "ok", ...) that carry no task information, and keep the most recent few
|
|
125
|
+
// substantive requests in chronological order.
|
|
126
|
+
const acknowledgements = new Set([
|
|
127
|
+
'继续', '继续吧', '接着', '继续搞', '好的', '好', '嗯', '知道了',
|
|
128
|
+
'ok', 'okay', 'go on', 'continue', 'go', 'next',
|
|
129
|
+
])
|
|
130
|
+
const isAcknowledgement = text => acknowledgements.has(text.trim().toLowerCase())
|
|
131
|
+
const requests = []
|
|
124
132
|
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
125
133
|
const event = events[i]
|
|
126
134
|
if (event.type !== 'user/message' || event.data?.turn > turn) continue
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
135
|
+
if (event.data.source?.kind !== 'user') continue
|
|
136
|
+
const text = (event.data.content ?? [])
|
|
137
|
+
.filter(b => b.type === 'text').map(b => b.text).join('\n').trim()
|
|
138
|
+
if (!text) continue
|
|
139
|
+
if (isAcknowledgement(text)) continue
|
|
140
|
+
requests.unshift(text)
|
|
141
|
+
if (requests.length >= 3) break
|
|
142
|
+
}
|
|
143
|
+
// Fall back to the nearest human message when every preceding one is a bare
|
|
144
|
+
// acknowledgement (or there are none); a weak anchor beats a blank one.
|
|
145
|
+
if (requests.length === 0) {
|
|
146
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
147
|
+
const event = events[i]
|
|
148
|
+
if (event.type !== 'user/message' || event.data?.turn > turn) continue
|
|
149
|
+
if (event.data.source?.kind !== 'user') continue
|
|
150
|
+
userRequest = (event.data.content ?? [])
|
|
151
|
+
.filter(b => b.type === 'text').map(b => b.text).join('\n').trim()
|
|
152
|
+
if (userRequest) break
|
|
153
|
+
}
|
|
154
|
+
} else {
|
|
155
|
+
userRequest = requests.join('\n')
|
|
132
156
|
}
|
|
133
157
|
|
|
134
158
|
const window = steps.slice(-maxSteps)
|
|
@@ -164,8 +188,13 @@ export function renderSummary(summary, maxTailChars) {
|
|
|
164
188
|
const tools = s.tools.length > 0 ? s.tools.join(', ') : 'no tool call'
|
|
165
189
|
return ` step ${s.step}: ${tools}`
|
|
166
190
|
})
|
|
191
|
+
// The model states its *next* action near the front ("now I will..."),
|
|
192
|
+
// then often rambles; the very end usually re-commits. Keep both ends so
|
|
193
|
+
// the judge sees the promise without eating the whole budget.
|
|
167
194
|
const tail = summary.text.length > maxTailChars
|
|
168
|
-
? `${summary.text.slice(
|
|
195
|
+
? `${summary.text.slice(0, Math.floor(maxTailChars / 2))}\n`
|
|
196
|
+
+ '...\n[truncated middle]\n...\n'
|
|
197
|
+
+ `${summary.text.slice(-Math.ceil(maxTailChars / 2))}`
|
|
169
198
|
: summary.text
|
|
170
199
|
const priorNote = summary.truncated
|
|
171
200
|
? ` (showing only the last ${summary.steps.length} steps)\n`
|
|
@@ -210,8 +239,12 @@ async function judge(ctx, route, summary, config, signal) {
|
|
|
210
239
|
'A turn ends when the agent writes text and calls no tool.',
|
|
211
240
|
'Decide whether that trailing text states an action the agent still',
|
|
212
241
|
'intends to perform, or merely reports completed work.',
|
|
213
|
-
'A promise of future action
|
|
214
|
-
'
|
|
242
|
+
'A promise of future action means the task is unfinished when no tool',
|
|
243
|
+
'call in the step list performed it. This includes explicit "now I will...",',
|
|
244
|
+
'"next I will..." as well as softer commitments like "let me check/confirm',
|
|
245
|
+
'/verify ... then ...", "I need to look at ...", "let me first ...", or',
|
|
246
|
+
'phrases that name a pending read, edit, run, or lookup the agent has not',
|
|
247
|
+
'yet performed.',
|
|
215
248
|
'A finished report, a question to the human, or a final answer means the',
|
|
216
249
|
'task is finished.',
|
|
217
250
|
'Reply with exactly one word: true or false.',
|
|
@@ -237,6 +270,14 @@ async function judge(ctx, route, summary, config, signal) {
|
|
|
237
270
|
|
|
238
271
|
const text = assembler.blocks()
|
|
239
272
|
.filter(b => b.type === 'text').map(b => b.text).join('')
|
|
273
|
+
// A missing or unknown provider is swallowed into an error finish chunk by
|
|
274
|
+
// the LLM service; surface it so a misconfigured judge route is visible in
|
|
275
|
+
// the log instead of reading as an endless stream of `verdict=false`.
|
|
276
|
+
const finish = assembler.finish
|
|
277
|
+
if (finish?.kind === 'error') {
|
|
278
|
+
const detail = finish.failure?.message ?? JSON.stringify(finish.failure ?? {})
|
|
279
|
+
throw new Error(`judge call failed: ${detail}`)
|
|
280
|
+
}
|
|
240
281
|
return parseVerdict(text)
|
|
241
282
|
}
|
|
242
283
|
|
|
@@ -246,26 +287,15 @@ async function judge(ctx, route, summary, config, signal) {
|
|
|
246
287
|
* @param config - plugin policy.
|
|
247
288
|
*/
|
|
248
289
|
export function apply(ctx, config) {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
...config,
|
|
254
|
-
maxContinuations,
|
|
255
|
-
maxSteps,
|
|
256
|
-
maxTailChars,
|
|
257
|
-
judgeMaxTokens: config.judgeMaxTokens ?? 64,
|
|
258
|
-
judgeTemperature: config.judgeTemperature ?? 0,
|
|
259
|
-
steerText: config.steerText ?? (
|
|
260
|
-
'You described an action but did not call any tool. Continue the task now: '
|
|
261
|
-
+ 'call the tool for the action you just described. Do not narrate — emit the tool call.'
|
|
262
|
-
),
|
|
263
|
-
debug: config.debug ?? false,
|
|
264
|
-
}
|
|
290
|
+
// The live config source. A Settings-UI change swaps this closure instead of
|
|
291
|
+
// rebuilding the plugin, so every evaluation below reads the current values.
|
|
292
|
+
let current = () => config
|
|
293
|
+
registerSettings(ctx, config, { setSource: source => { current = source } })
|
|
265
294
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
+
|
|
295
|
+
const initial = resolveConfig(current())
|
|
296
|
+
ctx.logger?.info?.(`[${name}] loaded: maxContinuations=${initial.maxContinuations} `
|
|
297
|
+
+ `maxSteps=${initial.maxSteps} judge=${initial.judgeProvider ?? '<session-route>'}`
|
|
298
|
+
+ `/${initial.judgeModel ?? '<session-route>'} debug=${String(initial.debug)}`)
|
|
269
299
|
|
|
270
300
|
/**
|
|
271
301
|
* Continuations already spent per turn. The turn number is stable while
|
|
@@ -274,7 +304,16 @@ export function apply(ctx, config) {
|
|
|
274
304
|
*/
|
|
275
305
|
const spent = new Map()
|
|
276
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Turns waiting to see whether their last steer changed anything. A steer
|
|
309
|
+
* that the model ignores costs a full judge round-trip and produces another
|
|
310
|
+
* narration, so one ignored steer ends the turn instead of burning the whole
|
|
311
|
+
* budget. Cleared as soon as the probe is read, or when the turn closes.
|
|
312
|
+
*/
|
|
313
|
+
const pendingProbe = new Map()
|
|
314
|
+
|
|
277
315
|
ctx.on('agent/turn-stopping', async ({ agent, turn, signal }) => {
|
|
316
|
+
const resolved = resolveConfig(current())
|
|
278
317
|
const used = spent.get(turn) ?? 0
|
|
279
318
|
if (used >= resolved.maxContinuations) {
|
|
280
319
|
// Keep the exhausted entry: the hook fires again after every extra step
|
|
@@ -283,10 +322,31 @@ export function apply(ctx, config) {
|
|
|
283
322
|
return
|
|
284
323
|
}
|
|
285
324
|
|
|
286
|
-
const
|
|
325
|
+
const events = readEvents(agent.session)
|
|
326
|
+
const summary = summarizeTurn(events, turn, resolved.maxSteps)
|
|
287
327
|
if (!looksUnfinished(summary)) return
|
|
288
328
|
|
|
289
|
-
|
|
329
|
+
// Did the previous steer actually make the model call a tool? A turn that
|
|
330
|
+
// narrates again after being told to emit the call will keep doing so, so
|
|
331
|
+
// stop here rather than spend the remaining budget on the same refusal.
|
|
332
|
+
const probe = pendingProbe.get(turn)
|
|
333
|
+
if (probe !== undefined) {
|
|
334
|
+
pendingProbe.delete(turn)
|
|
335
|
+
const calledTool = events.slice(probe.eventsAtSteer).some(event => {
|
|
336
|
+
if (event.type !== 'assistant/message' || event.data?.turn !== turn) return false
|
|
337
|
+
return (event.data.message?.content ?? []).some(b => b.type === 'tool-call')
|
|
338
|
+
})
|
|
339
|
+
if (!calledTool) {
|
|
340
|
+
spent.set(turn, resolved.maxContinuations)
|
|
341
|
+
if (resolved.debug) {
|
|
342
|
+
ctx.logger?.info?.(`[${name}] turn ${turn}: previous steer produced no tool call; `
|
|
343
|
+
+ 'stopping instead of steering again')
|
|
344
|
+
}
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const route = resolved.judgeProvider != null && resolved.judgeModel != null
|
|
290
350
|
? { provider: resolved.judgeProvider, model: resolved.judgeModel }
|
|
291
351
|
: routeOf(agent)
|
|
292
352
|
|
|
@@ -309,6 +369,7 @@ export function apply(ctx, config) {
|
|
|
309
369
|
}
|
|
310
370
|
|
|
311
371
|
spent.set(turn, used + 1)
|
|
372
|
+
pendingProbe.set(turn, { eventsAtSteer: events.length })
|
|
312
373
|
agent.steer(createUserMessage({
|
|
313
374
|
content: [{ type: 'text', text: resolved.steerText }],
|
|
314
375
|
source: PLUGIN_SOURCE,
|
|
@@ -317,15 +378,90 @@ export function apply(ctx, config) {
|
|
|
317
378
|
|
|
318
379
|
/** Drop bookkeeping once a turn truly closes. */
|
|
319
380
|
ctx.on('session/event', (_session, event) => {
|
|
320
|
-
if (event.type === 'turn/end')
|
|
381
|
+
if (event.type === 'turn/end') {
|
|
382
|
+
spent.delete(event.data.turn)
|
|
383
|
+
pendingProbe.delete(event.data.turn)
|
|
384
|
+
}
|
|
321
385
|
})
|
|
322
386
|
}
|
|
323
387
|
|
|
324
|
-
/**
|
|
388
|
+
/**
|
|
389
|
+
* Resolve one config snapshot.
|
|
390
|
+
*
|
|
391
|
+
* Called per evaluation rather than once at mount so a Settings-UI or patch
|
|
392
|
+
* change takes effect on the next turn-stopping hook without a restart.
|
|
393
|
+
*
|
|
394
|
+
* @param config - raw plugin config from the loader or the Settings section.
|
|
395
|
+
* @returns the config with every default applied.
|
|
396
|
+
*/
|
|
397
|
+
function resolveConfig(config) {
|
|
398
|
+
return {
|
|
399
|
+
...config,
|
|
400
|
+
maxContinuations: config.maxContinuations ?? DEFAULT_MAX_CONTINUATIONS,
|
|
401
|
+
maxSteps: config.maxSteps ?? DEFAULT_MAX_STEPS,
|
|
402
|
+
maxTailChars: config.maxTailChars ?? DEFAULT_MAX_TAIL_CHARS,
|
|
403
|
+
judgeMaxTokens: config.judgeMaxTokens ?? 64,
|
|
404
|
+
judgeTemperature: config.judgeTemperature ?? 0,
|
|
405
|
+
steerText: config.steerText ?? (
|
|
406
|
+
'You described an action but did not call any tool. Continue the task now: '
|
|
407
|
+
+ 'call the tool for the action you just described. Do not narrate — emit the tool call.'
|
|
408
|
+
),
|
|
409
|
+
debug: config.debug ?? false,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** Settings namespace so the harness Settings UI can drive this plugin live. */
|
|
414
|
+
export const SETTINGS_NAMESPACE = 'loop-continue'
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Mount the plugin's Settings section.
|
|
418
|
+
*
|
|
419
|
+
* Registration is cosmetic: the guard works from the loader config even when
|
|
420
|
+
* no settings service is present. On a new core the section is installed
|
|
421
|
+
* through this plugin's own injector scope, so it lives for exactly this
|
|
422
|
+
* plugin's lifetime and disappears with it; on an older core the top-level
|
|
423
|
+
* helper is imported lazily, because the symbol only exists there.
|
|
424
|
+
*
|
|
425
|
+
* @param ctx - plugin context.
|
|
426
|
+
* @param config - base config used until a settings source replaces it.
|
|
427
|
+
* @param hooks - `setSource` receives the live settings-backed getter.
|
|
428
|
+
*/
|
|
429
|
+
function registerSettings(ctx, config, hooks) {
|
|
430
|
+
if (typeof ctx.inject === 'function') {
|
|
431
|
+
ctx.inject(['settings'], (settingsCtx) => {
|
|
432
|
+
const provider = settingsCtx?.settings
|
|
433
|
+
if (provider !== undefined && typeof provider.installSection === 'function') {
|
|
434
|
+
provider.installSection(ctx, SETTINGS_NAMESPACE, Config, config, hooks)
|
|
435
|
+
}
|
|
436
|
+
})
|
|
437
|
+
return
|
|
438
|
+
}
|
|
439
|
+
import('@deepseek-ai/dsh-settings').then(mod => {
|
|
440
|
+
if (typeof mod.installSettingsSection === 'function') {
|
|
441
|
+
mod.installSettingsSection(ctx, SETTINGS_NAMESPACE, Config, config, hooks)
|
|
442
|
+
}
|
|
443
|
+
}).catch(() => {
|
|
444
|
+
// Settings registration is cosmetic: the guard still works from the
|
|
445
|
+
// loader config when the section could not be mounted.
|
|
446
|
+
})
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Resolve the provider/model the judge should call.
|
|
451
|
+
*
|
|
452
|
+
* The judge follows the model the current conversation is actually running:
|
|
453
|
+
* the turn is stopping right now, so that route is by definition working. No
|
|
454
|
+
* fallback and no derivation — a route that cannot be read is a real fault and
|
|
455
|
+
* must surface instead of being papered over with a different model.
|
|
456
|
+
*
|
|
457
|
+
* @param agent - the turn's agent subject.
|
|
458
|
+
* @returns the conversation's own provider/model route.
|
|
459
|
+
*/
|
|
325
460
|
function routeOf(agent) {
|
|
326
461
|
const config = agent.session.requestHeader()?.config
|
|
327
|
-
if (config
|
|
328
|
-
|
|
462
|
+
if (config?.provider !== undefined && config?.model !== undefined) {
|
|
463
|
+
return { provider: config.provider, model: config.model }
|
|
329
464
|
}
|
|
330
|
-
|
|
465
|
+
throw new Error(`[${name}] the session has no request header yet; `
|
|
466
|
+
+ 'set judgeProvider/judgeModel explicitly')
|
|
331
467
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-loop-continue",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Continue a DeepSeek Harness agent turn when the model narrated but did not call a tool.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -11,14 +11,25 @@
|
|
|
11
11
|
},
|
|
12
12
|
"./package.json": "./package.json"
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
14
|
+
"files": [
|
|
15
|
+
"lib/index.js",
|
|
16
|
+
"lib/index.d.ts",
|
|
17
|
+
"cordis.patch.yml",
|
|
18
|
+
"CHANGELOG.md",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
15
21
|
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/yunxiyang/dsh-loop-continue.git"
|
|
25
|
+
},
|
|
16
26
|
"dependencies": {
|
|
17
27
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
18
28
|
},
|
|
19
29
|
"peerDependencies": {
|
|
20
30
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
21
31
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
32
|
+
"@deepseek-ai/dsh-settings": "^0.1.1-rc.2",
|
|
22
33
|
"@deepseek-ai/dsh-session": "^0.1.1-rc.2"
|
|
23
34
|
},
|
|
24
35
|
"dsh": {
|