55using CrestApps . Core . AI . Orchestration ;
66using CrestApps . Core . AI . Services ;
77using CrestApps . Core . Support . Json ;
8+ using CrestApps . Core . Templates . Parsing ;
89using CrestApps . Core . Templates . Services ;
910using Microsoft . Extensions . AI ;
1011using Microsoft . Extensions . Logging ;
@@ -22,6 +23,7 @@ public sealed class PostSessionProcessingService
2223 private readonly IAIDeploymentManager _deploymentManager ;
2324 private readonly IAIToolsService _toolsService ;
2425 private readonly ITemplateService _aiTemplateService ;
26+ private readonly ITemplateParser _markdownTemplateParser ;
2527 private readonly IServiceProvider _serviceProvider ;
2628 private readonly TimeProvider _timeProvider ;
2729 private readonly DefaultAIOptions _defaultOptions ;
@@ -35,6 +37,7 @@ public sealed class PostSessionProcessingService
3537 /// <param name="clientFactory">The client factory.</param>
3638 /// <param name="toolsService">The tools service.</param>
3739 /// <param name="aiTemplateService">The ai template service.</param>
40+ /// <param name="templateParsers">The registered template parsers.</param>
3841 /// <param name="defaultOptions">The default options.</param>
3942 /// <param name="serviceProvider">The service provider.</param>
4043 /// <param name="timeProvider">The time provider.</param>
@@ -44,6 +47,7 @@ public PostSessionProcessingService(
4447 IAIClientFactory clientFactory ,
4548 IAIToolsService toolsService ,
4649 ITemplateService aiTemplateService ,
50+ IEnumerable < ITemplateParser > templateParsers ,
4751 DefaultAIOptions defaultOptions ,
4852 IServiceProvider serviceProvider ,
4953 TimeProvider timeProvider ,
@@ -54,6 +58,7 @@ public PostSessionProcessingService(
5458 _deploymentManager = deploymentManager ;
5559 _toolsService = toolsService ;
5660 _aiTemplateService = aiTemplateService ;
61+ _markdownTemplateParser = ResolveMarkdownTemplateParser ( templateParsers ) ;
5762 _serviceProvider = serviceProvider ;
5863 _timeProvider = timeProvider ;
5964 _defaultOptions = defaultOptions ;
@@ -479,79 +484,96 @@ private async Task<Dictionary<string, PostSessionResult>> ProcessWithToolsAsync(
479484 private PostSessionProcessingResponse TryParsePostSessionResponse ( string sessionId , string responseText )
480485 {
481486 // Strategy 1: Direct JSON deserialization.
482- try
487+ if ( TryDeserializePostSessionResponse ( responseText , out var directResult ) )
483488 {
484- var result = JsonSerializer . Deserialize < PostSessionProcessingResponse > (
485- responseText , JSOptions . CaseInsensitive ) ;
489+ return directResult ;
490+ }
486491
487- if ( result ? . Tasks != null && result . Tasks . Count > 0 )
492+ if ( _logger . IsEnabled ( LogLevel . Debug ) )
493+ {
494+ _logger . LogDebug (
495+ "Post-session response for session '{SessionId}' is not valid JSON. Trying fallback extraction." ,
496+ sessionId ) ;
497+ }
498+
499+ // Strategy 2: Extract JSON from markdown code fences.
500+ var jsonBlock = JsonExtractor . ExtractFromCodeFence ( responseText ) ;
501+
502+ if ( TryDeserializePostSessionResponse ( jsonBlock , out var fencedResult ) )
503+ {
504+ if ( _logger . IsEnabled ( LogLevel . Debug ) )
488505 {
489- return result ;
506+ _logger . LogDebug (
507+ "Post-session response for session '{SessionId}' parsed successfully from code fence." ,
508+ sessionId ) ;
490509 }
510+
511+ return fencedResult ;
491512 }
492- catch ( JsonException )
513+
514+ // Strategy 3: Extract the first JSON object from surrounding text.
515+ var jsonObject = JsonExtractor . ExtractJsonObject ( responseText ) ;
516+
517+ if ( jsonObject != null &&
518+ jsonObject != responseText &&
519+ TryDeserializePostSessionResponse ( jsonObject , out var objectResult ) )
493520 {
494521 if ( _logger . IsEnabled ( LogLevel . Debug ) )
495522 {
496523 _logger . LogDebug (
497- "Post-session response for session '{SessionId}' is not valid JSON. Trying fallback extraction ." ,
524+ "Post-session response for session '{SessionId}' parsed successfully from embedded JSON object ." ,
498525 sessionId ) ;
499526 }
527+
528+ return objectResult ;
500529 }
501530
502- // Strategy 2: Extract JSON from markdown code fences .
503- var jsonBlock = JsonExtractor . ExtractFromCodeFence ( responseText ) ;
531+ // Strategy 4: Normalize markdown/front matter, then retry the same lenient extraction .
532+ var normalizedBody = _markdownTemplateParser . Parse ( responseText ) . Body ? . Trim ( ) ;
504533
505- if ( jsonBlock != null )
534+ if ( ! string . IsNullOrWhiteSpace ( normalizedBody ) &&
535+ ! string . Equals ( normalizedBody , responseText . Trim ( ) , StringComparison . Ordinal ) )
506536 {
507- try
537+ if ( TryDeserializePostSessionResponse ( normalizedBody , out var normalizedResult ) )
508538 {
509- var result = JsonSerializer . Deserialize < PostSessionProcessingResponse > (
510- jsonBlock , JSOptions . CaseInsensitive ) ;
511-
512- if ( result ? . Tasks != null && result . Tasks . Count > 0 )
539+ if ( _logger . IsEnabled ( LogLevel . Debug ) )
513540 {
514- if ( _logger . IsEnabled ( LogLevel . Debug ) )
515- {
516- _logger . LogDebug (
517- "Post-session response for session '{SessionId}' parsed successfully from code fence." ,
518- sessionId ) ;
519- }
520-
521- return result ;
541+ _logger . LogDebug (
542+ "Post-session response for session '{SessionId}' parsed successfully from normalized markdown body." ,
543+ sessionId ) ;
522544 }
545+
546+ return normalizedResult ;
523547 }
524- catch ( JsonException )
525- {
526- // Code fence content wasn't valid JSON either, continue to next strategy.
527- }
528- }
529548
530- // Strategy 3: Extract the first JSON object from surrounding text.
531- var jsonObject = JsonExtractor . ExtractJsonObject ( responseText ) ;
549+ var normalizedJsonBlock = JsonExtractor . ExtractFromCodeFence ( normalizedBody ) ;
532550
533- if ( jsonObject != null && jsonObject != responseText )
534- {
535- try
551+ if ( TryDeserializePostSessionResponse ( normalizedJsonBlock , out var normalizedFencedResult ) )
536552 {
537- var result = JsonSerializer . Deserialize < PostSessionProcessingResponse > (
538- jsonObject , JSOptions . CaseInsensitive ) ;
539-
540- if ( result ? . Tasks != null && result . Tasks . Count > 0 )
553+ if ( _logger . IsEnabled ( LogLevel . Debug ) )
541554 {
542- if ( _logger . IsEnabled ( LogLevel . Debug ) )
543- {
544- _logger . LogDebug (
545- "Post-session response for session '{SessionId}' parsed successfully from embedded JSON object." ,
546- sessionId ) ;
547- }
548-
549- return result ;
555+ _logger . LogDebug (
556+ "Post-session response for session '{SessionId}' parsed successfully from normalized markdown code fence." ,
557+ sessionId ) ;
550558 }
559+
560+ return normalizedFencedResult ;
551561 }
552- catch ( JsonException )
562+
563+ var normalizedJsonObject = JsonExtractor . ExtractJsonObject ( normalizedBody ) ;
564+
565+ if ( normalizedJsonObject != null &&
566+ normalizedJsonObject != normalizedBody &&
567+ TryDeserializePostSessionResponse ( normalizedJsonObject , out var normalizedObjectResult ) )
553568 {
554- // Extracted text wasn't valid JSON either.
569+ if ( _logger . IsEnabled ( LogLevel . Debug ) )
570+ {
571+ _logger . LogDebug (
572+ "Post-session response for session '{SessionId}' parsed successfully from normalized markdown embedded JSON object." ,
573+ sessionId ) ;
574+ }
575+
576+ return normalizedObjectResult ;
555577 }
556578 }
557579
@@ -617,7 +639,7 @@ private async Task<Dictionary<string, PostSessionResult>> TryRecoverStructuredTo
617639 CreateResponseLogPreview ( recoveryResponseText ) ) ;
618640 }
619641
620- PostSessionProcessingResponse result ;
642+ PostSessionProcessingResponse result = null ;
621643
622644 try
623645 {
@@ -631,8 +653,6 @@ private async Task<Dictionary<string, PostSessionResult>> TryRecoverStructuredTo
631653 "Structured recovery for post-session tool response on session '{SessionId}' did not return JSON content." ,
632654 sessionId ) ;
633655 }
634-
635- return null ;
636656 }
637657 catch ( JsonException )
638658 {
@@ -642,11 +662,24 @@ private async Task<Dictionary<string, PostSessionResult>> TryRecoverStructuredTo
642662 "Structured recovery for post-session tool response on session '{SessionId}' returned invalid JSON content." ,
643663 sessionId ) ;
644664 }
665+ }
645666
646- return null ;
667+ if ( result ? . Tasks is { Count : > 0 } )
668+ {
669+ if ( _logger . IsEnabled ( LogLevel . Debug ) )
670+ {
671+ _logger . LogDebug (
672+ "Structured recovery for post-session tool response on session '{SessionId}' succeeded with {TaskCount} task result(s)." ,
673+ sessionId ,
674+ result . Tasks . Count ) ;
675+ }
676+
677+ return ApplyResults ( tasks , result . Tasks ) ;
647678 }
648679
649- if ( result ? . Tasks is null || result . Tasks . Count == 0 )
680+ var recoveredFromText = TryParsePostSessionResponse ( sessionId , recoveryResponseText ) ;
681+
682+ if ( recoveredFromText ? . Tasks is null || recoveredFromText . Tasks . Count == 0 )
650683 {
651684 if ( _logger . IsEnabled ( LogLevel . Debug ) )
652685 {
@@ -661,12 +694,46 @@ private async Task<Dictionary<string, PostSessionResult>> TryRecoverStructuredTo
661694 if ( _logger . IsEnabled ( LogLevel . Debug ) )
662695 {
663696 _logger . LogDebug (
664- "Structured recovery for post-session tool response on session '{SessionId}' succeeded with {TaskCount} task result(s)." ,
697+ "Structured recovery for post-session tool response on session '{SessionId}' succeeded by parsing assistant text with {TaskCount} task result(s)." ,
665698 sessionId ,
666- result . Tasks . Count ) ;
699+ recoveredFromText . Tasks . Count ) ;
700+ }
701+
702+ return ApplyResults ( tasks , recoveredFromText . Tasks ) ;
703+ }
704+
705+ private static bool TryDeserializePostSessionResponse (
706+ string responseText ,
707+ out PostSessionProcessingResponse response )
708+ {
709+ if ( string . IsNullOrWhiteSpace ( responseText ) )
710+ {
711+ response = null ;
712+ return false ;
713+ }
714+
715+ try
716+ {
717+ response = JsonSerializer . Deserialize < PostSessionProcessingResponse > (
718+ responseText ,
719+ JSOptions . CaseInsensitive ) ;
720+
721+ return response ? . Tasks is { Count : > 0 } ;
722+ }
723+ catch ( JsonException )
724+ {
725+ response = null ;
726+ return false ;
667727 }
728+ }
729+
730+ private static ITemplateParser ResolveMarkdownTemplateParser ( IEnumerable < ITemplateParser > templateParsers )
731+ {
732+ ArgumentNullException . ThrowIfNull ( templateParsers ) ;
668733
669- return ApplyResults ( tasks , result . Tasks ) ;
734+ return templateParsers . FirstOrDefault ( parser =>
735+ parser . SupportedExtensions . Any ( extension => string . Equals ( extension , ".md" , StringComparison . OrdinalIgnoreCase ) ) )
736+ ?? throw new InvalidOperationException ( "No markdown template parser is registered for post-session response recovery." ) ;
670737 }
671738
672739 private Dictionary < string , PostSessionResult > CreateFailedResults (
0 commit comments