@falai/agent 2.4.3 → 2.6.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/dist/cjs/core/FlowRouter.d.ts.map +1 -1
- package/dist/cjs/core/FlowRouter.js +0 -11
- package/dist/cjs/core/FlowRouter.js.map +1 -1
- package/dist/cjs/core/ResponseModal.d.ts +51 -2
- package/dist/cjs/core/ResponseModal.d.ts.map +1 -1
- package/dist/cjs/core/ResponseModal.js +272 -269
- package/dist/cjs/core/ResponseModal.js.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.d.ts +11 -0
- package/dist/cjs/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/cjs/core/ToolLoopExecutor.js +75 -51
- package/dist/cjs/core/ToolLoopExecutor.js.map +1 -1
- package/dist/cjs/index.d.ts +3 -0
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/cjs/providers/AnthropicProvider.js +18 -35
- package/dist/cjs/providers/AnthropicProvider.js.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.d.ts +1 -12
- package/dist/cjs/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/cjs/providers/DeepSeekProvider.js +3 -20
- package/dist/cjs/providers/DeepSeekProvider.js.map +1 -1
- package/dist/cjs/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/cjs/providers/GeminiProvider.js +22 -37
- package/dist/cjs/providers/GeminiProvider.js.map +1 -1
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js +93 -0
- package/dist/cjs/providers/GenericOpenAICompatibleProvider.js.map +1 -0
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts +23 -5
- package/dist/cjs/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/cjs/providers/OpenAICompatibleProvider.js +46 -44
- package/dist/cjs/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/cjs/providers/index.d.ts +3 -1
- package/dist/cjs/providers/index.d.ts.map +1 -1
- package/dist/cjs/providers/index.js +3 -1
- package/dist/cjs/providers/index.js.map +1 -1
- package/dist/cjs/utils/completion.d.ts +20 -0
- package/dist/cjs/utils/completion.d.ts.map +1 -0
- package/dist/cjs/utils/completion.js +33 -0
- package/dist/cjs/utils/completion.js.map +1 -0
- package/dist/cjs/utils/index.d.ts +3 -2
- package/dist/cjs/utils/index.d.ts.map +1 -1
- package/dist/cjs/utils/index.js +7 -1
- package/dist/cjs/utils/index.js.map +1 -1
- package/dist/cjs/utils/retry.d.ts +43 -7
- package/dist/cjs/utils/retry.d.ts.map +1 -1
- package/dist/cjs/utils/retry.js +100 -12
- package/dist/cjs/utils/retry.js.map +1 -1
- package/dist/cjs/utils/streamingMessage.d.ts +48 -0
- package/dist/cjs/utils/streamingMessage.d.ts.map +1 -0
- package/dist/cjs/utils/streamingMessage.js +210 -0
- package/dist/cjs/utils/streamingMessage.js.map +1 -0
- package/dist/core/FlowRouter.d.ts.map +1 -1
- package/dist/core/FlowRouter.js +0 -11
- package/dist/core/FlowRouter.js.map +1 -1
- package/dist/core/ResponseModal.d.ts +51 -2
- package/dist/core/ResponseModal.d.ts.map +1 -1
- package/dist/core/ResponseModal.js +272 -269
- package/dist/core/ResponseModal.js.map +1 -1
- package/dist/core/ToolLoopExecutor.d.ts +11 -0
- package/dist/core/ToolLoopExecutor.d.ts.map +1 -1
- package/dist/core/ToolLoopExecutor.js +76 -52
- package/dist/core/ToolLoopExecutor.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/providers/AnthropicProvider.d.ts.map +1 -1
- package/dist/providers/AnthropicProvider.js +19 -36
- package/dist/providers/AnthropicProvider.js.map +1 -1
- package/dist/providers/DeepSeekProvider.d.ts +1 -12
- package/dist/providers/DeepSeekProvider.d.ts.map +1 -1
- package/dist/providers/DeepSeekProvider.js +3 -20
- package/dist/providers/DeepSeekProvider.js.map +1 -1
- package/dist/providers/GeminiProvider.d.ts.map +1 -1
- package/dist/providers/GeminiProvider.js +23 -38
- package/dist/providers/GeminiProvider.js.map +1 -1
- package/dist/providers/GenericOpenAICompatibleProvider.d.ts +69 -0
- package/dist/providers/GenericOpenAICompatibleProvider.d.ts.map +1 -0
- package/dist/providers/GenericOpenAICompatibleProvider.js +87 -0
- package/dist/providers/GenericOpenAICompatibleProvider.js.map +1 -0
- package/dist/providers/OpenAICompatibleProvider.d.ts +23 -5
- package/dist/providers/OpenAICompatibleProvider.d.ts.map +1 -1
- package/dist/providers/OpenAICompatibleProvider.js +47 -45
- package/dist/providers/OpenAICompatibleProvider.js.map +1 -1
- package/dist/providers/index.d.ts +3 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -0
- package/dist/providers/index.js.map +1 -1
- package/dist/utils/completion.d.ts +20 -0
- package/dist/utils/completion.d.ts.map +1 -0
- package/dist/utils/completion.js +29 -0
- package/dist/utils/completion.js.map +1 -0
- package/dist/utils/index.d.ts +3 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +3 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/retry.d.ts +43 -7
- package/dist/utils/retry.d.ts.map +1 -1
- package/dist/utils/retry.js +98 -12
- package/dist/utils/retry.js.map +1 -1
- package/dist/utils/streamingMessage.d.ts +48 -0
- package/dist/utils/streamingMessage.d.ts.map +1 -0
- package/dist/utils/streamingMessage.js +205 -0
- package/dist/utils/streamingMessage.js.map +1 -0
- package/package.json +1 -1
- package/src/core/FlowRouter.ts +0 -14
- package/src/core/ResponseModal.ts +332 -299
- package/src/core/ToolLoopExecutor.ts +108 -52
- package/src/index.ts +3 -0
- package/src/providers/AnthropicProvider.ts +22 -43
- package/src/providers/DeepSeekProvider.ts +3 -35
- package/src/providers/GeminiProvider.ts +27 -46
- package/src/providers/GenericOpenAICompatibleProvider.ts +138 -0
- package/src/providers/OpenAICompatibleProvider.ts +87 -67
- package/src/providers/index.ts +4 -0
- package/src/utils/completion.ts +38 -0
- package/src/utils/index.ts +5 -2
- package/src/utils/retry.ts +136 -14
- package/src/utils/streamingMessage.ts +220 -0
|
@@ -12,6 +12,7 @@ import { SignalCoordinator } from "./SignalCoordinator";
|
|
|
12
12
|
import { ResponseGenerationError } from "./ResponseGenerationError";
|
|
13
13
|
import { cloneDeep, mergeCollected, logger, historyToEvents, completeCurrentFlow, render } from "../utils";
|
|
14
14
|
import { createTemplateContext } from "../utils/template";
|
|
15
|
+
import { StreamingMessageDecoder } from "../utils/streamingMessage";
|
|
15
16
|
/**
|
|
16
17
|
* ResponseModal class that encapsulates all response generation logic
|
|
17
18
|
* Uses unified approach for both streaming and non-streaming responses
|
|
@@ -335,46 +336,40 @@ export class ResponseModal {
|
|
|
335
336
|
}
|
|
336
337
|
}
|
|
337
338
|
/**
|
|
338
|
-
*
|
|
339
|
+
* Plan a turn: run signal-halt detection, the auto-chain walk, and flow/step
|
|
340
|
+
* selection, collapsing them into a single {@link TurnOutcome}. This is the
|
|
341
|
+
* shared decision spine for both the streaming and non-streaming paths — the
|
|
342
|
+
* only logic that genuinely differs between them is how each *renders* the
|
|
343
|
+
* outcome (await a value vs. yield chunks) and the leaf provider primitive it
|
|
344
|
+
* uses. Centralizing the decision here is what keeps the two paths from
|
|
345
|
+
* drifting (the class of bug behind the 2.4.x retry/empty fixes).
|
|
346
|
+
*
|
|
347
|
+
* The returned `session` reflects any auto-chain mutation; `signalFirings`
|
|
348
|
+
* is seeded with the pre-signal phase firings and is the live accumulator the
|
|
349
|
+
* post-phase tail appends to.
|
|
339
350
|
* @private
|
|
340
351
|
*/
|
|
341
|
-
async
|
|
342
|
-
const { effectiveContext,
|
|
343
|
-
let session =
|
|
352
|
+
async planTurn(responseContext) {
|
|
353
|
+
const { effectiveContext, history, selectedFlow, selectedStep, responseDirectives, isFlowComplete, signal, signalFirings: preSignalFirings, signalPreDirective, signalHalted, signalHaltReply, } = responseContext;
|
|
354
|
+
let session = responseContext.session;
|
|
344
355
|
// Accumulator for signal firings across both phases (fire order)
|
|
345
356
|
const signalFirings = [...(preSignalFirings || [])];
|
|
346
|
-
// Get last user message (needed for both flow and completion handling)
|
|
347
357
|
// Convert HistoryItem[] to Event[] for internal processing
|
|
348
358
|
const historyEvents = historyToEvents(history);
|
|
359
|
+
const base = { effectiveContext, history, historyEvents, signal, signalFirings };
|
|
349
360
|
// ── SIGNAL HALT (Requirement 8.2) ─────────────────────────────────────
|
|
350
|
-
// Pre-signal phase emitted halt → skip LLM call entirely.
|
|
361
|
+
// Pre-signal phase emitted halt → skip LLM call entirely. The post-signal
|
|
362
|
+
// phase still runs (it sees the complete turn context).
|
|
351
363
|
if (signalHalted) {
|
|
352
364
|
const haltMessage = signalHaltReply || '';
|
|
353
|
-
// Run post-signal phase even on halt (post-phase sees complete turn context)
|
|
354
|
-
const post = await this.signalCoordinator.applyPostPhase({
|
|
355
|
-
session, context: effectiveContext, historyEvents, message: haltMessage,
|
|
356
|
-
});
|
|
357
|
-
session = post.session;
|
|
358
|
-
signalFirings.push(...post.firings);
|
|
359
|
-
const message = post.message;
|
|
360
365
|
return {
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
toolCalls: undefined,
|
|
364
|
-
isFlowComplete: false,
|
|
365
|
-
executedSteps: [],
|
|
366
|
-
stoppedReason: haltMessage ? 'reply' : 'halt',
|
|
367
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
366
|
+
...base, session,
|
|
367
|
+
outcome: { kind: 'halt', message: haltMessage, stoppedReason: haltMessage ? 'reply' : 'halt', runPostPhase: true },
|
|
368
368
|
};
|
|
369
369
|
}
|
|
370
|
-
let message;
|
|
371
|
-
let toolCalls = undefined;
|
|
372
|
-
let executedSteps;
|
|
373
|
-
let stoppedReason;
|
|
374
|
-
let appliedInstructions;
|
|
375
370
|
if (selectedFlow && !isFlowComplete) {
|
|
376
|
-
// AUTO-CHAIN: Walk consecutive auto-steps before any LLM work.
|
|
377
|
-
//
|
|
371
|
+
// AUTO-CHAIN: Walk consecutive auto-steps before any LLM work. If the
|
|
372
|
+
// current step is auto, the executor advances through it (and any
|
|
378
373
|
// subsequent auto-steps) until an interactive step or terminal condition.
|
|
379
374
|
let resolvedStep = selectedStep;
|
|
380
375
|
const currentStepInstance = session.currentStep
|
|
@@ -390,132 +385,159 @@ export class ResponseModal {
|
|
|
390
385
|
flow: selectedFlow,
|
|
391
386
|
});
|
|
392
387
|
session = autoResult.session;
|
|
393
|
-
//
|
|
394
|
-
//
|
|
388
|
+
// Halt: emit the verbatim reply, no LLM call. Unlike signal halt,
|
|
389
|
+
// the auto-chain halt is a hard short-circuit that does NOT run the
|
|
390
|
+
// post-signal phase (preserved across both paths).
|
|
395
391
|
if (autoResult.stoppedReason === 'halt') {
|
|
396
|
-
message = autoResult.mergedDirective?.reply || '';
|
|
397
|
-
stoppedReason = 'halt';
|
|
398
|
-
executedSteps = [];
|
|
399
392
|
return {
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
toolCalls: undefined,
|
|
403
|
-
isFlowComplete: false,
|
|
404
|
-
executedSteps,
|
|
405
|
-
stoppedReason,
|
|
393
|
+
...base, session,
|
|
394
|
+
outcome: { kind: 'halt', message: autoResult.mergedDirective?.reply || '', stoppedReason: 'halt', runPostPhase: false },
|
|
406
395
|
};
|
|
407
396
|
}
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
// complete directive), or goto (cross-flow redirect).
|
|
397
|
+
// Flow completion or cross-flow redirect from auto-chain: the chain
|
|
398
|
+
// ended without resolving to an interactive step (last_step: no
|
|
399
|
+
// successor; completed: explicit complete; goto: cross-flow redirect).
|
|
412
400
|
if (autoResult.stoppedReason === 'last_step' || autoResult.stoppedReason === 'completed' || autoResult.stoppedReason === 'goto') {
|
|
413
401
|
logger.debug(`[ResponseModal] Auto-chain ended with ${autoResult.stoppedReason}`);
|
|
414
|
-
session = await this.applyFlowCompletion({
|
|
415
|
-
selectedFlow,
|
|
416
|
-
session,
|
|
417
|
-
context: effectiveContext,
|
|
418
|
-
history,
|
|
419
|
-
});
|
|
420
402
|
return {
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
toolCalls: undefined,
|
|
424
|
-
isFlowComplete: true,
|
|
425
|
-
executedSteps: [],
|
|
426
|
-
stoppedReason: autoResult.stoppedReason,
|
|
403
|
+
...base, session,
|
|
404
|
+
outcome: { kind: 'flowComplete', selectedFlow, stoppedReason: autoResult.stoppedReason },
|
|
427
405
|
};
|
|
428
406
|
}
|
|
429
407
|
// Normal case: auto-chain resolved to an interactive step.
|
|
430
408
|
resolvedStep = autoResult.resolvedStep;
|
|
431
409
|
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
selectedFlow,
|
|
437
|
-
selectedStep: resolvedStep,
|
|
438
|
-
responseDirectives,
|
|
439
|
-
session,
|
|
440
|
-
history,
|
|
441
|
-
context: effectiveContext,
|
|
442
|
-
historyEvents,
|
|
443
|
-
signal,
|
|
444
|
-
// Propagate signal pre-directive's appendPrompt for this turn's LLM call (Requirement 8.4)
|
|
445
|
-
transientAppendage: signalPreDirective?.appendPrompt,
|
|
446
|
-
// Merge signal pre-directive (halt/reply/injectTools) into the pre-LLM bus
|
|
447
|
-
mergedPreDirective: signalPreDirective,
|
|
448
|
-
});
|
|
449
|
-
message = result.message;
|
|
450
|
-
toolCalls = result.toolCalls;
|
|
451
|
-
session = result.session;
|
|
452
|
-
appliedInstructions = result.appliedInstructions;
|
|
453
|
-
// Track executed step for single-step execution
|
|
454
|
-
if (resolvedStep) {
|
|
455
|
-
executedSteps = [{
|
|
456
|
-
id: resolvedStep.id,
|
|
457
|
-
flowId: selectedFlow.id,
|
|
458
|
-
}];
|
|
459
|
-
}
|
|
460
|
-
// Use stoppedReason from processFlowResponse if set (halt/reply),
|
|
461
|
-
// otherwise default to 'needs_input' for normal LLM responses.
|
|
462
|
-
stoppedReason = result.stoppedReason || 'needs_input';
|
|
410
|
+
return {
|
|
411
|
+
...base, session,
|
|
412
|
+
outcome: { kind: 'flowStep', selectedFlow, step: resolvedStep, responseDirectives, signalPreDirective },
|
|
413
|
+
};
|
|
463
414
|
}
|
|
464
|
-
|
|
465
|
-
// Flow completion path: pure state transition, no LLM call.
|
|
466
|
-
//
|
|
467
|
-
// stoppedReason is 'last_step' because this completion was detected by
|
|
468
|
-
// implicit terminus (no successor or all successors skipped), not by an
|
|
469
|
-
// explicit `complete` directive.
|
|
415
|
+
if (isFlowComplete && selectedFlow) {
|
|
416
|
+
// Flow completion path: pure state transition, no LLM call. The reason
|
|
417
|
+
// is 'last_step' (implicit terminus — no successor or all skipped).
|
|
470
418
|
logger.debug(`[ResponseModal] Releasing session to idle for completed flow: ${selectedFlow.title}`);
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
history,
|
|
476
|
-
});
|
|
477
|
-
message = '';
|
|
478
|
-
stoppedReason = 'last_step';
|
|
479
|
-
executedSteps = [];
|
|
419
|
+
return {
|
|
420
|
+
...base, session,
|
|
421
|
+
outcome: { kind: 'flowComplete', selectedFlow, stoppedReason: 'last_step' },
|
|
422
|
+
};
|
|
480
423
|
}
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
424
|
+
// Fallback: no flows defined, generate a simple response.
|
|
425
|
+
return { ...base, session, outcome: { kind: 'fallback' } };
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* The shared post-signal phase tail (Requirement 9.1–9.4). Runs after the
|
|
429
|
+
* turn's message is known and before persistence, so post-phase signals see
|
|
430
|
+
* the complete turn result (assistant message, collected data, tool results)
|
|
431
|
+
* and can override the reply or wire a pendingDirective.
|
|
432
|
+
*
|
|
433
|
+
* `runPostPhase` is false only for the auto-chain halt short-circuit, which
|
|
434
|
+
* deliberately bypasses the post-phase in both paths; that branch still
|
|
435
|
+
* surfaces any pre-phase firings via `triggeredSignals`.
|
|
436
|
+
* @private
|
|
437
|
+
*/
|
|
438
|
+
async applyTurnPostPhase(params) {
|
|
439
|
+
const { session, context, historyEvents, message, signalFirings, runPostPhase } = params;
|
|
440
|
+
if (!runPostPhase) {
|
|
441
|
+
return {
|
|
442
|
+
session, message, replyOverridden: false,
|
|
443
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
444
|
+
};
|
|
494
445
|
}
|
|
495
|
-
|
|
496
|
-
// Runs after finalize/onComplete and before session persistence.
|
|
497
|
-
// Post-phase signals see the complete turn result: assistant message in
|
|
498
|
-
// history, collected data, tool results.
|
|
499
|
-
const post = await this.signalCoordinator.applyPostPhase({
|
|
500
|
-
session, context: effectiveContext, historyEvents, message,
|
|
501
|
-
});
|
|
502
|
-
session = post.session;
|
|
503
|
-
// Append post-phase firings to the accumulator (preserves fire order)
|
|
446
|
+
const post = await this.signalCoordinator.applyPostPhase({ session, context, historyEvents, message });
|
|
504
447
|
signalFirings.push(...post.firings);
|
|
505
|
-
message = post.message;
|
|
506
|
-
// Ensure response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
507
|
-
// - executedSteps: array of steps executed (empty array if none)
|
|
508
|
-
// - stoppedReason: why execution stopped (undefined for fallback)
|
|
509
|
-
// - session.currentStep: reflects final step position
|
|
510
448
|
return {
|
|
511
|
-
|
|
512
|
-
|
|
449
|
+
session: post.session,
|
|
450
|
+
message: post.message,
|
|
451
|
+
replyOverridden: post.replyOverridden ?? false,
|
|
452
|
+
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Unified response generation for non-streaming responses.
|
|
457
|
+
* Renders the shared {@link planTurn} outcome by awaiting the leaf primitive
|
|
458
|
+
* and running the shared post-phase tail; respond() owns the single finalize.
|
|
459
|
+
* @private
|
|
460
|
+
*/
|
|
461
|
+
async generateUnifiedResponse(responseContext) {
|
|
462
|
+
const plan = await this.planTurn(responseContext);
|
|
463
|
+
const { effectiveContext, history, historyEvents, signal, signalFirings } = plan;
|
|
464
|
+
let session = plan.session;
|
|
465
|
+
let message = '';
|
|
466
|
+
let toolCalls = undefined;
|
|
467
|
+
let executedSteps = [];
|
|
468
|
+
let stoppedReason;
|
|
469
|
+
let isFlowComplete = false;
|
|
470
|
+
let appliedInstructions;
|
|
471
|
+
let runPostPhase = true;
|
|
472
|
+
switch (plan.outcome.kind) {
|
|
473
|
+
case 'halt': {
|
|
474
|
+
message = plan.outcome.message;
|
|
475
|
+
stoppedReason = plan.outcome.stoppedReason;
|
|
476
|
+
runPostPhase = plan.outcome.runPostPhase;
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case 'flowComplete': {
|
|
480
|
+
session = await this.applyFlowCompletion({
|
|
481
|
+
selectedFlow: plan.outcome.selectedFlow,
|
|
482
|
+
session,
|
|
483
|
+
context: effectiveContext,
|
|
484
|
+
history,
|
|
485
|
+
});
|
|
486
|
+
isFlowComplete = true;
|
|
487
|
+
stoppedReason = plan.outcome.stoppedReason;
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case 'flowStep': {
|
|
491
|
+
const result = await this.processFlowResponse({
|
|
492
|
+
selectedFlow: plan.outcome.selectedFlow,
|
|
493
|
+
selectedStep: plan.outcome.step,
|
|
494
|
+
responseDirectives: plan.outcome.responseDirectives,
|
|
495
|
+
session,
|
|
496
|
+
history,
|
|
497
|
+
context: effectiveContext,
|
|
498
|
+
historyEvents,
|
|
499
|
+
signal,
|
|
500
|
+
// Propagate signal pre-directive's appendPrompt for this turn's LLM call (Requirement 8.4)
|
|
501
|
+
transientAppendage: plan.outcome.signalPreDirective?.appendPrompt,
|
|
502
|
+
// Merge signal pre-directive (halt/reply/injectTools) into the pre-LLM bus
|
|
503
|
+
mergedPreDirective: plan.outcome.signalPreDirective,
|
|
504
|
+
});
|
|
505
|
+
message = result.message;
|
|
506
|
+
toolCalls = result.toolCalls;
|
|
507
|
+
session = result.session;
|
|
508
|
+
appliedInstructions = result.appliedInstructions;
|
|
509
|
+
if (plan.outcome.step) {
|
|
510
|
+
executedSteps = [{ id: plan.outcome.step.id, flowId: plan.outcome.selectedFlow.id }];
|
|
511
|
+
}
|
|
512
|
+
// Use stoppedReason from processFlowResponse if set (halt/reply),
|
|
513
|
+
// otherwise default to 'needs_input' for normal LLM responses.
|
|
514
|
+
stoppedReason = result.stoppedReason || 'needs_input';
|
|
515
|
+
break;
|
|
516
|
+
}
|
|
517
|
+
case 'fallback': {
|
|
518
|
+
const fallbackResult = await this.generateFallbackResponse({
|
|
519
|
+
history,
|
|
520
|
+
context: effectiveContext,
|
|
521
|
+
session,
|
|
522
|
+
signal,
|
|
523
|
+
});
|
|
524
|
+
message = fallbackResult.message;
|
|
525
|
+
appliedInstructions = fallbackResult.appliedInstructions;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const tail = await this.applyTurnPostPhase({
|
|
530
|
+
session, context: effectiveContext, historyEvents, message, signalFirings, runPostPhase,
|
|
531
|
+
});
|
|
532
|
+
return {
|
|
533
|
+
message: tail.message,
|
|
534
|
+
session: tail.session,
|
|
513
535
|
toolCalls,
|
|
514
|
-
isFlowComplete
|
|
515
|
-
executedSteps
|
|
536
|
+
isFlowComplete,
|
|
537
|
+
executedSteps,
|
|
516
538
|
stoppedReason,
|
|
517
539
|
appliedInstructions,
|
|
518
|
-
triggeredSignals:
|
|
540
|
+
triggeredSignals: tail.triggeredSignals,
|
|
519
541
|
};
|
|
520
542
|
}
|
|
521
543
|
/**
|
|
@@ -643,162 +665,90 @@ export class ResponseModal {
|
|
|
643
665
|
}
|
|
644
666
|
}
|
|
645
667
|
/**
|
|
646
|
-
* Unified streaming response generation
|
|
668
|
+
* Unified streaming response generation.
|
|
669
|
+
* Renders the shared {@link planTurn} outcome as a chunk stream and runs the
|
|
670
|
+
* shared post-phase tail on the final chunk (finalizing exactly once).
|
|
647
671
|
* @private
|
|
648
672
|
*/
|
|
649
673
|
async *generateUnifiedStreamingResponse(responseContext) {
|
|
650
|
-
const
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
//
|
|
655
|
-
const historyEvents = historyToEvents(history);
|
|
656
|
-
// ── SIGNAL HALT (Requirement 8.2) ─────────────────────────────────────
|
|
657
|
-
if (signalHalted) {
|
|
658
|
-
const haltMessage = signalHaltReply || '';
|
|
659
|
-
// Run post-signal phase even on halt
|
|
660
|
-
const post = await this.signalCoordinator.applyPostPhase({
|
|
661
|
-
session, context: effectiveContext, historyEvents, message: haltMessage,
|
|
662
|
-
});
|
|
663
|
-
session = post.session;
|
|
664
|
-
signalFirings.push(...post.firings);
|
|
665
|
-
const message = post.message;
|
|
666
|
-
await this.sessionFinalizer.finalize(session, effectiveContext);
|
|
667
|
-
yield {
|
|
668
|
-
delta: message,
|
|
669
|
-
accumulated: message,
|
|
670
|
-
done: true,
|
|
671
|
-
session,
|
|
672
|
-
stoppedReason: haltMessage ? 'reply' : 'halt',
|
|
673
|
-
executedSteps: [],
|
|
674
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
675
|
-
};
|
|
676
|
-
return;
|
|
677
|
-
}
|
|
678
|
-
// ── Determine the inner stream generator based on flow state ────────
|
|
674
|
+
const plan = await this.planTurn(responseContext);
|
|
675
|
+
const { effectiveContext, history, historyEvents, signal, signalFirings } = plan;
|
|
676
|
+
const session = plan.session;
|
|
677
|
+
// Build the inner chunk stream for the planned outcome. `runPostPhase` is
|
|
678
|
+
// the single post-phase gate (false only for auto-chain halt).
|
|
679
679
|
let innerStream;
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
maxAutoStepsPerTurn: this.agent.maxAutoStepsPerTurn,
|
|
680
|
+
let runPostPhase = true;
|
|
681
|
+
switch (plan.outcome.kind) {
|
|
682
|
+
case 'halt': {
|
|
683
|
+
runPostPhase = plan.outcome.runPostPhase;
|
|
684
|
+
innerStream = this.streamTerminalMessage({
|
|
685
|
+
message: plan.outcome.message,
|
|
686
|
+
stoppedReason: plan.outcome.stoppedReason,
|
|
687
|
+
session,
|
|
689
688
|
});
|
|
690
|
-
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
case 'flowComplete': {
|
|
692
|
+
innerStream = this.streamFlowCompletion({
|
|
693
|
+
selectedFlow: plan.outcome.selectedFlow,
|
|
691
694
|
session,
|
|
692
695
|
context: effectiveContext,
|
|
693
|
-
|
|
696
|
+
history,
|
|
697
|
+
historyEvents,
|
|
698
|
+
stoppedReason: plan.outcome.stoppedReason,
|
|
694
699
|
});
|
|
695
|
-
|
|
696
|
-
// Handle halt: emit verbatim reply as a single chunk, done.
|
|
697
|
-
if (autoResult.stoppedReason === 'halt') {
|
|
698
|
-
const reply = autoResult.mergedDirective?.reply || '';
|
|
699
|
-
await this.sessionFinalizer.finalize(session, effectiveContext);
|
|
700
|
-
yield {
|
|
701
|
-
delta: reply,
|
|
702
|
-
accumulated: reply,
|
|
703
|
-
done: true,
|
|
704
|
-
session,
|
|
705
|
-
stoppedReason: 'halt',
|
|
706
|
-
executedSteps: [],
|
|
707
|
-
triggeredSignals: signalFirings.length > 0 ? signalFirings : undefined,
|
|
708
|
-
};
|
|
709
|
-
return;
|
|
710
|
-
}
|
|
711
|
-
// Handle flow completion or cross-flow redirect from auto-chain.
|
|
712
|
-
if (autoResult.stoppedReason === 'last_step' || autoResult.stoppedReason === 'completed' || autoResult.stoppedReason === 'goto') {
|
|
713
|
-
innerStream = this.streamFlowCompletion({
|
|
714
|
-
selectedFlow,
|
|
715
|
-
session,
|
|
716
|
-
context: effectiveContext,
|
|
717
|
-
history,
|
|
718
|
-
historyEvents,
|
|
719
|
-
stoppedReason: autoResult.stoppedReason,
|
|
720
|
-
});
|
|
721
|
-
}
|
|
722
|
-
else {
|
|
723
|
-
// Normal case: resolved to an interactive step.
|
|
724
|
-
resolvedStep = autoResult.resolvedStep;
|
|
725
|
-
innerStream = this.processFlowStreamingResponse({
|
|
726
|
-
selectedFlow,
|
|
727
|
-
selectedStep: resolvedStep,
|
|
728
|
-
responseDirectives,
|
|
729
|
-
session,
|
|
730
|
-
history,
|
|
731
|
-
context: effectiveContext,
|
|
732
|
-
historyEvents,
|
|
733
|
-
signal,
|
|
734
|
-
transientAppendage: signalPreDirective?.appendPrompt,
|
|
735
|
-
mergedPreDirective: signalPreDirective,
|
|
736
|
-
});
|
|
737
|
-
}
|
|
700
|
+
break;
|
|
738
701
|
}
|
|
739
|
-
|
|
740
|
-
// No auto-step: directly stream the interactive step.
|
|
702
|
+
case 'flowStep': {
|
|
741
703
|
innerStream = this.processFlowStreamingResponse({
|
|
742
|
-
selectedFlow,
|
|
743
|
-
selectedStep:
|
|
744
|
-
responseDirectives,
|
|
704
|
+
selectedFlow: plan.outcome.selectedFlow,
|
|
705
|
+
selectedStep: plan.outcome.step,
|
|
706
|
+
responseDirectives: plan.outcome.responseDirectives,
|
|
745
707
|
session,
|
|
746
708
|
history,
|
|
747
709
|
context: effectiveContext,
|
|
748
710
|
historyEvents,
|
|
749
711
|
signal,
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
mergedPreDirective: signalPreDirective,
|
|
712
|
+
transientAppendage: plan.outcome.signalPreDirective?.appendPrompt,
|
|
713
|
+
mergedPreDirective: plan.outcome.signalPreDirective,
|
|
753
714
|
});
|
|
715
|
+
break;
|
|
716
|
+
}
|
|
717
|
+
case 'fallback': {
|
|
718
|
+
innerStream = this.streamFallbackResponse({
|
|
719
|
+
history,
|
|
720
|
+
context: effectiveContext,
|
|
721
|
+
session,
|
|
722
|
+
signal,
|
|
723
|
+
});
|
|
724
|
+
break;
|
|
754
725
|
}
|
|
755
|
-
}
|
|
756
|
-
else if (isFlowComplete && selectedFlow) {
|
|
757
|
-
// Handle flow completion streaming — implicit terminus (no successor
|
|
758
|
-
// or all successors skipped), so the reason is 'last_step'.
|
|
759
|
-
innerStream = this.streamFlowCompletion({
|
|
760
|
-
selectedFlow,
|
|
761
|
-
session,
|
|
762
|
-
context: effectiveContext,
|
|
763
|
-
history,
|
|
764
|
-
historyEvents,
|
|
765
|
-
stoppedReason: 'last_step',
|
|
766
|
-
});
|
|
767
|
-
}
|
|
768
|
-
else {
|
|
769
|
-
// Fallback: No flows defined, stream a simple response
|
|
770
|
-
innerStream = this.streamFallbackResponse({
|
|
771
|
-
history,
|
|
772
|
-
context: effectiveContext,
|
|
773
|
-
session,
|
|
774
|
-
});
|
|
775
726
|
}
|
|
776
727
|
// ── Intercept the inner stream on the final chunk ──────────────────────
|
|
777
|
-
// Mirrors the non-streaming
|
|
778
|
-
//
|
|
779
|
-
//
|
|
728
|
+
// Mirrors the non-streaming tail: post-signal phase runs first (when
|
|
729
|
+
// applicable), then the session is finalized exactly once, attaching
|
|
730
|
+
// triggeredSignals to the final chunk (Requirement 11.2).
|
|
780
731
|
for await (const chunk of innerStream) {
|
|
781
732
|
if (chunk.done) {
|
|
782
|
-
|
|
783
|
-
const post = await this.signalCoordinator.applyPostPhase({
|
|
733
|
+
const tail = await this.applyTurnPostPhase({
|
|
784
734
|
session: chunk.session || session,
|
|
785
735
|
context: effectiveContext,
|
|
786
736
|
historyEvents,
|
|
787
737
|
message: chunk.accumulated,
|
|
738
|
+
signalFirings,
|
|
739
|
+
runPostPhase,
|
|
788
740
|
});
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
const accumulated = post.message;
|
|
792
|
-
const delta = post.replyOverridden ? accumulated : chunk.delta;
|
|
741
|
+
const accumulated = tail.message;
|
|
742
|
+
const delta = tail.replyOverridden ? accumulated : chunk.delta;
|
|
793
743
|
// Single streaming exit: finalize the post-phase session so
|
|
794
|
-
// post-signal mutations (e.g. pendingDirective) are persisted
|
|
795
|
-
await this.sessionFinalizer.finalize(
|
|
744
|
+
// post-signal mutations (e.g. pendingDirective) are persisted.
|
|
745
|
+
await this.sessionFinalizer.finalize(tail.session, effectiveContext);
|
|
796
746
|
yield {
|
|
797
747
|
...chunk,
|
|
798
748
|
delta,
|
|
799
749
|
accumulated,
|
|
800
|
-
session:
|
|
801
|
-
triggeredSignals:
|
|
750
|
+
session: tail.session,
|
|
751
|
+
triggeredSignals: tail.triggeredSignals,
|
|
802
752
|
};
|
|
803
753
|
}
|
|
804
754
|
else {
|
|
@@ -806,6 +756,42 @@ export class ResponseModal {
|
|
|
806
756
|
}
|
|
807
757
|
}
|
|
808
758
|
}
|
|
759
|
+
/**
|
|
760
|
+
* Emit a framework-authored message (a halt reply) as a single terminal
|
|
761
|
+
* chunk, to flow through the shared post-phase tail like any other inner
|
|
762
|
+
* stream. No LLM call, no provider text — so nothing to extract or finalize
|
|
763
|
+
* here; the caller's tail owns post-phase + finalize.
|
|
764
|
+
* @private
|
|
765
|
+
*/
|
|
766
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- yield-only async generator; must be `async *` to satisfy the AsyncGenerator return type the caller switches on
|
|
767
|
+
async *streamTerminalMessage(params) {
|
|
768
|
+
yield {
|
|
769
|
+
delta: params.message,
|
|
770
|
+
accumulated: params.message,
|
|
771
|
+
done: true,
|
|
772
|
+
session: params.session,
|
|
773
|
+
toolCalls: undefined,
|
|
774
|
+
isFlowComplete: false,
|
|
775
|
+
stoppedReason: params.stoppedReason,
|
|
776
|
+
executedSteps: [],
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Wrap a provider message stream so each chunk's `delta`/`accumulated` carry
|
|
781
|
+
* clean message text instead of the raw structured-JSON wrapper. The single
|
|
782
|
+
* point where streamed JSON is unwrapped — every streaming response variant
|
|
783
|
+
* (flow step, fallback) consumes provider chunks through here, so consumers
|
|
784
|
+
* and stored history never see `{"message":...}` fragments. `structured`,
|
|
785
|
+
* `done`, and `metadata` pass through untouched.
|
|
786
|
+
* @private
|
|
787
|
+
*/
|
|
788
|
+
async *decodeMessageStream(stream) {
|
|
789
|
+
const decoder = new StreamingMessageDecoder();
|
|
790
|
+
for await (const chunk of stream) {
|
|
791
|
+
const clean = decoder.push(chunk.accumulated);
|
|
792
|
+
yield { ...chunk, delta: clean.delta, accumulated: clean.message };
|
|
793
|
+
}
|
|
794
|
+
}
|
|
809
795
|
/**
|
|
810
796
|
* Process flow streaming response with unified tool execution and data collection
|
|
811
797
|
* @private
|
|
@@ -900,18 +886,24 @@ export class ResponseModal {
|
|
|
900
886
|
signal,
|
|
901
887
|
parameters: { jsonSchema: responseSchema, schemaName: "response_stream_output" },
|
|
902
888
|
});
|
|
903
|
-
// Stream chunks with unified tool handling
|
|
904
|
-
|
|
889
|
+
// Stream chunks with unified tool handling. decodeMessageStream gives
|
|
890
|
+
// each chunk clean message text in delta/accumulated, so the non-done
|
|
891
|
+
// deltas, the final accumulated, the post-phase message input, and the
|
|
892
|
+
// assistant message stored by stream() are all clean — never the raw
|
|
893
|
+
// JSON wrapper (matching the non-streaming structured.message extraction).
|
|
894
|
+
for await (const chunk of this.decodeMessageStream(stream)) {
|
|
905
895
|
let toolCalls = undefined;
|
|
906
896
|
// Final message/structured may be replaced by a forced post-tool
|
|
907
897
|
// response (see runStreamingBatch / gap: tools-ran-but-no-text).
|
|
898
|
+
let finalDelta = chunk.delta;
|
|
908
899
|
let finalAccumulated = chunk.accumulated;
|
|
909
900
|
let finalStructured = chunk.structured;
|
|
910
901
|
// Extract tool calls from AI response on final chunk
|
|
911
902
|
if (chunk.done && chunk.structured?.toolCalls) {
|
|
912
903
|
toolCalls = chunk.structured.toolCalls;
|
|
913
904
|
// Concurrent execution for the initial batch of tool calls,
|
|
914
|
-
// yielding tool-progress chunks as they arrive
|
|
905
|
+
// yielding tool-progress chunks as they arrive. The accumulated
|
|
906
|
+
// preamble is already clean text.
|
|
915
907
|
const batchResult = yield* this.toolLoopExecutor.runStreamingBatch({
|
|
916
908
|
toolCalls,
|
|
917
909
|
context,
|
|
@@ -927,19 +919,29 @@ export class ResponseModal {
|
|
|
927
919
|
});
|
|
928
920
|
session = batchResult.session;
|
|
929
921
|
toolCalls = batchResult.toolCalls;
|
|
922
|
+
// Prefer the post-tool follow-up structured for collection and
|
|
923
|
+
// emission whenever present — independent of whether a closing
|
|
924
|
+
// message was forced — matching the non-streaming path's
|
|
925
|
+
// `toolResult.structured ?? result` selection.
|
|
926
|
+
finalStructured = batchResult.structured ?? finalStructured;
|
|
930
927
|
// Tools ran but the model produced no result-aware text — use
|
|
931
|
-
// the forced closing message so we never emit the
|
|
932
|
-
// preamble (or an empty message) as the final response.
|
|
928
|
+
// the forced closing message (already clean) so we never emit the
|
|
929
|
+
// bare preamble (or an empty message) as the final response. Its
|
|
930
|
+
// delta is the portion not already streamed as the preamble.
|
|
933
931
|
if (batchResult.finalMessage) {
|
|
934
932
|
finalAccumulated = batchResult.finalMessage;
|
|
935
|
-
|
|
933
|
+
finalDelta = batchResult.finalMessage.startsWith(chunk.accumulated)
|
|
934
|
+
? batchResult.finalMessage.slice(chunk.accumulated.length)
|
|
935
|
+
: batchResult.finalMessage;
|
|
936
936
|
}
|
|
937
937
|
}
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
|
|
938
|
+
// Collect data on the final chunk for any flow step — flow
|
|
939
|
+
// required/optional fields are valid targets even without a step
|
|
940
|
+
// `collect` — preferring the post-tool follow-up structured so a
|
|
941
|
+
// tool-driven turn harvests fields the model produced after tools.
|
|
942
|
+
if (chunk.done && finalStructured) {
|
|
941
943
|
session = await this.collectDataFromResponse({
|
|
942
|
-
result: { structured:
|
|
944
|
+
result: { structured: finalStructured },
|
|
943
945
|
selectedFlow,
|
|
944
946
|
nextStep,
|
|
945
947
|
session,
|
|
@@ -950,7 +952,7 @@ export class ResponseModal {
|
|
|
950
952
|
// - stoppedReason: 'needs_input' for single-step execution (waiting for user input)
|
|
951
953
|
// - session.currentStep: reflects the executed step
|
|
952
954
|
yield {
|
|
953
|
-
delta:
|
|
955
|
+
delta: finalDelta,
|
|
954
956
|
accumulated: finalAccumulated,
|
|
955
957
|
done: chunk.done,
|
|
956
958
|
session,
|
|
@@ -1211,7 +1213,8 @@ export class ResponseModal {
|
|
|
1211
1213
|
schemaName: "fallback_stream_response",
|
|
1212
1214
|
},
|
|
1213
1215
|
});
|
|
1214
|
-
|
|
1216
|
+
// Decode the JSON wrapper to clean message text (same as the flow path).
|
|
1217
|
+
for await (const chunk of this.decodeMessageStream(stream)) {
|
|
1215
1218
|
// Response structure completeness (Requirement 8.1, 8.2, 8.3)
|
|
1216
1219
|
// - executedSteps: empty for fallback (no flow/step execution)
|
|
1217
1220
|
// - stoppedReason: undefined for fallback (no flow context)
|