Skip to content

Commit db1c1b9

Browse files
ooplesfranklinicclaude
authored
feat(agentic): Phase 4 — self-improving orchestration (epic #1544) (#1556)
* feat(agentic): Phase 4 — trajectory capture (self-improvement foundation) (#1544) The raw material every self-improvement mechanism learns from: - AgentTrajectory: a captured run (transcript, final answer, iterations, usage, agent name, optional evaluator-assigned Reward, metadata). - ITrajectoryStore (Add/Get/GetAll/Query/Clear) + InMemoryTrajectoryStore. - TracingAgent<T> (IAgent<T>): transparently records each run of any wrapped agent (executor/supervisor/swarm/memory) to the store without changing behavior; supports per-experiment metadata. - 5 tests (green net10.0 + net471): records runs, accumulates + queryable, metadata, get-by-id + reward annotation, clear. net471 nullability fix on GetAsync (Task.FromResult<AgentTrajectory?>). No null-forgiving. Stacked on Phase 3 (#1552). Next: eval harness scoring trajectories with the existing reward models, then learned routing / prompt-opt / online LoRA. Part of epic #1544 (Phase 4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 4 — evaluation harness (score trajectories) (#1544) Turns captured runs into a measurable quality signal — the continuous-eval step the learners optimize against: - ITrajectoryEvaluator (trajectory-native scoring seam; higher = better) + DelegateTrajectoryEvaluator (custom scoring functions: exact-match, JSON validity, cost penalties, LLM-as-judge, or an adapter over the reasoning reward models). - EvaluationReport (count + mean/min/max reward + pass-rate at a threshold) — the scoreboard a self-improvement loop watches. - TrajectoryEvaluationRunner: scores trajectories (writing AgentTrajectory.Reward) and aggregates the report; EvaluateAsync(list) + EvaluateStoreAsync(store). - 9 tests total (green net10.0 + net471): exact-match grading + reward annotation, pass-rate threshold, whole-store eval with write-back, empty-set report. No null-forgiving. Reward models (OutcomeRewardModel/etc.) are reasoning-chain-specific + internal; they plug in via an ITrajectoryEvaluator adapter (follow-up) rather than coupling the harness to reasoning types. Part of epic #1544 (Phase 4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 4 — learned routing (contextual reward-weighted bandit) (#1544) Replaces a hand-tuned router with one that learns from graded history which agent performs best — the orchestration improves as rewarded runs accumulate: - LearnedAgentRouter<T> (an IAgent<T>): LearnFrom(trajectories) builds per-agent (optionally per-context) mean-reward stats; routing exploits the best agent, explores unseen candidates first (optimistic), and takes a random agent with probability explorationRate. Optional contextKey learns different best-agents per task type. - Composable: drops in anywhere an agent is expected and, wrapped by TracingAgent, its routed runs feed back into the store — closing the self-improvement loop. - 5 tests (green net10.0 + net471): routes to highest-reward agent after learning, explores unseen first, learns per-context (math vs prose), closed-loop capture under the chosen agent, empty/duplicate guards. No null-forgiving. Part of epic #1544 (Phase 4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 4 — prompt auto-optimization (DSPy-like) (#1544) Evaluation-driven prompt search: pick the system prompt that scores best on a labeled eval set. - PromptOptimizer<T>.OptimizeAsync(candidatePrompts, agentFactory, cases): builds an agent per candidate prompt, runs the eval cases, scores each, returns the best prompt + ranked candidates. Default scorer = case-insensitive substring match vs expected; overridable (e.g., reuse an ITrajectoryEvaluator / reward model / length penalty). - PromptEvalCase (input + expected) + ScoredPrompt + PromptOptimizationResult. - The optimizer is the SELECTION half; candidate prompts (the SEARCH half) can come from a hand-written set, an LLM proposer, or AiDotNet's existing genetic/beam/annealing prompt optimizers feeding their population in. - 4 tests (green net10.0 + net471): selects best prompt (ranked), custom scorer, single candidate, empty-input guards. Tests use a real AgentExecutor whose scripted model keys off the system prompt (no manual result construction, no null-forgiving). Part of epic #1544 (Phase 4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(agentic): Phase 4 — reward-filtered fine-tuning dataset (online LoRA data prep) (#1544) The data-preparation half of online LoRA self-improvement (reward-filtered behavior cloning): - RewardFilteredDatasetBuilder.Build(trajectories): keeps graded runs at/above a reward threshold and distills each into a (prompt, completion) pair — prompt = conversation up to the final turn, completion = final answer; skips ungraded/degenerate runs. - FineTuningExample (prompt/completion/reward) + FineTuningDataset (examples + mean reward) — ready to hand to the repo's LoRA/FineTuning trainer to produce an adapter for the local model. - 3 tests (green net10.0 + net471): keeps only high-reward runs (prompt excludes completion), skips ungraded/too-short, threshold controls inclusion. No null-forgiving. The actual LoRA training (dataset -> adapter on the local model) is the model-layer step performed by the existing trainer; this is the agentic step that decides WHAT to learn from. Part of epic #1544 (Phase 4). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: franklinic <franklin@ivorycloud.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5c37400 commit db1c1b9

20 files changed

Lines changed: 1270 additions & 0 deletions
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
using AiDotNet.Agentic.Models;
2+
3+
// Disambiguate from the legacy AiDotNet.PromptEngineering.Templates.ChatMessage (global using).
4+
using ChatMessage = AiDotNet.Agentic.Models.ChatMessage;
5+
6+
namespace AiDotNet.Agentic.SelfImproving;
7+
8+
/// <summary>
9+
/// A captured record of one agent run — the structured "trajectory" the self-improving layer learns from.
10+
/// It pairs what the agent did (the full message transcript, the final answer, step count, token usage) with
11+
/// an optional quality <see cref="Reward"/> assigned later by an evaluator.
12+
/// </summary>
13+
/// <remarks>
14+
/// <para>
15+
/// Trajectories are the raw material for every self-improvement mechanism: continuous evaluation scores them,
16+
/// learned routers/tool policies train on them, prompt optimizers measure prompts against them, and
17+
/// reward-filtered trajectories become fine-tuning (LoRA) data. Capturing a run is non-invasive — a
18+
/// <see cref="TracingAgent{T}"/> wraps any agent and records each run without changing its behavior.
19+
/// </para>
20+
/// <para><b>For Beginners:</b> Think of this as the flight recorder for an agent: it saves exactly what was
21+
/// said and done on a run, plus (once graded) how good the outcome was. Collect many of these and the system
22+
/// can learn what works and get better over time.
23+
/// </para>
24+
/// </remarks>
25+
public sealed class AgentTrajectory
26+
{
27+
/// <summary>
28+
/// Initializes a new trajectory.
29+
/// </summary>
30+
/// <param name="id">The unique id for this trajectory.</param>
31+
/// <param name="agentName">The name of the agent that produced the run.</param>
32+
/// <param name="messages">The full conversation transcript the run produced.</param>
33+
/// <param name="finalText">The agent's final answer.</param>
34+
/// <param name="iterations">The number of model calls the run took.</param>
35+
/// <param name="usage">Aggregate token usage, when available.</param>
36+
/// <param name="reward">An optional quality score (assigned by an evaluator). <c>null</c> until graded.</param>
37+
/// <param name="metadata">Optional key/value metadata.</param>
38+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="id"/>, <paramref name="agentName"/>, <paramref name="messages"/>, or <paramref name="finalText"/> is <c>null</c>.</exception>
39+
public AgentTrajectory(
40+
string id,
41+
string agentName,
42+
IReadOnlyList<ChatMessage> messages,
43+
string finalText,
44+
int iterations,
45+
ChatUsage? usage = null,
46+
double? reward = null,
47+
IReadOnlyDictionary<string, string>? metadata = null)
48+
{
49+
Guard.NotNullOrWhiteSpace(id);
50+
Guard.NotNull(agentName);
51+
Guard.NotNull(messages);
52+
Guard.NotNull(finalText);
53+
Id = id;
54+
AgentName = agentName;
55+
Messages = messages;
56+
FinalText = finalText;
57+
Iterations = iterations;
58+
Usage = usage;
59+
Reward = reward;
60+
Metadata = metadata;
61+
}
62+
63+
/// <summary>Gets the unique id for this trajectory.</summary>
64+
public string Id { get; }
65+
66+
/// <summary>Gets the name of the agent that produced the run.</summary>
67+
public string AgentName { get; }
68+
69+
/// <summary>Gets the full conversation transcript the run produced.</summary>
70+
public IReadOnlyList<ChatMessage> Messages { get; }
71+
72+
/// <summary>Gets the agent's final answer.</summary>
73+
public string FinalText { get; }
74+
75+
/// <summary>Gets the number of model calls the run took.</summary>
76+
public int Iterations { get; }
77+
78+
/// <summary>Gets aggregate token usage, or <c>null</c> when not reported.</summary>
79+
public ChatUsage? Usage { get; }
80+
81+
/// <summary>
82+
/// Gets or sets the quality score for this trajectory, assigned by an evaluator (higher is better), or
83+
/// <c>null</c> while ungraded.
84+
/// </summary>
85+
public double? Reward { get; set; }
86+
87+
/// <summary>Gets optional key/value metadata, or <c>null</c> when none was supplied.</summary>
88+
public IReadOnlyDictionary<string, string>? Metadata { get; }
89+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
namespace AiDotNet.Agentic.SelfImproving;
2+
3+
/// <summary>
4+
/// An <see cref="ITrajectoryEvaluator"/> backed by a user-supplied scoring function — the general-purpose
5+
/// hook for custom rewards (exact-match against a labeled answer, regex/JSON validity, cost penalties, or any
6+
/// combination).
7+
/// </summary>
8+
/// <remarks>
9+
/// <para><b>For Beginners:</b> The do-it-yourself grader: you provide a small function that looks at a run and
10+
/// returns a score, and this turns it into an evaluator the rest of the system can use.
11+
/// </para>
12+
/// </remarks>
13+
public sealed class DelegateTrajectoryEvaluator : ITrajectoryEvaluator
14+
{
15+
private readonly Func<AgentTrajectory, double> _score;
16+
17+
/// <summary>
18+
/// Initializes a new evaluator from a synchronous scoring function.
19+
/// </summary>
20+
/// <param name="score">The function that scores a trajectory (higher is better).</param>
21+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="score"/> is <c>null</c>.</exception>
22+
public DelegateTrajectoryEvaluator(Func<AgentTrajectory, double> score)
23+
{
24+
Guard.NotNull(score);
25+
_score = score;
26+
}
27+
28+
/// <inheritdoc/>
29+
public Task<double> EvaluateAsync(AgentTrajectory trajectory, CancellationToken cancellationToken = default)
30+
{
31+
Guard.NotNull(trajectory);
32+
return Task.FromResult(_score(trajectory));
33+
}
34+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
namespace AiDotNet.Agentic.SelfImproving;
2+
3+
/// <summary>
4+
/// Aggregate statistics from evaluating a set of trajectories: how many were scored, the reward
5+
/// distribution, and the fraction that met a pass threshold. This is the scoreboard a self-improvement loop
6+
/// watches to know whether it is getting better.
7+
/// </summary>
8+
/// <remarks>
9+
/// <para><b>For Beginners:</b> The report card for a batch of runs — average score, best and worst, and what
10+
/// percentage "passed". Compare reports before and after a change to see whether the agents improved.
11+
/// </para>
12+
/// </remarks>
13+
public sealed class EvaluationReport
14+
{
15+
/// <summary>
16+
/// Initializes a new report.
17+
/// </summary>
18+
/// <param name="count">The number of trajectories scored.</param>
19+
/// <param name="meanReward">The mean reward.</param>
20+
/// <param name="minReward">The minimum reward.</param>
21+
/// <param name="maxReward">The maximum reward.</param>
22+
/// <param name="passRate">The fraction of trajectories with reward &gt;= <paramref name="passThreshold"/> (0–1).</param>
23+
/// <param name="passThreshold">The threshold used to compute <paramref name="passRate"/>.</param>
24+
public EvaluationReport(int count, double meanReward, double minReward, double maxReward, double passRate, double passThreshold)
25+
{
26+
Count = count;
27+
MeanReward = meanReward;
28+
MinReward = minReward;
29+
MaxReward = maxReward;
30+
PassRate = passRate;
31+
PassThreshold = passThreshold;
32+
}
33+
34+
/// <summary>Gets the number of trajectories scored.</summary>
35+
public int Count { get; }
36+
37+
/// <summary>Gets the mean reward across scored trajectories (0 when none).</summary>
38+
public double MeanReward { get; }
39+
40+
/// <summary>Gets the minimum reward (0 when none).</summary>
41+
public double MinReward { get; }
42+
43+
/// <summary>Gets the maximum reward (0 when none).</summary>
44+
public double MaxReward { get; }
45+
46+
/// <summary>Gets the fraction of trajectories meeting the pass threshold (0–1).</summary>
47+
public double PassRate { get; }
48+
49+
/// <summary>Gets the threshold used to compute <see cref="PassRate"/>.</summary>
50+
public double PassThreshold { get; }
51+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
namespace AiDotNet.Agentic.SelfImproving;
2+
3+
/// <summary>
4+
/// A set of reward-filtered <see cref="FineTuningExample"/>s ready to hand to a LoRA / fine-tuning trainer —
5+
/// the bridge from "the agent did well on these runs" to "make the local model better at them".
6+
/// </summary>
7+
/// <remarks>
8+
/// <para><b>For Beginners:</b> A study packet of known-good question/answer pairs. Feed it to the existing
9+
/// LoRA fine-tuning trainer and the local model improves at the kinds of task it already handled well —
10+
/// learning from its own successes (reward-filtered behavior cloning).
11+
/// </para>
12+
/// </remarks>
13+
public sealed class FineTuningDataset
14+
{
15+
/// <summary>
16+
/// Initializes a new dataset.
17+
/// </summary>
18+
/// <param name="examples">The fine-tuning examples.</param>
19+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="examples"/> is <c>null</c>.</exception>
20+
public FineTuningDataset(IReadOnlyList<FineTuningExample> examples)
21+
{
22+
Guard.NotNull(examples);
23+
Examples = examples;
24+
MeanReward = examples.Count == 0 ? 0.0 : examples.Average(e => e.Reward);
25+
}
26+
27+
/// <summary>Gets the fine-tuning examples.</summary>
28+
public IReadOnlyList<FineTuningExample> Examples { get; }
29+
30+
/// <summary>Gets the number of examples.</summary>
31+
public int Count => Examples.Count;
32+
33+
/// <summary>Gets the mean reward across the examples (0 when empty).</summary>
34+
public double MeanReward { get; }
35+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
namespace AiDotNet.Agentic.SelfImproving;
2+
3+
/// <summary>
4+
/// One supervised fine-tuning example distilled from a high-reward trajectory: the prompt the agent saw and
5+
/// the completion it produced that earned a good score.
6+
/// </summary>
7+
/// <remarks>
8+
/// <para><b>For Beginners:</b> A "do it like this" training pair — the question plus a known-good answer —
9+
/// harvested from a run that scored well. Many of these teach a local model to imitate its own best behavior.
10+
/// </para>
11+
/// </remarks>
12+
public sealed class FineTuningExample
13+
{
14+
/// <summary>
15+
/// Initializes a new example.
16+
/// </summary>
17+
/// <param name="prompt">The input/context the agent was given.</param>
18+
/// <param name="completion">The (good) completion to learn.</param>
19+
/// <param name="reward">The reward the originating trajectory earned.</param>
20+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="prompt"/> or <paramref name="completion"/> is <c>null</c>.</exception>
21+
public FineTuningExample(string prompt, string completion, double reward)
22+
{
23+
Guard.NotNull(prompt);
24+
Guard.NotNull(completion);
25+
Prompt = prompt;
26+
Completion = completion;
27+
Reward = reward;
28+
}
29+
30+
/// <summary>Gets the input/context the agent was given.</summary>
31+
public string Prompt { get; }
32+
33+
/// <summary>Gets the completion to learn.</summary>
34+
public string Completion { get; }
35+
36+
/// <summary>Gets the reward the originating trajectory earned.</summary>
37+
public double Reward { get; }
38+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
namespace AiDotNet.Agentic.SelfImproving;
2+
3+
/// <summary>
4+
/// Scores a captured <see cref="AgentTrajectory"/>, producing the reward signal the self-improving layer
5+
/// optimizes against (higher is better). This is the seam between "how good was that run?" and every learning
6+
/// mechanism that consumes the answer.
7+
/// </summary>
8+
/// <remarks>
9+
/// <para>
10+
/// Evaluators range from simple (exact-match against a known answer, length/cost penalties) to sophisticated
11+
/// (an LLM-as-judge, or an adapter over the reasoning reward models). Keeping the contract trajectory-native
12+
/// means routing/prompt-optimization/fine-tuning all share one definition of quality and can be swapped
13+
/// without touching the learners.
14+
/// </para>
15+
/// <para><b>For Beginners:</b> A grader. You hand it one recorded run and it returns a score saying how good
16+
/// the outcome was. Collect scores across many runs and the system can tell which behaviors to reinforce.
17+
/// </para>
18+
/// </remarks>
19+
public interface ITrajectoryEvaluator
20+
{
21+
/// <summary>
22+
/// Scores a trajectory. Higher is better; the scale is the evaluator's own (commonly 0–1).
23+
/// </summary>
24+
/// <param name="trajectory">The trajectory to score.</param>
25+
/// <param name="cancellationToken">Token used to cancel the evaluation.</param>
26+
/// <returns>The reward score.</returns>
27+
Task<double> EvaluateAsync(AgentTrajectory trajectory, CancellationToken cancellationToken = default);
28+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
namespace AiDotNet.Agentic.SelfImproving;
2+
3+
/// <summary>
4+
/// Stores captured <see cref="AgentTrajectory"/> records so the self-improving layer can replay, evaluate,
5+
/// and learn from past agent runs.
6+
/// </summary>
7+
/// <remarks>
8+
/// <para><b>For Beginners:</b> This is the logbook of everything the agents have done. Other parts of the
9+
/// system read from it to grade past runs and figure out how to do better next time.
10+
/// </para>
11+
/// </remarks>
12+
public interface ITrajectoryStore
13+
{
14+
/// <summary>
15+
/// Adds a trajectory and returns its id.
16+
/// </summary>
17+
/// <param name="trajectory">The trajectory to store.</param>
18+
/// <param name="cancellationToken">Token used to cancel the operation.</param>
19+
/// <returns>The stored trajectory's id.</returns>
20+
Task<string> AddAsync(AgentTrajectory trajectory, CancellationToken cancellationToken = default);
21+
22+
/// <summary>
23+
/// Gets a trajectory by id, or <c>null</c> when not found.
24+
/// </summary>
25+
/// <param name="id">The trajectory id.</param>
26+
/// <param name="cancellationToken">Token used to cancel the operation.</param>
27+
Task<AgentTrajectory?> GetAsync(string id, CancellationToken cancellationToken = default);
28+
29+
/// <summary>
30+
/// Returns all stored trajectories, oldest first.
31+
/// </summary>
32+
/// <param name="cancellationToken">Token used to cancel the operation.</param>
33+
Task<IReadOnlyList<AgentTrajectory>> GetAllAsync(CancellationToken cancellationToken = default);
34+
35+
/// <summary>
36+
/// Returns the trajectories matching a predicate, oldest first.
37+
/// </summary>
38+
/// <param name="predicate">The filter to apply.</param>
39+
/// <param name="cancellationToken">Token used to cancel the operation.</param>
40+
Task<IReadOnlyList<AgentTrajectory>> QueryAsync(
41+
Func<AgentTrajectory, bool> predicate,
42+
CancellationToken cancellationToken = default);
43+
44+
/// <summary>
45+
/// Removes all stored trajectories.
46+
/// </summary>
47+
/// <param name="cancellationToken">Token used to cancel the operation.</param>
48+
Task ClearAsync(CancellationToken cancellationToken = default);
49+
}

0 commit comments

Comments
 (0)