lemura 1.5.3 → 1.5.5

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.5] - 2026-06-01
9
+
10
+ ### Fixed
11
+
12
+ - **Duplicated response in `stream()` during goal correction** (significant): `stream()` yielded the model's first answer to the caller *live*, then ran goal verification *afterwards*. When the verifier returned `achieved: false` and a correction re-entry followed, the corrected answer was streamed immediately after the already-emitted first answer — surfacing both as one duplicated response (contradicting the documented "delivers only the clean final response" contract). The final response is now **buffered** until verification settles; a rejected attempt is silently discarded and corrected, so the stream emits only the single approved answer. `run()` was unaffected (it already buffered).
13
+
14
+ ## [1.5.4] - 2026-05-30
15
+
16
+ ### Fixed
17
+
18
+ - **Goal verification could not recover an incomplete response** (significant): when the goal verifier returned `achieved: false`, the runtime ran a single **tool-less** `complete()` correction whose output was never re-verified (the run was flagged done after the first check) and, in `stream()`, was never sent to the caller. A response missing a step that required a tool (read a file, write output, …) could therefore never actually be completed. The verifier now re-enters the ReAct loop with a corrective user turn so the model has **full tool access** and the corrected answer is **streamed**, bounded by the new `maxGoalCorrections` budget (default `1`). When the budget is exhausted and the goal is still unmet, a visible Goal Verification Warning is appended/streamed as before. Applies to both `run()` and `stream()`.
19
+
20
+ ### Added
21
+
22
+ - **`maxGoalCorrections`** (`SessionConfig`, default `1`): maximum goal-verification corrective re-entries per run. Set to `0` to disable corrective re-entry (a warning is surfaced instead).
23
+ - **`goalProgressReconciliation`** (`SessionConfig`, default `false`): when enabled, the agent periodically (every `goalInjectionN` tool rounds) reconciles which decomposed sub-goals are already complete and marks them done, so the re-injected goal block reflects real progress instead of always showing every sub-goal as pending. Counters goal drift on long runs at the cost of one small LLM call per reconciliation. This wires up `GoalInjector.markSubGoalDone()`, which was previously never invoked.
24
+
8
25
  ## [1.5.3] - 2026-05-30
9
26
 
10
27
  ### Fixed
package/README.md CHANGED
@@ -1,4 +1,6 @@
1
+ <p align="center">
1
2
  <img src="https://raw.githubusercontent.com/rzafiamy/lemura/main/sites/docs/public/lemura-logo.png" alt="lemura logo" width="200" />
3
+ </p>
2
4
 
3
5
  # lemura
4
6
 
@@ -24,10 +26,11 @@
24
26
 
25
27
  - **🧠 Dynamic Skill Market**: Switch skills on/off at runtime via tags, names, or tool dependencies.
26
28
  - **🔌 Native MCP Support**: Connect to any Model Context Protocol server with custom header support (Auth).
27
- - **🛡️ Tool Firewall**: Fully integrated ask/accept/deny policy layer for secure tool execution.
28
- - **🎯 Goal Maintenance**: LLM-powered sub-goal decomposition and status tracking across turns.
29
- - **🧹 Summary Injection**: Automatically compresses history while ensuring the model never "forgets" the context.
30
- - **🌊 Native Streaming**: Token-by-token completion for smooth, responsive user experiences.
29
+ - **🛡️ Tool Firewall**: Fully integrated ask/accept/deny policy layer for secure tool execution — fail-safe by design.
30
+ - **🎯 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.
31
+ - **🧹 Context Compression**: Sandwich, history, and summary-injection strategies keep long conversations within budget while ensuring the model never "forgets" the context.
32
+ - **🌊 Native Streaming**: `run()` and `stream()` share one ReAct core — `stream()` completes all tool use and verification, then emits the final answer token-by-token.
33
+ - **📚 RAG Connector**: Pluggable `IRAGAdapter` interface plus a bundled in-memory adapter for ingest → query → context injection.
31
34
  - **📊 Observability**: Detailed tracing, token tracking, and structured logging with actionable hints.
32
35
 
33
36
  ## 🚀 Install
@@ -142,13 +145,29 @@ Explore the architecture and advanced capabilities of `lemura` at [lemura.makix.
142
145
 
143
146
  | Export | Description |
144
147
  |---|---|
145
- | `SessionManager` | The main entry point orchestrating the ReAct loop and tools. |
146
- | `ContextManager` | Manages the conversation history using compression strategies. |
147
- | `OpenAICompatibleAdapter` | Reference adapter for OpenAI, Groq, Together, etc. |
148
- | `ToolRegistry` | Registers and executes tools for the agent. |
148
+ | `SessionManager` | The main entry point orchestrating the ReAct loop, tools, goals, and streaming. |
149
+ | `ContextManager` | Manages the conversation history using pluggable compression strategies. |
150
+ | `OpenAICompatibleAdapter` | Reference adapter for OpenAI, Groq, Together, Ollama, and any OpenAI-compatible endpoint. |
151
+ | `ToolRegistry` | Registers, validates, and executes tools with timeout and budget enforcement. |
149
152
  | `SkillInjector` | Loads and formats YAML/Markdown skills into system prompts. |
153
+ | `MCPClient` / `MCPClientRegistry` | Connect to Model Context Protocol servers and register their tools. |
154
+ | `InMemoryRAGAdapter` | Self-contained RAG adapter for testing the ingest → query round-trip. |
150
155
  | `DefaultLogger` | Colorized logger with Problem/Hints metadata support. |
151
156
 
157
+ ### Subpath Exports
158
+
159
+ Each layer is independently importable so consumers only bundle what they use:
160
+
161
+ ```ts
162
+ import { SandwichCompressionStrategy } from 'lemura/context';
163
+ import { OpenAICompatibleAdapter } from 'lemura/adapters';
164
+ import { ToolRegistry } from 'lemura/tools';
165
+ import { SkillInjector } from 'lemura/skills';
166
+ import { InMemoryRAGAdapter } from 'lemura/rag';
167
+ import { MCPClient } from 'lemura/mcp';
168
+ import { DefaultLogger } from 'lemura/logger';
169
+ ```
170
+
152
171
  ## 🪵 Logging and Tracing
153
172
 
154
173
  `lemura` features a premium, structured logging system designed for developer experience. It provides colorized output and actionable hints for errors.
@@ -190,7 +209,7 @@ When an error occurs (like an invalid API key), `lemura` provides beautiful, str
190
209
 
191
210
  ## 🤝 Contributing
192
211
 
193
- We welcome contributions! Please read our [Internal Rules](./.cursor/rules/Project.md) and [Documentation Guidelines](./.cursor/rules/Documentation.md) before submitting a PR.
212
+ We welcome contributions! Please read our [Internal Rules](./.agent/rules/Project.md) and [Documentation Guidelines](./.agent/rules/Documentation.md) before submitting a PR.
194
213
 
195
214
  ## 📄 License
196
215
 
@@ -304,6 +304,24 @@ interface SessionConfig {
304
304
  * @since 1.5.0
305
305
  */
306
306
  enableGoalVerification?: boolean;
307
+ /**
308
+ * Maximum number of goal-verification corrections per run. When verification
309
+ * finds the response incomplete, the agent re-enters the ReAct loop (with full
310
+ * tool access) up to this many times to actually resolve what is missing,
311
+ * rather than emitting a tool-less one-shot rewrite. Defaults to 1.
312
+ * Set to 0 to disable corrective re-entry (a warning is surfaced instead).
313
+ * @since 1.5.4
314
+ */
315
+ maxGoalCorrections?: number;
316
+ /**
317
+ * When true, the agent periodically reconciles which decomposed sub-goals are
318
+ * already complete (one small LLM call every `goalInjectionN` tool rounds) and
319
+ * marks them done, so the re-injected goal block reflects real progress instead
320
+ * of always showing every sub-goal as pending. Counters goal drift on long runs.
321
+ * Defaults to false (no extra calls). Requires `enableGoalPlanning`.
322
+ * @since 1.5.4
323
+ */
324
+ goalProgressReconciliation?: boolean;
307
325
  goalInjectionFrequency?: 'always' | 'every_N_turns' | 'on_compression';
308
326
  goalInjectionPosition?: 'system_prompt' | 'pre_turn';
309
327
  /** Skill budget — max tokens the skill injection block may consume */
@@ -304,6 +304,24 @@ interface SessionConfig {
304
304
  * @since 1.5.0
305
305
  */
306
306
  enableGoalVerification?: boolean;
307
+ /**
308
+ * Maximum number of goal-verification corrections per run. When verification
309
+ * finds the response incomplete, the agent re-enters the ReAct loop (with full
310
+ * tool access) up to this many times to actually resolve what is missing,
311
+ * rather than emitting a tool-less one-shot rewrite. Defaults to 1.
312
+ * Set to 0 to disable corrective re-entry (a warning is surfaced instead).
313
+ * @since 1.5.4
314
+ */
315
+ maxGoalCorrections?: number;
316
+ /**
317
+ * When true, the agent periodically reconciles which decomposed sub-goals are
318
+ * already complete (one small LLM call every `goalInjectionN` tool rounds) and
319
+ * marks them done, so the re-injected goal block reflects real progress instead
320
+ * of always showing every sub-goal as pending. Counters goal drift on long runs.
321
+ * Defaults to false (no extra calls). Requires `enableGoalPlanning`.
322
+ * @since 1.5.4
323
+ */
324
+ goalProgressReconciliation?: boolean;
307
325
  goalInjectionFrequency?: 'always' | 'every_N_turns' | 'on_compression';
308
326
  goalInjectionPosition?: 'system_prompt' | 'pre_turn';
309
327
  /** Skill budget — max tokens the skill injection block may consume */
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-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';
3
+ import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-UBaqufhp.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-UBaqufhp.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';
@@ -365,6 +365,15 @@ declare class SessionManager {
365
365
  * manually set via `setGoal()`.
366
366
  */
367
367
  private _runMiniPlanningStep;
368
+ /**
369
+ * Reconciles sub-goal completion against the recent conversation so the
370
+ * re-injected goal block reflects real progress (anti-drift). Runs only when
371
+ * `goalProgressReconciliation` is enabled, and is a no-op when there are no
372
+ * pending sub-goals. One small, non-fatal LLM call; failures are swallowed.
373
+ *
374
+ * @since 1.5.4
375
+ */
376
+ private _reconcileSubGoals;
368
377
  /** Builds the system prompt, injecting skills and goal if configured. */
369
378
  private buildSystemPrompt;
370
379
  /** Builds the messages array for the provider from the current context. */
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-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';
3
+ import { S as SessionConfig, G as Goal, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-CknicweT.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-CknicweT.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';
@@ -365,6 +365,15 @@ declare class SessionManager {
365
365
  * manually set via `setGoal()`.
366
366
  */
367
367
  private _runMiniPlanningStep;
368
+ /**
369
+ * Reconciles sub-goal completion against the recent conversation so the
370
+ * re-injected goal block reflects real progress (anti-drift). Runs only when
371
+ * `goalProgressReconciliation` is enabled, and is a no-op when there are no
372
+ * pending sub-goals. One small, non-fatal LLM call; failures are swallowed.
373
+ *
374
+ * @since 1.5.4
375
+ */
376
+ private _reconcileSubGoals;
368
377
  /** Builds the system prompt, injecting skills and goal if configured. */
369
378
  private buildSystemPrompt;
370
379
  /** Builds the messages array for the provider from the current context. */
package/dist/index.js CHANGED
@@ -3028,6 +3028,64 @@ Respond ONLY with valid JSON (no markdown, no explanations):
3028
3028
  this.logger.warn(`[GoalInjector] Mini-planning step failed: ${err.message ?? String(err)}`);
3029
3029
  }
3030
3030
  }
3031
+ /**
3032
+ * Reconciles sub-goal completion against the recent conversation so the
3033
+ * re-injected goal block reflects real progress (anti-drift). Runs only when
3034
+ * `goalProgressReconciliation` is enabled, and is a no-op when there are no
3035
+ * pending sub-goals. One small, non-fatal LLM call; failures are swallowed.
3036
+ *
3037
+ * @since 1.5.4
3038
+ */
3039
+ async _reconcileSubGoals() {
3040
+ if (!this.config.goalProgressReconciliation) return;
3041
+ if (!this.goalInjector) return;
3042
+ const goal = this.goalInjector.getGoal();
3043
+ const completed = new Set(goal.completedSubGoals ?? []);
3044
+ const pending = (goal.decomposition ?? []).filter((sg) => !completed.has(sg));
3045
+ if (pending.length === 0) return;
3046
+ const recentTurns = this.context.turns.slice(-6).map((t) => {
3047
+ const text = typeof t.content === "string" ? t.content : JSON.stringify(t.content);
3048
+ return `[${t.role}]: ${text.slice(0, 400)}`;
3049
+ }).join("\n\n");
3050
+ const pendingList = pending.map((sg, i) => `${i + 1}. ${sg}`).join("\n");
3051
+ try {
3052
+ const response = await this.adapter.complete({
3053
+ model: this.config.model,
3054
+ temperature: 0,
3055
+ maxTokens: 256,
3056
+ messages: [
3057
+ {
3058
+ role: "system",
3059
+ content: 'You track sub-goal progress. Given pending sub-goals and the recent conversation, return ONLY the 1-based indices of sub-goals that are now fully completed. Respond with valid JSON only, no prose: {"completed": number[]}'
3060
+ },
3061
+ {
3062
+ role: "user",
3063
+ content: `Pending sub-goals:
3064
+ ${pendingList}
3065
+
3066
+ Recent conversation:
3067
+ ${recentTurns}
3068
+
3069
+ Which pending sub-goals are now fully completed?`
3070
+ }
3071
+ ]
3072
+ });
3073
+ const match = response.content.match(/\{[\s\S]*\}/);
3074
+ if (!match) return;
3075
+ const parsed = JSON.parse(match[0]);
3076
+ if (!Array.isArray(parsed.completed)) return;
3077
+ for (const idx of parsed.completed) {
3078
+ const sg = pending[idx - 1];
3079
+ if (sg) {
3080
+ this.goalInjector.markSubGoalDone(sg);
3081
+ this.emitTrace("planning", "subgoal_done", { subGoal: sg });
3082
+ }
3083
+ }
3084
+ this.context.metadata["goal"] = this.goalInjector.getGoal();
3085
+ } catch (err) {
3086
+ this.logger.warn(`[GoalInjector] Sub-goal reconciliation failed (non-fatal): ${err.message ?? String(err)}`);
3087
+ }
3088
+ }
3031
3089
  // -----------------------------------------------------------------------
3032
3090
  // Internal helpers
3033
3091
  // -----------------------------------------------------------------------
@@ -3492,6 +3550,7 @@ ${blocks.join("\n\n")}
3492
3550
  this.iterations = 0;
3493
3551
  this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
3494
3552
  const maxCompletionTokens = this.config.maxCompletionTokens ?? 4e3;
3553
+ let correctionsRemaining = this.config.maxGoalCorrections ?? 1;
3495
3554
  while (this.iterations < maxIts) {
3496
3555
  this.iterations++;
3497
3556
  this.logger.debug(`[stream] ReAct Iteration ${this.iterations}/${maxIts}`);
@@ -3553,6 +3612,9 @@ ${blocks.join("\n\n")}
3553
3612
  if (this.config.onTurn) this.config.onTurn(toolTurn);
3554
3613
  }
3555
3614
  if (this.goalInjector) this.goalInjector.incrementTurn();
3615
+ if (this.config.goalProgressReconciliation && this.iterations % (this.config.goalInjectionN ?? 3) === 0) {
3616
+ await this._reconcileSubGoals();
3617
+ }
3556
3618
  continue;
3557
3619
  }
3558
3620
  this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
@@ -3571,7 +3633,6 @@ ${blocks.join("\n\n")}
3571
3633
  if (chunk.delta) {
3572
3634
  accumulated += chunk.delta;
3573
3635
  finalTokenCount += Math.ceil(chunk.delta.length / 4);
3574
- yield chunk.delta;
3575
3636
  }
3576
3637
  if (chunk.finished) {
3577
3638
  finalFinishReason = chunk.finishReason;
@@ -3594,62 +3655,49 @@ ${blocks.join("\n\n")}
3594
3655
  if (this.config.onTurn) this.config.onTurn(finalTurn);
3595
3656
  if (finalFinishReason === "stop") {
3596
3657
  const verdict = await this._verifyGoal(this.context.turns);
3597
- if (verdict && !verdict.achieved && verdict.missing) {
3598
- this.logger.info(`[GoalVerifier] Incomplete \u2014 running silent correction: "${verdict.missing}"`);
3658
+ if (verdict && !verdict.achieved && verdict.missing && correctionsRemaining > 0 && this.iterations < maxIts) {
3659
+ correctionsRemaining--;
3660
+ this.logger.info(`[GoalVerifier] Incomplete \u2014 re-entering loop with tools to correct: "${verdict.missing}" (${correctionsRemaining} correction(s) left)`);
3599
3661
  this.emitTrace("verification", "goal_correction_start", {
3600
3662
  missing: verdict.missing,
3601
- reason: verdict.reason
3663
+ reason: verdict.reason,
3664
+ correctionsRemaining
3602
3665
  });
3603
- try {
3604
- const corrMsgs = this.buildMessages(this.buildSystemPrompt(verdict.missing, this.iterations), this.iterations);
3605
- corrMsgs.push({ role: "user", content: verdict.missing });
3606
- const correction = await this.adapter.complete({
3607
- model: this.config.model,
3608
- messages: corrMsgs,
3609
- maxTokens: maxCompletionTokens
3610
- });
3611
- if (correction.content) {
3612
- this.context.turns.push({
3613
- role: "assistant",
3614
- content: correction.content,
3615
- tokenCount: correction.usage?.completionTokens ?? this.adapter.estimateTokens(correction.content),
3616
- turnIndex: this.context.turns.length,
3617
- compressed: false
3618
- });
3619
- this.emitTrace("verification", "goal_correction_done", {
3620
- missing: verdict.missing,
3621
- correctionTokens: correction.usage?.completionTokens
3622
- });
3623
- }
3624
- } catch (err) {
3625
- const errMsg = err.message;
3626
- this.logger.warn(`[GoalVerifier] Correction failed (non-fatal): ${errMsg}`);
3627
- this.emitTrace("error", "goal_correction_failed", { error: errMsg }, null, null, "error");
3628
- }
3629
- const finalVerdict = await this._verifyGoal(this.context.turns);
3630
- if (finalVerdict && !finalVerdict.achieved) {
3631
- this.logger.warn(`[GoalVerifier] Final verification failed: ${finalVerdict.reason}`);
3632
- this.emitTrace("verification", "goal_verification_result", {
3633
- achieved: false,
3634
- reason: finalVerdict.reason,
3635
- missing: finalVerdict.missing
3636
- }, null, null, "error");
3637
- const warningBlock = `
3666
+ const directive = `[Goal verification found the previous response incomplete. Continue working \u2014 use tools as needed \u2014 to address what is still missing, then provide the complete final answer.]
3667
+
3668
+ Still missing: ${verdict.missing}`;
3669
+ this.context.turns.push({
3670
+ role: "user",
3671
+ content: directive,
3672
+ tokenCount: this.adapter.estimateTokens(directive),
3673
+ turnIndex: this.context.turns.length,
3674
+ compressed: false
3675
+ });
3676
+ if (this.goalInjector) this.goalInjector.incrementTurn();
3677
+ continue;
3678
+ }
3679
+ if (verdict && !verdict.achieved) {
3680
+ this.logger.warn(`[GoalVerifier] Goal still unmet after corrections: ${verdict.reason}`);
3681
+ this.emitTrace("verification", "goal_verification_result", {
3682
+ achieved: false,
3683
+ reason: verdict.reason,
3684
+ missing: verdict.missing
3685
+ }, null, null, "error");
3686
+ const warningBlock = `
3638
3687
 
3639
3688
  ---
3640
3689
 
3641
3690
  \u26A0\uFE0F **Goal Verification Warning**
3642
3691
  * **Status:** Success criteria not fully met.
3643
- * **Reason:** ${finalVerdict.reason ?? "Unknown"}
3644
- * **Missing:** ${finalVerdict.missing ?? "Not specified"}
3692
+ * **Reason:** ${verdict.reason ?? "Unknown"}
3693
+ * **Missing:** ${verdict.missing ?? "Not specified"}
3645
3694
 
3646
3695
  `;
3647
- yield warningBlock;
3648
- const lastTurn = [...this.context.turns].reverse().find((t) => t.role === "assistant");
3649
- if (lastTurn) lastTurn.content = lastTurn.content + warningBlock;
3650
- }
3696
+ accumulated += warningBlock;
3697
+ finalTurn.content = accumulated;
3651
3698
  }
3652
3699
  }
3700
+ if (accumulated) yield accumulated;
3653
3701
  this.logger.info(`[stream] Streaming run completed`);
3654
3702
  return;
3655
3703
  }
@@ -3699,7 +3747,7 @@ ${blocks.join("\n\n")}
3699
3747
  this.iterations = 0;
3700
3748
  this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
3701
3749
  const maxCompletionTokens = this.config.maxCompletionTokens ?? 4e3;
3702
- let goalVerificationDone = false;
3750
+ let correctionsRemaining = this.config.maxGoalCorrections ?? 1;
3703
3751
  while (this.iterations < maxIts) {
3704
3752
  this.iterations++;
3705
3753
  this.logger.debug(`[${opts.label}] ReAct Iteration ${this.iterations}/${maxIts}`);
@@ -3834,6 +3882,9 @@ ${blocks.join("\n\n")}
3834
3882
  if (this.config.onTurn) this.config.onTurn(toolTurn);
3835
3883
  }
3836
3884
  if (this.goalInjector) this.goalInjector.incrementTurn();
3885
+ if (this.config.goalProgressReconciliation && this.iterations % (this.config.goalInjectionN ?? 3) === 0) {
3886
+ await this._reconcileSubGoals();
3887
+ }
3837
3888
  continue;
3838
3889
  }
3839
3890
  if (response.finishReason === "stop" || response.finishReason === "max_tokens" || response.finishReason === "error") {
@@ -3846,52 +3897,35 @@ ${blocks.join("\n\n")}
3846
3897
  };
3847
3898
  this.context.turns.push(finalTurn);
3848
3899
  if (this.config.onTurn) this.config.onTurn(finalTurn);
3849
- if (response.finishReason === "stop" && !goalVerificationDone) {
3850
- goalVerificationDone = true;
3900
+ if (response.finishReason === "stop") {
3851
3901
  const verdict = await this._verifyGoal(this.context.turns);
3852
- if (verdict && !verdict.achieved && verdict.missing) {
3853
- this.logger.info(`[GoalVerifier] Incomplete \u2014 running silent correction: "${verdict.missing}"`);
3902
+ if (verdict && !verdict.achieved && verdict.missing && correctionsRemaining > 0 && this.iterations < maxIts) {
3903
+ correctionsRemaining--;
3904
+ this.logger.info(`[GoalVerifier] Incomplete \u2014 re-entering loop with tools to correct: "${verdict.missing}" (${correctionsRemaining} correction(s) left)`);
3854
3905
  this.emitTrace("verification", "goal_correction_start", {
3855
3906
  missing: verdict.missing,
3856
- reason: verdict.reason
3907
+ reason: verdict.reason,
3908
+ correctionsRemaining
3857
3909
  });
3858
- try {
3859
- const correctionMessages = this.buildMessages(
3860
- this.buildSystemPrompt(verdict.missing, this.iterations),
3861
- this.iterations
3862
- );
3863
- correctionMessages.push({ role: "user", content: verdict.missing });
3864
- const correction = await this.adapter.complete({
3865
- model: this.config.model,
3866
- messages: correctionMessages,
3867
- maxTokens: maxCompletionTokens
3868
- });
3869
- if (correction.content) {
3870
- this.context.turns.push({
3871
- role: "assistant",
3872
- content: correction.content,
3873
- tokenCount: correction.usage?.completionTokens ?? this.adapter.estimateTokens(correction.content),
3874
- turnIndex: this.context.turns.length,
3875
- compressed: false
3876
- });
3877
- this.emitTrace("verification", "goal_correction_done", {
3878
- missing: verdict.missing,
3879
- correctionTokens: correction.usage?.completionTokens
3880
- });
3881
- }
3882
- } catch (err) {
3883
- const errMsg = err.message;
3884
- this.logger.warn(`[GoalVerifier] Correction failed (non-fatal): ${errMsg}`);
3885
- this.emitTrace("error", "goal_correction_failed", { error: errMsg }, null, null, "error");
3886
- }
3910
+ const directive = `[Goal verification found the previous response incomplete. Continue working \u2014 use tools as needed \u2014 to address what is still missing, then provide the complete final answer.]
3911
+
3912
+ Still missing: ${verdict.missing}`;
3913
+ this.context.turns.push({
3914
+ role: "user",
3915
+ content: directive,
3916
+ tokenCount: this.adapter.estimateTokens(directive),
3917
+ turnIndex: this.context.turns.length,
3918
+ compressed: false
3919
+ });
3920
+ if (this.goalInjector) this.goalInjector.incrementTurn();
3921
+ continue;
3887
3922
  }
3888
- const finalVerdict = await this._verifyGoal(this.context.turns);
3889
- if (finalVerdict && !finalVerdict.achieved) {
3890
- this.logger.warn(`[GoalVerifier] Final verification failed: ${finalVerdict.reason}`);
3923
+ if (verdict && !verdict.achieved) {
3924
+ this.logger.warn(`[GoalVerifier] Goal still unmet after corrections: ${verdict.reason}`);
3891
3925
  this.emitTrace("verification", "goal_verification_result", {
3892
3926
  achieved: false,
3893
- reason: finalVerdict.reason,
3894
- missing: finalVerdict.missing
3927
+ reason: verdict.reason,
3928
+ missing: verdict.missing
3895
3929
  }, null, null, "error");
3896
3930
  const warningBlock = `
3897
3931
 
@@ -3899,8 +3933,8 @@ ${blocks.join("\n\n")}
3899
3933
 
3900
3934
  \u26A0\uFE0F **Goal Verification Warning**
3901
3935
  * **Status:** Success criteria not fully met.
3902
- * **Reason:** ${finalVerdict.reason ?? "Unknown"}
3903
- * **Missing:** ${finalVerdict.missing ?? "Not specified"}
3936
+ * **Reason:** ${verdict.reason ?? "Unknown"}
3937
+ * **Missing:** ${verdict.missing ?? "Not specified"}
3904
3938
 
3905
3939
  `;
3906
3940
  const lastTurn = [...this.context.turns].reverse().find((t) => t.role === "assistant");