dsh-switchman 1.11.3 → 1.11.5
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.de.md +3 -3
- package/README.es.md +3 -3
- package/README.fr.md +3 -3
- package/README.it.md +3 -3
- package/README.ja.md +3 -3
- package/README.ko.md +3 -3
- package/README.md +7 -7
- package/README.pt.md +3 -3
- package/README.ru.md +3 -3
- package/README.zh-TW.md +3 -3
- package/README.zh.md +4 -4
- package/client.js +154 -52
- package/host/config.js +10 -8
- package/host/context-trim/index.js +6 -9
- package/host/context-trim/summarizer.js +46 -35
- package/host/ptc-guard.js +6 -0
- package/host/routes.js +10 -14
- package/package.json +1 -1
|
@@ -695,10 +695,9 @@ export function applyContextTrim(ctx, config) {
|
|
|
695
695
|
} else if (view.applied > 0) {
|
|
696
696
|
epochResetDoneBySession.delete(sessionId);
|
|
697
697
|
}
|
|
698
|
-
//
|
|
699
|
-
// provider/model
|
|
700
|
-
//
|
|
701
|
-
// verbatim instead of re-deriving (and losing) it.
|
|
698
|
+
// Record the active request's route and resolved effort for diagnostics only.
|
|
699
|
+
// The side summarizer follows provider/model only and resolves effort from its
|
|
700
|
+
// model metadata rather than inheriting this dispatch's effort.
|
|
702
701
|
const model = {
|
|
703
702
|
provider: options.provider ?? null,
|
|
704
703
|
model: options.model ?? null,
|
|
@@ -718,16 +717,14 @@ export function applyContextTrim(ctx, config) {
|
|
|
718
717
|
}
|
|
719
718
|
|
|
720
719
|
/** Resolve the side-model route: explicit trimSideModel route, else the
|
|
721
|
-
* session's own model
|
|
720
|
+
* session's own provider/model. Effort is resolved from that model's
|
|
721
|
+
* supported thinking metadata, not inherited from the active session call. */
|
|
722
722
|
function sideRouteFor(sessionId) {
|
|
723
723
|
const explicit = readRoute(config, "trimSideModel");
|
|
724
724
|
if (explicit !== null) return explicit;
|
|
725
725
|
const model = metrics.of(sessionId)?.lastModel ?? null;
|
|
726
726
|
if (model !== null && typeof model.provider === "string" && model.provider !== "" && typeof model.model === "string" && model.model !== "") {
|
|
727
|
-
|
|
728
|
-
return typeof model.effort === "string" && model.effort !== ""
|
|
729
|
-
? { provider: model.provider, model: model.model, effort: model.effort }
|
|
730
|
-
: { provider: model.provider, model: model.model };
|
|
727
|
+
return { provider: model.provider, model: model.model };
|
|
731
728
|
}
|
|
732
729
|
return null;
|
|
733
730
|
}
|
|
@@ -224,15 +224,25 @@ function describeFailure(chunk) {
|
|
|
224
224
|
return ` (${raw.replace(/\s+/gu, " ").trim().slice(0, 300)})`;
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
-
/**
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
|
|
231
|
-
|
|
227
|
+
/** Choose the lowest enabled tier when several reasoning levels are available.
|
|
228
|
+
* A model with only an on/off switch is pinned to "off"; when reasoning
|
|
229
|
+
* metadata is absent, leave the model's adapter default unchanged. */
|
|
230
|
+
export function recommendedReasoningEffort(modelInfo) {
|
|
231
|
+
const efforts = modelInfo?.reasoning?.efforts;
|
|
232
|
+
if (!Array.isArray(efforts) || efforts.length === 0) return null;
|
|
233
|
+
const ids = efforts.map((entry) => typeof entry === "string" ? entry : entry?.id).filter((id) => typeof id === "string" && id !== "");
|
|
234
|
+
const enabled = ids.filter((id) => id !== "off");
|
|
235
|
+
if (enabled.length > 1) return enabled[0];
|
|
236
|
+
return ids.includes("off") ? "off" : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Side-model summary. llm: the dsh llm service; route: {provider, model, effort?}.
|
|
240
|
+
* An empty effort pin is resolved from model reasoning metadata; unavailable
|
|
241
|
+
* metadata leaves the adapter default unchanged. Returns summary text or null. */
|
|
232
242
|
export async function summarizeWithModel(llm, route, materialText, options = {}) {
|
|
233
243
|
const fail = typeof options.onFail === "function" ? options.onFail : null;
|
|
234
|
-
// Effort-retry notice: fires
|
|
235
|
-
//
|
|
244
|
+
// Effort-retry notice: fires before a one-time effort fallback, whether
|
|
245
|
+
// or not that retry lands. Falls back to fail when no notice is supplied.
|
|
236
246
|
const notice = typeof options.onNotice === "function" ? options.onNotice : null;
|
|
237
247
|
if (route === null || route === undefined) { fail?.("no-route"); return null; }
|
|
238
248
|
if (typeof llm?.prepareCall !== "function") { fail?.("no-prepare-call"); return null; }
|
|
@@ -242,39 +252,40 @@ export async function summarizeWithModel(llm, route, materialText, options = {})
|
|
|
242
252
|
const controller = new AbortController();
|
|
243
253
|
const timer = setTimeout(() => controller.abort(new Error("trim summary deadline")), deadlineMs);
|
|
244
254
|
try {
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const
|
|
255
|
+
// Resolve from the selected model itself: an empty pin means the lowest
|
|
256
|
+
// advertised thinking effort, while absent reasoning metadata leaves
|
|
257
|
+
// the adapter default (no thinking) untouched. Explicit effort pins win.
|
|
258
|
+
let modelInfo = null;
|
|
259
|
+
if (typeof llm.resolveModelInfo === "function") {
|
|
260
|
+
try { modelInfo = await llm.resolveModelInfo(route.provider, route.model, controller.signal); }
|
|
261
|
+
catch { /* Metadata is advisory; prepareCall remains authoritative. */ }
|
|
262
|
+
}
|
|
263
|
+
const automaticEffort = recommendedReasoningEffort(modelInfo);
|
|
264
|
+
const hasOffEffort = Array.isArray(modelInfo?.reasoning?.efforts) && modelInfo.reasoning.efforts.some((entry) => (typeof entry === "string" ? entry : entry?.id) === "off");
|
|
265
|
+
const explicitEffort = typeof route.effort === "string" && route.effort !== "";
|
|
266
|
+
const requestedEffort = explicitEffort ? route.effort : automaticEffort;
|
|
267
|
+
const proposalFor = (effort) => ({
|
|
268
|
+
provider: route.provider,
|
|
269
|
+
model: route.model,
|
|
270
|
+
maxTokens,
|
|
271
|
+
...(typeof effort === "string" && effort !== "" ? { reasoningEffort: effort } : {}),
|
|
272
|
+
});
|
|
258
273
|
let call;
|
|
259
274
|
try {
|
|
260
|
-
call = await llm.prepareCall(
|
|
261
|
-
{ provider: route.provider, model: route.model, maxTokens, reasoningEffort: requestedEffort },
|
|
262
|
-
controller.signal,
|
|
263
|
-
(controls) => controls,
|
|
264
|
-
);
|
|
275
|
+
call = await llm.prepareCall(proposalFor(requestedEffort), controller.signal, (controls) => controls);
|
|
265
276
|
} catch (prepareError) {
|
|
266
|
-
// An inherited session effort can exceed what the route's adapter
|
|
267
|
-
// supports: retry once pinned low before giving up (the outer
|
|
268
|
-
// catch then turns the failure into a skipped fold cycle).
|
|
269
277
|
const code = prepareError?.code ?? null;
|
|
270
278
|
const named = typeof prepareError?.message === "string" && prepareError.message.includes("UNSUPPORTED_REASONING_EFFORT");
|
|
271
|
-
if (
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
279
|
+
if (code !== "UNSUPPORTED_REASONING_EFFORT" && !named) throw prepareError;
|
|
280
|
+
// Stale metadata or a stale pin should not prevent a summary: retry
|
|
281
|
+
// once at the model's automatic minimum, or without an effort. Never
|
|
282
|
+
// turn an explicitly requested "off" into enabled reasoning.
|
|
283
|
+
const retryEffort = requestedEffort === "off"
|
|
284
|
+
? null
|
|
285
|
+
: (explicitEffort && automaticEffort !== requestedEffort ? automaticEffort : (hasOffEffort ? "off" : null));
|
|
286
|
+
if (retryEffort === requestedEffort) throw prepareError;
|
|
287
|
+
(notice ?? fail)?.(`effort "${requestedEffort ?? "default"}" unsupported, retrying ${retryEffort ?? "without reasoning"}`);
|
|
288
|
+
call = await llm.prepareCall(proposalFor(retryEffort), controller.signal, (controls) => controls);
|
|
278
289
|
}
|
|
279
290
|
let text = "";
|
|
280
291
|
let finishKind = null;
|
package/host/ptc-guard.js
CHANGED
|
@@ -89,6 +89,12 @@ function renderPtcGuard() {
|
|
|
89
89
|
`- Every other tool MUST be invoked as an awaited SDK function inside a ${RUN_CODE_NAME} program: \`await tools.name(args)\` — e.g. \`await tools.skill({ name: "git-commit-message" })\` or \`await tools.spawn_teammate({ ... })\`.`,
|
|
90
90
|
`- A direct call fails with: unknown tool "x": only \`${RUN_CODE_NAME}\` is callable directly — call \`x\` from inside a \`${RUN_CODE_NAME}\` program instead. Never retry the direct form; wrap the call.`,
|
|
91
91
|
`- Wherever this prompt says "call the X tool" (skill catalog, doctrines, per-turn reminders), read it as "call X inside a ${RUN_CODE_NAME} program". Batch independent calls as parallel awaits inside ONE program.`,
|
|
92
|
+
`- \`grep.pattern\` is a regular expression. In a normal JavaScript string inside \`${RUN_CODE_NAME}\`, double backslashes: write \`"\\\\("\` to pass \`\\(\` to grep; \`String.raw\` tagged templates also preserve backslashes. If grep reports \`unclosed group\`, fix the pattern before retrying; never repeat it unchanged.`,
|
|
93
|
+
`- Unless this call must intentionally retain state across calls (e.g. the one-time CUA bridge initialization), MUST wrap the entire ${RUN_CODE_NAME} program body in an async IIFE: \`await (async () => { ... })();\`. Keep all temporary \`const\`/\`let\`/\`class\` declarations inside it; this makes reusing local names such as \`res\` safe even when an earlier call used them. Top-level bindings persist for this Agent: only create them for intentionally shared state, and never redeclare existing top-level \`const\`/\`let\`/\`class\`; reassign an existing top-level \`var\` or choose a fresh unique name. If \`Identifier ... has already been declared\` occurs, rewrite the program in the IIFE rather than merely rerunning it—the SyntaxError happens before the body runs.`,
|
|
94
|
+
`- For intentional top-level bindings, maintain a ledger of declared names at \`globalThis.__dshSwitchmanPtcBindings\` (initialize idempotently with \`globalThis.__dshSwitchmanPtcBindings ??= new Set()\`). Before emitting a program with top-level declarations, check each name against this ledger and the visible prior \`${RUN_CODE_NAME}\` snippets; after a successful declaration, add the name with \`globalThis.__dshSwitchmanPtcBindings.add("name")\`. This ledger is manually maintained, not automatic introspection: if the old history or ledger is incomplete, do not guess—keep the code in an IIFE or choose a fresh name.`,
|
|
95
|
+
`- For routine \`${RUN_CODE_NAME}\` calls — including programs that only call SDK tools such as \`tools.skill(...)\` — pass only \`code\`, \`description\`, and \`timeoutMs\`; omit both \`sandbox_permissions\` and \`justification\`. If a call is rejected for invalid justification, never repeat it unchanged; retry without those fields.`,
|
|
96
|
+
`- If ${RUN_CODE_NAME} returns a parser error such as "Expected ident", treat it as a syntax issue in the submitted code; when present, the appended "File sandbox: ..." reports execution context, not the cause. Correct the code before retrying; never repeat it unchanged.`,
|
|
97
|
+
`- When a ${RUN_CODE_NAME} program embeds long prompt text, do not hand-write it as a double-quoted JavaScript string if it contains quotes (for example, getResource(".") or System.getProperty("user.dir")): build the text as data, escape the inner quotes, or serialize it with JSON.stringify(text); inspect or syntax-check the complete source before retrying.`,
|
|
92
98
|
`- Never set \`sandbox_permissions\` on any call, direct or SDK: it is a privilege-escalation request and fails with "not strictly wider than this call's current ... mode" whenever the session already runs at least that access level — retry without the field.`,
|
|
93
99
|
].join("\n");
|
|
94
100
|
}
|
package/host/routes.js
CHANGED
|
@@ -150,13 +150,12 @@ const INTENT_NUMBER_FIELDS = [
|
|
|
150
150
|
{ field: "intentHistoryMaxChars", min: 200, max: 20000, def: 2400 },
|
|
151
151
|
{ field: "intentContextWindow", min: 1024, max: Number.POSITIVE_INFINITY, def: 200000 },
|
|
152
152
|
];
|
|
153
|
-
/** Intent
|
|
153
|
+
/** Intent classifier routes round-trip the "unset" {provider:"", model:""}
|
|
154
154
|
* pair, so they validate against the loosened cleanIntentRoute (both keys
|
|
155
|
-
* must be strings; either may be empty)
|
|
156
|
-
const INTENT_ROUTE_FIELDS = ["intentClassifierRoute"
|
|
157
|
-
/**
|
|
158
|
-
|
|
159
|
-
const TRIM_ROUTE_FIELDS = ["trimSideModel", "trimFoldFallbackModel"];
|
|
155
|
+
* must be strings; either may be empty). */
|
|
156
|
+
const INTENT_ROUTE_FIELDS = ["intentClassifierRoute"];
|
|
157
|
+
/** Summary routes and the intent fallback route can carry an effort pin. */
|
|
158
|
+
const EFFORT_ROUTE_FIELDS = ["trimSideModel", "trimFoldFallbackModel", "intentFallbackRoute"];
|
|
160
159
|
/** Intent candidate list (route + optional effort/note per entry). */
|
|
161
160
|
const INTENT_CANDIDATE_FIELDS = ["intentCandidates"];
|
|
162
161
|
|
|
@@ -315,11 +314,8 @@ function cleanIntentRoute(entry) {
|
|
|
315
314
|
return provider === null || model === null ? null : { provider, model };
|
|
316
315
|
}
|
|
317
316
|
|
|
318
|
-
/** Coerce one
|
|
319
|
-
|
|
320
|
-
* model, and stripping it here made every settings save silently revert the
|
|
321
|
-
* settings page's effort select to the default. Intent routes omit effort. */
|
|
322
|
-
function cleanTrimRoute(entry) {
|
|
317
|
+
/** Coerce one route that may carry an optional reasoning-effort pin. */
|
|
318
|
+
function cleanEffortRoute(entry) {
|
|
323
319
|
const route = cleanIntentRoute(entry);
|
|
324
320
|
if (route === null) return null;
|
|
325
321
|
const effort = typeof entry.effort === "string" ? entry.effort.trim() : "";
|
|
@@ -370,7 +366,7 @@ export function snapshotOf(config) {
|
|
|
370
366
|
? rawOf(config[field]).map(cleanEffortEntry).filter(Boolean)
|
|
371
367
|
: [];
|
|
372
368
|
for (const field of INTENT_ROUTE_FIELDS) values[field] = cleanIntentRoute(rawOf(config?.[field])) ?? { provider: "", model: "" };
|
|
373
|
-
for (const field of
|
|
369
|
+
for (const field of EFFORT_ROUTE_FIELDS) values[field] = cleanEffortRoute(rawOf(config?.[field])) ?? { provider: "", model: "" };
|
|
374
370
|
for (const field of INTENT_CANDIDATE_FIELDS)
|
|
375
371
|
values[field] = mergeIntentCandidates(rawOf(config?.[field]));
|
|
376
372
|
values.intentClassifierKind = ENUM_FIELDS.intentClassifierKind.includes(rawOf(config?.intentClassifierKind))
|
|
@@ -445,8 +441,8 @@ export function coerceValues(input) {
|
|
|
445
441
|
if (route === null) return null;
|
|
446
442
|
values[field] = route;
|
|
447
443
|
}
|
|
448
|
-
for (const field of
|
|
449
|
-
const route =
|
|
444
|
+
for (const field of EFFORT_ROUTE_FIELDS) {
|
|
445
|
+
const route = cleanEffortRoute(input[field]);
|
|
450
446
|
if (route === null) return null;
|
|
451
447
|
values[field] = route;
|
|
452
448
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-switchman",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.5",
|
|
4
4
|
"description": "Autonomous Agent Teams doctrine for the four shipped DeepSeek Harness presets, plus language preferences, dispatch pools with model ranking, and context watermark control.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|