@sema-agent/core 7.17.7 → 7.19.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.
Files changed (201) hide show
  1. package/CHANGELOG.md +233 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/anthropic.js +6 -3
  8. package/dist/brain/open-responses.js +10 -4
  9. package/dist/brain/openai.js +5 -2
  10. package/dist/brain/reasoning.d.ts +13 -13
  11. package/dist/brain/reasoning.js +2 -2
  12. package/dist/brain/route-adjudicator.d.ts +18 -0
  13. package/dist/brain/route-adjudicator.js +16 -0
  14. package/dist/brain/stream-engine.js +2 -1
  15. package/dist/brain/stream-shared.d.ts +3 -0
  16. package/dist/brain/stream-shared.js +1 -0
  17. package/dist/core/agent-definition.d.ts +10 -0
  18. package/dist/core/artifact-host.d.ts +41 -4
  19. package/dist/core/artifact-host.js +18 -2
  20. package/dist/core/ask-origin.d.ts +16 -0
  21. package/dist/core/ask-origin.js +8 -0
  22. package/dist/core/auto-mode-arming.d.ts +6 -0
  23. package/dist/core/auto-mode-arming.js +5 -5
  24. package/dist/core/auto-mode-defaults.d.ts +43 -15
  25. package/dist/core/auto-mode-defaults.js +4 -1
  26. package/dist/core/auto-mode.d.ts +184 -59
  27. package/dist/core/auto-mode.js +188 -13
  28. package/dist/core/checkpoint-store.d.ts +23 -2
  29. package/dist/core/checkpoint-store.js +2 -2
  30. package/dist/core/engine-notice.d.ts +77 -1
  31. package/dist/core/engine-notice.js +7 -0
  32. package/dist/core/gate-fold.js +5 -1
  33. package/dist/core/gate-lanes.js +9 -8
  34. package/dist/core/gate-outcome.d.ts +42 -2
  35. package/dist/core/gate-outcome.js +23 -0
  36. package/dist/core/governance-codes.d.ts +1 -1
  37. package/dist/core/governance-codes.js +14 -0
  38. package/dist/core/hook-seats.d.ts +66 -0
  39. package/dist/core/hook-seats.js +37 -0
  40. package/dist/core/hooks.d.ts +1 -1
  41. package/dist/core/read-root-candidate.d.ts +12 -0
  42. package/dist/core/read-root-candidate.js +24 -0
  43. package/dist/core/roles.d.ts +9 -1
  44. package/dist/core/roles.js +4 -0
  45. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  46. package/dist/core/runner/assemble-result.d.ts +2 -1
  47. package/dist/core/runner/assemble-result.js +1 -1
  48. package/dist/core/runner/classifier-ladder.d.ts +227 -0
  49. package/dist/core/runner/classifier-ladder.js +214 -0
  50. package/dist/core/runner/contracts.d.ts +71 -13
  51. package/dist/core/runner/decide-continuation.d.ts +1 -3
  52. package/dist/core/runner/decide-continuation.js +1 -7
  53. package/dist/core/runner/deferral-census.d.ts +52 -0
  54. package/dist/core/runner/deferral-census.js +44 -0
  55. package/dist/core/runner/denial-limit-arms.js +2 -2
  56. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  57. package/dist/core/runner/derived-route-fallback.js +8 -3
  58. package/dist/core/runner/gate-exit.d.ts +12 -0
  59. package/dist/core/runner/gate-exit.js +6 -3
  60. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  61. package/dist/core/runner/prepare-artifact.js +5 -4
  62. package/dist/core/runner/prepare-ask-lane.js +1 -0
  63. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  64. package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
  65. package/dist/core/runner/prepare-config-doors.d.ts +7 -1
  66. package/dist/core/runner/prepare-config-doors.js +13 -13
  67. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  68. package/dist/core/runner/prepare-defer-classify.js +8 -3
  69. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  70. package/dist/core/runner/prepare-gate-stations.js +1 -1
  71. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  72. package/dist/core/runner/prepare-hands-readface.js +60 -24
  73. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  74. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  75. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  76. package/dist/core/runner/prepare-lsp.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +2 -0
  78. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  79. package/dist/core/runner/prepare-project-context.js +7 -4
  80. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  81. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  82. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  83. package/dist/core/runner/prepare-run-refs.js +3 -2
  84. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  85. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  86. package/dist/core/runner/prepare-task.js +19 -14
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  89. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  90. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  91. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  92. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  93. package/dist/core/runner/resume-apply.js +3 -3
  94. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  95. package/dist/core/runner/run-harness-handlers.js +4 -1
  96. package/dist/core/runner/run-identity-wiring.js +6 -2
  97. package/dist/core/runner/run-leg.d.ts +2 -0
  98. package/dist/core/runner/run-leg.js +45 -45
  99. package/dist/core/runner/run-notification-lane.js +55 -27
  100. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  101. package/dist/core/runner/run-terminal-adoption.js +16 -13
  102. package/dist/core/runner/run-turn-boundary.js +15 -5
  103. package/dist/core/runner/runtask.d.ts +6 -5
  104. package/dist/core/runner/runtask.js +26 -14
  105. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  106. package/dist/core/runner/skill-base-dir.js +42 -0
  107. package/dist/core/runner/synthetic-tools.d.ts +83 -48
  108. package/dist/core/runner/synthetic-tools.js +69 -32
  109. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  110. package/dist/core/runner/tool-defer-gate.js +15 -5
  111. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  112. package/dist/core/runner/tool-disclosure.js +2 -0
  113. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  114. package/dist/core/runner/tool-progress-lane.js +61 -0
  115. package/dist/core/runner/turn-attachments.d.ts +43 -12
  116. package/dist/core/runner/turn-attachments.js +58 -14
  117. package/dist/core/runner-deps.d.ts +48 -7
  118. package/dist/core/sensitive-path-policy.d.ts +58 -17
  119. package/dist/core/sensitive-path-policy.js +33 -5
  120. package/dist/core/skill-listing-budget.d.ts +112 -0
  121. package/dist/core/skill-listing-budget.js +58 -0
  122. package/dist/core/swappable-deps.d.ts +9 -7
  123. package/dist/core/swappable-deps.js +7 -3
  124. package/dist/core/task-event.d.ts +96 -29
  125. package/dist/core/task-notification.d.ts +142 -16
  126. package/dist/core/task-notification.js +106 -0
  127. package/dist/core/task-result.d.ts +9 -1
  128. package/dist/core/task-spec.d.ts +46 -3
  129. package/dist/core/tighten-task-spec.js +7 -5
  130. package/dist/core/tool-model-gate.d.ts +1 -1
  131. package/dist/core/tool-policy.d.ts +19 -4
  132. package/dist/core/tool-policy.js +6 -7
  133. package/dist/core/tool-search-setting.d.ts +78 -0
  134. package/dist/core/tool-search-setting.js +90 -0
  135. package/dist/core/tool-spec.d.ts +45 -4
  136. package/dist/core/types.d.ts +2 -2
  137. package/dist/core/types.js +1 -1
  138. package/dist/core/wiring-manifest.d.ts +46 -0
  139. package/dist/core/wiring-manifest.js +9 -1
  140. package/dist/core/write-protect.d.ts +8 -3
  141. package/dist/core/write-protect.js +8 -5
  142. package/dist/engine/execution-env/node-execution-env.js +10 -4
  143. package/dist/engine/harness/agent-harness.d.ts +42 -0
  144. package/dist/engine/harness/agent-harness.js +61 -29
  145. package/dist/engine/loop/agent-loop.d.ts +68 -3
  146. package/dist/engine/loop/agent-loop.js +109 -16
  147. package/dist/index.d.ts +13 -8
  148. package/dist/index.js +11 -7
  149. package/dist/internal/harness.d.ts +1 -1
  150. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  151. package/dist/orchestration/run-workflow-tool.js +6 -1
  152. package/dist/orchestration/workflow-governance.d.ts +7 -1
  153. package/dist/orchestration/workflow-governance.js +9 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  155. package/dist/orchestration/workflow-primitives.js +6 -1
  156. package/dist/orchestration/workflow-types.d.ts +48 -13
  157. package/dist/orchestration/workflow-types.js +1 -1
  158. package/dist/orchestration/workflow.js +45 -17
  159. package/dist/prompt-assembly/epoch.js +2 -0
  160. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  161. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  162. package/dist/prompt-assembly/section-usage.js +10 -0
  163. package/dist/prompt-assembly/types.d.ts +8 -6
  164. package/dist/prompts/default.d.ts +13 -0
  165. package/dist/prompts/default.js +16 -8
  166. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  167. package/dist/tools/artifact/artifact-text.js +6 -3
  168. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  169. package/dist/tools/artifact/artifact-tool.js +203 -70
  170. package/dist/tools/artifact/conformance.d.ts +56 -0
  171. package/dist/tools/artifact/conformance.js +349 -0
  172. package/dist/tools/artifact/local-stub.js +1 -1
  173. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  174. package/dist/tools/artifact/reject-breaker.js +42 -0
  175. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  176. package/dist/tools/fs/bash-lexer.js +563 -0
  177. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  178. package/dist/tools/fs/bash-program-position.js +125 -0
  179. package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
  180. package/dist/tools/fs/bash-readonly-classifier.js +180 -558
  181. package/dist/tools/fs/containment-face.d.ts +89 -0
  182. package/dist/tools/fs/containment-face.js +81 -0
  183. package/dist/tools/fs/fs-bash.d.ts +46 -1
  184. package/dist/tools/fs/fs-bash.js +165 -25
  185. package/dist/tools/fs/fs-read.d.ts +1 -1
  186. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  187. package/dist/tools/fs/fs-write.d.ts +4 -3
  188. package/dist/tools/fs/fs-write.js +9 -9
  189. package/dist/tools/fs/index.d.ts +24 -5
  190. package/dist/tools/fs/index.js +18 -6
  191. package/dist/tools/fs/read-deny.d.ts +4 -3
  192. package/dist/tools/fs/repo-map.d.ts +2 -2
  193. package/dist/tools/fs/safety.d.ts +18 -3
  194. package/dist/tools/fs/safety.js +11 -4
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +99 -13
  197. package/wire-changes/core@7.17.7.json +49 -0
  198. package/wire-changes/core@7.18.0.json +1273 -0
  199. package/wire-changes/core@7.19.0.json +457 -0
  200. package/dist/tools/fs/read-face.d.ts +0 -68
  201. package/dist/tools/fs/read-face.js +0 -47
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A skill's BASE DIRECTORY: the placeholder grammar its material may use to name its own root, the bounded
3
+ * substitution that resolves it, and the delivered-length measure the load gate budgets on.
4
+ *
5
+ * Split out of the skill tool because it is a different concern: the tool decides what a skill INVOKE
6
+ * returns, this decides how the caller's material names files on the machine the tools run on.
7
+ */
8
+ /** The typed refusal a `SkillSpec.baseDir` outside its domain raises — one code, one sentence. */
9
+ export declare const SKILL_BASE_DIR_INVALID_CODE = "config.skill_base_dir_invalid";
10
+ /**
11
+ * The ONE screen of `SkillSpec.baseDir`: absent ⇒ no directory (nothing is substituted and no opening line is
12
+ * added); a NON-EMPTY string ⇒ itself; anything else ⇒ a loud refusal carrying {@link SKILL_BASE_DIR_INVALID_CODE}.
13
+ *
14
+ * Both rejected shapes were SILENT before, and each was silent in its own direction. `""` is not "no directory":
15
+ * it substitutes to nothing, so a body's `${SKILL_ROOT}/ref.md` is delivered as the filesystem-root path
16
+ * `/ref.md` and the opening line names no directory at all — a wrong answer where absence would have given the
17
+ * visible unresolved placeholder the seat's contract promises. A non-string makes the delivered LENGTH `NaN`,
18
+ * and `NaN <= limit` is false, so the load gate dropped the skill and reported "content is NaN chars" —
19
+ * a configuration typo reading as an over-limit body. Core still neither resolves nor reads the directory
20
+ * (the brain-outside rule); this screens the VALUE, not the path.
21
+ *
22
+ * It runs at the load gate ({@link substitutedSkillLength}, which spec assembly calls for every skill), so the
23
+ * substitution faces below are reached only with a value that already passed it.
24
+ */
25
+ export declare function skillBaseDirOf(value: unknown): string | undefined;
26
+ /**
27
+ * Substitute a skill's base directory for the root placeholders in ITS OWN MATERIAL.
28
+ *
29
+ * @contract skill.base_dir — with `SkillSpec.baseDir` set, a `Skill` result opens with the line
30
+ * `Base directory for this skill: <baseDir>` and every `${SKILL_ROOT}` / `${CLAUDE_PLUGIN_ROOT}` in the
31
+ * body AND in every attachment's content is replaced by that value; `files[].path` is then read relative to
32
+ * it. The substitution covers the CALLER'S material only — never the model-supplied `args` section, which is
33
+ * live input and is delivered as written. Core neither resolves nor reads the directory (the brain-outside
34
+ * rule that keeps `SkillSpec.files` inline); with `baseDir` absent no line is added, nothing is substituted,
35
+ * and the result is byte-identical to a pre-baseDir invoke.
36
+ */
37
+ export declare function substituteSkillRoot(text: string, baseDir: string): string;
38
+ /**
39
+ * Substitute under a CEILING, and report the size the full substitution would have had.
40
+ *
41
+ * Substitution is the one step here that can make text grow without bound: a placeholder is 13 or 21 chars and a real directory can be any length, so an
42
+ * attachment that is nothing but placeholders expands by a factor of the path's length. Building that string in order to measure it against the attachments
43
+ * cap — or worse, to discover it does not FIT and drop it — spends exactly the memory the cap exists to bound before the cap is consulted, and a big enough
44
+ * one exceeds the runtime's own string limit. So the length is counted as the loop walks, nothing is materialized past `maxChars`, and the caller budgets on
45
+ * `totalChars` while rendering only what fits.
46
+ */
47
+ /** The DELIVERED length of a body once its root placeholders resolve — counted, never materialized, so the load gate measures what the model will actually
48
+ * receive rather than what the caller typed. Equal to `text.length` when no directory is stated, and the one place the seat's value is SCREENED
49
+ * ({@link skillBaseDirOf}) — the gate that budgets on this number is the first reader of every skill on every leg. */
50
+ export declare function substitutedSkillLength(text: string, baseDir: unknown): number;
51
+ export declare function substituteSkillRootBounded(text: string, baseDir: string, maxChars: number): {
52
+ text: string;
53
+ totalChars: number;
54
+ };
@@ -0,0 +1,42 @@
1
+ const SKILL_ROOT_PLACEHOLDERS = /\$\{(?:SKILL_ROOT|CLAUDE_PLUGIN_ROOT)\}/g;
2
+ export const SKILL_BASE_DIR_INVALID_CODE = "config.skill_base_dir_invalid";
3
+ export function skillBaseDirOf(value) {
4
+ if (value === undefined)
5
+ return undefined;
6
+ if (typeof value === "string" && value !== "")
7
+ return value;
8
+ const e = new Error(`SkillSpec.baseDir must be a non-empty string naming its own directory (got ${value === "" ? '""' : typeof value}) — ` +
9
+ `omit the key where no directory is stated; an empty one resolves the body's \${SKILL_ROOT} placeholders to nothing.`);
10
+ e.code = SKILL_BASE_DIR_INVALID_CODE;
11
+ throw e;
12
+ }
13
+ export function substituteSkillRoot(text, baseDir) {
14
+ return text.replace(SKILL_ROOT_PLACEHOLDERS, () => baseDir);
15
+ }
16
+ export function substitutedSkillLength(text, baseDir) {
17
+ const dir = skillBaseDirOf(baseDir);
18
+ if (dir === undefined)
19
+ return text.length;
20
+ return substituteSkillRootBounded(text, dir, 0).totalChars;
21
+ }
22
+ export function substituteSkillRootBounded(text, baseDir, maxChars) {
23
+ const parts = [];
24
+ let totalChars = 0;
25
+ let built = 0;
26
+ const push = (piece) => {
27
+ totalChars += piece.length;
28
+ if (built >= maxChars)
29
+ return;
30
+ const kept = piece.length > maxChars - built ? piece.slice(0, maxChars - built) : piece;
31
+ parts.push(kept);
32
+ built += kept.length;
33
+ };
34
+ let last = 0;
35
+ for (const m of text.matchAll(SKILL_ROOT_PLACEHOLDERS)) {
36
+ push(text.slice(last, m.index));
37
+ push(baseDir);
38
+ last = m.index + m[0].length;
39
+ }
40
+ push(text.slice(last));
41
+ return { text: parts.join(""), totalChars };
42
+ }
@@ -1,6 +1,7 @@
1
1
  import { type TSchema } from "typebox";
2
2
  import type { AgentTool } from "../../internal/harness-types.js";
3
3
  import type { SkillManifest } from "../types.js";
4
+ import { type ResolvedSkillListingBudget } from "../skill-listing-budget.js";
4
5
  import type { ActiveSkillScope } from "./active-skill-scope.js";
5
6
  /** Reserved name of the injected structured-output tool (CC 2.1.187 `StructuredOutput`; batch-A rename — old
6
7
  * "submit_output" is canonicalized for pre-rename durable state). */
@@ -52,44 +53,34 @@ export interface ReportedFinding {
52
53
  export declare function createReportFindingsTool(): AgentTool;
53
54
  /** Reserved name of the injected skill-loading tool (present only when `spec.skills` is non-empty). */
54
55
  export declare const SKILL_TOOL_NAME = "Skill";
55
- /** Per-skill LOAD gate on `SkillSpec.content` (CC 2.1.207 parity: CC's only size door at invoke
56
- * time is a 1MB file-load gate — an over-limit skill is REJECTED WHOLE, never truncated; the
57
- * invoked body itself enters context untruncated). Sema skills are spec-supplied content, so the
58
- * equivalent gate sits at spec assembly (prepare-task): an oversized skill is excluded from both
59
- * the disclosure block and the tool, with an operator-visible config error. The compaction-time
60
- * retention caps live in the engine's compaction module — a different door, deliberately. */
56
+ /** Per-skill LOAD gate on `SkillSpec.content` (CC 2.1.207 parity: CC's only invoke-time size door is a 1MB file-load gate — an over-limit skill is REJECTED
57
+ * WHOLE, never truncated, and the invoked body enters context untruncated). Sema skills are spec-supplied, so the equivalent gate sits at spec assembly:
58
+ * an oversized skill is excluded from the disclosure block and the tool, with an operator-visible config error. The compaction-time retention caps are a
59
+ * different door, deliberately. */
61
60
  export declare const SKILL_CONTENT_MAX_CHARS: number;
62
- /** Total listing-block cap in UTF-8 BYTES — [c209-C] R2 C4①: the old 8000-CHAR cap measured a
63
- * different coordinate than the bundle's `ATTACHMENT_BYTE_CAP` (8192 UTF-8 bytes) and counted only
64
- * the description lines (fence framing + name-only tail rode free), so a near-cap frame could
65
- * exceed the bundle cap, get clip-mangled every boundary, never pass the intact-survival commit
66
- * predicate, and REPLAY FOREVER. The cap is now enforced byte-exact over the WHOLE rendered block
67
- * (fences, headers, markers, joiners included), so `buildSkillsBlock` output always fits the
68
- * bundle cap solo and an intact commit is always reachable. MUST stay ≤ ATTACHMENT_BYTE_CAP
69
- * (turn-attachments.ts; not imported here — that module imports this one — pinned by test instead). */
70
- export declare const SKILLS_BLOCK_MAX_BYTES = 8000;
71
- /** Total per-invoke cap for a skill's inlined attachments (`SkillSpec.files`) — an ATTACHMENT-face
72
- * budget, distinct from the (gone) body truncation door: the body itself is delivered untruncated
73
- * at invoke time (CC parity), while attachments are reference material riding along — bounded and
74
- * truncation-disclosed. */
61
+ /** The listing's budget vocabulary lives in `core/skill-listing-budget.ts` (the seat reader that lowers
62
+ * the two `TaskSpec` knobs to these numbers); re-exported here because this module is where every
63
+ * existing consumer of the cap imports it from, and the ceiling's structural reason is a property of
64
+ * the delivery lane rather than of the renderer. */
65
+ export { SKILLS_BLOCK_MAX_BYTES } from "../skill-listing-budget.js";
66
+ /** Total per-invoke cap for a skill's inlined attachments (`SkillSpec.files`) — an ATTACHMENT-face budget, distinct from the (gone) body truncation door:
67
+ * the body is delivered untruncated at invoke time (CC parity), while attachments ride along bounded and truncation-disclosed. */
75
68
  export declare const SKILL_ATTACHMENTS_MAX_CHARS = 50000;
76
- /** Per-attachment cap on the PATH rendered in its `###` header — a single pathological path must not
77
- * eat the attachments budget on its own. Overflow is truncated with an ellipsis. */
69
+ /** Per-attachment cap on the PATH rendered in its `###` header — one pathological path must not eat the budget. Overflow truncates with an ellipsis. */
78
70
  export declare const SKILL_ATTACHMENT_PATH_MAX_CHARS = 512;
79
71
  interface SkillLike {
80
72
  name: string;
81
73
  description: string;
82
74
  content: string;
83
- /** Optional Gate-3 runtime capability manifest — carried through so a successful
84
- * `skill` load can push its scope frame. Skills without one stay unscoped (backward-compatible). */
75
+ /** Optional Gate-3 runtime capability manifest — carried through so a successful `skill` load can push its scope frame; skills without one stay unscoped. */
85
76
  manifest?: SkillManifest;
86
- /** Optional attachments (SkillSpec.files): disclosed as paths in the block, delivered inline on
87
- * invoke under {@link SKILL_ATTACHMENTS_MAX_CHARS}. Same caller-trusted contract as `content`
88
- * (see the module note above) — core never writes them to disk (deployment's half). */
77
+ /** Optional attachments (SkillSpec.files): paths in the block, delivered inline on invoke under {@link SKILL_ATTACHMENTS_MAX_CHARS}. Same caller-trusted
78
+ * contract as `content` — core never writes them to disk (deployment's half). */
89
79
  files?: Array<{
90
80
  path: string;
91
81
  content: string;
92
82
  }>;
83
+ /** Optional base directory (SkillSpec.baseDir) — see `@contract skill.base_dir`. Absent ⇒ byte-identical result. */ baseDir?: string;
93
84
  }
94
85
  /** [c209-C] The metadata slice of a skill the LISTING faces consume (disclosure block + the
95
86
  * skills_listing turn attachment): name, description, attachment paths — never the body. The
@@ -116,8 +107,11 @@ export interface SkillListingEntry {
116
107
  * keep their descriptions"). Absent ⇒ the entry's array index is used, i.e. the prior behavior. */
117
108
  declaredRank?: number;
118
109
  }
119
- /** Dedupe by name (first occurrence wins) and sort by name — one deterministic list shared by the
120
- * disclosure block and the tool, so what the prompt promises is exactly what the tool serves. */
110
+ /** Dedupe by name (first occurrence wins) and sort by name — one deterministic list shared by the disclosure block
111
+ * and the tool, so what the prompt promises is exactly what the tool serves. Each field is READ ONCE, BY NAME, and
112
+ * both consumers see the copy: the gate screens this list while the invoke substitutes from it, so a `get baseDir()`
113
+ * answering those two reads differently passed a screen that measured a value the model never received — and a rest
114
+ * copy would silently drop an INHERITED or non-enumerable `manifest`, i.e. widen a skill's capability restriction. */
121
115
  export declare function normalizeSkills(skills: SkillLike[] | undefined): SkillLike[];
122
116
  /** [c209-C] The skills listing's lead line — single-sourced so the run loop's durable-resume
123
117
  * transcript probe (MED-3② skills arm, runtask) matches EXACTLY what the initial frame carries
@@ -126,31 +120,73 @@ export declare const SKILLS_LISTING_PROBE_HEADER = "The following skills are ava
126
120
  /** [c209-C] One skill's disclosure line — `- name: description[(files 尾缀)]` with the per-skill
127
121
  * description cap. Single-sourced so the skills_listing DELTA frame (turn-attachments) renders adds
128
122
  * with byte-identical line shape to the initial `<skills>` block. */
129
- export declare function skillListingLine(s: SkillListingEntry): string;
123
+ export declare function skillListingLine(s: SkillListingEntry, descMaxChars?: number): string;
124
+ /** What one {@link renderSkillsBlock} call produced: the block, and which skills the budget degraded. */
125
+ export interface SkillsBlockRender {
126
+ /** The rendered block — what {@link buildSkillsBlock} returns. */
127
+ text: string;
128
+ /** Skills listed WITHOUT their description (tier B's unspent candidates, or every listed name under
129
+ * tier C), in display order. Raw names: this is host-facing structured data, not prompt text. */
130
+ nameOnly: readonly string[];
131
+ /** Skills the block could not list at all (tier C's overflow tail). The `Skill` tool still serves them. */
132
+ dropped: readonly string[];
133
+ }
134
+ /** The text-only face of {@link renderSkillsBlock} — the `<skills>` BLOCK's face, not the listing
135
+ * family's: the drift frame renders its own arms through the shared tier engine
136
+ * ({@link renderSkillLinesWithin}), which is where "one ruler for every frame" lives. */
137
+ export declare function buildSkillsBlock(skills: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): string | undefined;
130
138
  /** Render the `<skills>` disclosure block (metadata only — bodies stay behind the tool).
131
139
  * [c209-C] REVERSAL: no longer composed into the stable system prompt — this is now the RENDERER for
132
140
  * the `skills_listing` initial frame (first-user-turn reminder / boundary re-announce), so the
133
141
  * cached prefix carries zero skill bytes (design/C §2.3; CC 209 classic ships the same listing as a
134
142
  * first-turn reminder; Q6: 8KB cap ≈ CC's observed 5.9KB listing).
135
143
  *
136
- * [c209-C] R2 C4① — the cap is BYTE-EXACT over the whole rendered block (see
137
- * {@link SKILLS_BLOCK_MAX_BYTES}): three honesty tiers, each chosen with the LATER tiers' fixed
138
- * overhead (cap marker / name-only tail / overflow count line) already reserved so the final
139
- * render can never blow the budget it was sized against —
140
- * A. everything fits → every skill gets its full `- name: description` line (byte-identical to
141
- * the pre-C4 render for every block that was legal under the old cap);
142
- * B. (CC 220 `sbs` @317547-317586) — reserve the cap marker + EVERY skill's name-only line
143
- * first, then spend the remaining slack on descriptions in PRIORITY order
144
- * ({@link SkillListingEntry.declaredRank}), admitting each description whose incremental cost
145
- * still fits and skipping (not stopping at) the ones that do not. Every skill stays listed;
146
- * the ones that lose their description are the low-priority ones, not the alphabetically late
147
- * ones. This replaces the prior "longest full-line PREFIX" cut, which had two defects the
148
- * anchor does not: the surviving set was decided by name sort order (a skill named `zip-deploy`
149
- * lost its description to `aardvark-lint` for no reason connected to either skill), and one
150
- * oversized description truncated the block at itself instead of being skipped over;
151
- * C. even all-name-only overflows (pathological name census) → list what fits and close with an
152
- * honest `(… +N more skills …)` count line — the tool still serves all of them. */
153
- export declare function buildSkillsBlock(skills: ReadonlyArray<SkillListingEntry>): string | undefined;
144
+ * The fence and its four header lines are this lane's frame; the budget arithmetic and the three
145
+ * degradation tiers belong to {@link renderSkillLinesWithin}, which the drift frame renders through
146
+ * as well. {@link buildSkillsBlock} is the text-only face; this one additionally reports what the
147
+ * budget cost, for the disclosure lane. */
148
+ export declare function renderSkillsBlock(skills: ReadonlyArray<SkillListingEntry>, budget?: ResolvedSkillListingBudget): SkillsBlockRender | undefined;
149
+ /**
150
+ * The three honesty tiers over ONE set of skills, measured byte-exact inside a caller-supplied FRAME —
151
+ * the engine both {@link renderSkillsBlock} and the drift frame's ADD arm render through, so the
152
+ * initial `<skills>` block and a later "new skills are now available" announcement degrade by the same
153
+ * rules against the same number. A lane that folded by its own rules (or by none) renders a body the
154
+ * bundle cap CLIPS, and a clipped body can never equal the intact-survival re-render, so the
155
+ * announcement replays at every boundary for the life of the task — the failure
156
+ * `@contract skills.listing_budget_ceiling` names, on whichever lane skipped the measurement.
157
+ *
158
+ * `frame` renders the body lines into the shape its lane carries; its fixed cost is priced once, by
159
+ * rendering it empty, so the tiers below decide against the bytes that actually ship.
160
+ *
161
+ * [c209-C] R2 C4① — the cap is BYTE-EXACT over the whole rendered frame (see
162
+ * {@link SKILLS_BLOCK_MAX_BYTES}): three honesty tiers, each chosen with the LATER tiers' fixed
163
+ * overhead (cap marker / name-only tail / overflow count line) already reserved so the final
164
+ * render can never blow the budget it was sized against —
165
+ * A. everything fits → every skill gets its full `- name: description` line (byte-identical to
166
+ * the pre-C4 render for every block that was legal under the old cap);
167
+ * B. (CC 220 `sbs` @317547-317586) — reserve the cap marker + EVERY skill's name-only line
168
+ * first, then spend the remaining slack on descriptions in PRIORITY order
169
+ * ({@link SkillListingEntry.declaredRank}), admitting each description whose incremental cost
170
+ * still fits and skipping (not stopping at) the ones that do not. Every skill stays listed;
171
+ * the ones that lose their description are the low-priority ones, not the alphabetically late
172
+ * ones. This replaces the prior "longest full-line PREFIX" cut, which had two defects the
173
+ * anchor does not: the surviving set was decided by name sort order (a skill named `zip-deploy`
174
+ * lost its description to `aardvark-lint` for no reason connected to either skill), and one
175
+ * oversized description truncated the block at itself instead of being skipped over;
176
+ * C. even all-name-only overflows (pathological name census) → list what fits and close with an
177
+ * honest `(… +N more skills …)` count line — the tool still serves all of them.
178
+ *
179
+ * Pure: the same entries, budget and frame always render the same bytes, which is what the
180
+ * prompt-prefix stability contract requires of the listing frames.
181
+ */
182
+ export declare function renderSkillLinesWithin(skills: ReadonlyArray<SkillListingEntry>, opts: {
183
+ /** The whole frame's byte budget. Re-clamped to {@link SKILLS_BLOCK_MAX_BYTES} here, not only in
184
+ * the seat reader: this renderer is reachable from every listing lane, and the ceiling invariant
185
+ * has to hold for a caller that computed its own number. */
186
+ blockMaxBytes: number;
187
+ descMaxChars: number;
188
+ frame: (lines: readonly string[]) => string;
189
+ }): SkillsBlockRender | undefined;
154
190
  /**
155
191
  * The injected `skill` tool: returns a skill's full body as a tool result.
156
192
  *
@@ -167,4 +203,3 @@ export declare function buildSkillsBlock(skills: ReadonlyArray<SkillListingEntry
167
203
  * widening, so it stays safe-by-construction.
168
204
  */
169
205
  export declare function createSkillTool(skills: SkillLike[], scope?: ActiveSkillScope): AgentTool;
170
- export {};
@@ -1,7 +1,9 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool } from "../tools.js";
3
3
  import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
4
+ import { SKILLS_BLOCK_MAX_BYTES as SKILL_LISTING_CEILING_BYTES, SKILL_LISTING_DESC_MAX_CHARS_DEFAULT, } from "../skill-listing-budget.js";
4
5
  import { toolFace } from "../tool-catalog-entries.js";
6
+ import { substituteSkillRoot, substituteSkillRootBounded } from "./skill-base-dir.js";
5
7
  export const OUTPUT_TOOL_NAME = "StructuredOutput";
6
8
  export const REPORT_BLOCKED_TOOL_NAME = "ReportBlocked";
7
9
  export const REPORT_FINDINGS_TOOL_NAME = "ReportFindings";
@@ -84,8 +86,7 @@ export function createReportFindingsTool() {
84
86
  }
85
87
  export const SKILL_TOOL_NAME = "Skill";
86
88
  export const SKILL_CONTENT_MAX_CHARS = 1024 * 1024;
87
- const SKILL_DESC_MAX_CHARS = 1_536;
88
- export const SKILLS_BLOCK_MAX_BYTES = 8_000;
89
+ export { SKILLS_BLOCK_MAX_BYTES } from "../skill-listing-budget.js";
89
90
  export const SKILL_ATTACHMENTS_MAX_CHARS = 50_000;
90
91
  export const SKILL_ATTACHMENT_PATH_MAX_CHARS = 512;
91
92
  const SKILL_FILES_LIST_MAX_CHARS = 200;
@@ -112,24 +113,26 @@ export function normalizeSkills(skills) {
112
113
  for (const s of skills ?? []) {
113
114
  if (!seen.has(s.name)) {
114
115
  seen.add(s.name);
115
- out.push(s);
116
+ const { name, description, content, manifest, files, baseDir, ...rest } = s;
117
+ void rest;
118
+ out.push({ name, description, content, ...(manifest !== undefined ? { manifest } : {}), ...(files !== undefined ? { files } : {}), ...(baseDir !== undefined ? { baseDir } : {}) });
116
119
  }
117
120
  }
118
121
  return out.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
119
122
  }
120
123
  export const SKILLS_LISTING_PROBE_HEADER = "The following skills are available for this task.";
121
- export function skillListingLine(s) {
122
- const desc = s.description.length > SKILL_DESC_MAX_CHARS
123
- ? `${s.description.slice(0, SKILL_DESC_MAX_CHARS - 1)}…`
124
- : s.description;
124
+ export function skillListingLine(s, descMaxChars = SKILL_LISTING_DESC_MAX_CHARS_DEFAULT) {
125
+ const desc = s.description.length > descMaxChars ? `${s.description.slice(0, descMaxChars - 1)}…` : s.description;
125
126
  const safe = (t) => sanitizeUntrustedText(t, SHELLED_BODY_ENVELOPE_TAGS);
126
127
  return `- ${safe(s.name)}: ${safe(desc)}${safe(skillFilesSuffix(s.files))}`;
127
128
  }
128
129
  const SKILLS_BLOCK_CAP_MARKER = "(block size cap reached — lower-priority skills are listed name-only; descriptions via the tool)";
129
- export function buildSkillsBlock(skills) {
130
+ export function buildSkillsBlock(skills, budget) {
131
+ return renderSkillsBlock(skills, budget)?.text;
132
+ }
133
+ export function renderSkillsBlock(skills, budget) {
130
134
  if (skills.length === 0)
131
135
  return undefined;
132
- const bytes = (s) => Buffer.byteLength(s, "utf8");
133
136
  const headerLines = [
134
137
  "<skills>",
135
138
  `${SKILLS_LISTING_PROBE_HEADER} A skill is a named package of instructions;`,
@@ -137,18 +140,34 @@ export function buildSkillsBlock(skills) {
137
140
  `instructions by calling the \`${SKILL_TOOL_NAME}\` tool with \`skill: "<name>"\` (plus optional \`args\`).`,
138
141
  "",
139
142
  ];
140
- const render = (lines) => [...headerLines, ...lines, "</skills>"].join("\n");
143
+ return renderSkillLinesWithin(skills, {
144
+ blockMaxBytes: budget?.blockMaxBytes ?? SKILL_LISTING_CEILING_BYTES,
145
+ descMaxChars: budget?.descMaxChars ?? SKILL_LISTING_DESC_MAX_CHARS_DEFAULT,
146
+ frame: (lines) => [...headerLines, ...lines, "</skills>"].join("\n"),
147
+ });
148
+ }
149
+ export function renderSkillLinesWithin(skills, opts) {
150
+ if (skills.length === 0)
151
+ return undefined;
152
+ const blockMaxBytes = Math.min(opts.blockMaxBytes, SKILL_LISTING_CEILING_BYTES);
153
+ const descMaxChars = opts.descMaxChars;
154
+ const bytes = (s) => Buffer.byteLength(s, "utf8");
155
+ const render = opts.frame;
141
156
  const lineCost = (l) => bytes(l) + 1;
142
- const budget = SKILLS_BLOCK_MAX_BYTES - bytes(render([]));
143
- const fullLines = skills.map((s) => skillListingLine(s));
144
- const nameLines = skills.map((s) => `- ${sanitizeUntrustedText(s.name, SHELLED_BODY_ENVELOPE_TAGS)}`);
157
+ const bodyBudget = blockMaxBytes - bytes(render([]));
158
+ const fullLines = skills.map((s) => skillListingLine(s, descMaxChars));
145
159
  const fullTotal = fullLines.reduce((acc, l) => acc + lineCost(l), 0);
146
- if (fullTotal <= budget)
147
- return render(fullLines);
160
+ if (fullTotal <= bodyBudget)
161
+ return { text: render(fullLines), nameOnly: [], dropped: [] };
162
+ const nameLines = skills.map((s) => `- ${sanitizeUntrustedText(s.name, SHELLED_BODY_ENVELOPE_TAGS)}`);
163
+ const namePrefix = new Array(nameLines.length + 1);
164
+ namePrefix[0] = 0;
165
+ for (let i = 0; i < nameLines.length; i++)
166
+ namePrefix[i + 1] = namePrefix[i] + lineCost(nameLines[i]);
148
167
  const markerCost = lineCost(SKILLS_BLOCK_CAP_MARKER);
149
- const floorCost = markerCost + nameLines.reduce((acc, l) => acc + lineCost(l), 0);
150
- if (floorCost <= budget) {
151
- let slack = budget - floorCost;
168
+ const floorCost = markerCost + namePrefix[nameLines.length];
169
+ if (floorCost <= bodyBudget) {
170
+ let slack = bodyBudget - floorCost;
152
171
  const byPriority = skills
153
172
  .map((s, i) => ({ i, rank: s.declaredRank ?? i }))
154
173
  .toSorted((a, b) => a.rank - b.rank || a.i - b.i);
@@ -160,15 +179,28 @@ export function buildSkillsBlock(skills) {
160
179
  slack -= delta;
161
180
  }
162
181
  }
163
- return render([SKILLS_BLOCK_CAP_MARKER, ...skills.map((_s, i) => (withDescription.has(i) ? fullLines[i] : nameLines[i]))]);
182
+ return {
183
+ text: render([SKILLS_BLOCK_CAP_MARKER, ...skills.map((_s, i) => (withDescription.has(i) ? fullLines[i] : nameLines[i]))]),
184
+ nameOnly: skills.filter((_s, i) => !withDescription.has(i)).map((s) => s.name),
185
+ dropped: [],
186
+ };
164
187
  }
165
188
  for (let m = skills.length - 1; m >= 0; m--) {
166
189
  const overflowLine = `(… +${skills.length - m} more skills not listed — the ${SKILL_TOOL_NAME} tool serves them all)`;
167
- const cost = markerCost + nameLines.slice(0, m).reduce((acc, l) => acc + lineCost(l), 0) + lineCost(overflowLine);
168
- if (cost <= budget)
169
- return render([SKILLS_BLOCK_CAP_MARKER, ...nameLines.slice(0, m), overflowLine]);
190
+ const cost = markerCost + namePrefix[m] + lineCost(overflowLine);
191
+ if (cost <= bodyBudget) {
192
+ return {
193
+ text: render([SKILLS_BLOCK_CAP_MARKER, ...nameLines.slice(0, m), overflowLine]),
194
+ nameOnly: skills.slice(0, m).map((s) => s.name),
195
+ dropped: skills.slice(m).map((s) => s.name),
196
+ };
197
+ }
170
198
  }
171
- return render([SKILLS_BLOCK_CAP_MARKER, `(… +${skills.length} more skills not listed — the ${SKILL_TOOL_NAME} tool serves them all)`]);
199
+ return {
200
+ text: render([SKILLS_BLOCK_CAP_MARKER, `(… +${skills.length} more skills not listed — the ${SKILL_TOOL_NAME} tool serves them all)`]),
201
+ nameOnly: [],
202
+ dropped: skills.map((s) => s.name),
203
+ };
172
204
  }
173
205
  export function createSkillTool(skills, scope) {
174
206
  const byName = new Map(skills.map((s) => [s.name, s]));
@@ -202,7 +234,8 @@ export function createSkillTool(skills, scope) {
202
234
  if (scope && skill.manifest) {
203
235
  scope.push({ kind: "manifest", manifest: skill.manifest });
204
236
  }
205
- let body = skill.content;
237
+ const baseDir = skill.baseDir;
238
+ let body = baseDir !== undefined ? substituteSkillRoot(skill.content, baseDir) : skill.content;
206
239
  if (skill.files !== undefined && skill.files.length > 0) {
207
240
  const SEPARATOR_CHARS = 2;
208
241
  const SECTION_HEADER = "## Attachments";
@@ -217,27 +250,30 @@ export function createSkillTool(skills, scope) {
217
250
  : f.path;
218
251
  const header = `### ${path}\n`;
219
252
  const renderedOverhead = header.length + SEPARATOR_CHARS;
253
+ const expanded = baseDir !== undefined ? substituteSkillRootBounded(f.content, baseDir, Math.max(0, budget)) : { text: f.content, totalChars: f.content.length };
254
+ const content = expanded.text;
255
+ const contentChars = expanded.totalChars;
220
256
  if (budget <= renderedOverhead) {
221
257
  omittedCount += 1;
222
- omittedChars += header.length + f.content.length;
258
+ omittedChars += header.length + contentChars;
223
259
  continue;
224
260
  }
225
261
  const contentBudget = budget - renderedOverhead;
226
- if (f.content.length > contentBudget) {
227
- const markerFor = (shown) => `\n[attachment truncated: showing the first ${shown} of ${f.content.length} chars — attachments cap reached]`;
262
+ if (contentChars > contentBudget) {
263
+ const markerFor = (shown) => `\n[attachment truncated: showing the first ${shown} of ${contentChars} chars — attachments cap reached]`;
228
264
  const shown = contentBudget - markerFor(contentBudget).length;
229
265
  if (shown <= 0) {
230
266
  omittedCount += 1;
231
- omittedChars += header.length + f.content.length;
267
+ omittedChars += header.length + contentChars;
232
268
  budget = 0;
233
269
  continue;
234
270
  }
235
- sections.push(`${header}${f.content.slice(0, shown)}${markerFor(shown)}`);
271
+ sections.push(`${header}${content.slice(0, shown)}${markerFor(shown)}`);
236
272
  budget = 0;
237
273
  continue;
238
274
  }
239
- sections.push(`${header}${f.content}`);
240
- budget -= renderedOverhead + f.content.length;
275
+ sections.push(`${header}${content}`);
276
+ budget -= renderedOverhead + contentChars;
241
277
  }
242
278
  if (omittedCount > 0) {
243
279
  sections.push(`[+${omittedCount} more attachment(s) omitted (${omittedChars} chars) — the ${SKILL_ATTACHMENTS_MAX_CHARS}-char attachments cap is exhausted]`);
@@ -245,7 +281,8 @@ export function createSkillTool(skills, scope) {
245
281
  body = `${body}\n\n${sections.join("\n\n")}`;
246
282
  }
247
283
  const skillArgs = typeof a.args === "string" && a.args.length > 0 ? a.args.slice(0, 10_000) : undefined;
248
- return skillArgs ? `${body}\n\n## Input\n${skillArgs}` : body;
284
+ const delivered = skillArgs ? `${body}\n\n## Input\n${skillArgs}` : body;
285
+ return baseDir !== undefined ? `Base directory for this skill: ${baseDir}\n\n${delivered}` : delivered;
249
286
  },
250
287
  });
251
288
  }
@@ -1,15 +1,16 @@
1
1
  import type { Model } from "../../internal/llm.js";
2
2
  import type { ToolSpec } from "../types.js";
3
3
  import type { ToolFingerprintInput } from "../cache-break-detector.js";
4
+ import { type ResolvedToolSearch, type ToolSearchSetting } from "../tool-search-setting.js";
4
5
  /**
5
6
  * The deferral GATE: which mounted tool names ship as placeholders on a request. Pure
6
7
  * registry logic over names and sizes — no harness, no I/O. `tool-disclosure.ts` owns the placeholder
7
8
  * and ToolSearch mechanics that act on the set this module decides; `prepare-defer-classify.ts` is the
8
9
  * phase that assembles the inputs from the mounted face.
9
10
  */
10
- /** The deferral gate's threshold: the engine's candidate set is deferred when its inlined size reaches
11
- * this fraction of the model's context window (that set measured, never the rest of the face). */
12
- export declare const DEFER_THRESHOLD_FRACTION = 0.1;
11
+ /** The deferral gate's DEFAULT threshold: with no `toolSearch` seat, the candidate set defers when its inlined size reaches this fraction of the model's
12
+ * context window (that set measured, never the rest of the face). Derived from the seat's own default percent — one default, two units. */
13
+ export declare const DEFER_THRESHOLD_FRACTION: number;
13
14
  /** The typed refusal a `deferMode` outside its declared domain (`"auto"` or absent) raises at the classifier —
14
15
  * a bad value never selects an arm silently. */
15
16
  export declare const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
@@ -24,10 +25,13 @@ export interface DeferralGate {
24
25
  gated: Set<string>;
25
26
  /** Σ inlined chars (name + description + serialized schema) over `gated` — the bytes the gate could reclaim. */
26
27
  gatedChars: number;
27
- /** `floor(contextTokens × fraction) × CHARS_PER_TOKEN`; the char reading of the token threshold. */
28
+ /** `floor(contextTokens × fraction) × CHARS_PER_TOKEN`; the char reading of the token threshold. `0` under `toolSearch: true` and `Infinity` under
29
+ * `false` — the two boolean arms ARE this number. */
28
30
  thresholdChars: number;
29
31
  /** Whether `gated` is deferred on this request: `gatedChars ≥ thresholdChars`. */
30
32
  open: boolean;
33
+ /** The lowered seat this verdict was measured under ({@link ToolSearchSetting}); always present, since an absent seat resolves to the default `auto`. */
34
+ toolSearch: ResolvedToolSearch;
31
35
  }
32
36
  /**
33
37
  * Deferral has TWO rules, and only two.
@@ -44,11 +48,12 @@ export interface DeferralGate {
44
48
  * "auto"` WIDENS the candidate set to every caller spec — undeclared caller tools included — and nothing
45
49
  * else: it is not a second gate, and engine built-ins the deployment did not name are candidates under no
46
50
  * setting. The gate decides the set all or nothing: deferred iff `gatedChars ≥ thresholdChars`, where the
47
- * threshold is {@link DEFER_THRESHOLD_FRACTION} of the model's context window (`contextTokens ??
48
- * contextWindow`, in chars at {@link CHARS_PER_TOKEN}). The measure is the candidate set ONLY — the bytes
49
- * the gate could reclaim — never the caller's list, the pins, or non-candidates: a request whose weight
50
- * sits elsewhere gains nothing from a ToolSearch round-trip, so nothing about it argues for one. A window
51
- * of zero reads as a zero threshold (every candidate byte is over budget), so the candidates defer.
51
+ * threshold is the DEPLOYMENT'S FRACTION of the model's context window (`contextTokens ?? contextWindow`,
52
+ * in chars at {@link CHARS_PER_TOKEN}) — {@link ToolSearchSetting} lowered by {@link resolveToolSearch},
53
+ * which is {@link DEFER_THRESHOLD_FRACTION} when no seat is wired. The measure is the candidate set ONLY —
54
+ * the bytes the gate could reclaim — never the caller's list, the pins, or non-candidates: a request whose
55
+ * weight sits elsewhere gains nothing from a ToolSearch round-trip, so nothing about it argues for one. A
56
+ * window of zero reads as a zero threshold (every candidate byte is over budget), so the candidates defer.
52
57
  *
53
58
  * Invariants: `explicit ∩ gated = ∅`; `deferred = explicit ∪ (open ? gated : ∅)`; a pinned name is in no
54
59
  * list; the same inputs always yield the same verdict (a pure function of the request).
@@ -65,6 +70,9 @@ export declare function deferralGate(opts: {
65
70
  fullTools: ReadonlyArray<ToolFingerprintInput>;
66
71
  /** `"auto"` widens the candidate set to every caller spec (see above). Any other value refuses. */
67
72
  deferMode?: "auto";
73
+ /** The deployment's gate policy for rule ② ({@link ToolSearchSetting}); absent ⇒ the default `auto` at
74
+ * {@link TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT}, byte-for-byte the prior behavior. */
75
+ toolSearch?: ToolSearchSetting;
68
76
  model: Model;
69
77
  /** Rule ① — `TaskSpec.deferTools`: ALREADY-MOUNTED wire names (built-ins included; `ToolSpec.defer`
70
78
  * only reaches caller specs) the caller defers on every request. Caller pre-filters to mounted names. */
@@ -84,3 +92,8 @@ export declare function deferralGate(opts: {
84
92
  * gate; the `TaskSpec` / `RunnerDeps` JSDoc points here.
85
93
  */
86
94
  export declare function classifyDeferred(opts: Parameters<typeof deferralGate>[0]): Set<string>;
95
+ /** The SAME fold, with the verdict it folded — a disclosure that reports the policy must read the one the gate measured under, never a later re-resolution. */
96
+ export declare function classifyDeferredWithGate(opts: Parameters<typeof deferralGate>[0]): {
97
+ deferred: Set<string>;
98
+ gate: DeferralGate;
99
+ };
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_CHARS_PER_TOKEN } from "../../internal/harness.js";
2
- export const DEFER_THRESHOLD_FRACTION = 0.1;
2
+ import { TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT, describeRejectedValue, resolveToolSearch } from "../tool-search-setting.js";
3
+ export const DEFER_THRESHOLD_FRACTION = TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT / 100;
3
4
  const CHARS_PER_TOKEN = DEFAULT_CHARS_PER_TOKEN;
4
5
  function inlinedChars(t) {
5
6
  let schema = "";
@@ -14,10 +15,11 @@ function inlinedChars(t) {
14
15
  export const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
15
16
  export function deferralGate(opts) {
16
17
  if (opts.deferMode !== undefined && opts.deferMode !== "auto") {
17
- const e = new Error(`RunnerDeps.deferMode: invalid value ${JSON.stringify(opts.deferMode)} — expected "auto" or omitted; a bad value refuses loudly, nothing falls back to a default.`);
18
+ const e = new Error(`RunnerDeps.deferMode: invalid value ${describeRejectedValue(opts.deferMode)} — expected "auto" or omitted; a bad value refuses loudly, nothing falls back to a default.`);
18
19
  e.code = DEFER_MODE_INVALID_CODE;
19
20
  throw e;
20
21
  }
22
+ const toolSearch = resolveToolSearch(opts.toolSearch);
21
23
  const pinned = new Set(opts.alwaysLoadNames ?? []);
22
24
  for (const s of opts.specs) {
23
25
  if (s.alwaysLoad === true)
@@ -44,14 +46,22 @@ export function deferralGate(opts) {
44
46
  if (gated.has(t.name))
45
47
  gatedChars += inlinedChars(t);
46
48
  const windowTokens = Math.max(0, opts.model.contextTokens ?? opts.model.contextWindow ?? 0);
47
- const thresholdChars = Math.floor(windowTokens * DEFER_THRESHOLD_FRACTION) * CHARS_PER_TOKEN;
48
- return { explicit, gated, gatedChars, thresholdChars, open: gatedChars >= thresholdChars };
49
+ const thresholdTokens = windowTokens * toolSearch.fraction;
50
+ const thresholdChars = Number.isFinite(thresholdTokens)
51
+ ? Math.floor(thresholdTokens) * CHARS_PER_TOKEN
52
+ : toolSearch.fraction === 0
53
+ ? 0
54
+ : Number.POSITIVE_INFINITY;
55
+ return { explicit, gated, gatedChars, thresholdChars, open: gatedChars >= thresholdChars, toolSearch };
49
56
  }
50
57
  export function classifyDeferred(opts) {
58
+ return classifyDeferredWithGate(opts).deferred;
59
+ }
60
+ export function classifyDeferredWithGate(opts) {
51
61
  const gate = deferralGate(opts);
52
62
  const deferred = new Set(gate.explicit);
53
63
  if (gate.open)
54
64
  for (const n of gate.gated)
55
65
  deferred.add(n);
56
- return deferred;
66
+ return { deferred, gate };
57
67
  }
@@ -247,6 +247,11 @@ export declare function createToolSearchTool(opts: {
247
247
  */
248
248
  isMounted?: (name: string) => boolean;
249
249
  serializeActivation?: <T>(section: () => Promise<T>) => Promise<T>;
250
+ /** Called at the TAIL of this lane's activation section, after the rollback window has closed — the one point at which the shared active set is committed
251
+ * for this call. The disclosure census rides it, because a publisher reading that set from OUTSIDE the section (an MCP refresh's rematerialize, which runs
252
+ * unserialized and after an await) can observe a name a racing activation staged and then rolled back, and announce an activation that never happened.
253
+ * Absent ⇒ nothing is published. */
254
+ onActivationCommitted?: () => void;
250
255
  /** Resolve a deferred name's REAL declared
251
256
  * parameter schema, read from the live roster at call time. Presence switches this face to the
252
257
  * static posture: activation never swaps the placeholder in the tools block, so the result text
@@ -336,6 +336,8 @@ export function createToolSearchTool(opts) {
336
336
  throw e;
337
337
  }
338
338
  }
339
+ if (newly.length > 0)
340
+ opts.onActivationCommitted?.();
339
341
  return { newly, ride: newly.length > 0 ? listingRide?.(newly) : undefined };
340
342
  });
341
343
  const { newly, ride } = await section;