@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.
- package/CHANGELOG.md +233 -13
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/agents/teacher.js +1 -0
- package/dist/agents/verify.js +3 -0
- package/dist/brain/anthropic.js +6 -3
- package/dist/brain/open-responses.js +10 -4
- package/dist/brain/openai.js +5 -2
- package/dist/brain/reasoning.d.ts +13 -13
- package/dist/brain/reasoning.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +18 -0
- package/dist/brain/route-adjudicator.js +16 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/brain/stream-shared.d.ts +3 -0
- package/dist/brain/stream-shared.js +1 -0
- package/dist/core/agent-definition.d.ts +10 -0
- package/dist/core/artifact-host.d.ts +41 -4
- package/dist/core/artifact-host.js +18 -2
- package/dist/core/ask-origin.d.ts +16 -0
- package/dist/core/ask-origin.js +8 -0
- package/dist/core/auto-mode-arming.d.ts +6 -0
- package/dist/core/auto-mode-arming.js +5 -5
- package/dist/core/auto-mode-defaults.d.ts +43 -15
- package/dist/core/auto-mode-defaults.js +4 -1
- package/dist/core/auto-mode.d.ts +184 -59
- package/dist/core/auto-mode.js +188 -13
- package/dist/core/checkpoint-store.d.ts +23 -2
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/engine-notice.d.ts +77 -1
- package/dist/core/engine-notice.js +7 -0
- package/dist/core/gate-fold.js +5 -1
- package/dist/core/gate-lanes.js +9 -8
- package/dist/core/gate-outcome.d.ts +42 -2
- package/dist/core/gate-outcome.js +23 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +14 -0
- package/dist/core/hook-seats.d.ts +66 -0
- package/dist/core/hook-seats.js +37 -0
- package/dist/core/hooks.d.ts +1 -1
- package/dist/core/read-root-candidate.d.ts +12 -0
- package/dist/core/read-root-candidate.js +24 -0
- package/dist/core/roles.d.ts +9 -1
- package/dist/core/roles.js +4 -0
- package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
- package/dist/core/runner/assemble-result.d.ts +2 -1
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/classifier-ladder.d.ts +227 -0
- package/dist/core/runner/classifier-ladder.js +214 -0
- package/dist/core/runner/contracts.d.ts +71 -13
- package/dist/core/runner/decide-continuation.d.ts +1 -3
- package/dist/core/runner/decide-continuation.js +1 -7
- package/dist/core/runner/deferral-census.d.ts +52 -0
- package/dist/core/runner/deferral-census.js +44 -0
- package/dist/core/runner/denial-limit-arms.js +2 -2
- package/dist/core/runner/derived-route-fallback.d.ts +8 -4
- package/dist/core/runner/derived-route-fallback.js +8 -3
- package/dist/core/runner/gate-exit.d.ts +12 -0
- package/dist/core/runner/gate-exit.js +6 -3
- package/dist/core/runner/prepare-artifact.d.ts +10 -2
- package/dist/core/runner/prepare-artifact.js +5 -4
- package/dist/core/runner/prepare-ask-lane.js +1 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
- package/dist/core/runner/prepare-config-doors.d.ts +7 -1
- package/dist/core/runner/prepare-config-doors.js +13 -13
- package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
- package/dist/core/runner/prepare-defer-classify.js +8 -3
- package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
- package/dist/core/runner/prepare-hands-readface.js +60 -24
- package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
- package/dist/core/runner/prepare-inherited-gate.js +10 -2
- package/dist/core/runner/prepare-lsp.d.ts +2 -2
- package/dist/core/runner/prepare-lsp.js +1 -1
- package/dist/core/runner/prepare-policy-chain.js +2 -0
- package/dist/core/runner/prepare-project-context.d.ts +5 -1
- package/dist/core/runner/prepare-project-context.js +7 -4
- package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
- package/dist/core/runner/prepare-prompt-inputs.js +4 -1
- package/dist/core/runner/prepare-run-refs.d.ts +9 -1
- package/dist/core/runner/prepare-run-refs.js +3 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
- package/dist/core/runner/prepare-suspend-saga.js +5 -2
- package/dist/core/runner/prepare-task.js +19 -14
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
- package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
- package/dist/core/runner/prepare-turn-wiring.js +3 -2
- package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
- package/dist/core/runner/prepare-wiring-manifest.js +5 -2
- package/dist/core/runner/resume-apply.js +3 -3
- package/dist/core/runner/resume-checkpoint-screen.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +4 -1
- package/dist/core/runner/run-identity-wiring.js +6 -2
- package/dist/core/runner/run-leg.d.ts +2 -0
- package/dist/core/runner/run-leg.js +45 -45
- package/dist/core/runner/run-notification-lane.js +55 -27
- package/dist/core/runner/run-settle-and-teardown.js +1 -0
- package/dist/core/runner/run-terminal-adoption.js +16 -13
- package/dist/core/runner/run-turn-boundary.js +15 -5
- package/dist/core/runner/runtask.d.ts +6 -5
- package/dist/core/runner/runtask.js +26 -14
- package/dist/core/runner/skill-base-dir.d.ts +54 -0
- package/dist/core/runner/skill-base-dir.js +42 -0
- package/dist/core/runner/synthetic-tools.d.ts +83 -48
- package/dist/core/runner/synthetic-tools.js +69 -32
- package/dist/core/runner/tool-defer-gate.d.ts +22 -9
- package/dist/core/runner/tool-defer-gate.js +15 -5
- package/dist/core/runner/tool-disclosure.d.ts +5 -0
- package/dist/core/runner/tool-disclosure.js +2 -0
- package/dist/core/runner/tool-progress-lane.d.ts +42 -0
- package/dist/core/runner/tool-progress-lane.js +61 -0
- package/dist/core/runner/turn-attachments.d.ts +43 -12
- package/dist/core/runner/turn-attachments.js +58 -14
- package/dist/core/runner-deps.d.ts +48 -7
- package/dist/core/sensitive-path-policy.d.ts +58 -17
- package/dist/core/sensitive-path-policy.js +33 -5
- package/dist/core/skill-listing-budget.d.ts +112 -0
- package/dist/core/skill-listing-budget.js +58 -0
- package/dist/core/swappable-deps.d.ts +9 -7
- package/dist/core/swappable-deps.js +7 -3
- package/dist/core/task-event.d.ts +96 -29
- package/dist/core/task-notification.d.ts +142 -16
- package/dist/core/task-notification.js +106 -0
- package/dist/core/task-result.d.ts +9 -1
- package/dist/core/task-spec.d.ts +46 -3
- package/dist/core/tighten-task-spec.js +7 -5
- package/dist/core/tool-model-gate.d.ts +1 -1
- package/dist/core/tool-policy.d.ts +19 -4
- package/dist/core/tool-policy.js +6 -7
- package/dist/core/tool-search-setting.d.ts +78 -0
- package/dist/core/tool-search-setting.js +90 -0
- package/dist/core/tool-spec.d.ts +45 -4
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +46 -0
- package/dist/core/wiring-manifest.js +9 -1
- package/dist/core/write-protect.d.ts +8 -3
- package/dist/core/write-protect.js +8 -5
- package/dist/engine/execution-env/node-execution-env.js +10 -4
- package/dist/engine/harness/agent-harness.d.ts +42 -0
- package/dist/engine/harness/agent-harness.js +61 -29
- package/dist/engine/loop/agent-loop.d.ts +68 -3
- package/dist/engine/loop/agent-loop.js +109 -16
- package/dist/index.d.ts +13 -8
- package/dist/index.js +11 -7
- package/dist/internal/harness.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +3 -0
- package/dist/orchestration/run-workflow-tool.js +6 -1
- package/dist/orchestration/workflow-governance.d.ts +7 -1
- package/dist/orchestration/workflow-governance.js +9 -5
- package/dist/orchestration/workflow-primitives.d.ts +3 -0
- package/dist/orchestration/workflow-primitives.js +6 -1
- package/dist/orchestration/workflow-types.d.ts +48 -13
- package/dist/orchestration/workflow-types.js +1 -1
- package/dist/orchestration/workflow.js +45 -17
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/section-usage.d.ts +26 -0
- package/dist/prompt-assembly/section-usage.js +10 -0
- package/dist/prompt-assembly/types.d.ts +8 -6
- package/dist/prompts/default.d.ts +13 -0
- package/dist/prompts/default.js +16 -8
- package/dist/tools/artifact/artifact-text.d.ts +10 -1
- package/dist/tools/artifact/artifact-text.js +6 -3
- package/dist/tools/artifact/artifact-tool.d.ts +12 -2
- package/dist/tools/artifact/artifact-tool.js +203 -70
- package/dist/tools/artifact/conformance.d.ts +56 -0
- package/dist/tools/artifact/conformance.js +349 -0
- package/dist/tools/artifact/local-stub.js +1 -1
- package/dist/tools/artifact/reject-breaker.d.ts +28 -0
- package/dist/tools/artifact/reject-breaker.js +42 -0
- package/dist/tools/fs/bash-lexer.d.ts +132 -0
- package/dist/tools/fs/bash-lexer.js +563 -0
- package/dist/tools/fs/bash-program-position.d.ts +142 -0
- package/dist/tools/fs/bash-program-position.js +125 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
- package/dist/tools/fs/bash-readonly-classifier.js +180 -558
- package/dist/tools/fs/containment-face.d.ts +89 -0
- package/dist/tools/fs/containment-face.js +81 -0
- package/dist/tools/fs/fs-bash.d.ts +46 -1
- package/dist/tools/fs/fs-bash.js +165 -25
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-search-tools.d.ts +3 -3
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +24 -5
- package/dist/tools/fs/index.js +18 -6
- package/dist/tools/fs/read-deny.d.ts +4 -3
- package/dist/tools/fs/repo-map.d.ts +2 -2
- package/dist/tools/fs/safety.d.ts +18 -3
- package/dist/tools/fs/safety.js +11 -4
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +99 -13
- package/wire-changes/core@7.17.7.json +49 -0
- package/wire-changes/core@7.18.0.json +1273 -0
- package/wire-changes/core@7.19.0.json +457 -0
- package/dist/tools/fs/read-face.d.ts +0 -68
- 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
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
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
|
-
/**
|
|
63
|
-
*
|
|
64
|
-
* the
|
|
65
|
-
*
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
*
|
|
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 —
|
|
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):
|
|
87
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
137
|
-
* {@link
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
-
|
|
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
|
|
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 <=
|
|
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.
|
|
150
|
-
if (floorCost <=
|
|
151
|
-
let slack =
|
|
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
|
|
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 +
|
|
168
|
-
if (cost <=
|
|
169
|
-
return
|
|
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
|
|
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
|
-
|
|
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 +
|
|
258
|
+
omittedChars += header.length + contentChars;
|
|
223
259
|
continue;
|
|
224
260
|
}
|
|
225
261
|
const contentBudget = budget - renderedOverhead;
|
|
226
|
-
if (
|
|
227
|
-
const markerFor = (shown) => `\n[attachment truncated: showing the first ${shown} of ${
|
|
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 +
|
|
267
|
+
omittedChars += header.length + contentChars;
|
|
232
268
|
budget = 0;
|
|
233
269
|
continue;
|
|
234
270
|
}
|
|
235
|
-
sections.push(`${header}${
|
|
271
|
+
sections.push(`${header}${content.slice(0, shown)}${markerFor(shown)}`);
|
|
236
272
|
budget = 0;
|
|
237
273
|
continue;
|
|
238
274
|
}
|
|
239
|
-
sections.push(`${header}${
|
|
240
|
-
budget -= renderedOverhead +
|
|
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
|
-
|
|
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
|
|
11
|
-
*
|
|
12
|
-
export declare const DEFER_THRESHOLD_FRACTION
|
|
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
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
48
|
-
|
|
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;
|