Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,7 @@ AI-generated changes should:
- Follow repository naming and style conventions.
- Include appropriate tests.
- Keep documentation aligned with implementation.

## Source Control Workflow
- Leave all changes local and uncommitted. Do not run `git commit`, `git push`, or open pull requests; the maintainer reviews the uncommitted diff first and then commits, pushes, and opens the PR.
- Do not create or switch branches unless asked. Summarize what changed so the diff can be reviewed.
6 changes: 5 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,8 @@
- Prefer Blazor components over raw HTML to enhance maintainability and reusability.
- Implement SEO and GEO optimization support by using semantic headings (H1, H2) while minimizing direct HTML usage.
- Prioritize root-cause fixes over brute-force styling overrides and adhere to global Blazor/Fluent UI conventions. Verify behavior against current component rendering.
- Use Blazor/Fluent component properties over CSS for layout/styling because CSS is harder and more brittle to maintain.
- Use Blazor/Fluent component properties over CSS for layout/styling because CSS is harder and more brittle to maintain.

## Source Control Workflow
- Leave all changes local and uncommitted. Do not run `git commit`, `git push`, or open pull requests; the maintainer reviews the uncommitted diff first and then commits, pushes, and opens the PR.
- Do not create or switch branches unless asked. Summarize what changed so the diff can be reviewed.
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"dotnet.defaultSolution": "Goodtocode.AgentFramework.Web.slnx"
}
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,8 +411,11 @@ Execution") for the full entity model.

### Repeatability: Rerun / Recall / Replay

Every `Run*PlaybookCommand` accepts a `PlaybookReplayMode` (from
`Goodtocode.Agents.Playbook.Execution`) plus an optional `SourceExecutionId`:
Every `Run*PlaybookCommand` accepts a Governance `RepeatMode` (from
`Goodtocode.Agents.Governance.Domain`, available transitively through
`Goodtocode.Agents.Playbook`) plus an optional `SourceExecutionId`. The app references only
Playbook; it builds a `PlaybookExecutionRequest` directly, with no mappers. See
[feature-playbook-repeatability.md](./docs/product/features/feature-playbook-repeatability.md):

- **Rerun** (default) — a fresh execution: Collect, Evaluate, and Record all run against new
input. This is the only mode available on a Playbook's first execution.
Expand Down
6 changes: 0 additions & 6 deletions buildout.txt

This file was deleted.

101 changes: 0 additions & 101 deletions buildout2.txt

This file was deleted.

101 changes: 0 additions & 101 deletions buildout3.txt

This file was deleted.

19 changes: 19 additions & 0 deletions docs/product/features/feature-playbook-repeatability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Feature: Playbook Repeatability (Rerun / Recall / Replay)

## Overview
Every example playbook "run" endpoint can repeat a prior execution. Repeat semantics come from `Goodtocode.Agents.Governance`, executed through `Goodtocode.Agents.Playbook`. This app references only Playbook and uses both concepts directly, with no mappers.

## Behavior
| Mode | What runs | Source needed |
|------|-----------|---------------|
| Rerun | Collect, Evaluate, Record against live data | no |
| Replay | Evaluate and Record on the recorded evidence | yes |
| Recall | Nothing; the recorded result is returned | yes |

The source is the given `SourceExecutionId`, otherwise the user's latest execution of the playbook. A missing source returns Not Found; a source without recorded evidence/finding returns Conflict.

## Where
`PlaybookRepeatCoordinator` resolves the source and builds the `PlaybookExecutionRequest`; `PlaybookWorkflowGraphExecutor` plans actions through `IRepeatPolicy`.

## Tests
`src/Tests.Integration/Playbooks/PlaybookRepeatability.feature` (Reqnroll) and `PlaybookReplayTests.cs`.
2 changes: 2 additions & 0 deletions src/Core.Application/ConfigureServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection
services.AddSingleton<ChatGovernanceGate>();
services.AddSingleton<IChatSelectionTokenParser, ChatSelectionTokenParser>();

services.AddGovernanceRepeatability();
services.AddScoped<Playbooks.Repeatability.PlaybookRepeatCoordinator>();
services.AddSingleton<IRepeatabilityHashStrategy, DefaultRepeatabilityHashStrategy>();
services.AddScoped<PlaybookGovernanceActivityRecorder>();
services.AddScoped<DocumentReviewPlaybookDefinition>();
Expand Down
9 changes: 4 additions & 5 deletions src/Core.Application/Core.Application.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="Goodtocode.Agents.Governance" Version="2.0.5" />
<PackageReference Include="Goodtocode.Agents.Playbook" Version="1.2.14" />
<PackageReference Include="Goodtocode.Agents.Playbook" Version="2.1.22" />
<PackageReference Include="Goodtocode.Mediator" Version="1.1.26" />
<PackageReference Include="Goodtocode.Validation" Version="1.1.38" />
<PackageReference Include="Microsoft.Agents.AI.Abstractions" Version="1.23.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.12" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.12" />
<PackageReference Include="Microsoft.Agents.AI.Abstractions" Version="1.25.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.13" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.13" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface IEssayEvaluationRunner
Task<PlaybookExecutionResult<EssayEvidence, EssayRubricFinding, EssayScorecardMaterialization>> EvaluateAsync(
string essayText,
CancellationToken cancellationToken,
PlaybookReplayContext<EssayEvidence, EssayRubricFinding>? replayContext = null);
PlaybookExecutionRequest<EssayEvidence, EssayRubricFinding>? request = null);
}

/// <summary>
Expand Down
33 changes: 10 additions & 23 deletions src/Core.Application/Playbooks/Essay/RunEssayPlaybookCommand.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Goodtocode.AgentFramework.Core.Application.Playbooks.Persistence;
using Goodtocode.AgentFramework.Core.Application.Playbooks.Repeatability;
using Goodtocode.Agents.Governance.Domain;
using Goodtocode.Agents.Playbook.Execution;

namespace Goodtocode.AgentFramework.Core.Application.Playbooks.Essay;
Expand All @@ -16,13 +18,13 @@ namespace Goodtocode.AgentFramework.Core.Application.Playbooks.Essay;
public sealed class RunEssayPlaybookCommand : UserScopedRequest, IRequest<PlaybookExecutionResultDto>
{
public required string EssayText { get; init; }
public PlaybookReplayMode ReplayMode { get; init; } = PlaybookReplayMode.Rerun;
public RepeatMode ReplayMode { get; init; } = RepeatMode.Rerun;
public string? SourceExecutionId { get; init; }
}

public sealed class RunEssayPlaybookCommandHandler(
ISender sender,
IAgentFrameworkContext context,
PlaybookRepeatCoordinator repeats,
IEssayEvaluationRunner runner,
EssayStageSummarySelector summarySelector)
: IRequestHandler<RunEssayPlaybookCommand, PlaybookExecutionResultDto>
Expand All @@ -31,28 +33,13 @@ public async Task<PlaybookExecutionResultDto> Handle(RunEssayPlaybookCommand req
{
ArgumentNullException.ThrowIfNull(request);

string essayText;
PlaybookReplayContext<EssayEvidence, EssayRubricFinding>? replayContext = null;
var repeat = await repeats.PrepareAsync<EssayEvidence, EssayRubricFinding>(
"essay", request.UserContext, request.ReplayMode, request.SourceExecutionId, cancellationToken);
var essayText = repeat.SourceCollectInput ?? request.EssayText;

if (request.ReplayMode == PlaybookReplayMode.Rerun)
{
essayText = request.EssayText;
}
else
{
var source = await PlaybookReplaySourceResolver.ResolveAsync(
context, "essay", request.UserContext.OwnerId, request.UserContext.TenantId,
request.SourceExecutionId, cancellationToken);

essayText = source.CollectInput;
var priorEvidence = System.Text.Json.JsonSerializer.Deserialize<EssayEvidence>(source.EvidenceJson!)!;
var priorFinding = System.Text.Json.JsonSerializer.Deserialize<EssayRubricFinding>(source.FindingJson!)!;
replayContext = new PlaybookReplayContext<EssayEvidence, EssayRubricFinding>(
request.ReplayMode, source.Id.ToString(), priorEvidence, priorFinding);
}

var result = await runner.EvaluateAsync(essayText, cancellationToken, replayContext);
var dto = PlaybookExecutionResultDtoFactory.CreateFrom(essayText, result, summarySelector);
var result = await runner.EvaluateAsync(essayText, cancellationToken, repeat.ExecutionRequest);
var dto = PlaybookExecutionResultDtoFactory.CreateFrom(
essayText, result, summarySelector, repeat.Request.Mode, repeat.SourceRecordSummary);
dto.ExecutionId = await Persistence.PlaybookExecutionPersister.SaveAsync(sender, dto, cancellationToken);
return dto;
}
Expand Down
13 changes: 10 additions & 3 deletions src/Core.Application/Playbooks/PlaybookExecutionResultDto.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,23 +99,30 @@ public static PlaybookExecutionResultDto CreateFrom(Core.Domain.Playbooks.Playbo
public static PlaybookExecutionResultDto CreateFrom<TEvidence, TFinding, TMaterialization>(
string collectInput,
PlaybookExecutionResult<TEvidence, TFinding, TMaterialization> result,
IPlaybookStageSummarySelector<TEvidence, TFinding, TMaterialization> summarySelector)
IPlaybookStageSummarySelector<TEvidence, TFinding, TMaterialization> summarySelector,
Goodtocode.Agents.Governance.Domain.RepeatMode repeatMode = Goodtocode.Agents.Governance.Domain.RepeatMode.Rerun,
string? sourceRecordSummary = null)
{
ArgumentNullException.ThrowIfNull(result);
ArgumentNullException.ThrowIfNull(summarySelector);

// A skipped Record leaves no materialization, so Recall shows the source's stored record output.
var recordSummary = result.ActionsExecuted.HasFlag(Goodtocode.Agents.Governance.Domain.ActionType.Record)
? summarySelector.SummarizeMaterialization(result.Materialization)
: sourceRecordSummary ?? string.Empty;

return new PlaybookExecutionResultDto
{
PlaybookKey = result.Metadata.PlaybookKey,
Version = result.Metadata.Version,
StartedUtc = result.Metadata.StartedUtc,
CompletedUtc = result.Metadata.CompletedUtc,
ReplayMode = result.Metadata.ReplayMode.ToString(),
ReplayMode = repeatMode.ToString(),
SourceExecutionId = result.Metadata.SourceExecutionId,
CollectInput = collectInput,
CollectSummary = summarySelector.SummarizeEvidence(result.Evidence),
EvaluateSummary = summarySelector.SummarizeFinding(result.Finding),
RecordSummary = summarySelector.SummarizeMaterialization(result.Materialization),
RecordSummary = recordSummary,
EvidenceJson = System.Text.Json.JsonSerializer.Serialize(result.Evidence),
FindingJson = System.Text.Json.JsonSerializer.Serialize(result.Finding)
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using Goodtocode.AgentFramework.Core.Application.Playbooks.Persistence;
using Goodtocode.Agents.Governance.Domain;
using Goodtocode.Agents.Playbook.Execution;

namespace Goodtocode.AgentFramework.Core.Application.Playbooks.Repeatability;

/// <summary>
/// Everything a Run*PlaybookCommand handler needs to execute a repeat: the Governance request, the Playbook
/// execution request carrying it and the prior state, and the source's collect input and record summary.
/// </summary>
public sealed record PreparedRepeat<TEvidence, TFinding>(
RepeatRequest Request,
PlaybookExecutionRequest<TEvidence, TFinding> ExecutionRequest,
string? SourceCollectInput,
string? SourceRecordSummary);

/// <summary>
/// Resolves the source execution for a repeat and builds the Playbook execution request. Which stages run is
/// decided by the Governance policy inside the Playbook executor, not here.
/// </summary>
public sealed class PlaybookRepeatCoordinator(IAgentFrameworkContext context)
{
public async Task<PreparedRepeat<TEvidence, TFinding>> PrepareAsync<TEvidence, TFinding>(
string playbookKey,
IUserContext user,
RepeatMode mode,
string? sourceExecutionId,
CancellationToken cancellationToken)
{
if (mode == RepeatMode.Rerun)
{
return new(RepeatRequest.Rerun(), PlaybookExecutionRequest<TEvidence, TFinding>.Default, null, null);
}

// Quick-start convention: an omitted source means "the user's latest execution".
var source = await PlaybookReplaySourceResolver.ResolveAsync(
context, playbookKey, user.OwnerId, user.TenantId, sourceExecutionId, cancellationToken);

var request = RepeatRequest.Create(mode, source.Id.ToString());
var executionRequest = new PlaybookExecutionRequest<TEvidence, TFinding>
{
Repeat = request,
SeedEvidence = System.Text.Json.JsonSerializer.Deserialize<TEvidence>(source.EvidenceJson!),
SeedFinding = System.Text.Json.JsonSerializer.Deserialize<TFinding>(source.FindingJson!)
};

return new(request, executionRequest, source.CollectInput, source.RecordOutput);
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Goodtocode.AgentFramework.Core.Application.Playbooks.Persistence;
using Goodtocode.AgentFramework.Core.Application.Playbooks.Repeatability;
using Goodtocode.Agents.Governance.Domain;
using Goodtocode.Agents.Playbook.Execution;

namespace Goodtocode.AgentFramework.Core.Application.Playbooks.SqlStatistics;
Expand All @@ -14,13 +16,13 @@ namespace Goodtocode.AgentFramework.Core.Application.Playbooks.SqlStatistics;
public sealed class RunSqlStatisticsPlaybookCommand : UserScopedRequest, IRequest<PlaybookExecutionResultDto>
{
public required string DatabaseName { get; init; }
public PlaybookReplayMode ReplayMode { get; init; } = PlaybookReplayMode.Rerun;
public RepeatMode ReplayMode { get; init; } = RepeatMode.Rerun;
public string? SourceExecutionId { get; init; }
}

public sealed class RunSqlStatisticsPlaybookCommandHandler(
ISender sender,
IAgentFrameworkContext context,
PlaybookRepeatCoordinator repeats,
ISqlStatisticsClassificationRunner runner,
SqlStatisticsStageSummarySelector summarySelector)
: IRequestHandler<RunSqlStatisticsPlaybookCommand, PlaybookExecutionResultDto>
Expand All @@ -29,28 +31,13 @@ public async Task<PlaybookExecutionResultDto> Handle(RunSqlStatisticsPlaybookCom
{
ArgumentNullException.ThrowIfNull(request);

string databaseName;
PlaybookReplayContext<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding>? replayContext = null;
var repeat = await repeats.PrepareAsync<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding>(
"sql-statistics", request.UserContext, request.ReplayMode, request.SourceExecutionId, cancellationToken);
var databaseName = repeat.SourceCollectInput ?? request.DatabaseName;

if (request.ReplayMode == PlaybookReplayMode.Rerun)
{
databaseName = request.DatabaseName;
}
else
{
var source = await PlaybookReplaySourceResolver.ResolveAsync(
context, "sql-statistics", request.UserContext.OwnerId, request.UserContext.TenantId,
request.SourceExecutionId, cancellationToken);

databaseName = source.CollectInput;
var priorEvidence = System.Text.Json.JsonSerializer.Deserialize<SqlDatabaseStatisticsEvidence>(source.EvidenceJson!)!;
var priorFinding = System.Text.Json.JsonSerializer.Deserialize<SqlDatabaseSizeFinding>(source.FindingJson!)!;
replayContext = new PlaybookReplayContext<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding>(
request.ReplayMode, source.Id.ToString(), priorEvidence, priorFinding);
}

var result = await runner.ClassifyAsync(databaseName, cancellationToken, replayContext);
var dto = PlaybookExecutionResultDtoFactory.CreateFrom(databaseName, result, summarySelector);
var result = await runner.ClassifyAsync(databaseName, cancellationToken, repeat.ExecutionRequest);
var dto = PlaybookExecutionResultDtoFactory.CreateFrom(
databaseName, result, summarySelector, repeat.Request.Mode, repeat.SourceRecordSummary);
dto.ExecutionId = await Persistence.PlaybookExecutionPersister.SaveAsync(sender, dto, cancellationToken);
return dto;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ public interface ISqlStatisticsClassificationRunner
Task<PlaybookExecutionResult<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding, SqlDatabaseSizeMaterialization>> ClassifyAsync(
string databaseName,
CancellationToken cancellationToken,
PlaybookReplayContext<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding>? replayContext = null);
PlaybookExecutionRequest<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding>? request = null);
}

public sealed class SqlStatisticsClassificationRunner(
Expand All @@ -27,12 +27,12 @@ public sealed class SqlStatisticsClassificationRunner(
public Task<PlaybookExecutionResult<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding, SqlDatabaseSizeMaterialization>> ClassifyAsync(
string databaseName,
CancellationToken cancellationToken,
PlaybookReplayContext<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding>? replayContext = null)
PlaybookExecutionRequest<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding>? request = null)
{
ArgumentException.ThrowIfNullOrWhiteSpace(databaseName);

return replayContext is null
? executor.ExecuteAsync(definition, databaseName, cancellationToken, activityRecorder: recorder)
: executor.ExecuteAsync(definition, databaseName, replayContext, cancellationToken, activityRecorder: recorder);
return executor.ExecuteAsync(
definition, databaseName, request ?? PlaybookExecutionRequest<SqlDatabaseStatisticsEvidence, SqlDatabaseSizeFinding>.Default,
cancellationToken, activityRecorder: recorder);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public interface ITaxonomyClassificationRunner
Task<PlaybookExecutionResult<TaxonomyEvidence, TaxonomyFinding, TaxonomyMaterialization>> ClassifyAsync(
string text,
CancellationToken cancellationToken,
PlaybookReplayContext<TaxonomyEvidence, TaxonomyFinding>? replayContext = null);
PlaybookExecutionRequest<TaxonomyEvidence, TaxonomyFinding>? request = null);
}

public sealed class ClassifyTaxonomyCommand : IRequest<PlaybookExecutionResult<TaxonomyEvidence, TaxonomyFinding, TaxonomyMaterialization>>
Expand Down
Loading
Loading