@arnilo/prism 0.0.19 → 0.0.20

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.
@@ -0,0 +1,112 @@
1
+ import { estimateTextBytes } from "./context-budget.js";
2
+ import { HARD_MAX_SKILL_INSTRUCTION_BYTES } from "./skill-disclosure.js";
3
+ export const DEFAULT_LOAD_SKILL_TOOL_NAME = "load_skill";
4
+ export const SKILL_LOAD_ERROR_CODE = "skill_load_failed";
5
+ export const MAX_LOAD_SKILL_RESULT_BYTES = 512;
6
+ export class SkillLoadError extends Error {
7
+ code = SKILL_LOAD_ERROR_CODE;
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "SkillLoadError";
11
+ }
12
+ }
13
+ export function isSkillLoadError(error) {
14
+ return error instanceof Error && error.code === SKILL_LOAD_ERROR_CODE;
15
+ }
16
+ export function resolveSkillLoad(options) {
17
+ const name = options.name.trim();
18
+ if (!name)
19
+ throw new SkillLoadError("Skill name is required");
20
+ let skill;
21
+ try {
22
+ skill = options.registry.resolve(name);
23
+ }
24
+ catch {
25
+ throw new SkillLoadError(`Unknown skill: ${name}`);
26
+ }
27
+ if (options.activeSkillNames && !options.activeSkillNames.includes(skill.name)) {
28
+ throw new SkillLoadError(`Skill ${skill.name} is not active for this run`);
29
+ }
30
+ const toolNames = new Set((options.tools ?? []).map((tool) => tool.name));
31
+ const missingTool = skill.toolNames?.find((toolName) => !toolNames.has(toolName));
32
+ if (missingTool)
33
+ throw new SkillLoadError(`Skill ${skill.name} requires inactive tool: ${missingTool}`);
34
+ if (!skill.instructions?.trim())
35
+ throw new SkillLoadError(`Skill ${skill.name} has no instructions to load`);
36
+ const bytes = estimateTextBytes(skill.instructions);
37
+ if (bytes > HARD_MAX_SKILL_INSTRUCTION_BYTES) {
38
+ throw new SkillLoadError(`Skill ${skill.name} instructions exceed hard cap (${HARD_MAX_SKILL_INSTRUCTION_BYTES} bytes)`);
39
+ }
40
+ if (options.loaded?.has(skill.name))
41
+ throw new SkillLoadError(`Skill ${skill.name} is already loaded`);
42
+ return skill;
43
+ }
44
+ function skillLoadMetadata(context) {
45
+ const metadata = context.metadata;
46
+ if (!metadata || typeof metadata !== "object")
47
+ return undefined;
48
+ return metadata;
49
+ }
50
+ function capLoadSkillText(text) {
51
+ const bytes = estimateTextBytes(text);
52
+ if (bytes <= MAX_LOAD_SKILL_RESULT_BYTES)
53
+ return text;
54
+ const suffix = "…";
55
+ let end = MAX_LOAD_SKILL_RESULT_BYTES - new TextEncoder().encode(suffix).length;
56
+ const encoded = new TextEncoder().encode(text);
57
+ while (end > 0 && (encoded[end] & 0xc0) === 0x80)
58
+ end--;
59
+ return new TextDecoder().decode(encoded.slice(0, end)) + suffix;
60
+ }
61
+ export function createLoadSkillTool(options) {
62
+ const toolName = options.name ?? DEFAULT_LOAD_SKILL_TOOL_NAME;
63
+ return {
64
+ name: toolName,
65
+ description: "Load the full instructions for an active skill by exact name.",
66
+ parameters: {
67
+ type: "object",
68
+ properties: {
69
+ name: { type: "string", description: "Exact skill name from the catalog." },
70
+ },
71
+ required: ["name"],
72
+ },
73
+ execute(args, context) {
74
+ const fail = (reason, text) => {
75
+ const bounded = capLoadSkillText(text);
76
+ return {
77
+ toolCallId: context.toolCallId,
78
+ name: toolName,
79
+ value: { ok: false, reason, text: bounded },
80
+ content: [{ type: "text", text: bounded }],
81
+ };
82
+ };
83
+ const name = typeof args.name === "string" ? args.name : "";
84
+ const metadata = skillLoadMetadata(context);
85
+ const loaded = metadata?.loadedSkills ?? options.loaded;
86
+ if (!loaded)
87
+ return fail("no_loaded_set", "Skill load state is unavailable for this session.");
88
+ try {
89
+ const skill = resolveSkillLoad({
90
+ registry: options.registry,
91
+ name,
92
+ tools: metadata?.activeTools ?? options.tools,
93
+ loaded,
94
+ activeSkillNames: metadata?.activeSkillNames,
95
+ });
96
+ loaded.add(skill.name);
97
+ const text = capLoadSkillText(`Loaded skill ${skill.name} for this session.`);
98
+ return {
99
+ toolCallId: context.toolCallId,
100
+ name: toolName,
101
+ value: { ok: true, name: skill.name, text },
102
+ content: [{ type: "text", text }],
103
+ };
104
+ }
105
+ catch (error) {
106
+ const message = error instanceof Error ? error.message : "Skill load failed";
107
+ return fail("skill_load_failed", message);
108
+ }
109
+ },
110
+ };
111
+ }
112
+ //# sourceMappingURL=skill-load.js.map
@@ -0,0 +1,40 @@
1
+ import type { Message, ToolResult } from "./contracts.js";
2
+ export declare const DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS = 2;
3
+ export declare const DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES = 4096;
4
+ export declare const DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 512;
5
+ export declare const HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 4096;
6
+ export declare const TOOL_RESULT_FOLD_TURN_METADATA_KEY: "prismToolResultTurn";
7
+ export interface ToolResultFoldInput {
8
+ readonly sessionId: string;
9
+ readonly runId: string;
10
+ readonly turn: number;
11
+ readonly toolCallId: string;
12
+ readonly toolName: string;
13
+ readonly text: string;
14
+ }
15
+ export interface ToolResultFoldOptions {
16
+ readonly minAgeTurns?: number;
17
+ readonly minBytes?: number;
18
+ readonly maxSummaryBytes?: number;
19
+ readonly summarize: (input: ToolResultFoldInput) => Promise<string> | string;
20
+ }
21
+ export interface ResolvedToolResultFoldOptions {
22
+ readonly minAgeTurns: number;
23
+ readonly minBytes: number;
24
+ readonly maxSummaryBytes: number;
25
+ readonly summarize: (input: ToolResultFoldInput) => Promise<string> | string;
26
+ }
27
+ export interface FoldToolResultsContext {
28
+ readonly sessionId: string;
29
+ readonly runId: string;
30
+ readonly turn: number;
31
+ readonly signal?: AbortSignal;
32
+ }
33
+ /** Run overrides agent; disabled when neither supplies `summarize`. */
34
+ export declare function resolveToolResultFold(run?: ToolResultFoldOptions, agent?: ToolResultFoldOptions): ResolvedToolResultFoldOptions | undefined;
35
+ /** Projection-only fold for history tool messages; does not mutate the input array. */
36
+ export declare function foldToolResultHistory(history: readonly Message[], options: ResolvedToolResultFoldOptions, context: FoldToolResultsContext): Promise<readonly Message[]>;
37
+ /** Projection-only fold for in-flight tool results before message conversion. */
38
+ export declare function foldToolResults(results: readonly ToolResult[], options: ResolvedToolResultFoldOptions, context: FoldToolResultsContext): Promise<readonly ToolResult[]>;
39
+ export declare function formatFoldedToolResult(summary: string): string;
40
+ export declare function foldedToolResultHeader(toolName: string, toolCallId: string, summary: string): string;
@@ -0,0 +1,164 @@
1
+ import { estimateTextBytes } from "./context-budget.js";
2
+ export const DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS = 2;
3
+ export const DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES = 4_096;
4
+ export const DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 512;
5
+ export const HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES = 4_096;
6
+ export const TOOL_RESULT_FOLD_TURN_METADATA_KEY = "prismToolResultTurn";
7
+ /** Run overrides agent; disabled when neither supplies `summarize`. */
8
+ export function resolveToolResultFold(run, agent) {
9
+ const options = run ?? agent;
10
+ if (!options?.summarize)
11
+ return undefined;
12
+ const minAgeTurns = options.minAgeTurns ?? DEFAULT_TOOL_RESULT_FOLD_MIN_AGE_TURNS;
13
+ const minBytes = options.minBytes ?? DEFAULT_TOOL_RESULT_FOLD_MIN_BYTES;
14
+ const maxSummaryBytes = options.maxSummaryBytes ?? DEFAULT_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES;
15
+ assertPositiveInt(minAgeTurns, "minAgeTurns", 1, 1_024);
16
+ assertPositiveInt(minBytes, "minBytes", 1, 32 * 1024 * 1024);
17
+ assertPositiveInt(maxSummaryBytes, "maxSummaryBytes", 1, HARD_TOOL_RESULT_FOLD_MAX_SUMMARY_BYTES);
18
+ return { minAgeTurns, minBytes, maxSummaryBytes, summarize: options.summarize };
19
+ }
20
+ /** Projection-only fold for history tool messages; does not mutate the input array. */
21
+ export async function foldToolResultHistory(history, options, context) {
22
+ if (history.length === 0)
23
+ return history;
24
+ const turns = inferToolResultTurns(history);
25
+ const out = [];
26
+ for (let index = 0; index < history.length; index += 1) {
27
+ const message = history[index];
28
+ const folded = await foldToolResultMessage(message, options, {
29
+ ...context,
30
+ toolResultTurn: turns[index] ?? context.turn,
31
+ });
32
+ out.push(folded);
33
+ }
34
+ return out;
35
+ }
36
+ /** Projection-only fold for in-flight tool results before message conversion. */
37
+ export async function foldToolResults(results, options, context) {
38
+ if (results.length === 0)
39
+ return results;
40
+ const out = [];
41
+ for (const result of results) {
42
+ const folded = await foldToolResultValue(result, options, { ...context, toolResultTurn: context.turn });
43
+ out.push(folded);
44
+ }
45
+ return out;
46
+ }
47
+ async function foldToolResultMessage(message, options, context) {
48
+ if (message.role !== "tool")
49
+ return message;
50
+ const block = message.content.find((part) => part.type === "tool_result");
51
+ if (!block || block.type !== "tool_result")
52
+ return message;
53
+ const text = toolResultText(block.result, block.error, message.content);
54
+ const folded = await maybeFold({
55
+ options,
56
+ context,
57
+ toolCallId: block.toolCallId,
58
+ toolName: block.name,
59
+ text,
60
+ apply: (summary) => ({
61
+ ...message,
62
+ content: message.content.map((part) => part.type === "tool_result"
63
+ ? { ...part, result: foldedToolResultHeader(block.name, block.toolCallId, summary), error: undefined }
64
+ : part),
65
+ metadata: { ...message.metadata, prismFolded: true },
66
+ }),
67
+ });
68
+ return folded ?? message;
69
+ }
70
+ async function foldToolResultValue(result, options, context) {
71
+ const text = toolResultText(result.value, result.error, result.content);
72
+ const folded = await maybeFold({
73
+ options,
74
+ context,
75
+ toolCallId: result.toolCallId,
76
+ toolName: result.name,
77
+ text,
78
+ apply: (summary) => ({
79
+ ...result,
80
+ value: foldedToolResultHeader(result.name, result.toolCallId, summary),
81
+ error: undefined,
82
+ metadata: { ...result.metadata, prismFolded: true },
83
+ }),
84
+ });
85
+ return folded ?? result;
86
+ }
87
+ async function maybeFold(input) {
88
+ const age = input.context.turn - input.context.toolResultTurn;
89
+ if (age < input.options.minAgeTurns)
90
+ return undefined;
91
+ if (estimateTextBytes(input.text) < input.options.minBytes)
92
+ return undefined;
93
+ throwIfAborted(input.context.signal);
94
+ try {
95
+ const summary = await input.options.summarize({
96
+ sessionId: input.context.sessionId,
97
+ runId: input.context.runId,
98
+ turn: input.context.toolResultTurn,
99
+ toolCallId: input.toolCallId,
100
+ toolName: input.toolName,
101
+ text: input.text,
102
+ });
103
+ return input.apply(capSummaryBytes(String(summary), input.options.maxSummaryBytes));
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ export function formatFoldedToolResult(summary) {
110
+ return summary;
111
+ }
112
+ export function foldedToolResultHeader(toolName, toolCallId, summary) {
113
+ return `Tool result ${toolName} [${toolCallId}]: ${summary}`;
114
+ }
115
+ function toolResultText(result, error, extra) {
116
+ const parts = [JSON.stringify(error ?? result ?? null)];
117
+ for (const block of extra ?? []) {
118
+ if (block.type === "text" && "text" in block && typeof block.text === "string")
119
+ parts.push(block.text);
120
+ }
121
+ return parts.join("\n");
122
+ }
123
+ function capSummaryBytes(summary, maxBytes) {
124
+ const bytes = estimateTextBytes(summary);
125
+ if (bytes <= maxBytes)
126
+ return summary;
127
+ const encoded = new TextEncoder().encode(summary);
128
+ const suffix = new TextEncoder().encode("…");
129
+ let end = Math.max(0, maxBytes - suffix.length);
130
+ while (end > 0 && (encoded[end] & 0xc0) === 0x80)
131
+ end--;
132
+ return new TextDecoder().decode(encoded.slice(0, end)) + "…";
133
+ }
134
+ function inferToolResultTurns(history) {
135
+ const turns = new Array(history.length).fill(1);
136
+ let providerTurn = 0;
137
+ let toolTurn = 1;
138
+ for (let index = 0; index < history.length; index += 1) {
139
+ const message = history[index];
140
+ const stamped = readToolResultTurn(message.metadata);
141
+ if (message.role === "assistant") {
142
+ providerTurn += 1;
143
+ toolTurn = providerTurn;
144
+ }
145
+ if (message.role === "tool") {
146
+ turns[index] = stamped ?? toolTurn;
147
+ }
148
+ }
149
+ return turns;
150
+ }
151
+ function readToolResultTurn(metadata) {
152
+ const value = metadata?.[TOOL_RESULT_FOLD_TURN_METADATA_KEY];
153
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
154
+ }
155
+ function assertPositiveInt(value, name, min, max) {
156
+ if (!Number.isSafeInteger(value) || value < min || value > max) {
157
+ throw new TypeError(`toolResultFold.${name} must be a safe integer from ${min} to ${max}`);
158
+ }
159
+ }
160
+ function throwIfAborted(signal) {
161
+ if (signal?.aborted)
162
+ throw signal.reason instanceof Error ? signal.reason : new Error("Tool result fold aborted");
163
+ }
164
+ //# sourceMappingURL=tool-result-fold.js.map
@@ -1,6 +1,6 @@
1
1
  # 0.1.0 / 1.0 Readiness Gates
2
2
 
3
- Status: **0.0.19** is the current release line (Phase 2 observational memory lifecycle); **1.0** readiness remains operator-gated, not automatic.
3
+ Status: **0.0.20** is the current release line (Phase 3 skills progressive disclosure); **1.0** readiness remains operator-gated, not automatic.
4
4
 
5
5
  This page distills runnable readiness gates into one command-per-gate table. The
6
6
  **Last evidence** column records the 2026-07-26 **0.0.16** baseline snapshot
@@ -14,13 +14,13 @@ Evidence trail: [`docs/review-coverage-2026-07-26-phase-11.md`](./review-coverag
14
14
  (addenda 0–9), [`docs/release-and-install.md`](./release-and-install.md),
15
15
  [`docs/migration.md`](./migration.md), [`docs/performance.md`](./performance.md).
16
16
 
17
- ## Current line (0.0.19)
17
+ ## Current line (0.0.20)
18
18
 
19
19
  | Item | Status |
20
20
  |---|---|
21
- | Published graph | **44** publishable manifests at **0.0.19** (`docs/release-and-install.md`) |
22
- | Phase 2 observational memory | `createObservationalMemory().attach()`, four-layer context, nested settings, recall paging, hard fold/render caps |
23
- | Docs tripwires | `node --test dist/__tests__/docs.test.js` — migration section `0.0.18 → 0.0.19 observational memory lifecycle` documents intentional breaks |
21
+ | Published graph | **44** publishable manifests at **0.0.20** (`docs/release-and-install.md`) |
22
+ | Phase 3 progressive disclosure | `skillsDisclosure`, `load_skill`, empty registry default + `activateAllSkills`, priority budget demotion, optional `toolResultFold` |
23
+ | Docs tripwires | `node --test dist/__tests__/docs.test.js` — migration section `0.0.19 → 0.0.20 skills and context progressive disclosure` documents intentional breaks |
24
24
  | Readiness table below | **0.0.16 measured values** — refresh evidence columns when 1.0 RC gates are recorded |
25
25
 
26
26
  ## Gate table
@@ -51,6 +51,8 @@ string | Message | readonly Message[]
51
51
 
52
52
  `RunOptions.model` can override the request model for a run. Model overrides append a `model_change` entry. `AgentConfig.inputLayout` selects the default input assembly layout (`"cache_aware"` by default, or opt-in `"legacy"`); `RunOptions.inputLayout` wins for one run. `AgentConfig.providerOptions`/`RunOptions.providerOptions` supply generic provider request options; `timeoutMs`, `maxRetries`, and `maxRetryDelayMs` are deprecated inert provider-level hints in first-party providers. Use `RunOptions.signal`/host abort controllers for timeouts and `AgentConfig.retry`/`RunOptions.retry` for retry. `AgentConfig.providerRequestPolicies`/`RunOptions.providerRequestPolicies` run before `AIProvider.generate()` and before `provider_request` middleware. `AgentConfig.systemPrompt` and `RunOptions.systemPrompt` add explicit layered system prompt contributions; `RunOptions.systemPrompt: false` disables configured prompt layers for that run while keeping `AgentConfig.instructions` as the base path. `RunOptions.compaction` can enable auto-compaction for that run or use `false` to disable configured auto-compaction. `RunOptions.retry` can enable provider-turn retry for that run or use `false` to disable configured retry. `RunOptions.metadata` is merged with agent/session metadata for assembly, provider requests, and tool contexts. Deprecated `RunOptions.maxToolRounds` narrows `limits.maxToolRounds`. `RunOptions.signal` is bridged into the per-run abort signal passed to assembly, providers, tools, auto-compaction, and retry backoff.
53
53
 
54
+ `RunOptions.activeSkills` selects named skills from a configured `SkillRegistry`; `RunOptions.skills` replaces a plain `Skill[]` config for one run. When `AgentConfig.skills` is a registry and neither is set, **no skills activate** unless `activateAllSkills: true` (run or agent). `skillsDisclosure` (`"progressive"` default, `"eager"` opt-in; run wins) controls catalog vs full instruction bodies; the session-owned `LoadedSkillSet` is populated by `load_skill` when the host registers `createLoadSkillTool`. `toolResultFold` (off unless the host supplies `summarize`) optionally folds aged large tool results in provider input only. See [Context and skills](context-and-skills.md).
55
+
54
56
  ## Outputs / response / events
55
57
 
56
58
  `session.run()` / `session.prompt()` resolve to an `AgentRunResult` with `sessionId`, `runId`, `status`, `text`, `content`, optional `message`/`usage`/`leafId`, and terminal `error`/`abortReason` when applicable. Callers may ignore the return value. Failed and aborted runs still emit their terminal events, then reject with `AgentRunError` whose `.result` carries the same shape.
@@ -6,7 +6,7 @@
6
6
 
7
7
  ## When to use it
8
8
 
9
- Use context resolution when a host wants project/session/context blocks resolved before prompt composition. Use the skill registry when a host wants explicit progressive skill disclosure. Declarative `AgentDefinition.skills` are inactive unless listed; omitted skills means none unless the host uses the migration-only `activateAllCapabilities: true` option.
9
+ Use context resolution when a host wants project/session/context blocks resolved before prompt composition. Use the skill registry when a host wants explicit progressive skill disclosure: catalog `name` + `description` every turn by default, full `instructions` only after `load_skill` or when the host opts into eager mode. Declarative `AgentDefinition.skills` are inactive unless listed; omitted skills means none unless the host uses the migration-only `activateAllCapabilities: true` option.
10
10
 
11
11
  Do not use these helpers as an agent loop, package discovery mechanism, context cache, token budgeter, retrier, credential resolver, semantic skill ranker, tool activator, or permission system.
12
12
 
@@ -107,7 +107,8 @@ The agent/session runtime resolves skills per run and wires each active skill's
107
107
  | Surface | Config shape | Run override | Active skills |
108
108
  | --- | --- | --- | --- |
109
109
  | Runtime agent | `AgentConfig.skills: SkillRegistry` | `RunOptions.activeSkills: ["brief"]` | Named skills only, resolved with `resolveActiveSkills({ registry, names, tools })`. |
110
- | Runtime agent | `AgentConfig.skills: SkillRegistry` | no `activeSkills` / no `skills` | All registry skills (`SkillRegistry.list()`). |
110
+ | Runtime agent | `AgentConfig.skills: SkillRegistry` | no `activeSkills` / no `skills` / no `activateAllSkills` | No skills active (fail-closed default). |
111
+ | Runtime agent | `AgentConfig.skills: SkillRegistry` | `activateAllSkills: true` (run or agent) | All registry skills (`SkillRegistry.list()`), migration opt-in. |
111
112
  | Runtime agent | `AgentConfig.skills: Skill[]` | `RunOptions.skills: [...]` | Override array only. |
112
113
  | Runtime agent | `AgentConfig.skills: Skill[]` | no `RunOptions.skills` | All configured array skills. |
113
114
  | Declarative definition | `AgentDefinition.skills: ["brief"]` | later runtime `activeSkills` optional | Listed names only. |
@@ -118,13 +119,13 @@ Runtime selection precedence mirrors the other `RunOptions` overrides (`redactor
118
119
 
119
120
  1. `AgentConfig.skills` is a `SkillRegistry` and `RunOptions.activeSkills: readonly string[]` (names) is set → the runtime calls `resolveActiveSkills({ registry, names, tools })`.
120
121
  2. `RunOptions.skills: readonly Skill[]` is set → that array replaces `AgentConfig.skills` for the run. This override exists for the case where `AgentConfig.skills` is a plain `Skill[]` (no registry), so name resolution is impossible.
121
- 3. Neither set → all configured runtime skills are active (current behavior; `SkillRegistry.list()` or the plain array as-is). This is not the declarative default.
122
+ 3. Neither set → no skills active when `AgentConfig.skills` is a `SkillRegistry` (fail-closed). Use `activateAllSkills: true` on the run or agent to restore prior list-all behavior (`SkillRegistry.list()`). Plain `Skill[]` configs still activate every configured array skill. This is not the declarative default.
122
123
 
123
124
  names win when a registry exists. `RunOptions.activeSkills` cannot be used against a plain-array `AgentConfig.skills` — use `RunOptions.skills` instead. Use `RunOptions.skills: []` for an explicit no-skills runtime run.
124
125
 
125
- Each active skill contributes two things the runtime now wires together:
126
+ Each active skill contributes two things the runtime wires together:
126
127
 
127
- - `Skill.instructions` → rendered as system messages by `skillMessages()` (active set only).
128
+ - `Skill` prompt text → rendered as system messages by `skillMessages()` / `skillPromptText()` (active set only). Default `skillsDisclosure: "progressive"` sends `Skill <name>: <description>`; full `instructions` appear only when the skill is in the session `LoadedSkillSet` or disclosure is `"eager"`.
128
129
  - `Skill.context: ContextProvider[]` → collected across active skills (`activeSkills.flatMap(s => s.context ?? [])`), resolved through the existing `resolveContextProviders(...)`, and merged into the request's `context` **after** host `AgentConfig.context` blocks. Inactive skills contribute neither instructions nor context.
129
130
 
130
131
  `toolNames` enforcement is live: because selection routes through `resolveActiveSkills()`, a skill demanding a host-inactive tool throws with `Skill ${name} requires inactive tool: ${missing}` **before the first provider turn** — no provider call, no store write, no partial side effect. This is the fail-fast contract the docs already claimed; the runtime now honors it.
@@ -153,7 +154,69 @@ await session.run(input, { skills: [{ name: "verbose", instructions: "Be verbose
153
154
  await session.run(input, { skills: [] }); // explicit no skills for this run
154
155
  ```
155
156
 
156
- Skill selection grants no tool access and cannot bypass permissions — a skill's `toolNames` can only *require* host-active tools, never activate or grant them. Declarative skills also do not activate themselves by presence in a registry; list names on `AgentDefinition.skills` (or pass runtime `activeSkills`) when wanted. Per-skill token budgeting is deferred; the merge order (host context, then skill context) is the only priority knob today.
157
+ Skill selection grants no tool access and cannot bypass permissions — a skill's `toolNames` can only *require* host-active tools, never activate or grant them. Declarative skills also do not activate themselves by presence in a registry; list names on `AgentDefinition.skills` (or pass runtime `activeSkills`) when wanted.
158
+
159
+ ### Progressive skill disclosure
160
+
161
+ `skillsDisclosure` on `AgentConfig` / `RunOptions` (`"progressive"` default, `"eager"` opt-in; run wins) controls how active skills render in provider input:
162
+
163
+ | Mode | Provider view per active skill |
164
+ | --- | --- |
165
+ | `"progressive"` (default) | `Skill <name>: <description>` (or `(no description)` when empty) |
166
+ | `"eager"` | `Skill <name>:\n<instructions>` every turn (pre-0.0.20 behavior) |
167
+
168
+ Catalog caps: **64** entries default / **256** hard; descriptions **512 B** default / **4 KiB** hard; instruction bodies **32 KiB** default / **256 KiB** hard on load/eager render. Oversize catalog/description/instruction payloads fail closed (`SkillDisclosureError` / `SkillLoadError`).
169
+
170
+ ```ts
171
+ import { assembleProviderInput, createLoadedSkillSet } from "@arnilo/prism";
172
+
173
+ const loaded = createLoadedSkillSet(); // session-owned; not checkpoint-persisted in 0.0.20
174
+ const request = await assembleProviderInput({
175
+ model,
176
+ input: "Hi",
177
+ skills: active,
178
+ skillsDisclosure: "progressive",
179
+ loadedSkills: loaded,
180
+ });
181
+ ```
182
+
183
+ ### On-demand skill load (`load_skill`)
184
+
185
+ Hosts opt in by registering `createLoadSkillTool({ registry, loaded })` on the active tool set. The model calls `load_skill { name }` with an exact registry name; success adds the name to the session `LoadedSkillSet` so later turns include `instructions` under progressive mode. The tool does **not** activate tools, widen permissions, or load skills that were not active for the run.
186
+
187
+ Fail-closed cases: unknown name, inactive skill for the run, inactive required `toolNames`, oversize body, duplicate load, missing session loaded-set wiring. Tool output and errors are size-capped; skill text is untrusted host/extension data.
188
+
189
+ ```ts
190
+ import { createAgent, createLoadSkillTool, createSkillRegistry } from "@arnilo/prism";
191
+
192
+ const registry = createSkillRegistry([ponytail, brief]);
193
+ const loadSkill = createLoadSkillTool({ registry }); // session injects loadedSkills at dispatch
194
+ const agent = createAgent({ model, provider, skills: registry, tools: [loadSkill, /* host */] });
195
+ await agent.createSession().run("…", { activeSkills: ["ponytail"] });
196
+ // Turn 1: catalog only. After load_skill({ name: "ponytail" }), later turns include instructions.
197
+ ```
198
+
199
+ Pure validation without the tool: `resolveSkillLoad({ registry, name, tools, loaded, activeSkillNames })`.
200
+
201
+ ### Context budget priority and skill demotion
202
+
203
+ When `assembleProviderInput` runs with `contextBudget`, `applyContextBudget` evicts droppable sections in layout order. Within `context` blocks and skills, victims sort by ascending `ContextBlock.priority` (missing = **0**), then LIFO within the same priority.
204
+
205
+ Under pressure on a skill with a loaded body, eviction may demote to catalog-only first (`ContextBudgetOmissionKind: "skill_body"`), then remove the skill entirely (`"skills"`). Demoted bodies render as description-only even when the name remains in `LoadedSkillSet`. See [Input and prompt assembly](input-and-prompt-assembly.md).
206
+
207
+ ### Optional tool-result fold
208
+
209
+ `toolResultFold` on `AgentConfig` / `RunOptions` (run wins) is **off** unless the host supplies a `summarize` callback. When enabled, aged large tool-result messages in the **provider view** become a one-line header plus bounded summary text; session store entries stay raw. Defaults: `minAgeTurns` **2**, `minBytes` **4096**, `maxSummaryBytes` **512** (hard **4096**). Summarizer failure keeps the raw tool result (fail closed). Not a second memory system — use observational memory / compaction for durable recall.
210
+
211
+ ```ts
212
+ await session.run("…", {
213
+ toolResultFold: {
214
+ minAgeTurns: 2,
215
+ minBytes: 4_096,
216
+ summarize: async ({ toolCallId, text }) => `ref:${toolCallId} ${text.slice(0, 80)}`,
217
+ },
218
+ });
219
+ ```
157
220
 
158
221
  ### Migration note
159
222
 
@@ -164,12 +227,25 @@ For declarative agents, old configs that omitted `skills` should now add explici
164
227
  resolveAgentDefinition({ name: "doc", model, skills: ["brief"] }, context);
165
228
  ```
166
229
 
167
- Use `activateAllCapabilities: true` only as a temporary all-skills/all-tools compatibility opt-in during migration. Runtime `RunOptions.activeSkills` remains the per-run narrowing tool after an agent has a skill registry configured.
230
+ Runtime hosts that relied on `SkillRegistry.list()` when `activeSkills` was omitted must opt in explicitly:
231
+
232
+ ```ts
233
+ // Restore pre-0.0.20 list-all activation (still subject to progressive disclosure):
234
+ await session.run("Hi", { activateAllSkills: true });
235
+
236
+ // Or restore full instruction bodies every turn:
237
+ const agent = createAgent({ model, provider, skills: registry, skillsDisclosure: "eager" });
238
+ ```
239
+
240
+ Use `activateAllCapabilities: true` only as a temporary all-skills/all-tools compatibility opt-in during migration for **declarative** definitions. Runtime `RunOptions.activeSkills` remains the per-run narrowing tool after an agent has a skill registry configured.
168
241
 
169
242
  ## Security and performance notes
170
243
 
171
244
  - Context providers run sequentially and deterministically in caller order.
172
245
  - Skill registry lookup is `Map`-backed, and selection is linear in requested skills plus active tools. Strict duplicate mode adds one O(1) `Map.has()` check during registration only.
246
+ - Progressive catalog render is O(active skills) with byte/count caps; `load_skill` lookup is O(1). Budget eviction over context/skills is O(n log n) worst case.
247
+ - `load_skill` cannot grant tools; loaded instructions are untrusted text bounded by hard caps. `toolResultFold` summarizer output is untrusted and capped; failures keep raw tool results.
248
+ - Loaded-skill names are session-scoped in memory only in 0.0.20 — not checkpoint-persisted; new sessions start catalog-only until reload.
173
249
  - These helpers perform no provider calls, tool execution, resource loading, package discovery, filesystem/network access, retries, timers, or watchers by themselves.
174
250
  - Context and skill output is host/extension data. Do not include secrets unless the host explicitly accepts that prompt exposure.
175
251
  - Active tools remain host-supplied; skills and middleware do not activate tools or grant permissions. Use `duplicate: "error"` when loading third-party skills to prevent silent name shadowing.
package/docs/index.md CHANGED
@@ -34,7 +34,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
34
34
  - [Database persistence](database-persistence.md): production persistence contracts, shared checksummed migration/full-shape catalog primitives (`@arnilo/prism/testing/persistence-schema`), conditional append, indexes, `readBranchPath`, reference relational schema, retention/legal-hold/quota lifecycle (`lifecycle`), and NoSQL mapping.
35
35
  - [SQLite persistence](sqlite-persistence.md): optional `better-sqlite3` adapter with session/run storage, checkpoints/leases, feedback, FTS `searchSessions` (migration-v4), and transactionally verified/backfilled migration metadata.
36
36
  - [PostgreSQL persistence](postgres-persistence.md): optional pooled `pg` adapter with session/run/checkpoint/lease/feedback storage, FTS `searchSessions` (migration-v4), advisory-locked checksummed/full-shape migrations, and opt-in live conformance.
37
- - [Migration guide](migration.md): **0.0.15** OpenAI hosted tools/continuation/Realtime, exact AI SDK v4 matrix, RAG lifecycle/reranking/trust/status, and memory export/rebuild; **0.0.14** conversations, memory consent/lifecycle, artifact co-work review, AG-UI co-work events, scoped M365/GWS OAuth connectors, browser checkpoints, device contracts, and Alibaba/Ollama providers; plus prior release migrations.
37
+ - [Migration guide](migration.md): **0.0.20** progressive skill disclosure, empty registry default, `load_skill`, priority budget demotion, optional tool-result fold; **0.0.19** observational memory lifecycle; **0.0.15** OpenAI hosted tools/continuation/Realtime, exact AI SDK v4 matrix, RAG lifecycle/reranking/trust/status, and memory export/rebuild; **0.0.14** conversations, memory consent/lifecycle, artifact co-work review, AG-UI co-work events, scoped M365/GWS OAuth connectors, browser checkpoints, device contracts, and Alibaba/Ollama providers; plus prior release migrations.
38
38
  - [Node JSONL session store](node-jsonl-session-store.md): development-only JSONL file adapter for single-process Node hosts; no cross-process safety; `searchSessions` throws `SessionSearchUnsupportedError`.
39
39
  - [Persistence, credentials, and multimodality primitives](persistence-credentials-multimodality-primitives.md): Plan 056 inventory — session/run-ledger/persistence contracts, credential/OAuth seams, content/resource/model capabilities, package dependency matrix, conformance matrix, and threat model for production adapters.
40
40
 
@@ -58,7 +58,7 @@ Prism is a TypeScript/Node.js agent harness. Host apps and extension packages ow
58
58
  - [Multimodal content](multimodal-content.md): complete-request media resolution and aggregate bounds, DNS-classified/address-pinned URLs, SSRF/MIME policy, `ModelCapabilities.input` tags, and first-party content-type mapping.
59
59
  - [System prompts](system-prompts.md): compose explicit user/package/app/run system prompt layers, auto-load the standard `AGENTS.md` (workspace) / `SYSTEM.md` prompt files via the Node `loadSystemPromptFiles` loader (trust-gated for `AGENTS.md`), and append `SYSTEM.md` → per-agent `AGENT.md` body → repo `AGENTS.md` layers from a discovered agent bundle via `resolveAgentBundle`.
60
60
  - [Instruction injection](instruction-injection.md): register package injectors that layer redacted instructions/context blocks without granting tools, permissions, or resource escapes.
61
- - [Context and skills](context-and-skills.md): resolve ordered context providers and keep context/skill selection host-owned; omitted declarative skills stay inactive by default, `toolNames` fail closed before provider turns, and strict skill registries prevent silent shadowing.
61
+ - [Context and skills](context-and-skills.md): resolve ordered context providers; progressive skill catalog (`skillsDisclosure`, default catalog-only), `load_skill` on-demand bodies, fail-closed registry activation (`activateAllSkills` migration opt-in), `toolNames` fail closed before provider turns, priority-aware budget demotion, and optional `toolResultFold`.
62
62
  - [Retrieval-augmented generation](rag.md): optional bounded source lifecycle, document adapters, host reranking, ingestion status, attributable citations, and inert context injection.
63
63
 
64
64
  ## Tools
package/docs/migration.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Migration guide
2
2
 
3
+ ## 0.0.19 → 0.0.20 skills and context progressive disclosure (small intentional breaks)
4
+
5
+ Release **0.0.20** completes Phase 3 progressive skill disclosure in core `@arnilo/prism`:
6
+
7
+ 1. **Default skill prompt is catalog-only.** Active skills render `Skill <name>: <description>` every turn (`skillsDisclosure: "progressive"` default). Full `instructions` appear only after a successful `load_skill` for that session or when the host sets `skillsDisclosure: "eager"`.
8
+ 2. **Runtime `SkillRegistry` without activation is empty.** When `AgentConfig.skills` is a `SkillRegistry` and neither `RunOptions.activeSkills` nor `RunOptions.skills` is set, **zero** skills activate (was `SkillRegistry.list()`). Migration: `activateAllSkills: true` on the run or agent restores list-all activation (still subject to disclosure rules). Plain `Skill[]` configs are unchanged.
9
+ 3. **`load_skill` is host-opt-in.** Export `createLoadSkillTool({ registry, loaded })` from `@arnilo/prism`; register on the active tool set. Unknown names, inactive required tools, oversize bodies, and duplicate loads fail closed; load cannot widen tools or permissions.
10
+ 4. **Context budget honors `ContextBlock.priority`.** Within `context` and `skills` victims, lower priority drops first (missing = 0), then LIFO. Skills with loaded bodies may demote to description-only (`skill_body` omission) before full removal.
11
+ 5. **Optional `toolResultFold`.** Off by default; host `summarize` + thresholds fold aged large tool results in provider view only (session store untouched). Summarizer failure keeps raw results.
12
+
13
+ Example: `node examples/skills-progressive-disclosure.ts` (network-free).
14
+
15
+ ```ts
16
+ // Migration for hosts that relied on activate-all registry behavior:
17
+ await session.run("Hi", { activateAllSkills: true });
18
+
19
+ // Migration for hosts that want full bodies every turn without load_skill:
20
+ const agent = createAgent({ model, provider, skills: registry, skillsDisclosure: "eager" });
21
+ ```
22
+
23
+ Declarative `activateAllCapabilities: true` is unchanged and does **not** set runtime `activateAllSkills`.
24
+
3
25
  ## 0.0.18 → 0.0.19 observational memory lifecycle (small intentional breaks)
4
26
 
5
27
  Release **0.0.19** completes Phase 2 observational memory in `@arnilo/prism-compaction-observational-memory` only; core `@arnilo/prism` runtime behavior is unchanged.