lemura 1.5.1 → 1.5.3

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,23 @@ 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.5.3] - 2026-05-30
9
+
10
+ ### Fixed
11
+
12
+ - **Tool firewall `ask` decision could execute denied tools** (critical): when a `ToolFirewallConfig.onAsk` handler returned anything other than the exact string `'deny'` — including the boolean `false`, `undefined`, or by throwing — the tool was executed anyway. A user pressing "Stop"/deny could not actually stop the tool call. The `ask` path is now **fail-safe**: only an explicit accept signal (`'accept'` or `true`) allows execution; `'deny'`, `false`, `undefined`, `void`, and thrown errors all block the tool and inject a `Blocked by tool firewall` observation.
13
+ - `ToolFirewallConfig.onAsk` now accepts `'accept' | 'deny' | boolean | void` (and the `Promise` thereof) for ergonomics. The previous `'accept' | 'deny'` return remains valid.
14
+
15
+ ## [1.5.2] - 2026-05-30
16
+
17
+ ### Changed
18
+
19
+ - **Namespaced XML delimiters for all runtime-injected blocks**: All blocks appended to the system prompt or user messages by the lemura runtime now use a consistent `lemura:` XML namespace, preventing collisions with user-supplied prompt content.
20
+ - `GoalInjector`: `[CURRENT GOAL]…[/CURRENT GOAL]` → `<lemura:goal>` with `<lemura:statement>`, `<lemura:criteria>`, `<lemura:subgoals status="pending|done">`
21
+ - `ContinuationPlanner`: `[CONTINUATION PLAN — Step X/Y]` (no closing tag) → `<lemura:plan step="X" total="Y">` with `<lemura:step id="…" tool="…" status="…">` children
22
+ - `SkillInjector`: `[Skill: name (Tier: tier)]` (no closing tag) → `<lemura:skill name="…" tier="…">…</lemura:skill>`
23
+ - `SessionManager` frozen-turn wrapper: `[System Guidance / Agent State]` (no closing tag) → `<lemura:agent-state>…</lemura:agent-state>`
24
+
8
25
  ## [1.5.1] - 2026-05-29
9
26
 
10
27
  ### Added
@@ -126,7 +126,7 @@ declare class GoalInjector {
126
126
  private turnsSinceInjection;
127
127
  constructor(goal: Goal);
128
128
  /**
129
- * Returns the formatted `[CURRENT GOAL]` block string — without caring about
129
+ * Returns the formatted `<lemura:goal>` block string — without caring about
130
130
  * where it will be placed. Callers decide whether to append to a system prompt
131
131
  * or push as a separate message.
132
132
  */
@@ -199,9 +199,14 @@ interface ToolFirewallConfig {
199
199
  rules?: ToolFirewallRule[];
200
200
  /**
201
201
  * Called when a tool hits the 'ask' decision.
202
- * Return 'accept' or 'deny'. If omitted, 'ask' behaves like 'deny'.
202
+ *
203
+ * Return `'accept'` (or `true`) to allow the tool to run; return `'deny'`
204
+ * (or `false`) to block it. The decision is **fail-safe**: only an explicit
205
+ * accept signal allows execution — any other value (`'deny'`, `false`,
206
+ * `undefined`, `null`, a thrown error) blocks the tool. If `onAsk` is
207
+ * omitted entirely, an `'ask'` decision behaves like `'deny'`.
203
208
  */
204
- onAsk?: (toolName: string, argsJson: string) => Promise<'accept' | 'deny'> | 'accept' | 'deny';
209
+ onAsk?: (toolName: string, argsJson: string) => Promise<'accept' | 'deny' | boolean | void> | 'accept' | 'deny' | boolean | void;
205
210
  }
206
211
  /**
207
212
  * Execution budget constraints for tool calls within a session.
@@ -126,7 +126,7 @@ declare class GoalInjector {
126
126
  private turnsSinceInjection;
127
127
  constructor(goal: Goal);
128
128
  /**
129
- * Returns the formatted `[CURRENT GOAL]` block string — without caring about
129
+ * Returns the formatted `<lemura:goal>` block string — without caring about
130
130
  * where it will be placed. Callers decide whether to append to a system prompt
131
131
  * or push as a separate message.
132
132
  */
@@ -199,9 +199,14 @@ interface ToolFirewallConfig {
199
199
  rules?: ToolFirewallRule[];
200
200
  /**
201
201
  * Called when a tool hits the 'ask' decision.
202
- * Return 'accept' or 'deny'. If omitted, 'ask' behaves like 'deny'.
202
+ *
203
+ * Return `'accept'` (or `true`) to allow the tool to run; return `'deny'`
204
+ * (or `false`) to block it. The decision is **fail-safe**: only an explicit
205
+ * accept signal allows execution — any other value (`'deny'`, `false`,
206
+ * `undefined`, `null`, a thrown error) blocks the tool. If `onAsk` is
207
+ * omitted entirely, an `'ask'` decision behaves like `'deny'`.
203
208
  */
204
- onAsk?: (toolName: string, argsJson: string) => Promise<'accept' | 'deny'> | 'accept' | 'deny';
209
+ onAsk?: (toolName: string, argsJson: string) => Promise<'accept' | 'deny' | boolean | void> | 'accept' | 'deny' | boolean | void;
205
210
  }
206
211
  /**
207
212
  * Execution budget constraints for tool calls within a session.
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
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-DAzmrg4l.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-DAzmrg4l.mjs';
3
- import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-57E_Tvby.mjs';
4
- export { b as GoalInjector, c as GoalVerifierResult, d as MCPJsonRpcRequest, e as MCPJsonRpcResponse, f as MCPTransportType, g as MediaConfig, h as ToolDecision, i as ToolExecutionBudget, j as ToolFirewallConfig, k as ToolFirewallRule, l as TraceEvent } from './agent-57E_Tvby.mjs';
3
+ import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-DFh31XIA.mjs';
4
+ export { b as GoalInjector, c as GoalVerifierResult, d as MCPJsonRpcRequest, e as MCPJsonRpcResponse, f as MCPTransportType, g as MediaConfig, h as ToolDecision, i as ToolExecutionBudget, j as ToolFirewallConfig, k as ToolFirewallRule, l as TraceEvent } from './agent-DFh31XIA.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';
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
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-CIRkrCHl.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-CIRkrCHl.js';
3
- import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-BImCcYVg.js';
4
- export { b as GoalInjector, c as GoalVerifierResult, d as MCPJsonRpcRequest, e as MCPJsonRpcResponse, f as MCPTransportType, g as MediaConfig, h as ToolDecision, i as ToolExecutionBudget, j as ToolFirewallConfig, k as ToolFirewallRule, l as TraceEvent } from './agent-BImCcYVg.js';
3
+ import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-Cd704J0J.js';
4
+ export { b as GoalInjector, c as GoalVerifierResult, d as MCPJsonRpcRequest, e as MCPJsonRpcResponse, f as MCPTransportType, g as MediaConfig, h as ToolDecision, i as ToolExecutionBudget, j as ToolFirewallConfig, k as ToolFirewallRule, l as TraceEvent } from './agent-Cd704J0J.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';
package/dist/index.js CHANGED
@@ -1315,8 +1315,9 @@ var SkillInjector = class {
1315
1315
  if (!content) continue;
1316
1316
  const tierLabel = skill.tier ?? "standard";
1317
1317
  const skillEntry = `
1318
- [Skill: ${skill.name} (Tier: ${tierLabel})]
1318
+ <lemura:skill name="${skill.name}" tier="${tierLabel}">
1319
1319
  ${content}
1320
+ </lemura:skill>
1320
1321
  `;
1321
1322
  const skillTokens = Math.ceil(skillEntry.length / 4);
1322
1323
  if (tokenBudget !== void 0 && usedTokens + skillTokens > tokenBudget) {
@@ -1853,7 +1854,7 @@ var GoalInjector = class {
1853
1854
  };
1854
1855
  }
1855
1856
  /**
1856
- * Returns the formatted `[CURRENT GOAL]` block string — without caring about
1857
+ * Returns the formatted `<lemura:goal>` block string — without caring about
1857
1858
  * where it will be placed. Callers decide whether to append to a system prompt
1858
1859
  * or push as a separate message.
1859
1860
  */
@@ -1861,27 +1862,28 @@ var GoalInjector = class {
1861
1862
  const { statement, successCriteria, decomposition, completedSubGoals = [] } = this.goal;
1862
1863
  const pending = decomposition.filter((sg) => !completedSubGoals.includes(sg));
1863
1864
  const completed = decomposition.filter((sg) => completedSubGoals.includes(sg));
1864
- let block = `[CURRENT GOAL]
1865
- ${statement}
1865
+ let block = `<lemura:goal>
1866
+ <lemura:statement>${statement}</lemura:statement>
1866
1867
  `;
1867
1868
  if (successCriteria.length > 0) {
1868
- block += `
1869
- Success criteria:
1870
- ${successCriteria.map((c) => `- ${c}`).join("\n")}`;
1869
+ block += `<lemura:criteria>
1870
+ ${successCriteria.map((c) => `- ${c}`).join("\n")}
1871
+ </lemura:criteria>
1872
+ `;
1871
1873
  }
1872
1874
  if (pending.length > 0) {
1873
- block += `
1874
-
1875
- Sub-goals remaining:
1876
- ${pending.map((sg) => `- ${sg} \u2190 pending`).join("\n")}`;
1875
+ block += `<lemura:subgoals status="pending">
1876
+ ${pending.map((sg) => `- ${sg}`).join("\n")}
1877
+ </lemura:subgoals>
1878
+ `;
1877
1879
  }
1878
1880
  if (completed.length > 0) {
1879
- block += `
1880
-
1881
- Sub-goals completed:
1882
- ${completed.map((sg) => `- \u2705 ${sg}`).join("\n")}`;
1881
+ block += `<lemura:subgoals status="done">
1882
+ ${completed.map((sg) => `- \u2705 ${sg}`).join("\n")}
1883
+ </lemura:subgoals>
1884
+ `;
1883
1885
  }
1884
- block += "\n[/CURRENT GOAL]";
1886
+ block += "</lemura:goal>";
1885
1887
  return block;
1886
1888
  }
1887
1889
  /**
@@ -1970,14 +1972,17 @@ var ContinuationPlanner = class {
1970
1972
  }
1971
1973
  /** Returns a human-readable status string with icons (injected before each iteration) */
1972
1974
  getPlanStatusString() {
1973
- let result = `[CONTINUATION PLAN \u2014 Step ${this.plan.currentStepIndex + 1}/${this.plan.steps.length}]
1975
+ const current = this.plan.currentStepIndex + 1;
1976
+ const total = this.plan.steps.length;
1977
+ let result = `<lemura:plan step="${current}" total="${total}">
1974
1978
  `;
1975
1979
  for (const step of this.plan.steps) {
1976
1980
  const icon = this._icon(step.status);
1977
1981
  const statusText = step.status === "pending" && step.dependsOn.length > 0 ? `Waiting on Step ${step.dependsOn.join(", ")}` : step.status.charAt(0).toUpperCase() + step.status.slice(1);
1978
- result += `${icon} Step ${step.stepId} (${step.toolName}): ${statusText}
1982
+ result += `<lemura:step id="${step.stepId}" tool="${step.toolName}" status="${step.status}">${icon} ${statusText}</lemura:step>
1979
1983
  `;
1980
1984
  }
1985
+ result += "</lemura:plan>";
1981
1986
  return result;
1982
1987
  }
1983
1988
  _icon(status) {
@@ -3105,8 +3110,9 @@ ${planStatus}`;
3105
3110
  }
3106
3111
  injectionBlock = blocks.length > 0 ? `
3107
3112
 
3108
- [System Guidance / Agent State]
3109
- ${blocks.join("\n\n")}` : "";
3113
+ <lemura:agent-state>
3114
+ ${blocks.join("\n\n")}
3115
+ </lemura:agent-state>` : "";
3110
3116
  this._turnInjections.set(i, injectionBlock);
3111
3117
  if (injectionBlock) {
3112
3118
  this.emitTrace("planning", "goal_injected", { position: "frozen_turn", turnIndex: i, iteration });
@@ -3254,8 +3260,17 @@ ${blocks.join("\n\n")}` : "";
3254
3260
  }
3255
3261
  if (firewall.decision === "ask") {
3256
3262
  if (this.config.toolFirewall?.onAsk) {
3257
- const userDecision = await this.config.toolFirewall.onAsk(toolName, argsJson);
3258
- if (userDecision === "deny") {
3263
+ let accepted = false;
3264
+ try {
3265
+ const userDecision = await this.config.toolFirewall.onAsk(toolName, argsJson);
3266
+ accepted = userDecision === "accept" || userDecision === true;
3267
+ } catch (e) {
3268
+ this.logger.warn(`Tool firewall onAsk handler threw \u2014 treating as deny: ${toolName}`, {
3269
+ error: e instanceof Error ? e.message : String(e)
3270
+ });
3271
+ accepted = false;
3272
+ }
3273
+ if (!accepted) {
3259
3274
  this.logger.warn(`Tool blocked by firewall (ask \u2192 deny): ${toolName}`, { reason: firewall.reason });
3260
3275
  toolResults.push({ toolCallId, content: `Blocked by tool firewall: ${firewall.reason}` });
3261
3276
  return false;