lemura 1.5.0 → 1.5.2

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/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
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-S96Ka9wt.mjs';
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-S96Ka9wt.mjs';
3
- import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-BefjpRHZ.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-BefjpRHZ.mjs';
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
+ 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-DPSUNlK6.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-DPSUNlK6.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';
@@ -226,6 +226,8 @@ declare class SessionManager {
226
226
  private toolResponseProcessor;
227
227
  private goalInjector;
228
228
  private continuationPlanner;
229
+ /** Frozen goal/plan injection text keyed by turn index — used when staticSystemPrompt is on */
230
+ private _turnInjections;
229
231
  private mcpRegistry;
230
232
  /** Resolves when all MCP servers are connected; awaited by run() and stream() */
231
233
  private mcpReady;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
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-DHQOGl8Y.js';
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-DHQOGl8Y.js';
3
- import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-CjoFQGlk.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-CjoFQGlk.js';
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
+ 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-Cq_oRvoc.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-Cq_oRvoc.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';
@@ -226,6 +226,8 @@ declare class SessionManager {
226
226
  private toolResponseProcessor;
227
227
  private goalInjector;
228
228
  private continuationPlanner;
229
+ /** Frozen goal/plan injection text keyed by turn index — used when staticSystemPrompt is on */
230
+ private _turnInjections;
229
231
  private mcpRegistry;
230
232
  /** Resolves when all MCP servers are connected; awaited by run() and stream() */
231
233
  private mcpReady;
package/dist/index.js CHANGED
@@ -234,13 +234,27 @@ var OpenAICompatibleAdapter = class {
234
234
  return msg;
235
235
  });
236
236
  }
237
+ /**
238
+ * Returns true for reasoning/o-series models (o1, o3, o4, gpt-5, *-mini variants).
239
+ * These models use `max_completion_tokens` instead of `max_tokens` and do not
240
+ * support sampling hyperparameters (temperature, top_p, presence_penalty, frequency_penalty).
241
+ */
242
+ isReasoningModel(model) {
243
+ return /\bo[1-9]\b|\bo[1-9]-|\bgpt-5\b|(?:^|[-_])mini(?:$|[-_])/i.test(model);
244
+ }
237
245
  buildPayload(request) {
246
+ const model = request.model || this.defaultModel;
247
+ const reasoning = this.isReasoningModel(model);
238
248
  const payload = {
239
- model: request.model || this.defaultModel,
249
+ model,
240
250
  messages: this.toOpenAIMessages(request.messages)
241
251
  };
242
- if (request.maxTokens !== void 0) payload.max_tokens = request.maxTokens;
243
- if (request.temperature !== void 0) payload.temperature = request.temperature;
252
+ if (request.maxTokens !== void 0) {
253
+ payload[reasoning ? "max_completion_tokens" : "max_tokens"] = request.maxTokens;
254
+ }
255
+ if (!reasoning) {
256
+ if (request.temperature !== void 0) payload.temperature = request.temperature;
257
+ }
244
258
  if (request.stopSequences?.length) payload.stop = request.stopSequences;
245
259
  if (request.stream) payload.stream = true;
246
260
  if (request.tools && request.tools.length > 0) {
@@ -1039,8 +1053,8 @@ var ToolRegistry = class {
1039
1053
  );
1040
1054
  }
1041
1055
  }
1042
- const toolTimeoutMs = tool["timeoutMs"];
1043
- const timeoutMs = typeof toolTimeoutMs === "number" ? toolTimeoutMs : this.defaultTimeoutMs;
1056
+ const timeoutMs = tool.timeoutMs ?? this.defaultTimeoutMs;
1057
+ const startMs = Date.now();
1044
1058
  const executionPromise = tool.execute(params, context);
1045
1059
  const timeoutPromise = new Promise((_, reject) => {
1046
1060
  const id = setTimeout(() => {
@@ -1051,12 +1065,26 @@ var ToolRegistry = class {
1051
1065
  }, timeoutMs);
1052
1066
  });
1053
1067
  try {
1054
- return await Promise.race([executionPromise, timeoutPromise]);
1068
+ const result = await Promise.race([executionPromise, timeoutPromise]);
1069
+ context.logger.debug(`Tool '${name}' completed in ${Date.now() - startMs}ms`);
1070
+ return result;
1055
1071
  } catch (err) {
1056
- if (err instanceof LemuraToolValidationError || err instanceof LemuraToolTimeoutError) {
1072
+ const elapsedMs = Date.now() - startMs;
1073
+ if (err instanceof LemuraToolTimeoutError) {
1074
+ context.logger.error(`Tool '${name}' timed out after ${elapsedMs}ms (limit: ${timeoutMs}ms)`, {
1075
+ problem: `Tool '${name}' did not respond within its timeout.`,
1076
+ hints: [
1077
+ `Increase the tool's timeoutMs (currently ${timeoutMs}ms) or optimise its implementation.`,
1078
+ `Check whether the external service the tool depends on is healthy.`
1079
+ ]
1080
+ });
1081
+ throw err;
1082
+ }
1083
+ if (err instanceof LemuraToolValidationError) {
1057
1084
  throw err;
1058
1085
  }
1059
1086
  const message = err instanceof Error ? err.message : String(err);
1087
+ context.logger.error(`Tool '${name}' failed after ${elapsedMs}ms: ${message}`);
1060
1088
  throw new LemuraToolValidationError(
1061
1089
  `Tool '${name}' execution failed: ${message}`
1062
1090
  );
@@ -1287,8 +1315,9 @@ var SkillInjector = class {
1287
1315
  if (!content) continue;
1288
1316
  const tierLabel = skill.tier ?? "standard";
1289
1317
  const skillEntry = `
1290
- [Skill: ${skill.name} (Tier: ${tierLabel})]
1318
+ <lemura:skill name="${skill.name}" tier="${tierLabel}">
1291
1319
  ${content}
1320
+ </lemura:skill>
1292
1321
  `;
1293
1322
  const skillTokens = Math.ceil(skillEntry.length / 4);
1294
1323
  if (tokenBudget !== void 0 && usedTokens + skillTokens > tokenBudget) {
@@ -1825,7 +1854,7 @@ var GoalInjector = class {
1825
1854
  };
1826
1855
  }
1827
1856
  /**
1828
- * Returns the formatted `[CURRENT GOAL]` block string — without caring about
1857
+ * Returns the formatted `<lemura:goal>` block string — without caring about
1829
1858
  * where it will be placed. Callers decide whether to append to a system prompt
1830
1859
  * or push as a separate message.
1831
1860
  */
@@ -1833,27 +1862,28 @@ var GoalInjector = class {
1833
1862
  const { statement, successCriteria, decomposition, completedSubGoals = [] } = this.goal;
1834
1863
  const pending = decomposition.filter((sg) => !completedSubGoals.includes(sg));
1835
1864
  const completed = decomposition.filter((sg) => completedSubGoals.includes(sg));
1836
- let block = `[CURRENT GOAL]
1837
- ${statement}
1865
+ let block = `<lemura:goal>
1866
+ <lemura:statement>${statement}</lemura:statement>
1838
1867
  `;
1839
1868
  if (successCriteria.length > 0) {
1840
- block += `
1841
- Success criteria:
1842
- ${successCriteria.map((c) => `- ${c}`).join("\n")}`;
1869
+ block += `<lemura:criteria>
1870
+ ${successCriteria.map((c) => `- ${c}`).join("\n")}
1871
+ </lemura:criteria>
1872
+ `;
1843
1873
  }
1844
1874
  if (pending.length > 0) {
1845
- block += `
1846
-
1847
- Sub-goals remaining:
1848
- ${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
+ `;
1849
1879
  }
1850
1880
  if (completed.length > 0) {
1851
- block += `
1852
-
1853
- Sub-goals completed:
1854
- ${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
+ `;
1855
1885
  }
1856
- block += "\n[/CURRENT GOAL]";
1886
+ block += "</lemura:goal>";
1857
1887
  return block;
1858
1888
  }
1859
1889
  /**
@@ -1942,14 +1972,17 @@ var ContinuationPlanner = class {
1942
1972
  }
1943
1973
  /** Returns a human-readable status string with icons (injected before each iteration) */
1944
1974
  getPlanStatusString() {
1945
- 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}">
1946
1978
  `;
1947
1979
  for (const step of this.plan.steps) {
1948
1980
  const icon = this._icon(step.status);
1949
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);
1950
- 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>
1951
1983
  `;
1952
1984
  }
1985
+ result += "</lemura:plan>";
1953
1986
  return result;
1954
1987
  }
1955
1988
  _icon(status) {
@@ -2161,7 +2194,31 @@ var MCPClient = class {
2161
2194
  method: "tools/call",
2162
2195
  params: { name: toolName, arguments: args }
2163
2196
  };
2164
- const response = await this._rpc(request);
2197
+ const startMs = Date.now();
2198
+ let response;
2199
+ try {
2200
+ response = await this._rpc(request);
2201
+ } catch (err) {
2202
+ const elapsedMs = Date.now() - startMs;
2203
+ if (err instanceof LemuraMCPTimeoutError) {
2204
+ this.logger.error(
2205
+ `[MCP:${this._serverName}] Tool '${toolName}' timed out after ${elapsedMs}ms (limit: ${this.timeoutMs}ms)`,
2206
+ {
2207
+ problem: `MCP server '${this._serverName}' did not respond to tool '${toolName}' in time.`,
2208
+ hints: [
2209
+ `Increase 'timeoutMs' in the MCPServerConfig for '${this._serverName}' (currently ${this.timeoutMs}ms).`,
2210
+ `Check whether the MCP server process is healthy and not blocked.`
2211
+ ]
2212
+ }
2213
+ );
2214
+ } else {
2215
+ this.logger.error(
2216
+ `[MCP:${this._serverName}] Tool '${toolName}' RPC failed after ${elapsedMs}ms: ${err.message}`
2217
+ );
2218
+ }
2219
+ throw err;
2220
+ }
2221
+ this.logger.debug(`[MCP:${this._serverName}] Tool '${toolName}' completed in ${Date.now() - startMs}ms`);
2165
2222
  this._assertNoError(response, `tool '${toolName}'`);
2166
2223
  const result = response.result;
2167
2224
  if (result && Array.isArray(result["content"])) {
@@ -2579,6 +2636,8 @@ var SessionManager = class {
2579
2636
  toolResponseProcessor;
2580
2637
  goalInjector = null;
2581
2638
  continuationPlanner = null;
2639
+ /** Frozen goal/plan injection text keyed by turn index — used when staticSystemPrompt is on */
2640
+ _turnInjections = /* @__PURE__ */ new Map();
2582
2641
  // MCP
2583
2642
  mcpRegistry = null;
2584
2643
  /** Resolves when all MCP servers are connected; awaited by run() and stream() */
@@ -2947,8 +3006,12 @@ Respond ONLY with valid JSON (no markdown, no explanations):
2947
3006
  messages: [{ role: "user", content: planningPrompt }],
2948
3007
  maxTokens: this.config.maxCompletionTokens ?? 4e3
2949
3008
  });
2950
- const raw = response.content.replace(/```json|```/g, "").trim();
2951
- const parsed = JSON.parse(raw);
3009
+ const stripped = response.content.replace(/```json|```/g, "").trim();
3010
+ const jsonMatch = stripped.match(/\{[\s\S]*\}/);
3011
+ if (!jsonMatch) {
3012
+ throw new Error(`No JSON object found in mini-planning response: "${stripped.slice(0, 200)}"`);
3013
+ }
3014
+ const parsed = JSON.parse(jsonMatch[0]);
2952
3015
  if (this.goalInjector && Array.isArray(parsed.subGoals)) {
2953
3016
  this.goalInjector.updateDecomposition(
2954
3017
  parsed.subGoals,
@@ -2971,7 +3034,8 @@ Respond ONLY with valid JSON (no markdown, no explanations):
2971
3034
  /** Builds the system prompt, injecting skills and goal if configured. */
2972
3035
  buildSystemPrompt(userMessage, iteration = 0) {
2973
3036
  let prompt = this.context.systemPrompt || "";
2974
- if (this.goalInjector && this.config.goalInjectionPosition !== "pre_turn") {
3037
+ const isStatic = this.config.staticSystemPrompt === true;
3038
+ if (!isStatic && this.goalInjector && this.config.goalInjectionPosition !== "pre_turn") {
2975
3039
  const shouldInject = this.goalInjector.shouldInjectThisTurn(
2976
3040
  iteration,
2977
3041
  false,
@@ -2985,7 +3049,7 @@ Respond ONLY with valid JSON (no markdown, no explanations):
2985
3049
  });
2986
3050
  }
2987
3051
  }
2988
- if (this.continuationPlanner && this.config.enableContinuationPlanning) {
3052
+ if (!isStatic && this.continuationPlanner && this.config.enableContinuationPlanning) {
2989
3053
  const planStatus = this.continuationPlanner.getPlanStatusString();
2990
3054
  prompt += `
2991
3055
 
@@ -3011,7 +3075,7 @@ ${planStatus}`;
3011
3075
  if (systemPrompt) {
3012
3076
  messages.unshift({ role: "system", content: systemPrompt });
3013
3077
  }
3014
- if (this.goalInjector && this.config.goalInjectionPosition === "pre_turn") {
3078
+ if (!this.config.staticSystemPrompt && this.goalInjector && this.config.goalInjectionPosition === "pre_turn") {
3015
3079
  const shouldInject = this.goalInjector.shouldInjectThisTurn(
3016
3080
  iteration,
3017
3081
  false,
@@ -3023,6 +3087,52 @@ ${planStatus}`;
3023
3087
  this.emitTrace("planning", "goal_injected", { position: "pre_turn", iteration });
3024
3088
  }
3025
3089
  }
3090
+ if (this.config.staticSystemPrompt) {
3091
+ const totalTurns = this.context.turns.length;
3092
+ for (let i = 0; i < totalTurns; i++) {
3093
+ const msgIndex = i + 1;
3094
+ if (msgIndex >= messages.length) continue;
3095
+ const msg = messages[msgIndex];
3096
+ if (msg.role !== "user" && msg.role !== "tool") continue;
3097
+ let injectionBlock;
3098
+ if (i === totalTurns - 1) {
3099
+ const blocks = [];
3100
+ if (this.goalInjector) {
3101
+ const shouldInject = this.goalInjector.shouldInjectThisTurn(
3102
+ iteration,
3103
+ false,
3104
+ this.config.goalInjectionN ?? 3
3105
+ );
3106
+ if (shouldInject) blocks.push(this.goalInjector.getFormattedBlock());
3107
+ }
3108
+ if (this.continuationPlanner && this.config.enableContinuationPlanning) {
3109
+ blocks.push(this.continuationPlanner.getPlanStatusString());
3110
+ }
3111
+ injectionBlock = blocks.length > 0 ? `
3112
+
3113
+ <lemura:agent-state>
3114
+ ${blocks.join("\n\n")}
3115
+ </lemura:agent-state>` : "";
3116
+ this._turnInjections.set(i, injectionBlock);
3117
+ if (injectionBlock) {
3118
+ this.emitTrace("planning", "goal_injected", { position: "frozen_turn", turnIndex: i, iteration });
3119
+ }
3120
+ } else {
3121
+ injectionBlock = this._turnInjections.get(i) ?? "";
3122
+ }
3123
+ if (!injectionBlock) continue;
3124
+ if (Array.isArray(msg.content)) {
3125
+ messages[msgIndex] = {
3126
+ ...msg,
3127
+ content: msg.content.map(
3128
+ (item) => item.type === "text" ? { ...item, text: item.text + injectionBlock } : item
3129
+ )
3130
+ };
3131
+ } else {
3132
+ messages[msgIndex] = { ...msg, content: (msg.content ?? "") + injectionBlock };
3133
+ }
3134
+ }
3135
+ }
3026
3136
  return messages;
3027
3137
  }
3028
3138
  /** Checks the tool execution budget and throws descriptively if exceeded. */
@@ -3477,6 +3587,10 @@ ${planStatus}`;
3477
3587
  const verdict = await this._verifyGoal(this.context.turns);
3478
3588
  if (verdict && !verdict.achieved && verdict.missing) {
3479
3589
  this.logger.info(`[GoalVerifier] Incomplete \u2014 running silent correction: "${verdict.missing}"`);
3590
+ this.emitTrace("verification", "goal_correction_start", {
3591
+ missing: verdict.missing,
3592
+ reason: verdict.reason
3593
+ });
3480
3594
  try {
3481
3595
  const corrMsgs = this.buildMessages(this.buildSystemPrompt(verdict.missing, this.iterations), this.iterations);
3482
3596
  corrMsgs.push({ role: "user", content: verdict.missing });
@@ -3493,9 +3607,37 @@ ${planStatus}`;
3493
3607
  turnIndex: this.context.turns.length,
3494
3608
  compressed: false
3495
3609
  });
3610
+ this.emitTrace("verification", "goal_correction_done", {
3611
+ missing: verdict.missing,
3612
+ correctionTokens: correction.usage?.completionTokens
3613
+ });
3496
3614
  }
3497
3615
  } catch (err) {
3498
- this.logger.warn(`[GoalVerifier] Correction failed (non-fatal): ${err.message}`);
3616
+ const errMsg = err.message;
3617
+ this.logger.warn(`[GoalVerifier] Correction failed (non-fatal): ${errMsg}`);
3618
+ this.emitTrace("error", "goal_correction_failed", { error: errMsg }, null, null, "error");
3619
+ }
3620
+ const finalVerdict = await this._verifyGoal(this.context.turns);
3621
+ if (finalVerdict && !finalVerdict.achieved) {
3622
+ this.logger.warn(`[GoalVerifier] Final verification failed: ${finalVerdict.reason}`);
3623
+ this.emitTrace("verification", "goal_verification_result", {
3624
+ achieved: false,
3625
+ reason: finalVerdict.reason,
3626
+ missing: finalVerdict.missing
3627
+ }, null, null, "error");
3628
+ const warningBlock = `
3629
+
3630
+ ---
3631
+
3632
+ \u26A0\uFE0F **Goal Verification Warning**
3633
+ * **Status:** Success criteria not fully met.
3634
+ * **Reason:** ${finalVerdict.reason ?? "Unknown"}
3635
+ * **Missing:** ${finalVerdict.missing ?? "Not specified"}
3636
+
3637
+ `;
3638
+ yield warningBlock;
3639
+ const lastTurn = [...this.context.turns].reverse().find((t) => t.role === "assistant");
3640
+ if (lastTurn) lastTurn.content = lastTurn.content + warningBlock;
3499
3641
  }
3500
3642
  }
3501
3643
  }
@@ -3624,6 +3766,12 @@ ${planStatus}`;
3624
3766
  const msg = e instanceof Error ? e.message : String(e);
3625
3767
  const isTimeout = e instanceof LemuraToolTimeoutError;
3626
3768
  this.logger.error(`Tool ${tc.name} ${isTimeout ? "timed out" : "failed"}: ${msg}`);
3769
+ this.emitTrace("error", isTimeout ? "tool_timeout" : "tool_error", {
3770
+ toolName: tc.name,
3771
+ id: tc.id,
3772
+ error: msg,
3773
+ timeoutMs: isTimeout ? this.config.toolRegistryTimeoutMs ?? 3e4 : void 0
3774
+ }, null, null, "error");
3627
3775
  return { toolCallId: tc.id, content: `Error: ${msg}` };
3628
3776
  }
3629
3777
  })
@@ -3644,6 +3792,12 @@ ${planStatus}`;
3644
3792
  problem: `Tool ${tc.name} ${isTimeout ? "timed out" : "failed to execute"}.`,
3645
3793
  hints: isTimeout ? ["Increase toolRegistryTimeoutMs or optimise the tool implementation."] : ["Check the tool parameters and ensure required services are running."]
3646
3794
  });
3795
+ this.emitTrace("error", isTimeout ? "tool_timeout" : "tool_error", {
3796
+ toolName: tc.name,
3797
+ id: tc.id,
3798
+ error: msg,
3799
+ timeoutMs: isTimeout ? this.config.toolRegistryTimeoutMs ?? 3e4 : void 0
3800
+ }, null, null, "error");
3647
3801
  toolResults.push({ toolCallId: tc.id, content: `Error: ${msg}` });
3648
3802
  }
3649
3803
  }
@@ -3688,6 +3842,10 @@ ${planStatus}`;
3688
3842
  const verdict = await this._verifyGoal(this.context.turns);
3689
3843
  if (verdict && !verdict.achieved && verdict.missing) {
3690
3844
  this.logger.info(`[GoalVerifier] Incomplete \u2014 running silent correction: "${verdict.missing}"`);
3845
+ this.emitTrace("verification", "goal_correction_start", {
3846
+ missing: verdict.missing,
3847
+ reason: verdict.reason
3848
+ });
3691
3849
  try {
3692
3850
  const correctionMessages = this.buildMessages(
3693
3851
  this.buildSystemPrompt(verdict.missing, this.iterations),
@@ -3707,11 +3865,40 @@ ${planStatus}`;
3707
3865
  turnIndex: this.context.turns.length,
3708
3866
  compressed: false
3709
3867
  });
3868
+ this.emitTrace("verification", "goal_correction_done", {
3869
+ missing: verdict.missing,
3870
+ correctionTokens: correction.usage?.completionTokens
3871
+ });
3710
3872
  }
3711
3873
  } catch (err) {
3712
- this.logger.warn(`[GoalVerifier] Correction failed (non-fatal): ${err.message}`);
3874
+ const errMsg = err.message;
3875
+ this.logger.warn(`[GoalVerifier] Correction failed (non-fatal): ${errMsg}`);
3876
+ this.emitTrace("error", "goal_correction_failed", { error: errMsg }, null, null, "error");
3713
3877
  }
3714
3878
  }
3879
+ const finalVerdict = await this._verifyGoal(this.context.turns);
3880
+ if (finalVerdict && !finalVerdict.achieved) {
3881
+ this.logger.warn(`[GoalVerifier] Final verification failed: ${finalVerdict.reason}`);
3882
+ this.emitTrace("verification", "goal_verification_result", {
3883
+ achieved: false,
3884
+ reason: finalVerdict.reason,
3885
+ missing: finalVerdict.missing
3886
+ }, null, null, "error");
3887
+ const warningBlock = `
3888
+
3889
+ ---
3890
+
3891
+ \u26A0\uFE0F **Goal Verification Warning**
3892
+ * **Status:** Success criteria not fully met.
3893
+ * **Reason:** ${finalVerdict.reason ?? "Unknown"}
3894
+ * **Missing:** ${finalVerdict.missing ?? "Not specified"}
3895
+
3896
+ `;
3897
+ const lastTurn = [...this.context.turns].reverse().find((t) => t.role === "assistant");
3898
+ if (lastTurn) lastTurn.content = lastTurn.content + warningBlock;
3899
+ this.logger.info(`[${opts.label}] Run completed with goal warning`);
3900
+ return lastTurn?.content ?? response.content;
3901
+ }
3715
3902
  }
3716
3903
  this.logger.info(`[${opts.label}] Run completed successfully`);
3717
3904
  return response.content;