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
30 changes: 30 additions & 0 deletions AgentExperience.NET.sln
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgentExperience.MicrosoftAg
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgentExperience.MicrosoftAgentFramework.Tests", "tests\AgentExperience.MicrosoftAgentFramework.Tests\AgentExperience.MicrosoftAgentFramework.Tests.csproj", "{9B771769-47C4-4EA1-8808-3E4728899A04}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgentExperience.Storage.Postgres", "src\AgentExperience.Storage.Postgres\AgentExperience.Storage.Postgres.csproj", "{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AgentExperience.Storage.Postgres.Tests", "tests\AgentExperience.Storage.Postgres.Tests\AgentExperience.Storage.Postgres.Tests.csproj", "{86C76642-C4EB-49A8-9E4C-A885644EE49F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -115,6 +119,30 @@ Global
{9B771769-47C4-4EA1-8808-3E4728899A04}.Release|x64.Build.0 = Release|Any CPU
{9B771769-47C4-4EA1-8808-3E4728899A04}.Release|x86.ActiveCfg = Release|Any CPU
{9B771769-47C4-4EA1-8808-3E4728899A04}.Release|x86.Build.0 = Release|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Debug|x64.ActiveCfg = Debug|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Debug|x64.Build.0 = Debug|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Debug|x86.ActiveCfg = Debug|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Debug|x86.Build.0 = Debug|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Release|Any CPU.Build.0 = Release|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Release|x64.ActiveCfg = Release|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Release|x64.Build.0 = Release|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Release|x86.ActiveCfg = Release|Any CPU
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E}.Release|x86.Build.0 = Release|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Debug|x64.ActiveCfg = Debug|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Debug|x64.Build.0 = Debug|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Debug|x86.ActiveCfg = Debug|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Debug|x86.Build.0 = Debug|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Release|Any CPU.Build.0 = Release|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Release|x64.ActiveCfg = Release|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Release|x64.Build.0 = Release|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Release|x86.ActiveCfg = Release|Any CPU
{86C76642-C4EB-49A8-9E4C-A885644EE49F}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -127,5 +155,7 @@ Global
{6E9367BD-1AE0-4D8E-84C2-B73809F57779} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{9CDECFCD-7D5E-4794-9909-5F836FCD35BF} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{9B771769-47C4-4EA1-8808-3E4728899A04} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
{DFBB8572-3FF3-43F5-8A16-2ADB91C0E25E} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
{86C76642-C4EB-49A8-9E4C-A885644EE49F} = {0AB3BF05-4346-4AA6-1389-037BE0695223}
EndGlobalSection
EndGlobal
13 changes: 8 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

AgentExperience.NET captures what an AI agent actually tried, verifies whether it worked, and turns the result into an auditable lesson that future runs can reuse safely. It sits between [Microsoft Agent Framework](https://github.com/microsoft/agent-framework) (MAF) execution and durable storage, without replacing either.

> **Status: early development.** Epic 1 (capture and explain agent experience) is implemented and tested. Persistence, retrieval, and governance are planned (see [Roadmap](#roadmap)). Nothing is published to NuGet yet, and APIs may change.
> **Status: early development.** Epic 1 (capture and explain agent experience) is implemented and tested. Epic 2 has started: Experience Records can be stored in PostgreSQL. Retrieval, injection, and governance are planned (see [Roadmap](#roadmap)). Nothing is published to NuGet yet, and APIs may change.

## Why

Expand All @@ -30,6 +30,7 @@ AgentExperience.NET records observable evidence (tool calls, results, errors, ve
| Deterministic task verification: exit codes, tests, workflow and human checks; host-closed rounds; no LLM | `AgentExperience.Core` |
| Auditable, template-based reflections traceable to evidence IDs | `AgentExperience.Core` |
| MAF adapter: captures ordinary, streaming, failed, and cancelled runs plus tool calls, without altering results | `AgentExperience.MicrosoftAgentFramework` |
| PostgreSQL Experience Record store: create, get, and scoped query; host authorization checked before database access; exact scope matching in SQL | `AgentExperience.Storage.Postgres` |

## Quick look

Expand All @@ -49,7 +50,7 @@ await agent.RunAsync("Triage ticket #4812", session);
// The run, its tool calls, and its sanitized outcome are now available from captureService.
```

See the [adapter README](src/AgentExperience.MicrosoftAgentFramework/README.md) for options, supported agent types, and caveats.
See the [adapter README](src/AgentExperience.MicrosoftAgentFramework/README.md) for options, supported agent types, and caveats. See the [PostgreSQL store README](src/AgentExperience.Storage.Postgres/README.md) for the trust boundary, schema script, and data semantics.

## Design principles

Expand All @@ -66,10 +67,12 @@ src/
AgentExperience.Abstractions/ domain contracts and ports (BCL only)
AgentExperience.Core/ sanitization, capture, verification, reflection
AgentExperience.MicrosoftAgentFramework/ MAF adapter (pinned Microsoft.Agents.AI 1.20.0)
AgentExperience.Storage.Postgres/ PostgreSQL Experience Record store (pinned Npgsql 10.0.3)
tests/
AgentExperience.Abstractions.Tests/ contract and dependency-boundary tests
AgentExperience.Core.Tests/ sanitizer, capture, verification, reflection tests
AgentExperience.MicrosoftAgentFramework.Tests/ real ChatClientAgent runs against a scripted fake model
AgentExperience.Storage.Postgres.Tests/ store tests, mostly against a PostgreSQL container
AgentExperience.CompatibilityProof/ executable proofs for MAF hooks, context providers, pgvector, redaction
docs/ original production architecture research
_bmad-output/ product brief, PRD, architecture, epics, and specs
Expand All @@ -85,16 +88,16 @@ dotnet build
dotnet test
```

Unit and adapter tests run in memory, with no network, database, or model credentials. `AgentExperience.CompatibilityProof` starts a PostgreSQL/pgvector container through Testcontainers, so it needs Docker. To skip it:
Unit and MAF adapter tests run in memory, with no network, database, or model credentials. `AgentExperience.CompatibilityProof` and the `PostgresExperienceRecordStoreTests` in `AgentExperience.Storage.Postgres.Tests` start a PostgreSQL/pgvector container through Testcontainers, so they need Docker. If Testcontainers' Ryuk container fails to start under your local Docker setup, set `TESTCONTAINERS_RYUK_DISABLED=true`. To skip the container-backed tests:

```bash
dotnet test --filter "FullyQualifiedName!~CompatibilityProof"
dotnet test --filter "FullyQualifiedName!~CompatibilityProof&FullyQualifiedName!~PostgresExperienceRecordStoreTests"
```

## Roadmap

1. **Capture and explain agent experience** ✅ contracts, sanitization, capture, verification, reflection, MAF adapter
2. **Reuse relevant experience:** PostgreSQL persistence, hybrid text and vector retrieval, historical-reference injection into MAF
2. **Reuse relevant experience:** PostgreSQL persistence (Experience Record store in place), hybrid text and vector retrieval, historical-reference injection into MAF
3. **Govern experience safely:** sharing grants, audited lifecycle transitions, evidence-based confidence updates
4. **Operate and measure the learning loop:** OpenTelemetry instrumentation, an end-to-end demo, measured reuse against a baseline, data deletion and expiry

Expand Down
45 changes: 45 additions & 0 deletions src/AgentExperience.Abstractions/ExperienceRecord.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
namespace AgentExperience.Abstractions;

/// <summary>
/// A durable, scoped Experience Record: the canonical, immutable snapshot of what was learned from
/// one captured <see cref="ExperienceRun"/> -- its attempts, verified outcome, reflection, environment,
/// provenance, lifecycle status, and reuse-confidence inputs. Schema versioning of the persisted
/// form is owned by the storage adapter; this contract carries no version field.
/// </summary>
/// <param name="ExperienceId">Unique identifier for this record. Must not be <see cref="Guid.Empty"/>.</param>
/// <param name="SourceRunId">The <see cref="ExperienceRun.RunId"/> this record was derived from.</param>
/// <param name="Scope">The tenancy/ownership scope this record belongs to. Required fields must be non-blank; optional fields are either <see langword="null"/> or non-blank.</param>
/// <param name="TaskId">Identifies which task this experience is about. Must be non-blank.</param>
/// <param name="TaskSummary">Optional, sanitized, human-readable summary of the task.</param>
/// <param name="Attempts">The observable attempts from the source run, in the order they occurred.</param>
/// <param name="Outcome">The task verification outcome the record was finalized against.</param>
/// <param name="CompletionScore">The fraction of required checks that conclusively passed, in [0, 1]. Not reuse confidence.</param>
/// <param name="Reflection">The auditable reflection derived from the run; <see langword="null"/> when the record is quarantined without an eligible lesson.</param>
/// <param name="Environment">The runtime environment the source run executed in.</param>
/// <param name="Provenance">Where this record's source capture originated.</param>
/// <param name="Status">The record's current lifecycle status.</param>
/// <param name="ReuseConfidence">The record's current reuse confidence, in [0, 1].</param>
/// <param name="SupportingValidations">Non-negative count of validations supporting reuse, kept so confidence can be recomputed.</param>
/// <param name="Contradictions">Non-negative count of contradictions observed against reuse, kept so confidence can be recomputed.</param>
/// <param name="Revision">Non-negative revision number used for optimistic concurrency by lifecycle commits.</param>
/// <param name="CreatedAt">When the record was created. Persisted and returned in UTC.</param>
/// <param name="UpdatedAt">When the record was last changed. Persisted and returned in UTC.</param>
public sealed record ExperienceRecord(
Guid ExperienceId,
Guid SourceRunId,
Scope Scope,
string TaskId,
string? TaskSummary,
IReadOnlyList<Attempt> Attempts,
Outcome Outcome,
double CompletionScore,
Reflection? Reflection,
EnvironmentFingerprint Environment,
Provenance Provenance,
ExperienceStatus Status,
double ReuseConfidence,
int SupportingValidations,
int Contradictions,
long Revision,
DateTimeOffset CreatedAt,
DateTimeOffset UpdatedAt);
Loading