lemura 1.5.2 → 1.5.4
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 +18 -0
- package/README.md +28 -9
- package/dist/{agent-Cq_oRvoc.d.ts → agent-CknicweT.d.ts} +25 -2
- package/dist/{agent-DPSUNlK6.d.mts → agent-UBaqufhp.d.mts} +25 -2
- package/dist/index.d.mts +11 -2
- package/dist/index.d.ts +11 -2
- package/dist/index.js +132 -88
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +132 -88
- package/dist/index.mjs.map +1 -1
- package/dist/tools/index.d.mts +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/types/index.d.mts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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.4] - 2026-05-30
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **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()`.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- **`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).
|
|
17
|
+
- **`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.
|
|
18
|
+
|
|
19
|
+
## [1.5.3] - 2026-05-30
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- **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.
|
|
24
|
+
- `ToolFirewallConfig.onAsk` now accepts `'accept' | 'deny' | boolean | void` (and the `Promise` thereof) for ergonomics. The previous `'accept' | 'deny'` return remains valid.
|
|
25
|
+
|
|
8
26
|
## [1.5.2] - 2026-05-30
|
|
9
27
|
|
|
10
28
|
### Changed
|
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
|
|
29
|
-
- **🧹
|
|
30
|
-
- **🌊 Native Streaming**:
|
|
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
|
|
146
|
-
| `ContextManager` | Manages the conversation history using compression strategies. |
|
|
147
|
-
| `OpenAICompatibleAdapter` | Reference adapter for OpenAI, Groq, Together,
|
|
148
|
-
| `ToolRegistry` | Registers and executes tools
|
|
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](./.
|
|
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
|
|
|
@@ -199,9 +199,14 @@ interface ToolFirewallConfig {
|
|
|
199
199
|
rules?: ToolFirewallRule[];
|
|
200
200
|
/**
|
|
201
201
|
* Called when a tool hits the 'ask' decision.
|
|
202
|
-
*
|
|
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.
|
|
@@ -299,6 +304,24 @@ interface SessionConfig {
|
|
|
299
304
|
* @since 1.5.0
|
|
300
305
|
*/
|
|
301
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;
|
|
302
325
|
goalInjectionFrequency?: 'always' | 'every_N_turns' | 'on_compression';
|
|
303
326
|
goalInjectionPosition?: 'system_prompt' | 'pre_turn';
|
|
304
327
|
/** Skill budget — max tokens the skill injection block may consume */
|
|
@@ -199,9 +199,14 @@ interface ToolFirewallConfig {
|
|
|
199
199
|
rules?: ToolFirewallRule[];
|
|
200
200
|
/**
|
|
201
201
|
* Called when a tool hits the 'ask' decision.
|
|
202
|
-
*
|
|
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.
|
|
@@ -299,6 +304,24 @@ interface SessionConfig {
|
|
|
299
304
|
* @since 1.5.0
|
|
300
305
|
*/
|
|
301
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;
|
|
302
325
|
goalInjectionFrequency?: 'always' | 'every_N_turns' | 'on_compression';
|
|
303
326
|
goalInjectionPosition?: 'system_prompt' | 'pre_turn';
|
|
304
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
// -----------------------------------------------------------------------
|
|
@@ -3260,8 +3318,17 @@ ${blocks.join("\n\n")}
|
|
|
3260
3318
|
}
|
|
3261
3319
|
if (firewall.decision === "ask") {
|
|
3262
3320
|
if (this.config.toolFirewall?.onAsk) {
|
|
3263
|
-
|
|
3264
|
-
|
|
3321
|
+
let accepted = false;
|
|
3322
|
+
try {
|
|
3323
|
+
const userDecision = await this.config.toolFirewall.onAsk(toolName, argsJson);
|
|
3324
|
+
accepted = userDecision === "accept" || userDecision === true;
|
|
3325
|
+
} catch (e) {
|
|
3326
|
+
this.logger.warn(`Tool firewall onAsk handler threw \u2014 treating as deny: ${toolName}`, {
|
|
3327
|
+
error: e instanceof Error ? e.message : String(e)
|
|
3328
|
+
});
|
|
3329
|
+
accepted = false;
|
|
3330
|
+
}
|
|
3331
|
+
if (!accepted) {
|
|
3265
3332
|
this.logger.warn(`Tool blocked by firewall (ask \u2192 deny): ${toolName}`, { reason: firewall.reason });
|
|
3266
3333
|
toolResults.push({ toolCallId, content: `Blocked by tool firewall: ${firewall.reason}` });
|
|
3267
3334
|
return false;
|
|
@@ -3483,6 +3550,7 @@ ${blocks.join("\n\n")}
|
|
|
3483
3550
|
this.iterations = 0;
|
|
3484
3551
|
this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
|
|
3485
3552
|
const maxCompletionTokens = this.config.maxCompletionTokens ?? 4e3;
|
|
3553
|
+
let correctionsRemaining = this.config.maxGoalCorrections ?? 1;
|
|
3486
3554
|
while (this.iterations < maxIts) {
|
|
3487
3555
|
this.iterations++;
|
|
3488
3556
|
this.logger.debug(`[stream] ReAct Iteration ${this.iterations}/${maxIts}`);
|
|
@@ -3544,6 +3612,9 @@ ${blocks.join("\n\n")}
|
|
|
3544
3612
|
if (this.config.onTurn) this.config.onTurn(toolTurn);
|
|
3545
3613
|
}
|
|
3546
3614
|
if (this.goalInjector) this.goalInjector.incrementTurn();
|
|
3615
|
+
if (this.config.goalProgressReconciliation && this.iterations % (this.config.goalInjectionN ?? 3) === 0) {
|
|
3616
|
+
await this._reconcileSubGoals();
|
|
3617
|
+
}
|
|
3547
3618
|
continue;
|
|
3548
3619
|
}
|
|
3549
3620
|
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
@@ -3585,60 +3656,47 @@ ${blocks.join("\n\n")}
|
|
|
3585
3656
|
if (this.config.onTurn) this.config.onTurn(finalTurn);
|
|
3586
3657
|
if (finalFinishReason === "stop") {
|
|
3587
3658
|
const verdict = await this._verifyGoal(this.context.turns);
|
|
3588
|
-
if (verdict && !verdict.achieved && verdict.missing) {
|
|
3589
|
-
|
|
3659
|
+
if (verdict && !verdict.achieved && verdict.missing && correctionsRemaining > 0 && this.iterations < maxIts) {
|
|
3660
|
+
correctionsRemaining--;
|
|
3661
|
+
this.logger.info(`[GoalVerifier] Incomplete \u2014 re-entering loop with tools to correct: "${verdict.missing}" (${correctionsRemaining} correction(s) left)`);
|
|
3590
3662
|
this.emitTrace("verification", "goal_correction_start", {
|
|
3591
3663
|
missing: verdict.missing,
|
|
3592
|
-
reason: verdict.reason
|
|
3664
|
+
reason: verdict.reason,
|
|
3665
|
+
correctionsRemaining
|
|
3593
3666
|
});
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
} catch (err) {
|
|
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 = `
|
|
3667
|
+
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.]
|
|
3668
|
+
|
|
3669
|
+
Still missing: ${verdict.missing}`;
|
|
3670
|
+
this.context.turns.push({
|
|
3671
|
+
role: "user",
|
|
3672
|
+
content: directive,
|
|
3673
|
+
tokenCount: this.adapter.estimateTokens(directive),
|
|
3674
|
+
turnIndex: this.context.turns.length,
|
|
3675
|
+
compressed: false
|
|
3676
|
+
});
|
|
3677
|
+
if (this.goalInjector) this.goalInjector.incrementTurn();
|
|
3678
|
+
continue;
|
|
3679
|
+
}
|
|
3680
|
+
if (verdict && !verdict.achieved) {
|
|
3681
|
+
this.logger.warn(`[GoalVerifier] Goal still unmet after corrections: ${verdict.reason}`);
|
|
3682
|
+
this.emitTrace("verification", "goal_verification_result", {
|
|
3683
|
+
achieved: false,
|
|
3684
|
+
reason: verdict.reason,
|
|
3685
|
+
missing: verdict.missing
|
|
3686
|
+
}, null, null, "error");
|
|
3687
|
+
const warningBlock = `
|
|
3629
3688
|
|
|
3630
3689
|
---
|
|
3631
3690
|
|
|
3632
3691
|
\u26A0\uFE0F **Goal Verification Warning**
|
|
3633
3692
|
* **Status:** Success criteria not fully met.
|
|
3634
|
-
* **Reason:** ${
|
|
3635
|
-
* **Missing:** ${
|
|
3693
|
+
* **Reason:** ${verdict.reason ?? "Unknown"}
|
|
3694
|
+
* **Missing:** ${verdict.missing ?? "Not specified"}
|
|
3636
3695
|
|
|
3637
3696
|
`;
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
}
|
|
3697
|
+
yield warningBlock;
|
|
3698
|
+
const lastTurn = [...this.context.turns].reverse().find((t) => t.role === "assistant");
|
|
3699
|
+
if (lastTurn) lastTurn.content = lastTurn.content + warningBlock;
|
|
3642
3700
|
}
|
|
3643
3701
|
}
|
|
3644
3702
|
this.logger.info(`[stream] Streaming run completed`);
|
|
@@ -3690,7 +3748,7 @@ ${blocks.join("\n\n")}
|
|
|
3690
3748
|
this.iterations = 0;
|
|
3691
3749
|
this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
|
|
3692
3750
|
const maxCompletionTokens = this.config.maxCompletionTokens ?? 4e3;
|
|
3693
|
-
let
|
|
3751
|
+
let correctionsRemaining = this.config.maxGoalCorrections ?? 1;
|
|
3694
3752
|
while (this.iterations < maxIts) {
|
|
3695
3753
|
this.iterations++;
|
|
3696
3754
|
this.logger.debug(`[${opts.label}] ReAct Iteration ${this.iterations}/${maxIts}`);
|
|
@@ -3825,6 +3883,9 @@ ${blocks.join("\n\n")}
|
|
|
3825
3883
|
if (this.config.onTurn) this.config.onTurn(toolTurn);
|
|
3826
3884
|
}
|
|
3827
3885
|
if (this.goalInjector) this.goalInjector.incrementTurn();
|
|
3886
|
+
if (this.config.goalProgressReconciliation && this.iterations % (this.config.goalInjectionN ?? 3) === 0) {
|
|
3887
|
+
await this._reconcileSubGoals();
|
|
3888
|
+
}
|
|
3828
3889
|
continue;
|
|
3829
3890
|
}
|
|
3830
3891
|
if (response.finishReason === "stop" || response.finishReason === "max_tokens" || response.finishReason === "error") {
|
|
@@ -3837,52 +3898,35 @@ ${blocks.join("\n\n")}
|
|
|
3837
3898
|
};
|
|
3838
3899
|
this.context.turns.push(finalTurn);
|
|
3839
3900
|
if (this.config.onTurn) this.config.onTurn(finalTurn);
|
|
3840
|
-
if (response.finishReason === "stop"
|
|
3841
|
-
goalVerificationDone = true;
|
|
3901
|
+
if (response.finishReason === "stop") {
|
|
3842
3902
|
const verdict = await this._verifyGoal(this.context.turns);
|
|
3843
|
-
if (verdict && !verdict.achieved && verdict.missing) {
|
|
3844
|
-
|
|
3903
|
+
if (verdict && !verdict.achieved && verdict.missing && correctionsRemaining > 0 && this.iterations < maxIts) {
|
|
3904
|
+
correctionsRemaining--;
|
|
3905
|
+
this.logger.info(`[GoalVerifier] Incomplete \u2014 re-entering loop with tools to correct: "${verdict.missing}" (${correctionsRemaining} correction(s) left)`);
|
|
3845
3906
|
this.emitTrace("verification", "goal_correction_start", {
|
|
3846
3907
|
missing: verdict.missing,
|
|
3847
|
-
reason: verdict.reason
|
|
3908
|
+
reason: verdict.reason,
|
|
3909
|
+
correctionsRemaining
|
|
3848
3910
|
});
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
this.context.turns.push({
|
|
3862
|
-
role: "assistant",
|
|
3863
|
-
content: correction.content,
|
|
3864
|
-
tokenCount: correction.usage?.completionTokens ?? this.adapter.estimateTokens(correction.content),
|
|
3865
|
-
turnIndex: this.context.turns.length,
|
|
3866
|
-
compressed: false
|
|
3867
|
-
});
|
|
3868
|
-
this.emitTrace("verification", "goal_correction_done", {
|
|
3869
|
-
missing: verdict.missing,
|
|
3870
|
-
correctionTokens: correction.usage?.completionTokens
|
|
3871
|
-
});
|
|
3872
|
-
}
|
|
3873
|
-
} catch (err) {
|
|
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");
|
|
3877
|
-
}
|
|
3911
|
+
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.]
|
|
3912
|
+
|
|
3913
|
+
Still missing: ${verdict.missing}`;
|
|
3914
|
+
this.context.turns.push({
|
|
3915
|
+
role: "user",
|
|
3916
|
+
content: directive,
|
|
3917
|
+
tokenCount: this.adapter.estimateTokens(directive),
|
|
3918
|
+
turnIndex: this.context.turns.length,
|
|
3919
|
+
compressed: false
|
|
3920
|
+
});
|
|
3921
|
+
if (this.goalInjector) this.goalInjector.incrementTurn();
|
|
3922
|
+
continue;
|
|
3878
3923
|
}
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
this.logger.warn(`[GoalVerifier] Final verification failed: ${finalVerdict.reason}`);
|
|
3924
|
+
if (verdict && !verdict.achieved) {
|
|
3925
|
+
this.logger.warn(`[GoalVerifier] Goal still unmet after corrections: ${verdict.reason}`);
|
|
3882
3926
|
this.emitTrace("verification", "goal_verification_result", {
|
|
3883
3927
|
achieved: false,
|
|
3884
|
-
reason:
|
|
3885
|
-
missing:
|
|
3928
|
+
reason: verdict.reason,
|
|
3929
|
+
missing: verdict.missing
|
|
3886
3930
|
}, null, null, "error");
|
|
3887
3931
|
const warningBlock = `
|
|
3888
3932
|
|
|
@@ -3890,8 +3934,8 @@ ${blocks.join("\n\n")}
|
|
|
3890
3934
|
|
|
3891
3935
|
\u26A0\uFE0F **Goal Verification Warning**
|
|
3892
3936
|
* **Status:** Success criteria not fully met.
|
|
3893
|
-
* **Reason:** ${
|
|
3894
|
-
* **Missing:** ${
|
|
3937
|
+
* **Reason:** ${verdict.reason ?? "Unknown"}
|
|
3938
|
+
* **Missing:** ${verdict.missing ?? "Not specified"}
|
|
3895
3939
|
|
|
3896
3940
|
`;
|
|
3897
3941
|
const lastTurn = [...this.context.turns].reverse().find((t) => t.role === "assistant");
|