lemura 1.6.0 → 1.7.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 CHANGED
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.7.0] - 2026-06-13
9
+
10
+ ### Added
11
+
12
+ - **Progressive skills — model-driven skill selection** (`strategy: 'progressive'`): A third skill strategy alongside `fixed` and `dynamic`. The model sees a lightweight catalog (`name: description`) of progressive skills in its system prompt and decides which to pull in by calling a built-in `load_skill` tool; only the chosen skill's full content is then injected. This is the "progressive disclosure" pattern — the host wires nothing, the agent selects its own skills. Fully backward-compatible (default strategy remains `fixed`).
13
+ - **Built-in `load_skill` tool** (`src/tools/builtin/load_skill.ts`, `createLoadSkillTool`): Auto-registered by `SessionManager` whenever a `progressive` skill is present, and auto-trusted by the tool firewall (it only toggles skill injection, no external side effects). The `name` parameter is constrained to the available progressive skills; unknown names and `maxConcurrent` violations return soft errors that keep the ReAct loop going.
14
+ - **`SkillSelectionConfig`** (`SessionConfig.skillSelection`): Tunes progressive selection — `persistence` (`'per_turn'` default, or `'session'`), `maxConcurrent` (cap on simultaneously loaded skills), and `catalogHeader` (override the catalog preamble). Ignored when no progressive skills are present.
15
+ - **`SkillInjector.buildCatalog(header?)`**: Builds the `name: description` catalog block for progressive skills. Plus `resetProgressiveSkills()`, `getProgressiveSkills()`, and `countEnabledProgressive()` helpers. `enableSkill`/`disableSkill`/`enableByTags`/`disableByTags` now operate on both `dynamic` and `progressive` skills.
16
+ - **Full skill-lifecycle tracing**: `skill_load` now fires for **every registered skill** (not just active ones) and carries an `enabled` flag, so inactive `dynamic`/`progressive` skills are visible from session init. New `skill_enable` trace (`{ name, source: 'load_skill' }`) records the model's selection decision when it loads a progressive skill, and new `skill_reset` trace (`{ skills, reason: 'per_turn' }`) records per-turn clearing. `session_init` reports a `progressive` skill count; `skill_inject` fires for both dynamic and progressive injections.
17
+ - **Tests**: `tests/unit/skills/ProgressiveSkills.test.ts` (catalog, activation, `load_skill` tool, `maxConcurrent`) and new `SessionManager` cases (auto-registration, per-turn vs session persistence).
18
+
8
19
  ## [1.6.0] - 2026-06-05
9
20
 
10
21
  ### Added
package/README.md CHANGED
@@ -25,6 +25,7 @@
25
25
  ### ✨ Key Features
26
26
 
27
27
  - **🧠 Dynamic Skill Market**: Switch skills on/off at runtime via tags, names, or tool dependencies.
28
+ - **📖 Progressive Skills**: Let the agent pick its own skills — it reads a catalog and loads only what's relevant via the built-in `load_skill` tool (`strategy: 'progressive'`).
28
29
  - **🔌 Native MCP Support**: Connect to any Model Context Protocol server with custom header support (Auth).
29
30
  - **🛡️ Tool Firewall**: Fully integrated ask/accept/deny policy layer for secure tool execution — fail-safe by design.
30
31
  - **🎯 Goal Maintenance & Verification**: LLM-powered sub-goal decomposition, progress reconciliation, and post-run goal verification that re-enters the loop with full tool access to finish incomplete answers.
@@ -1,6 +1,6 @@
1
1
  import { I as IToolDefinition, a as IProviderAdapter, b as IContextStrategy, S as ShortTermMemoryRegistry, c as IScratchpadAdapter, T as Turn } from './adapters-CnsgefYR.js';
2
2
  import { I as ILogger } from './logger-DxvKliuk.js';
3
- import { I as ISkill } from './skills-Y6D7zSSw.js';
3
+ import { I as ISkill, S as SkillSelectionConfig } from './skills-DWCC0K1B.js';
4
4
  import { I as IRAGAdapter } from './rag-La_Bo-J8.js';
5
5
 
6
6
  /**
@@ -323,6 +323,17 @@ interface SessionConfig {
323
323
  * @since 1.4.0
324
324
  */
325
325
  activeDynamicTags?: string[];
326
+ /**
327
+ * Configuration for model-driven (`progressive`) skill selection. When any
328
+ * skill in `skills` has `strategy: 'progressive'`, Lemura automatically appends
329
+ * a skill catalog to the system prompt, registers the built-in `load_skill`
330
+ * tool (auto-trusted by the firewall), and resets progressive skills according
331
+ * to `persistence`. This object tunes that behaviour; it is ignored when no
332
+ * progressive skills are present.
333
+ *
334
+ * @since 1.7.0
335
+ */
336
+ skillSelection?: SkillSelectionConfig;
326
337
  /** RAG adapter */
327
338
  ragAdapter?: IRAGAdapter;
328
339
  /** Context compression strategies */
@@ -1,6 +1,6 @@
1
1
  import { I as IToolDefinition, a as IProviderAdapter, b as IContextStrategy, S as ShortTermMemoryRegistry, c as IScratchpadAdapter, T as Turn } from './adapters-BN6und82.mjs';
2
2
  import { I as ILogger } from './logger-DxvKliuk.mjs';
3
- import { I as ISkill } from './skills-Y6D7zSSw.mjs';
3
+ import { I as ISkill, S as SkillSelectionConfig } from './skills-DWCC0K1B.mjs';
4
4
  import { I as IRAGAdapter } from './rag-La_Bo-J8.mjs';
5
5
 
6
6
  /**
@@ -323,6 +323,17 @@ interface SessionConfig {
323
323
  * @since 1.4.0
324
324
  */
325
325
  activeDynamicTags?: string[];
326
+ /**
327
+ * Configuration for model-driven (`progressive`) skill selection. When any
328
+ * skill in `skills` has `strategy: 'progressive'`, Lemura automatically appends
329
+ * a skill catalog to the system prompt, registers the built-in `load_skill`
330
+ * tool (auto-trusted by the firewall), and resets progressive skills according
331
+ * to `persistence`. This object tunes that behaviour; it is ignored when no
332
+ * progressive skills are present.
333
+ *
334
+ * @since 1.7.0
335
+ */
336
+ skillSelection?: SkillSelectionConfig;
326
337
  /** RAG adapter */
327
338
  ragAdapter?: IRAGAdapter;
328
339
  /** Context compression strategies */
package/dist/index.d.mts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { a as IProviderAdapter, d as TranscriptionRequest, e as TranscriptionResponse, f as SynthesisRequest, A as AudioChunk, V as VisionRequest, g as VisionResponse, h as ImageGenRequest, i as ImageGenResponse, M as ModelInfo, C as ContextWindow, T as Turn, j as ContentBlock, I as IToolDefinition } from './adapters-BN6und82.mjs';
2
2
  export { k as CompletionChunk, l as CompletionRequest, m as CompletionResponse, b as IContextStrategy, c as IScratchpadAdapter, n as IStorageAdapter, N as NormalizedMessage, o as STMItem, p as STMRegistryConfig, S as ShortTermMemoryRegistry, q as TokenUsage, r as ToolCall, s as ToolContext, t as ToolResult } from './adapters-BN6und82.mjs';
3
- import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, a as IRouterAdapter, b as ToolCategoryInfo, R as RouterDecision, M as MCPServerConfig, c as MCPToolDefinition } from './agent-B2okLlzq.mjs';
4
- export { d as GoalInjector, e as GoalVerifierResult, f as MCPJsonRpcRequest, g as MCPJsonRpcResponse, h as MCPTransportType, i as MediaConfig, j as ToolDecision, k as ToolExecutionBudget, l as ToolFirewallConfig, m as ToolFirewallRule, n as TraceEvent } from './agent-B2okLlzq.mjs';
3
+ import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, a as IRouterAdapter, b as ToolCategoryInfo, R as RouterDecision, M as MCPServerConfig, c as MCPToolDefinition } from './agent-Dus44yQd.mjs';
4
+ export { d as GoalInjector, e as GoalVerifierResult, f as MCPJsonRpcRequest, g as MCPJsonRpcResponse, h as MCPTransportType, i as MediaConfig, j as ToolDecision, k as ToolExecutionBudget, l as ToolFirewallConfig, m as ToolFirewallRule, n as TraceEvent } from './agent-Dus44yQd.mjs';
5
5
  export { LemuraAdapterError, LemuraContextOverflowError, LemuraError, LemuraMCPConnectionError, LemuraMCPError, LemuraMCPTimeoutError, LemuraMaxIterationsError, LemuraSkillInjectionError, LemuraToolNotFoundError, LemuraToolTimeoutError, LemuraToolValidationError } from './types/index.mjs';
6
6
  import { I as ILogger } from './logger-DxvKliuk.mjs';
7
7
  export { L as LogLevel, a as LogMetadata, S as Severity } from './logger-DxvKliuk.mjs';
8
8
  export { I as IRAGAdapter, R as RAGDocument, a as RAGIngestOptions, b as RAGIngestRequest, c as RAGIngestResponse, d as RAGQueryRequest, e as RAGQueryResponse, f as RAGResult } from './rag-La_Bo-J8.mjs';
9
- export { I as ISkill, S as SkillStrategy } from './skills-Y6D7zSSw.mjs';
9
+ export { I as ISkill, S as SkillSelectionConfig, a as SkillStrategy } from './skills-DWCC0K1B.mjs';
10
10
  export { OpenAICompatibleAdapter, OpenAICompatibleAdapterConfig, RetryConfig } from './adapters/index.mjs';
11
11
  export { ContextManager, HistoryCompressionConfig, HistoryCompressionStrategy, InMemoryScratchpadAdapter, InMemoryStorageAdapter, SandwichCompressionConfig, SandwichCompressionStrategy, ScratchpadStrategy, SummaryInjectionConfig, SummaryInjectionStrategy } from './context/index.mjs';
12
12
  import { ToolRegistry } from './tools/index.mjs';
@@ -213,6 +213,8 @@ declare class SessionManager {
213
213
  private contextManager;
214
214
  private toolRegistry;
215
215
  private skillInjector;
216
+ /** True when any registered skill uses `strategy: 'progressive'`. */
217
+ private hasProgressiveSkills;
216
218
  private context;
217
219
  private adapter;
218
220
  private config;
@@ -428,6 +430,13 @@ declare class SessionManager {
428
430
  * @throws {LemuraMaxIterationsError} When the loop exceeds `maxIterations`
429
431
  */
430
432
  run(userMessage: string | ContentBlock[]): Promise<string>;
433
+ /**
434
+ * Resets progressive skills at the start of a turn when
435
+ * `skillSelection.persistence` is `'per_turn'` (the default), so each user
436
+ * message re-decides which skills to load from the catalog. No-op when
437
+ * persistence is `'session'` or there are no progressive skills.
438
+ */
439
+ private _resetProgressiveSkillsForTurn;
431
440
  /**
432
441
  * Runs the ReAct loop and streams the final assistant response token-by-token.
433
442
  *
package/dist/index.d.ts CHANGED
@@ -1,12 +1,12 @@
1
1
  import { a as IProviderAdapter, d as TranscriptionRequest, e as TranscriptionResponse, f as SynthesisRequest, A as AudioChunk, V as VisionRequest, g as VisionResponse, h as ImageGenRequest, i as ImageGenResponse, M as ModelInfo, C as ContextWindow, T as Turn, j as ContentBlock, I as IToolDefinition } from './adapters-CnsgefYR.js';
2
2
  export { k as CompletionChunk, l as CompletionRequest, m as CompletionResponse, b as IContextStrategy, c as IScratchpadAdapter, n as IStorageAdapter, N as NormalizedMessage, o as STMItem, p as STMRegistryConfig, S as ShortTermMemoryRegistry, q as TokenUsage, r as ToolCall, s as ToolContext, t as ToolResult } from './adapters-CnsgefYR.js';
3
- import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, a as IRouterAdapter, b as ToolCategoryInfo, R as RouterDecision, M as MCPServerConfig, c as MCPToolDefinition } from './agent-BppqfsIZ.js';
4
- export { d as GoalInjector, e as GoalVerifierResult, f as MCPJsonRpcRequest, g as MCPJsonRpcResponse, h as MCPTransportType, i as MediaConfig, j as ToolDecision, k as ToolExecutionBudget, l as ToolFirewallConfig, m as ToolFirewallRule, n as TraceEvent } from './agent-BppqfsIZ.js';
3
+ import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, a as IRouterAdapter, b as ToolCategoryInfo, R as RouterDecision, M as MCPServerConfig, c as MCPToolDefinition } from './agent-BcAg3gCz.js';
4
+ export { d as GoalInjector, e as GoalVerifierResult, f as MCPJsonRpcRequest, g as MCPJsonRpcResponse, h as MCPTransportType, i as MediaConfig, j as ToolDecision, k as ToolExecutionBudget, l as ToolFirewallConfig, m as ToolFirewallRule, n as TraceEvent } from './agent-BcAg3gCz.js';
5
5
  export { LemuraAdapterError, LemuraContextOverflowError, LemuraError, LemuraMCPConnectionError, LemuraMCPError, LemuraMCPTimeoutError, LemuraMaxIterationsError, LemuraSkillInjectionError, LemuraToolNotFoundError, LemuraToolTimeoutError, LemuraToolValidationError } from './types/index.js';
6
6
  import { I as ILogger } from './logger-DxvKliuk.js';
7
7
  export { L as LogLevel, a as LogMetadata, S as Severity } from './logger-DxvKliuk.js';
8
8
  export { I as IRAGAdapter, R as RAGDocument, a as RAGIngestOptions, b as RAGIngestRequest, c as RAGIngestResponse, d as RAGQueryRequest, e as RAGQueryResponse, f as RAGResult } from './rag-La_Bo-J8.js';
9
- export { I as ISkill, S as SkillStrategy } from './skills-Y6D7zSSw.js';
9
+ export { I as ISkill, S as SkillSelectionConfig, a as SkillStrategy } from './skills-DWCC0K1B.js';
10
10
  export { OpenAICompatibleAdapter, OpenAICompatibleAdapterConfig, RetryConfig } from './adapters/index.js';
11
11
  export { ContextManager, HistoryCompressionConfig, HistoryCompressionStrategy, InMemoryScratchpadAdapter, InMemoryStorageAdapter, SandwichCompressionConfig, SandwichCompressionStrategy, ScratchpadStrategy, SummaryInjectionConfig, SummaryInjectionStrategy } from './context/index.js';
12
12
  import { ToolRegistry } from './tools/index.js';
@@ -213,6 +213,8 @@ declare class SessionManager {
213
213
  private contextManager;
214
214
  private toolRegistry;
215
215
  private skillInjector;
216
+ /** True when any registered skill uses `strategy: 'progressive'`. */
217
+ private hasProgressiveSkills;
216
218
  private context;
217
219
  private adapter;
218
220
  private config;
@@ -428,6 +430,13 @@ declare class SessionManager {
428
430
  * @throws {LemuraMaxIterationsError} When the loop exceeds `maxIterations`
429
431
  */
430
432
  run(userMessage: string | ContentBlock[]): Promise<string>;
433
+ /**
434
+ * Resets progressive skills at the start of a turn when
435
+ * `skillSelection.persistence` is `'per_turn'` (the default), so each user
436
+ * message re-decides which skills to load from the catalog. No-op when
437
+ * persistence is `'session'` or there are no progressive skills.
438
+ */
439
+ private _resetProgressiveSkillsForTurn;
431
440
  /**
432
441
  * Runs the ReAct loop and streams the final assistant response token-by-token.
433
442
  *
package/dist/index.js CHANGED
@@ -1183,7 +1183,7 @@ var MediaBridge = class {
1183
1183
  };
1184
1184
 
1185
1185
  // src/skills/SkillInjector.ts
1186
- var SkillInjector = class {
1186
+ var SkillInjector = class _SkillInjector {
1187
1187
  skills = [];
1188
1188
  constructor(skills = []) {
1189
1189
  this.skills = skills.map((s) => this._normalise(s));
@@ -1199,14 +1199,22 @@ var SkillInjector = class {
1199
1199
  sortSkills() {
1200
1200
  this.skills.sort((a, b) => a.priority - b.priority);
1201
1201
  }
1202
+ /**
1203
+ * Strategies whose skills form an opt-in pool — inactive until explicitly
1204
+ * enabled. Both `dynamic` (host-enabled) and `progressive` (model-enabled via
1205
+ * the `load_skill` tool) behave this way.
1206
+ */
1207
+ _isPoolStrategy(strategy) {
1208
+ return strategy === "dynamic" || strategy === "progressive";
1209
+ }
1202
1210
  /**
1203
1211
  * Normalises a skill to ensure consistent defaults.
1204
- * For dynamic skills, `enabled` defaults to `false` unless explicitly set.
1205
- * For fixed skills (or those without a strategy), `enabled` is ignored.
1212
+ * For pool skills (`dynamic` / `progressive`), `enabled` defaults to `false`
1213
+ * unless explicitly set. For `fixed` skills, `enabled` is ignored.
1206
1214
  */
1207
1215
  _normalise(skill) {
1208
1216
  const strategy = skill.strategy ?? "fixed";
1209
- const enabled = strategy === "dynamic" ? skill.enabled ?? false : true;
1217
+ const enabled = this._isPoolStrategy(strategy) ? skill.enabled ?? false : true;
1210
1218
  return { ...skill, strategy, enabled };
1211
1219
  }
1212
1220
  // -----------------------------------------------------------------------
@@ -1218,42 +1226,76 @@ var SkillInjector = class {
1218
1226
  */
1219
1227
  enableSkill(name) {
1220
1228
  const skill = this.skills.find((s) => s.name === name);
1221
- if (skill && skill.strategy === "dynamic") {
1229
+ if (skill && this._isPoolStrategy(skill.strategy)) {
1222
1230
  skill.enabled = true;
1223
1231
  }
1224
1232
  }
1225
1233
  /**
1226
- * Disable a dynamic skill by name.
1234
+ * Disable a pool skill (`dynamic` or `progressive`) by name.
1227
1235
  * Has no effect on fixed skills.
1228
1236
  */
1229
1237
  disableSkill(name) {
1230
1238
  const skill = this.skills.find((s) => s.name === name);
1231
- if (skill && skill.strategy === "dynamic") {
1239
+ if (skill && this._isPoolStrategy(skill.strategy)) {
1232
1240
  skill.enabled = false;
1233
1241
  }
1234
1242
  }
1235
1243
  /**
1236
- * Enable all dynamic skills whose `tags` array intersects with `tags`.
1244
+ * Enable all pool skills (`dynamic` / `progressive`) whose `tags` array
1245
+ * intersects with `tags`.
1237
1246
  */
1238
1247
  enableByTags(tags) {
1239
1248
  const tagSet = new Set(tags);
1240
1249
  for (const skill of this.skills) {
1241
- if (skill.strategy === "dynamic" && skill.tags?.some((t) => tagSet.has(t))) {
1250
+ if (this._isPoolStrategy(skill.strategy) && skill.tags?.some((t) => tagSet.has(t))) {
1242
1251
  skill.enabled = true;
1243
1252
  }
1244
1253
  }
1245
1254
  }
1246
1255
  /**
1247
- * Disable all dynamic skills whose `tags` array intersects with `tags`.
1256
+ * Disable all pool skills (`dynamic` / `progressive`) whose `tags` array
1257
+ * intersects with `tags`.
1248
1258
  */
1249
1259
  disableByTags(tags) {
1250
1260
  const tagSet = new Set(tags);
1251
1261
  for (const skill of this.skills) {
1252
- if (skill.strategy === "dynamic" && skill.tags?.some((t) => tagSet.has(t))) {
1262
+ if (this._isPoolStrategy(skill.strategy) && skill.tags?.some((t) => tagSet.has(t))) {
1263
+ skill.enabled = false;
1264
+ }
1265
+ }
1266
+ }
1267
+ /**
1268
+ * Disable every progressive skill. Called by SessionManager between turns when
1269
+ * `skillSelection.persistence` is `'per_turn'` so each turn re-decides from the
1270
+ * catalog. Has no effect on `fixed` or `dynamic` skills.
1271
+ *
1272
+ * @since 1.7.0
1273
+ */
1274
+ resetProgressiveSkills() {
1275
+ for (const skill of this.skills) {
1276
+ if (skill.strategy === "progressive") {
1253
1277
  skill.enabled = false;
1254
1278
  }
1255
1279
  }
1256
1280
  }
1281
+ /**
1282
+ * Returns all progressive skills, regardless of enabled state. Used to detect
1283
+ * whether the catalog / `load_skill` machinery should be activated.
1284
+ *
1285
+ * @since 1.7.0
1286
+ */
1287
+ getProgressiveSkills() {
1288
+ return this.skills.filter((s) => s.strategy === "progressive");
1289
+ }
1290
+ /**
1291
+ * Number of currently-enabled progressive skills. Used to enforce
1292
+ * `skillSelection.maxConcurrent`.
1293
+ *
1294
+ * @since 1.7.0
1295
+ */
1296
+ countEnabledProgressive() {
1297
+ return this.skills.filter((s) => s.strategy === "progressive" && s.enabled === true).length;
1298
+ }
1257
1299
  // -----------------------------------------------------------------------
1258
1300
  // Queries
1259
1301
  // -----------------------------------------------------------------------
@@ -1292,8 +1334,45 @@ var SkillInjector = class {
1292
1334
  return [...tools];
1293
1335
  }
1294
1336
  _isActive(skill) {
1295
- return skill.strategy !== "dynamic" || skill.enabled === true;
1337
+ return !this._isPoolStrategy(skill.strategy) || skill.enabled === true;
1338
+ }
1339
+ // -----------------------------------------------------------------------
1340
+ // Catalog builder (progressive skills)
1341
+ // -----------------------------------------------------------------------
1342
+ /**
1343
+ * Builds a compact `name: description` catalog of all progressive skills, with
1344
+ * an instructional preamble telling the agent to call `load_skill` for relevant
1345
+ * entries. SessionManager appends this to the system prompt so the model can
1346
+ * decide which skills to pull in — full content is injected only on `load_skill`.
1347
+ *
1348
+ * Returns an empty string when there are no progressive skills (the catalog and
1349
+ * `load_skill` tool are then never activated).
1350
+ *
1351
+ * @param header - Optional override for the instructional preamble.
1352
+ * @returns The catalog block, or `''` when no progressive skills exist.
1353
+ *
1354
+ * @since 1.7.0
1355
+ *
1356
+ * @example
1357
+ * ```typescript
1358
+ * const catalog = injector.buildCatalog();
1359
+ * // You have access to specialized skills...
1360
+ * // Available skills:
1361
+ * // - summarize: Condenses text or documents concisely.
1362
+ * ```
1363
+ */
1364
+ buildCatalog(header) {
1365
+ const progressive = this.getProgressiveSkills();
1366
+ if (progressive.length === 0) return "";
1367
+ const preamble = header ?? _SkillInjector.DEFAULT_CATALOG_HEADER;
1368
+ const lines = progressive.map((s) => `- ${s.name}: ${s.description}`);
1369
+ return `${preamble}
1370
+
1371
+ Available skills:
1372
+ ${lines.join("\n")}`;
1296
1373
  }
1374
+ /** Default instructional preamble for the progressive-skill catalog. */
1375
+ static DEFAULT_CATALOG_HEADER = "You have access to specialized skills. Each provides focused guidance for a particular kind of request. When a skill is relevant to the user's message, call the `load_skill` tool with its name BEFORE answering \u2014 its full instructions will then be injected for you to follow. Load only what is relevant; skip it for small talk or unrelated questions.";
1297
1376
  // -----------------------------------------------------------------------
1298
1377
  // Injection block builder
1299
1378
  // -----------------------------------------------------------------------
@@ -1731,6 +1810,42 @@ function createMediaTools(prefix = defaultPrefix) {
1731
1810
  return [transcribeTool, synthesizeTool, visionTool, imageGenTool];
1732
1811
  }
1733
1812
 
1813
+ // src/tools/builtin/load_skill.ts
1814
+ var LOAD_SKILL_TOOL_NAME = "load_skill";
1815
+ function createLoadSkillTool(injector, config, onLoad) {
1816
+ const progressiveNames = injector.getProgressiveSkills().map((s) => s.name);
1817
+ return {
1818
+ name: LOAD_SKILL_TOOL_NAME,
1819
+ description: `Load a specialized skill by name to get its full instructions for the current turn. Call this when the user's request matches a skill listed in the "Available skills" catalog in your system prompt. Loading a skill injects its detailed guidance, which you should then follow.`,
1820
+ category: "utility",
1821
+ parameters: {
1822
+ type: "object",
1823
+ properties: {
1824
+ name: {
1825
+ type: "string",
1826
+ description: "The skill name, exactly as listed in the Available skills catalog.",
1827
+ enum: progressiveNames
1828
+ }
1829
+ },
1830
+ required: ["name"]
1831
+ },
1832
+ async execute(params) {
1833
+ const name = params?.name;
1834
+ if (!name || !progressiveNames.includes(name)) {
1835
+ return `No progressive skill named "${name}". Available: ${progressiveNames.join(", ") || "(none)"}.`;
1836
+ }
1837
+ const max = config?.maxConcurrent;
1838
+ const alreadyEnabled = injector.getActiveSkills().some((s) => s.name === name && s.strategy === "progressive");
1839
+ if (max !== void 0 && !alreadyEnabled && injector.countEnabledProgressive() >= max) {
1840
+ return `Cannot load "${name}": at most ${max} skill(s) may be active at once. Finish or skip a loaded skill first.`;
1841
+ }
1842
+ injector.enableSkill(name);
1843
+ onLoad?.(name);
1844
+ return `Skill "${name}" loaded. Follow its instructions for this response.`;
1845
+ }
1846
+ };
1847
+ }
1848
+
1734
1849
  // src/agent/execution/StepCounter.ts
1735
1850
  var StepCounter = class {
1736
1851
  constructor(maxSteps = 20) {
@@ -2693,6 +2808,8 @@ var SessionManager = class {
2693
2808
  contextManager;
2694
2809
  toolRegistry;
2695
2810
  skillInjector;
2811
+ /** True when any registered skill uses `strategy: 'progressive'`. */
2812
+ hasProgressiveSkills = false;
2696
2813
  context;
2697
2814
  adapter;
2698
2815
  config;
@@ -2787,6 +2904,17 @@ var SessionManager = class {
2787
2904
  this.toolRegistry.register(tool);
2788
2905
  }
2789
2906
  }
2907
+ this.hasProgressiveSkills = this.skillInjector.getProgressiveSkills().length > 0;
2908
+ if (this.hasProgressiveSkills) {
2909
+ this.toolRegistry.register(
2910
+ createLoadSkillTool(
2911
+ this.skillInjector,
2912
+ config.skillSelection,
2913
+ // Trace the model's selection decision as a first-class skill event.
2914
+ (name) => this.emitTrace("skill", "skill_enable", { name, source: "load_skill" })
2915
+ )
2916
+ );
2917
+ }
2790
2918
  this.context = {
2791
2919
  systemPrompt: config.systemPrompt || "",
2792
2920
  scratchpad: "",
@@ -2812,11 +2940,12 @@ var SessionManager = class {
2812
2940
  skills: {
2813
2941
  total: (config.skills || []).length,
2814
2942
  active: activeSkills.length,
2815
- fixed: activeSkills.filter((s) => s.strategy !== "dynamic").length,
2816
- dynamic: activeSkills.filter((s) => s.strategy === "dynamic").length
2943
+ fixed: activeSkills.filter((s) => s.strategy === "fixed" || s.strategy === void 0).length,
2944
+ dynamic: activeSkills.filter((s) => s.strategy === "dynamic").length,
2945
+ progressive: this.skillInjector.getProgressiveSkills().length
2817
2946
  }
2818
2947
  });
2819
- for (const skill of activeSkills) {
2948
+ for (const skill of this.skillInjector.getAll()) {
2820
2949
  this.emitTrace("skill", "skill_load", {
2821
2950
  name: skill.name,
2822
2951
  version: skill.version,
@@ -2824,7 +2953,8 @@ var SessionManager = class {
2824
2953
  inject: skill.inject,
2825
2954
  priority: skill.priority,
2826
2955
  tags: skill.tags ?? [],
2827
- requiredTools: skill.requiredTools ?? []
2956
+ requiredTools: skill.requiredTools ?? [],
2957
+ enabled: skill.enabled === true
2828
2958
  });
2829
2959
  }
2830
2960
  }
@@ -3237,6 +3367,12 @@ Which pending sub-goals are now fully completed?`
3237
3367
  /** Builds the system prompt, injecting skills and goal if configured. */
3238
3368
  buildSystemPrompt(userMessage, iteration = 0) {
3239
3369
  let prompt = this.context.systemPrompt || "";
3370
+ if (this.hasProgressiveSkills) {
3371
+ const catalog = this.skillInjector.buildCatalog(
3372
+ this.config.skillSelection?.catalogHeader
3373
+ );
3374
+ if (catalog) prompt += "\n\n" + catalog;
3375
+ }
3240
3376
  const isStatic = this.config.staticSystemPrompt === true;
3241
3377
  if (!isStatic && this.goalInjector && this.config.goalInjectionPosition !== "pre_turn") {
3242
3378
  const shouldInject = this.goalInjector.shouldInjectThisTurn(
@@ -3264,6 +3400,10 @@ ${planStatus}`;
3264
3400
  );
3265
3401
  if (injectedSkills) {
3266
3402
  prompt += "\n\n" + injectedSkills;
3403
+ const poolInjected = this.skillInjector.getSkillsForInjection("system_prompt").filter((s) => s.strategy === "dynamic" || s.strategy === "progressive").map((s) => s.name);
3404
+ if (poolInjected.length > 0) {
3405
+ this.emitTrace("skill", "skill_inject", { skills: poolInjected, position: "system_prompt" });
3406
+ }
3267
3407
  }
3268
3408
  return prompt.trim();
3269
3409
  }
@@ -3450,6 +3590,9 @@ ${blocks.join("\n\n")}
3450
3590
  * Returns true to proceed, false to block.
3451
3591
  */
3452
3592
  async passesFirewall(toolName, argsJson, toolCallId, toolResults) {
3593
+ if (toolName === LOAD_SKILL_TOOL_NAME && this.hasProgressiveSkills) {
3594
+ return true;
3595
+ }
3453
3596
  const firewall = evaluateToolFirewall(
3454
3597
  this.config.toolFirewall,
3455
3598
  toolName,
@@ -3649,8 +3792,25 @@ ${blocks.join("\n\n")}
3649
3792
  async run(userMessage) {
3650
3793
  if (this.mcpReady) await this.mcpReady;
3651
3794
  await this.ensureScratchpadLoaded();
3795
+ this._resetProgressiveSkillsForTurn();
3652
3796
  return this._executeLoop(userMessage, { label: "run" });
3653
3797
  }
3798
+ /**
3799
+ * Resets progressive skills at the start of a turn when
3800
+ * `skillSelection.persistence` is `'per_turn'` (the default), so each user
3801
+ * message re-decides which skills to load from the catalog. No-op when
3802
+ * persistence is `'session'` or there are no progressive skills.
3803
+ */
3804
+ _resetProgressiveSkillsForTurn() {
3805
+ if (!this.hasProgressiveSkills) return;
3806
+ const persistence = this.config.skillSelection?.persistence ?? "per_turn";
3807
+ if (persistence !== "per_turn") return;
3808
+ const cleared = this.skillInjector.getProgressiveSkills().filter((s) => s.enabled === true).map((s) => s.name);
3809
+ this.skillInjector.resetProgressiveSkills();
3810
+ if (cleared.length > 0) {
3811
+ this.emitTrace("skill", "skill_reset", { skills: cleared, reason: "per_turn" });
3812
+ }
3813
+ }
3654
3814
  /**
3655
3815
  * Runs the ReAct loop and streams the final assistant response token-by-token.
3656
3816
  *
@@ -3670,6 +3830,7 @@ ${blocks.join("\n\n")}
3670
3830
  async *stream(userMessage) {
3671
3831
  if (this.mcpReady) await this.mcpReady;
3672
3832
  await this.ensureScratchpadLoaded();
3833
+ this._resetProgressiveSkillsForTurn();
3673
3834
  const userMessageStr = Array.isArray(userMessage) ? "[Multimodal Content]" : userMessage;
3674
3835
  this.logger.info(`Starting streaming session run`, { model: this.config.model, message: userMessageStr });
3675
3836
  const routeDecision = await this._runRoutingStep(userMessageStr);