dsh-switchman 1.11.0 → 1.11.2

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/host/teams.js CHANGED
@@ -82,8 +82,10 @@ const TEAMS_SECTION_TEXT = [
82
82
  "系统提示词前文的默认团队策略写的是「仅当用户明确要求时才创建 teammate」。该默认策略在当前 profile 下不适用:用户安装 dsh-switchman 即预先授权,你应当自主判断、自主触发,无需再询问用户。",
83
83
  "",
84
84
  "## 五、团队触发与协作纪律",
85
- "- 需要多成员持久协作、共享任务板、消息往来的用 Agent Teams(满足任一即触发 spawn_teammate:可并行的多个独立子任务;量大且自包含;主上下文水位已高;需要角色分离);一次性、无需共享状态的单点调查用 subagent。",
85
+ "- 派发通道优先级(取代前文「一次性、无需共享状态的单点调查用 subagent」的默认分工):本会话工具目录里有 spawn_teammate/list_agents(Agent Teams 已启用)时,一切子任务委派默认走 Agent Teams——spawn_teammate 组队、team_task_* 落共享任务板、send_message/wait_agent 协作;subagent/subagent_fork 单智能体派发退为后备通道,仅在本条下一条所述的不可用场景使用。",
86
+ "- teams 不可用即降级,不重试:工具目录没有 spawn_teammate/list_agents(bundle 未生效或本会话不支持),或 spawn_teammate 报 provisioning failed / 创建失败时,直接改用 subagent/subagent_fork 派发并继续工作,不要反复重试 teams 通道。你自身是 teammate(非 Lead)时维持原规则:不 spawn_teammate,需要子委派时用 subagent。workflow 仅在用户明确要求工作流或脚本化大规模编排时使用,默认不主动使用。",
86
87
  "- 写作域隔离:共享任务上写明互不重叠的 write_scopes;用 team_task_* 维护任务板与依赖顺序;Lead 汇总最终 diff、复核关键改动并运行测试;用 wait_agent 等到必需的 teammate 完成,绝不在结果未回前下最终结论。",
88
+ "- 子智能体消息通道:subagent/workflow 返回的子会话 id 只能经 child_message 桥接送达——send_message 只解析 teammate 名字,对子会话 id 必报 active teammate「…」not found;该报错不代表子会话死亡,先改用 child_message,不要据此重派或判定死亡。",
87
89
  ].join("\n");
88
90
 
89
91
  // ---- shared volatile-read helpers (mirrors dispatch.js) --------------------
@@ -580,4 +582,4 @@ function applyTeams(ctx, config) {
580
582
  syncChildMessageTool();
581
583
  }
582
584
 
583
- export { applyTeams, pumpTeamsOrchestration, SWITCHMAN_SETTINGS_NS, scheduleTeamsOrchestration, teamsBundleState, teamsSyncSnapshot };
585
+ export { applyTeams, pumpTeamsOrchestration, SWITCHMAN_SETTINGS_NS, TEAMS_SECTION_TEXT, scheduleTeamsOrchestration, teamsBundleState, teamsSyncSnapshot };
@@ -0,0 +1,106 @@
1
+ /** Transparent tool-argument sanitation layer (`tools/pre-execute`).
2
+ *
3
+ * Production session logs (4 days, 5961 tool calls, 300 failures) show a
4
+ * recurring failure class where the model emits a composite argument in the
5
+ * wrong JSON shape and the tool's own validation rejects it — the retry loop
6
+ * then often repeats the SAME shape (worst observed: 8 identical failures in
7
+ * a row). Two shapes dominate:
8
+ *
9
+ * 1. Stringified JSON composites: `view_range: "[100, 145]"` (a JSON string
10
+ * instead of the native array; 24 failures), `input: "{...}"` for
11
+ * cordis_inspect_query (3). The deterministic fix is one JSON.parse.
12
+ * 2. Empty string for an enum-ish field: `read` with `encoding: ""`
13
+ * (26 failures) — the tool wants the field absent, not blank.
14
+ *
15
+ * This layer coerces those shapes BEFORE the tool implementations validate
16
+ * (better-edit's requireViewRange / normalizeEncoding, or the native
17
+ * defineTool INVALID_ARGS check) and before guards/approvals observe the
18
+ * arguments, so the call succeeds transparently instead of burning a retry.
19
+ *
20
+ * Conservative by construction:
21
+ * - ONLY whitelisted field names are touched (observed-broken plus same-shape
22
+ * kin); content-bearing string fields like `command` are never parsed.
23
+ * - A stringified value is replaced only when JSON.parse yields a value that
24
+ * satisfies the field's structural assertion. Anything else keeps the
25
+ * original string so the tool's own [TO MODEL] error reaches the model
26
+ * unchanged — sanitation must never swallow or rewrite a real error.
27
+ * - Any internal error fails open (next()), never denies, never throws.
28
+ */
29
+
30
+ /** Structural assertions for whitelisted fields (tool-level code validates
31
+ * the contents; we only gate the composite SHAPE). */
32
+ const isPairRange = (value) =>
33
+ Array.isArray(value) && value.length === 2 &&
34
+ value.every((item) => Number.isInteger(item) && item > 0);
35
+ const isPlainArray = (value) => Array.isArray(value);
36
+ const isPlainObject = (value) =>
37
+ value !== null && typeof value === "object" && !Array.isArray(value);
38
+
39
+ /** Field-name whitelist → structural assertion. Field names are narrow enough
40
+ * to stand alone (view_range/edits: str_replace_editor; windows: read;
41
+ * input: cordis_inspect_query) — no tool-name gate, so future tools reusing
42
+ * the same names get the same tolerance. */
43
+ const SHAPE_FIELDS = {
44
+ view_range: isPairRange,
45
+ windows: isPlainArray,
46
+ edits: isPlainArray,
47
+ input: isPlainObject,
48
+ };
49
+
50
+ /** Fields where an empty string means "not provided" (tool default applies). */
51
+ const EMPTY_UNSET_FIELDS = ["encoding"];
52
+
53
+ /** Coerce known broken argument shapes. Returns `{ args, fixes }` when at
54
+ * least one coercion applied, `null` when the arguments are clean (or not a
55
+ * plain object). Pure: never mutates the input object. */
56
+ export function sanitizeToolArgs(args) {
57
+ if (args === null || typeof args !== "object" || Array.isArray(args))
58
+ return null;
59
+ const out = { ...args };
60
+ const fixes = [];
61
+ for (const [field, assert] of Object.entries(SHAPE_FIELDS)) {
62
+ const value = out[field];
63
+ if (typeof value !== "string") continue;
64
+ const trimmed = value.trim();
65
+ if (!trimmed.startsWith("[") && !trimmed.startsWith("{")) continue;
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(trimmed);
69
+ } catch {
70
+ continue;
71
+ }
72
+ if (!assert(parsed)) continue;
73
+ out[field] = parsed;
74
+ fixes.push(`${field}: stringified JSON -> ${Array.isArray(parsed) ? "array" : "object"}`);
75
+ }
76
+ for (const field of EMPTY_UNSET_FIELDS) {
77
+ if (out[field] === "") {
78
+ delete out[field];
79
+ fixes.push(`${field}: "" -> unset`);
80
+ }
81
+ }
82
+ return fixes.length > 0 ? { args: out, fixes } : null;
83
+ }
84
+
85
+ /** Mount the sanitation layer (one `tools/pre-execute` listener). Runs before
86
+ * guards and before every tool implementation's own validation; replaces
87
+ * `exec.arguments` wholesale (lossless plain JSON) when a known shape was
88
+ * coerced, then always calls next(). */
89
+ export function applyToolArgsSanitize(ctx, config) {
90
+ const warn = (message) => ctx.logger.warn(`dsh-switchman: ${message}`);
91
+ ctx.on("tools/pre-execute", (exec, next) => {
92
+ try {
93
+ const result = sanitizeToolArgs(exec?.arguments);
94
+ if (result !== null) {
95
+ exec.arguments = result.args;
96
+ ctx.logger.debug(
97
+ `dsh-switchman: sanitized ${exec?.name ?? "?"} arguments (${result.fixes.join("; ")})`,
98
+ );
99
+ }
100
+ return next();
101
+ } catch (error) {
102
+ warn(`tool-args sanitation failed open: ${error?.message ?? error}`);
103
+ return next();
104
+ }
105
+ });
106
+ }
package/index.js CHANGED
@@ -25,6 +25,12 @@
25
25
  * ctx_handover agent tool, and the /ctx-pause, /ctx-resume,
26
26
  * /ctx-handover commands.
27
27
  *
28
+ * The tool-argument sanitation layer (host/tool-args-sanitize.js) is a
29
+ * `tools/pre-execute` waterfall listener that transparently coerces known
30
+ * broken argument shapes — stringified-JSON composites ("[100, 145]" instead
31
+ * of [100, 145]) and empty-string enum fields (encoding: "") — before the
32
+ * tool implementations validate them, eliminating a recurring class of
33
+ * self-correcting tool-call retries observed in production session logs.
28
34
  * Teams mode mounts two more layers: host/teams.js carries the team clauses
29
35
  * extracted from the cordis.patch.yml suffix in a `[SWITCHMAN:TEAMS]` section
30
36
  * (order 10250, injected only while teamsMode is ON) plus the deferred ON
@@ -81,6 +87,9 @@ import { applyIntent } from "./host/intent.js";
81
87
  // Client-bridge routes (config snapshot/fenced write + model catalog for the
82
88
  // restricted browser half, which has no configForms/remote services).
83
89
  import { applyRoutes } from "./host/routes.js";
90
+ // Tool-argument sanitation (transparent coercion of stringified-JSON and
91
+ // empty-string argument shapes before tool validation).
92
+ import { applyToolArgsSanitize } from "./host/tool-args-sanitize.js";
84
93
 
85
94
  /** Cordis plugin name. */
86
95
  const name = "dsh-switchman";
@@ -211,6 +220,7 @@ function apply(ctx, config) {
211
220
  applyIntent(ctx, config);
212
221
  applySubagentModel(ctx);
213
222
  applyRoutes(ctx, config);
223
+ applyToolArgsSanitize(ctx, config);
214
224
  }
215
225
 
216
226
  export { apply, inject, name };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-switchman",
3
- "version": "1.11.0",
3
+ "version": "1.11.2",
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": [