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