lemura 1.4.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +5 -0
- package/dist/adapters/index.d.mts +1 -1
- package/dist/adapters/index.d.ts +1 -1
- package/dist/{adapters-CVcfWf85.d.ts → adapters-DHQOGl8Y.d.ts} +1 -1
- package/dist/{adapters-BhTAnrOM.d.mts → adapters-S96Ka9wt.d.mts} +1 -1
- package/dist/{agent-81ZUiP-T.d.mts → agent-BefjpRHZ.d.mts} +107 -2
- package/dist/{agent-BH_uE2nT.d.ts → agent-CjoFQGlk.d.ts} +107 -2
- package/dist/context/index.d.mts +2 -2
- package/dist/context/index.d.ts +2 -2
- package/dist/index.d.mts +30 -71
- package/dist/index.d.ts +30 -71
- package/dist/index.js +297 -155
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +297 -155
- package/dist/index.mjs.map +1 -1
- package/dist/tools/index.d.mts +2 -2
- package/dist/tools/index.d.ts +2 -2
- package/dist/types/index.d.mts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { a as IProviderAdapter,
|
|
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-
|
|
3
|
-
import { S as SessionConfig, I as IToolResponseProcessor, T as ToolResponseEvaluation, M as MCPServerConfig, a as MCPToolDefinition } from './agent-
|
|
4
|
-
export { b as
|
|
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';
|
|
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';
|
|
@@ -188,67 +188,6 @@ declare class ContinuationPlanner {
|
|
|
188
188
|
private _advanceIndex;
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
-
interface Goal {
|
|
192
|
-
id: string;
|
|
193
|
-
statement: string;
|
|
194
|
-
/** High-level sub-goals decomposed from the main statement */
|
|
195
|
-
decomposition: string[];
|
|
196
|
-
successCriteria: string[];
|
|
197
|
-
injectionFrequency: 'always' | 'every_N_turns' | 'on_compression';
|
|
198
|
-
injectionPosition: 'system_prompt' | 'pre_turn';
|
|
199
|
-
/** Sub-goals already completed — updated via `markSubGoalDone()` */
|
|
200
|
-
completedSubGoals?: string[];
|
|
201
|
-
}
|
|
202
|
-
/**
|
|
203
|
-
* GoalInjector keeps the original task objective visible throughout the ReAct loop,
|
|
204
|
-
* preventing goal drift after many tool calls and context compressions.
|
|
205
|
-
*
|
|
206
|
-
* Usage in SessionManager:
|
|
207
|
-
* - For `system_prompt` position: call `injectInto(prompt)` which appends the goal block.
|
|
208
|
-
* - For `pre_turn` position: call `getFormattedBlock()` and push as a system message.
|
|
209
|
-
*/
|
|
210
|
-
declare class GoalInjector {
|
|
211
|
-
private goal;
|
|
212
|
-
private turnsSinceInjection;
|
|
213
|
-
constructor(goal: Goal);
|
|
214
|
-
/**
|
|
215
|
-
* Returns the formatted `[CURRENT GOAL]` block string — without caring about
|
|
216
|
-
* where it will be placed. Callers decide whether to append to a system prompt
|
|
217
|
-
* or push as a separate message.
|
|
218
|
-
*/
|
|
219
|
-
getFormattedBlock(): string;
|
|
220
|
-
/**
|
|
221
|
-
* Appends the goal block to the given prompt string (for `system_prompt` position).
|
|
222
|
-
* For `pre_turn` position, use `getFormattedBlock()` directly.
|
|
223
|
-
*
|
|
224
|
-
* @param prompt - The existing system prompt to append to.
|
|
225
|
-
*/
|
|
226
|
-
injectInto(prompt: string): string;
|
|
227
|
-
/**
|
|
228
|
-
* Returns true when the goal should be re-injected this turn,
|
|
229
|
-
* based on `injectionFrequency`.
|
|
230
|
-
*
|
|
231
|
-
* @param turnIndex - The current turn index in the ReAct loop (0-based)
|
|
232
|
-
* @param compressionOccurred - Whether context was compressed this iteration
|
|
233
|
-
* @param injectionN - The N for 'every_N_turns' frequency (default: 3)
|
|
234
|
-
*/
|
|
235
|
-
shouldInjectThisTurn(turnIndex: number, compressionOccurred?: boolean, injectionN?: number): boolean;
|
|
236
|
-
/**
|
|
237
|
-
* Updates the goal with new sub-goal decomposition and success criteria,
|
|
238
|
-
* typically populated by the mini-planning LLM call.
|
|
239
|
-
*/
|
|
240
|
-
updateDecomposition(decomposition: string[], successCriteria?: string[]): void;
|
|
241
|
-
/**
|
|
242
|
-
* Marks a sub-goal as completed so it moves to the "completed" section
|
|
243
|
-
* in subsequent injections.
|
|
244
|
-
*/
|
|
245
|
-
markSubGoalDone(subGoal: string): void;
|
|
246
|
-
/** Returns a snapshot of the current goal state (safe to store in context.metadata). */
|
|
247
|
-
getGoal(): Goal;
|
|
248
|
-
/** Increments the internal turn counter (used for `every_N_turns` frequency). */
|
|
249
|
-
incrementTurn(): void;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
191
|
/**
|
|
253
192
|
* Core entry point for lemura agent sessions.
|
|
254
193
|
*
|
|
@@ -449,9 +388,6 @@ declare class SessionManager {
|
|
|
449
388
|
/**
|
|
450
389
|
* Runs the full ReAct loop for a user message and returns the final assistant response.
|
|
451
390
|
*
|
|
452
|
-
* When `enableGoalPlanning` is true and no goal has been manually set, a mini-planning
|
|
453
|
-
* LLM call is made before the first iteration to decompose the task into sub-goals.
|
|
454
|
-
*
|
|
455
391
|
* @param userMessage - The user's message (string or multimodal content blocks)
|
|
456
392
|
* @returns The assistant's final response string
|
|
457
393
|
* @throws {LemuraMaxIterationsError} When the loop exceeds `maxIterations`
|
|
@@ -460,8 +396,8 @@ declare class SessionManager {
|
|
|
460
396
|
/**
|
|
461
397
|
* Runs the ReAct loop and streams the final assistant response token-by-token.
|
|
462
398
|
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
399
|
+
* All tool calls, goal verification, and corrections complete before any token
|
|
400
|
+
* is yielded — the stream delivers only the clean final response.
|
|
465
401
|
*
|
|
466
402
|
* @param userMessage - The user's message (string or multimodal content blocks)
|
|
467
403
|
* @returns An `AsyncIterable<string>` of delta tokens from the final response
|
|
@@ -474,6 +410,29 @@ declare class SessionManager {
|
|
|
474
410
|
* ```
|
|
475
411
|
*/
|
|
476
412
|
stream(userMessage: string | ContentBlock[]): AsyncIterable<string>;
|
|
413
|
+
/**
|
|
414
|
+
* Core ReAct execution loop shared by `run()` and `stream()`.
|
|
415
|
+
*
|
|
416
|
+
* Uses `adapter.complete()` exclusively — no streaming occurs here.
|
|
417
|
+
* Goal verification and silent corrections run inside this method,
|
|
418
|
+
* fully isolated from the caller's delivery path.
|
|
419
|
+
*
|
|
420
|
+
* @returns The final assistant response string
|
|
421
|
+
* @throws {LemuraMaxIterationsError} When the loop exceeds `maxIterations`
|
|
422
|
+
*/
|
|
423
|
+
private _executeLoop;
|
|
424
|
+
/**
|
|
425
|
+
* Verifies whether the goal was achieved after a `stop` finish.
|
|
426
|
+
*
|
|
427
|
+
* Priority:
|
|
428
|
+
* 1. `config.goalVerifier` callback (Option A — user-supplied)
|
|
429
|
+
* 2. Built-in LLM check against `successCriteria` (Option C — fallback)
|
|
430
|
+
*
|
|
431
|
+
* Returns `null` when verification is skipped (no goal, planning disabled, etc.).
|
|
432
|
+
*
|
|
433
|
+
* @since 1.5.0
|
|
434
|
+
*/
|
|
435
|
+
private _verifyGoal;
|
|
477
436
|
/**
|
|
478
437
|
* Resets the session: clears conversation history, resets iteration counters,
|
|
479
438
|
* tool execution budget tallies, and goal/plan state.
|
|
@@ -687,4 +646,4 @@ declare class MCPClientRegistry {
|
|
|
687
646
|
private _bridge;
|
|
688
647
|
}
|
|
689
648
|
|
|
690
|
-
export { AudioChunk, ContentBlock, ContextWindow, type ContinuationPlan, ContinuationPlanner, type ContinuationStep, FinalResponseFormatter,
|
|
649
|
+
export { AudioChunk, ContentBlock, ContextWindow, type ContinuationPlan, ContinuationPlanner, type ContinuationStep, FinalResponseFormatter, Goal, ILogger, IProviderAdapter, IToolDefinition, IToolResponseProcessor, ImageGenRequest, ImageGenResponse, MCPClient, MCPClientRegistry, MCPServerConfig, MCPToolDefinition, MediaBridge, ModelInfo, SessionConfig, SessionManager, SkillInjector, type StepCondition, StepCounter, type StepVerifier, type StepVerifierResult, SynthesisRequest, ToolRegistry, ToolResponseEvaluation, ToolResponseProcessor, type ToolResponseProcessorConfig, TranscriptionRequest, TranscriptionResponse, Turn, VisionRequest, VisionResponse };
|
package/dist/index.js
CHANGED
|
@@ -3320,9 +3320,6 @@ ${planStatus}`;
|
|
|
3320
3320
|
/**
|
|
3321
3321
|
* Runs the full ReAct loop for a user message and returns the final assistant response.
|
|
3322
3322
|
*
|
|
3323
|
-
* When `enableGoalPlanning` is true and no goal has been manually set, a mini-planning
|
|
3324
|
-
* LLM call is made before the first iteration to decompose the task into sub-goals.
|
|
3325
|
-
*
|
|
3326
3323
|
* @param userMessage - The user's message (string or multimodal content blocks)
|
|
3327
3324
|
* @returns The assistant's final response string
|
|
3328
3325
|
* @throws {LemuraMaxIterationsError} When the loop exceeds `maxIterations`
|
|
@@ -3330,6 +3327,194 @@ ${planStatus}`;
|
|
|
3330
3327
|
async run(userMessage) {
|
|
3331
3328
|
if (this.mcpReady) await this.mcpReady;
|
|
3332
3329
|
await this.ensureScratchpadLoaded();
|
|
3330
|
+
return this._executeLoop(userMessage, { label: "run" });
|
|
3331
|
+
}
|
|
3332
|
+
/**
|
|
3333
|
+
* Runs the ReAct loop and streams the final assistant response token-by-token.
|
|
3334
|
+
*
|
|
3335
|
+
* All tool calls, goal verification, and corrections complete before any token
|
|
3336
|
+
* is yielded — the stream delivers only the clean final response.
|
|
3337
|
+
*
|
|
3338
|
+
* @param userMessage - The user's message (string or multimodal content blocks)
|
|
3339
|
+
* @returns An `AsyncIterable<string>` of delta tokens from the final response
|
|
3340
|
+
*
|
|
3341
|
+
* @example
|
|
3342
|
+
* ```typescript
|
|
3343
|
+
* for await (const token of session.stream('Tell me a story')) {
|
|
3344
|
+
* process.stdout.write(token);
|
|
3345
|
+
* }
|
|
3346
|
+
* ```
|
|
3347
|
+
*/
|
|
3348
|
+
async *stream(userMessage) {
|
|
3349
|
+
if (this.mcpReady) await this.mcpReady;
|
|
3350
|
+
await this.ensureScratchpadLoaded();
|
|
3351
|
+
const userMessageStr = Array.isArray(userMessage) ? "[Multimodal Content]" : userMessage;
|
|
3352
|
+
this.logger.info(`Starting streaming session run`, { model: this.config.model, message: userMessageStr });
|
|
3353
|
+
if (this.config.enableGoalPlanning && !this.goalInjector) {
|
|
3354
|
+
this.goalInjector = new GoalInjector({
|
|
3355
|
+
id: "auto",
|
|
3356
|
+
statement: typeof userMessage === "string" ? userMessage : "[multimodal]",
|
|
3357
|
+
decomposition: [],
|
|
3358
|
+
successCriteria: ["The user request is fully answered"],
|
|
3359
|
+
injectionFrequency: this.config.goalInjectionFrequency ?? "always",
|
|
3360
|
+
injectionPosition: this.config.goalInjectionPosition ?? "system_prompt"
|
|
3361
|
+
});
|
|
3362
|
+
this.context.metadata["goal"] = this.goalInjector.getGoal();
|
|
3363
|
+
await this._runMiniPlanningStep(userMessageStr);
|
|
3364
|
+
}
|
|
3365
|
+
this.context.turns.push({
|
|
3366
|
+
role: "user",
|
|
3367
|
+
content: userMessage,
|
|
3368
|
+
tokenCount: Array.isArray(userMessage) ? userMessage.length * 50 : this.adapter.estimateTokens(userMessage),
|
|
3369
|
+
turnIndex: this.context.turns.length,
|
|
3370
|
+
compressed: false
|
|
3371
|
+
});
|
|
3372
|
+
const maxIts = this.config.maxIterations || 10;
|
|
3373
|
+
this.iterations = 0;
|
|
3374
|
+
this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
|
|
3375
|
+
const maxCompletionTokens = this.config.maxCompletionTokens ?? 4e3;
|
|
3376
|
+
while (this.iterations < maxIts) {
|
|
3377
|
+
this.iterations++;
|
|
3378
|
+
this.logger.debug(`[stream] ReAct Iteration ${this.iterations}/${maxIts}`);
|
|
3379
|
+
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
3380
|
+
this.context = await this.contextManager.prepare(this.context);
|
|
3381
|
+
const systemPrompt = this.buildSystemPrompt(userMessageStr, this.iterations);
|
|
3382
|
+
const messages = this.buildMessages(systemPrompt, this.iterations);
|
|
3383
|
+
if (this.stepCounter.isMaxReached()) {
|
|
3384
|
+
messages.push({
|
|
3385
|
+
role: "system",
|
|
3386
|
+
content: this.stepCounter.getForcedConclusionPrompt() + "\n\n" + FinalResponseFormatter.getRequiredStructure()
|
|
3387
|
+
});
|
|
3388
|
+
}
|
|
3389
|
+
let response;
|
|
3390
|
+
try {
|
|
3391
|
+
response = await this.adapter.complete({
|
|
3392
|
+
model: this.config.model,
|
|
3393
|
+
messages,
|
|
3394
|
+
tools: this.stepCounter.isMaxReached() ? [] : this.toolRegistry.getAll(),
|
|
3395
|
+
maxTokens: maxCompletionTokens
|
|
3396
|
+
});
|
|
3397
|
+
} catch (err) {
|
|
3398
|
+
this.logger.fatal(`Provider call failed: ${err.message}`);
|
|
3399
|
+
throw err;
|
|
3400
|
+
}
|
|
3401
|
+
if (response.finishReason === "tool_call" && response.toolCalls) {
|
|
3402
|
+
this.logger.info(`[stream] Tool calls: ${response.toolCalls.map((tc) => tc.name).join(", ")}`);
|
|
3403
|
+
this.stepCounter.increment(response.toolCalls.length);
|
|
3404
|
+
const toolResults = [];
|
|
3405
|
+
for (const tc of response.toolCalls) {
|
|
3406
|
+
const ok = await this.passesFirewall(tc.name, tc.arguments, tc.id, toolResults);
|
|
3407
|
+
if (!ok) continue;
|
|
3408
|
+
try {
|
|
3409
|
+
toolResults.push({ toolCallId: tc.id, content: await this.executeSingleToolCall(tc) });
|
|
3410
|
+
} catch (e) {
|
|
3411
|
+
toolResults.push({ toolCallId: tc.id, content: `Error: ${e instanceof Error ? e.message : String(e)}` });
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
3414
|
+
const assistantTurn = {
|
|
3415
|
+
role: "assistant",
|
|
3416
|
+
content: response.content || "",
|
|
3417
|
+
tokenCount: this.adapter.estimateTokens(response.content || "") + 50,
|
|
3418
|
+
turnIndex: this.context.turns.length,
|
|
3419
|
+
compressed: false,
|
|
3420
|
+
toolCalls: response.toolCalls
|
|
3421
|
+
};
|
|
3422
|
+
this.context.turns.push(assistantTurn);
|
|
3423
|
+
if (this.config.onTurn) this.config.onTurn(assistantTurn);
|
|
3424
|
+
for (const res of toolResults) {
|
|
3425
|
+
const toolTurn = {
|
|
3426
|
+
role: "tool",
|
|
3427
|
+
content: res.content,
|
|
3428
|
+
tokenCount: this.adapter.estimateTokens(res.content),
|
|
3429
|
+
turnIndex: this.context.turns.length,
|
|
3430
|
+
compressed: false,
|
|
3431
|
+
toolResults: [res]
|
|
3432
|
+
};
|
|
3433
|
+
this.context.turns.push(toolTurn);
|
|
3434
|
+
if (this.config.onTurn) this.config.onTurn(toolTurn);
|
|
3435
|
+
}
|
|
3436
|
+
if (this.goalInjector) this.goalInjector.incrementTurn();
|
|
3437
|
+
continue;
|
|
3438
|
+
}
|
|
3439
|
+
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
3440
|
+
this.context = await this.contextManager.prepare(this.context);
|
|
3441
|
+
const finalSystemPrompt = this.buildSystemPrompt(userMessageStr, this.iterations);
|
|
3442
|
+
const finalMessages = this.buildMessages(finalSystemPrompt, this.iterations);
|
|
3443
|
+
let accumulated = "";
|
|
3444
|
+
let finalTokenCount = 0;
|
|
3445
|
+
let finalFinishReason;
|
|
3446
|
+
for await (const chunk of this.adapter.stream({
|
|
3447
|
+
model: this.config.model,
|
|
3448
|
+
messages: finalMessages,
|
|
3449
|
+
maxTokens: maxCompletionTokens,
|
|
3450
|
+
stream: true
|
|
3451
|
+
})) {
|
|
3452
|
+
if (chunk.delta) {
|
|
3453
|
+
accumulated += chunk.delta;
|
|
3454
|
+
finalTokenCount += Math.ceil(chunk.delta.length / 4);
|
|
3455
|
+
yield chunk.delta;
|
|
3456
|
+
}
|
|
3457
|
+
if (chunk.finished) {
|
|
3458
|
+
finalFinishReason = chunk.finishReason;
|
|
3459
|
+
if (chunk.usage) this.totalTokens += chunk.usage.totalTokens;
|
|
3460
|
+
this.emitTrace("thinking", "llm_stream_finished", {
|
|
3461
|
+
usage: chunk.usage,
|
|
3462
|
+
totalTokens: this.totalTokens,
|
|
3463
|
+
finishReason: chunk.finishReason
|
|
3464
|
+
});
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
const finalTurn = {
|
|
3468
|
+
role: "assistant",
|
|
3469
|
+
content: accumulated,
|
|
3470
|
+
tokenCount: finalTokenCount,
|
|
3471
|
+
turnIndex: this.context.turns.length,
|
|
3472
|
+
compressed: false
|
|
3473
|
+
};
|
|
3474
|
+
this.context.turns.push(finalTurn);
|
|
3475
|
+
if (this.config.onTurn) this.config.onTurn(finalTurn);
|
|
3476
|
+
if (finalFinishReason === "stop") {
|
|
3477
|
+
const verdict = await this._verifyGoal(this.context.turns);
|
|
3478
|
+
if (verdict && !verdict.achieved && verdict.missing) {
|
|
3479
|
+
this.logger.info(`[GoalVerifier] Incomplete \u2014 running silent correction: "${verdict.missing}"`);
|
|
3480
|
+
try {
|
|
3481
|
+
const corrMsgs = this.buildMessages(this.buildSystemPrompt(verdict.missing, this.iterations), this.iterations);
|
|
3482
|
+
corrMsgs.push({ role: "user", content: verdict.missing });
|
|
3483
|
+
const correction = await this.adapter.complete({
|
|
3484
|
+
model: this.config.model,
|
|
3485
|
+
messages: corrMsgs,
|
|
3486
|
+
maxTokens: maxCompletionTokens
|
|
3487
|
+
});
|
|
3488
|
+
if (correction.content) {
|
|
3489
|
+
this.context.turns.push({
|
|
3490
|
+
role: "assistant",
|
|
3491
|
+
content: correction.content,
|
|
3492
|
+
tokenCount: correction.usage?.completionTokens ?? this.adapter.estimateTokens(correction.content),
|
|
3493
|
+
turnIndex: this.context.turns.length,
|
|
3494
|
+
compressed: false
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
} catch (err) {
|
|
3498
|
+
this.logger.warn(`[GoalVerifier] Correction failed (non-fatal): ${err.message}`);
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
this.logger.info(`[stream] Streaming run completed`);
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
throw new LemuraMaxIterationsError(`Exceeded max iterations of ${maxIts}`);
|
|
3506
|
+
}
|
|
3507
|
+
/**
|
|
3508
|
+
* Core ReAct execution loop shared by `run()` and `stream()`.
|
|
3509
|
+
*
|
|
3510
|
+
* Uses `adapter.complete()` exclusively — no streaming occurs here.
|
|
3511
|
+
* Goal verification and silent corrections run inside this method,
|
|
3512
|
+
* fully isolated from the caller's delivery path.
|
|
3513
|
+
*
|
|
3514
|
+
* @returns The final assistant response string
|
|
3515
|
+
* @throws {LemuraMaxIterationsError} When the loop exceeds `maxIterations`
|
|
3516
|
+
*/
|
|
3517
|
+
async _executeLoop(userMessage, opts) {
|
|
3333
3518
|
const userMessageStr = Array.isArray(userMessage) ? "[Multimodal Content]" : userMessage;
|
|
3334
3519
|
this.logger.info(`Starting new session run`, {
|
|
3335
3520
|
model: this.config.model,
|
|
@@ -3363,13 +3548,14 @@ ${planStatus}`;
|
|
|
3363
3548
|
this.iterations = 0;
|
|
3364
3549
|
this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
|
|
3365
3550
|
const maxCompletionTokens = this.config.maxCompletionTokens ?? 4e3;
|
|
3551
|
+
let goalVerificationDone = false;
|
|
3366
3552
|
while (this.iterations < maxIts) {
|
|
3367
3553
|
this.iterations++;
|
|
3368
|
-
this.logger.debug(`ReAct Iteration ${this.iterations}/${maxIts}`);
|
|
3554
|
+
this.logger.debug(`[${opts.label}] ReAct Iteration ${this.iterations}/${maxIts}`);
|
|
3369
3555
|
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
3370
3556
|
this.context = await this.contextManager.prepare(this.context);
|
|
3371
3557
|
const systemPrompt = this.buildSystemPrompt(userMessageStr, this.iterations);
|
|
3372
|
-
|
|
3558
|
+
const messages = this.buildMessages(systemPrompt, this.iterations);
|
|
3373
3559
|
if (this.stepCounter.isMaxReached()) {
|
|
3374
3560
|
this.logger.warn(`maxSteps (${this.config.maxSteps ?? 20}) reached \u2014 forcing final response`);
|
|
3375
3561
|
messages.push({
|
|
@@ -3402,9 +3588,7 @@ ${planStatus}`;
|
|
|
3402
3588
|
this.emitTrace("error", "llm_call_failed", { error: e.message ?? String(err) });
|
|
3403
3589
|
throw err;
|
|
3404
3590
|
}
|
|
3405
|
-
if (response.usage)
|
|
3406
|
-
this.totalTokens += response.usage.totalTokens;
|
|
3407
|
-
}
|
|
3591
|
+
if (response.usage) this.totalTokens += response.usage.totalTokens;
|
|
3408
3592
|
this.emitTrace("thinking", "llm_call", {
|
|
3409
3593
|
model: this.config.model,
|
|
3410
3594
|
usage: response.usage,
|
|
@@ -3499,7 +3683,37 @@ ${planStatus}`;
|
|
|
3499
3683
|
};
|
|
3500
3684
|
this.context.turns.push(finalTurn);
|
|
3501
3685
|
if (this.config.onTurn) this.config.onTurn(finalTurn);
|
|
3502
|
-
|
|
3686
|
+
if (response.finishReason === "stop" && !goalVerificationDone) {
|
|
3687
|
+
goalVerificationDone = true;
|
|
3688
|
+
const verdict = await this._verifyGoal(this.context.turns);
|
|
3689
|
+
if (verdict && !verdict.achieved && verdict.missing) {
|
|
3690
|
+
this.logger.info(`[GoalVerifier] Incomplete \u2014 running silent correction: "${verdict.missing}"`);
|
|
3691
|
+
try {
|
|
3692
|
+
const correctionMessages = this.buildMessages(
|
|
3693
|
+
this.buildSystemPrompt(verdict.missing, this.iterations),
|
|
3694
|
+
this.iterations
|
|
3695
|
+
);
|
|
3696
|
+
correctionMessages.push({ role: "user", content: verdict.missing });
|
|
3697
|
+
const correction = await this.adapter.complete({
|
|
3698
|
+
model: this.config.model,
|
|
3699
|
+
messages: correctionMessages,
|
|
3700
|
+
maxTokens: maxCompletionTokens
|
|
3701
|
+
});
|
|
3702
|
+
if (correction.content) {
|
|
3703
|
+
this.context.turns.push({
|
|
3704
|
+
role: "assistant",
|
|
3705
|
+
content: correction.content,
|
|
3706
|
+
tokenCount: correction.usage?.completionTokens ?? this.adapter.estimateTokens(correction.content),
|
|
3707
|
+
turnIndex: this.context.turns.length,
|
|
3708
|
+
compressed: false
|
|
3709
|
+
});
|
|
3710
|
+
}
|
|
3711
|
+
} catch (err) {
|
|
3712
|
+
this.logger.warn(`[GoalVerifier] Correction failed (non-fatal): ${err.message}`);
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
this.logger.info(`[${opts.label}] Run completed successfully`);
|
|
3503
3717
|
return response.content;
|
|
3504
3718
|
}
|
|
3505
3719
|
}
|
|
@@ -3514,165 +3728,93 @@ ${planStatus}`;
|
|
|
3514
3728
|
throw maxItsErr;
|
|
3515
3729
|
}
|
|
3516
3730
|
/**
|
|
3517
|
-
*
|
|
3731
|
+
* Verifies whether the goal was achieved after a `stop` finish.
|
|
3518
3732
|
*
|
|
3519
|
-
*
|
|
3520
|
-
*
|
|
3733
|
+
* Priority:
|
|
3734
|
+
* 1. `config.goalVerifier` callback (Option A — user-supplied)
|
|
3735
|
+
* 2. Built-in LLM check against `successCriteria` (Option C — fallback)
|
|
3521
3736
|
*
|
|
3522
|
-
*
|
|
3523
|
-
* @returns An `AsyncIterable<string>` of delta tokens from the final response
|
|
3737
|
+
* Returns `null` when verification is skipped (no goal, planning disabled, etc.).
|
|
3524
3738
|
*
|
|
3525
|
-
* @
|
|
3526
|
-
* ```typescript
|
|
3527
|
-
* for await (const token of session.stream('Tell me a story')) {
|
|
3528
|
-
* process.stdout.write(token);
|
|
3529
|
-
* }
|
|
3530
|
-
* ```
|
|
3739
|
+
* @since 1.5.0
|
|
3531
3740
|
*/
|
|
3532
|
-
async
|
|
3533
|
-
if (this.
|
|
3534
|
-
|
|
3535
|
-
const
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
injectionFrequency: this.config.goalInjectionFrequency ?? "always",
|
|
3547
|
-
injectionPosition: this.config.goalInjectionPosition ?? "system_prompt"
|
|
3548
|
-
});
|
|
3549
|
-
this.context.metadata["goal"] = this.goalInjector.getGoal();
|
|
3550
|
-
await this._runMiniPlanningStep(userMessageStr);
|
|
3551
|
-
}
|
|
3552
|
-
this.context.turns.push({
|
|
3553
|
-
role: "user",
|
|
3554
|
-
content: userMessage,
|
|
3555
|
-
tokenCount: Array.isArray(userMessage) ? userMessage.length * 50 : this.adapter.estimateTokens(userMessage),
|
|
3556
|
-
turnIndex: this.context.turns.length,
|
|
3557
|
-
compressed: false
|
|
3558
|
-
});
|
|
3559
|
-
const maxIts = this.config.maxIterations || 10;
|
|
3560
|
-
this.iterations = 0;
|
|
3561
|
-
this.stepCounter = new StepCounter(this.config.maxSteps ?? 20);
|
|
3562
|
-
const maxCompletionTokens = this.config.maxCompletionTokens ?? 4e3;
|
|
3563
|
-
while (this.iterations < maxIts) {
|
|
3564
|
-
this.iterations++;
|
|
3565
|
-
this.logger.debug(`[stream] ReAct Iteration ${this.iterations}/${maxIts}`);
|
|
3566
|
-
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
3567
|
-
this.context = await this.contextManager.prepare(this.context);
|
|
3568
|
-
const systemPrompt = this.buildSystemPrompt(userMessageStr, this.iterations);
|
|
3569
|
-
const messages = this.buildMessages(systemPrompt, this.iterations);
|
|
3570
|
-
if (this.stepCounter.isMaxReached()) {
|
|
3571
|
-
messages.push({
|
|
3572
|
-
role: "system",
|
|
3573
|
-
content: this.stepCounter.getForcedConclusionPrompt() + "\n\n" + FinalResponseFormatter.getRequiredStructure()
|
|
3741
|
+
async _verifyGoal(turns) {
|
|
3742
|
+
if (!this.config.enableGoalPlanning || !this.goalInjector) return null;
|
|
3743
|
+
if (this.config.enableGoalVerification === false) return null;
|
|
3744
|
+
const goal = this.goalInjector.getGoal();
|
|
3745
|
+
if (!goal.statement) return null;
|
|
3746
|
+
this.emitTrace("verification", "goal_verification_start", { goalStatement: goal.statement });
|
|
3747
|
+
try {
|
|
3748
|
+
if (this.config.goalVerifier) {
|
|
3749
|
+
const result = await this.config.goalVerifier(goal, turns);
|
|
3750
|
+
this.emitTrace("verification", "goal_verification_result", {
|
|
3751
|
+
achieved: result.achieved,
|
|
3752
|
+
reason: result.reason,
|
|
3753
|
+
missing: result.missing,
|
|
3754
|
+
source: "custom"
|
|
3574
3755
|
});
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3756
|
+
return result;
|
|
3757
|
+
}
|
|
3758
|
+
const GENERIC_CRITERION = "The user request is fully answered";
|
|
3759
|
+
const meaningfulCriteria = goal.successCriteria?.filter((c) => c !== GENERIC_CRITERION) ?? [];
|
|
3760
|
+
if (meaningfulCriteria.length > 0) {
|
|
3761
|
+
const recentTurns = turns.slice(-6).map((t) => {
|
|
3762
|
+
const text = typeof t.content === "string" ? t.content : JSON.stringify(t.content);
|
|
3763
|
+
return `[${t.role}]: ${text.slice(0, 400)}`;
|
|
3764
|
+
}).join("\n\n");
|
|
3765
|
+
const criteriaList = meaningfulCriteria.map((c, i) => `${i + 1}. ${c}`).join("\n");
|
|
3766
|
+
const response = await this.adapter.complete({
|
|
3579
3767
|
model: this.config.model,
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3768
|
+
temperature: 0,
|
|
3769
|
+
maxTokens: 256,
|
|
3770
|
+
messages: [
|
|
3771
|
+
{
|
|
3772
|
+
role: "system",
|
|
3773
|
+
content: 'You are a strict goal-completion verifier. Respond ONLY with a valid JSON object \u2014 no markdown, no prose:\n{"achieved": true|false, "reason": "<short explanation>", "missing": "<what is still needed, or empty string>"}'
|
|
3774
|
+
},
|
|
3775
|
+
{
|
|
3776
|
+
role: "user",
|
|
3777
|
+
content: `Goal: ${goal.statement}
|
|
3778
|
+
|
|
3779
|
+
Success criteria:
|
|
3780
|
+
${criteriaList}
|
|
3781
|
+
|
|
3782
|
+
Recent conversation:
|
|
3783
|
+
${recentTurns}
|
|
3784
|
+
|
|
3785
|
+
Were ALL success criteria met?`
|
|
3786
|
+
}
|
|
3787
|
+
]
|
|
3583
3788
|
});
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
if (!ok) continue;
|
|
3595
|
-
try {
|
|
3596
|
-
const content = await this.executeSingleToolCall(tc);
|
|
3597
|
-
toolResults.push({ toolCallId: tc.id, content });
|
|
3598
|
-
} catch (e) {
|
|
3599
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
3600
|
-
toolResults.push({ toolCallId: tc.id, content: `Error: ${msg}` });
|
|
3789
|
+
let verdict = null;
|
|
3790
|
+
try {
|
|
3791
|
+
const jsonMatch = response.content.match(/\{[\s\S]*\}/);
|
|
3792
|
+
if (jsonMatch) {
|
|
3793
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
3794
|
+
verdict = {
|
|
3795
|
+
achieved: parsed.achieved === true,
|
|
3796
|
+
...typeof parsed.reason === "string" && { reason: parsed.reason },
|
|
3797
|
+
...typeof parsed.missing === "string" && { missing: parsed.missing }
|
|
3798
|
+
};
|
|
3601
3799
|
}
|
|
3800
|
+
} catch {
|
|
3801
|
+
this.logger.warn("[GoalVerifier] Failed to parse built-in verifier response \u2014 skipping");
|
|
3602
3802
|
}
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
toolCalls: response.toolCalls
|
|
3610
|
-
};
|
|
3611
|
-
this.context.turns.push(assistantTurn);
|
|
3612
|
-
if (this.config.onTurn) this.config.onTurn(assistantTurn);
|
|
3613
|
-
for (const res of toolResults) {
|
|
3614
|
-
const toolTurn = {
|
|
3615
|
-
role: "tool",
|
|
3616
|
-
content: res.content,
|
|
3617
|
-
tokenCount: this.adapter.estimateTokens(res.content),
|
|
3618
|
-
turnIndex: this.context.turns.length,
|
|
3619
|
-
compressed: false,
|
|
3620
|
-
toolResults: [res]
|
|
3621
|
-
};
|
|
3622
|
-
this.context.turns.push(toolTurn);
|
|
3623
|
-
if (this.config.onTurn) this.config.onTurn(toolTurn);
|
|
3624
|
-
}
|
|
3625
|
-
if (this.goalInjector) this.goalInjector.incrementTurn();
|
|
3626
|
-
continue;
|
|
3627
|
-
}
|
|
3628
|
-
this.context.tokenCount = this.context.turns.reduce((sum, t) => sum + t.tokenCount, 0) + this.adapter.estimateTokens(this.context.systemPrompt || "");
|
|
3629
|
-
this.context = await this.contextManager.prepare(this.context);
|
|
3630
|
-
const finalSystemPrompt = this.buildSystemPrompt(userMessageStr, this.iterations);
|
|
3631
|
-
const finalMessages = this.buildMessages(finalSystemPrompt, this.iterations);
|
|
3632
|
-
let accumulated = "";
|
|
3633
|
-
let finalFinishReason = void 0;
|
|
3634
|
-
let finalTokenCount = 0;
|
|
3635
|
-
for await (const chunk of this.adapter.stream({
|
|
3636
|
-
model: this.config.model,
|
|
3637
|
-
messages: finalMessages,
|
|
3638
|
-
maxTokens: maxCompletionTokens,
|
|
3639
|
-
stream: true
|
|
3640
|
-
})) {
|
|
3641
|
-
if (chunk.delta) {
|
|
3642
|
-
accumulated += chunk.delta;
|
|
3643
|
-
finalTokenCount += Math.ceil(chunk.delta.length / 4);
|
|
3644
|
-
yield chunk.delta;
|
|
3645
|
-
}
|
|
3646
|
-
if (chunk.finished) {
|
|
3647
|
-
finalFinishReason = chunk.finishReason;
|
|
3648
|
-
if (chunk.usage) {
|
|
3649
|
-
this.totalTokens += chunk.usage.totalTokens;
|
|
3650
|
-
}
|
|
3651
|
-
this.emitTrace("thinking", "llm_stream_finished", {
|
|
3652
|
-
usage: chunk.usage,
|
|
3653
|
-
totalTokens: this.totalTokens,
|
|
3654
|
-
finishReason: chunk.finishReason
|
|
3803
|
+
if (verdict) {
|
|
3804
|
+
this.emitTrace("verification", "goal_verification_result", {
|
|
3805
|
+
achieved: verdict.achieved,
|
|
3806
|
+
reason: verdict.reason,
|
|
3807
|
+
missing: verdict.missing,
|
|
3808
|
+
source: "built_in"
|
|
3655
3809
|
});
|
|
3810
|
+
return verdict;
|
|
3656
3811
|
}
|
|
3657
3812
|
}
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
tokenCount: finalTokenCount,
|
|
3662
|
-
turnIndex: this.context.turns.length,
|
|
3663
|
-
compressed: false
|
|
3664
|
-
};
|
|
3665
|
-
this.context.turns.push(finalTurn);
|
|
3666
|
-
if (this.config.onTurn) this.config.onTurn(finalTurn);
|
|
3667
|
-
this.logger.info(`[stream] Streaming run completed (finishReason: ${finalFinishReason ?? "stop"})`);
|
|
3668
|
-
return;
|
|
3813
|
+
} catch (err) {
|
|
3814
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3815
|
+
this.logger.warn(`[GoalVerifier] Verification step failed (non-fatal): ${msg}`);
|
|
3669
3816
|
}
|
|
3670
|
-
|
|
3671
|
-
this.logger.fatal(maxItsErr.message, {
|
|
3672
|
-
problem: "The streaming agent loop exceeded its max iterations.",
|
|
3673
|
-
hints: ["Increase maxIterations or reduce task complexity."]
|
|
3674
|
-
});
|
|
3675
|
-
throw maxItsErr;
|
|
3817
|
+
return null;
|
|
3676
3818
|
}
|
|
3677
3819
|
/**
|
|
3678
3820
|
* Resets the session: clears conversation history, resets iteration counters,
|