diff --git a/.github/workflows/dotnet-verify-samples.yml b/.github/workflows/dotnet-verify-samples.yml
index 1f1b40e9f4a..1f3ffc703c2 100644
--- a/.github/workflows/dotnet-verify-samples.yml
+++ b/.github/workflows/dotnet-verify-samples.yml
@@ -109,6 +109,8 @@ jobs:
FOUNDRY_PROJECT_ENDPOINT: ${{ vars.FOUNDRY_PROJECT_ENDPOINT }}
FOUNDRY_MODEL: ${{ vars.FOUNDRY_MODEL }}
AZURE_AI_BING_CONNECTION_ID: ${{ vars.AZURE_AI_BING_CONNECTION_ID }}
+ # TypeSafe (Jev)
+ TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
- name: Write Job Summary
if: always()
diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx
index 77e772cf214..c60b7e4dded 100644
--- a/dotnet/agent-framework-dotnet.slnx
+++ b/dotnet/agent-framework-dotnet.slnx
@@ -36,6 +36,8 @@
+
+
@@ -69,6 +71,7 @@
+
@@ -631,6 +634,7 @@
+
@@ -689,6 +693,7 @@
+
diff --git a/dotnet/agent-framework-src-and-tests.slnf b/dotnet/agent-framework-src-and-tests.slnf
index b6639c7a09b..bf22d4be168 100644
--- a/dotnet/agent-framework-src-and-tests.slnf
+++ b/dotnet/agent-framework-src-and-tests.slnf
@@ -29,6 +29,7 @@
"src\\Microsoft.Agents.AI.OpenAI\\Microsoft.Agents.AI.OpenAI.csproj",
"src\\Microsoft.Agents.AI.Purview\\Microsoft.Agents.AI.Purview.csproj",
"src\\Microsoft.Agents.AI.Tools.Shell\\Microsoft.Agents.AI.Tools.Shell.csproj",
+ "src\\Microsoft.Agents.AI.TypeSafe\\Microsoft.Agents.AI.TypeSafe.csproj",
"src\\Microsoft.Agents.AI.Valkey\\Microsoft.Agents.AI.Valkey.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.Foundry\\Microsoft.Agents.AI.Workflows.Declarative.Foundry.csproj",
"src\\Microsoft.Agents.AI.Workflows.Declarative.Mcp\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.csproj",
@@ -64,6 +65,7 @@
"tests\\Microsoft.Agents.AI.OpenAI.UnitTests\\Microsoft.Agents.AI.OpenAI.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.Purview.UnitTests\\Microsoft.Agents.AI.Purview.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.Tools.Shell.UnitTests\\Microsoft.Agents.AI.Tools.Shell.UnitTests.csproj",
+ "tests\\Microsoft.Agents.AI.TypeSafe.UnitTests\\Microsoft.Agents.AI.TypeSafe.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.UnitTests\\Microsoft.Agents.AI.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.Valkey.UnitTests\\Microsoft.Agents.AI.Valkey.UnitTests.csproj",
"tests\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests\\Microsoft.Agents.AI.Workflows.Declarative.Mcp.UnitTests.csproj",
diff --git a/dotnet/eng/verify-samples/AgentsSamples.cs b/dotnet/eng/verify-samples/AgentsSamples.cs
index 3caac4bb037..938fe72a73f 100644
--- a/dotnet/eng/verify-samples/AgentsSamples.cs
+++ b/dotnet/eng/verify-samples/AgentsSamples.cs
@@ -77,6 +77,49 @@ internal static class AgentsSamples
],
},
+ new SampleDefinition
+ {
+ Name = "Agent_With_TypeSafe",
+ ProjectPath = "samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe",
+ RequiredEnvironmentVariables = ["TYPESAFE_API_KEY"],
+ Inputs = ["Y"],
+ MustContain =
+ [
+ "Direct JevChatClient result:",
+ "Agent result:",
+ "Default questions result:",
+ "Function calling result:",
+ "better_city:",
+ "Tool approval:",
+ "issue_refund(plan: Pro)",
+ ],
+ ExpectedOutputDescription =
+ [
+ "The direct result should route the duplicate subscription charge to billing, and the agent result should route the failing checkout integration to technical with a high urgency probability.",
+ "The default questions result should route the invoice request to billing with a low urgency probability.",
+ "The function calling result should contain one weather line for Seattle and one for Amsterdam, with temperatures in Fahrenheit, followed by a better_city line that names one of the two cities.",
+ "The tool approval step should ask to approve issue_refund for the Pro plan and, after approval, show the refund result followed by a department line.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
+ new SampleDefinition
+ {
+ Name = "Agent_TypeSafe_Step01_LoopJudge",
+ ProjectPath = "samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge",
+ RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "TYPESAFE_API_KEY"],
+ OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
+ MustContain = ["Answer 1:", "Judge:", "red_sunsets:", "Final answer:"],
+ ExpectedOutputDescription =
+ [
+ "The first answer should explain only why the sky is blue, and the judge should mark red_sunsets as missing with a low probability.",
+ "A later answer should also explain why sunsets are red, and the judge should mark red_sunsets as met for it.",
+ "Every numbered answer (Answer 1, Answer 2, and so on) should be followed by a Judge block with probabilities. The Final answer section repeats the last judged answer and has no Judge block of its own.",
+ "The final answer should explain both why the sky is blue and why sunsets are red.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
// ── Agents ──────────────────────────────────────────────────────────
new SampleDefinition
@@ -403,6 +446,29 @@ internal static class AgentsSamples
],
},
+ new SampleDefinition
+ {
+ Name = "Agent_Step25_ClassificationWithJev",
+ ProjectPath = "samples/02-agents/Agents/Agent_Step25_ClassificationWithJev",
+ RequiredEnvironmentVariables = ["FOUNDRY_PROJECT_ENDPOINT", "TYPESAFE_API_KEY"],
+ OptionalEnvironmentVariables = ["FOUNDRY_MODEL"],
+ MustContain =
+ [
+ "Ticket:",
+ "Tool result:",
+ "The same tool, answered by a local keyword classifier:",
+ "local-keyword-classifier",
+ ],
+ ExpectedOutputDescription =
+ [
+ "Each of the three tickets should show a tool result with department, urgent, and frustration answers from a jev model, followed by a one-line agent reply.",
+ "The failing Stripe integration ticket should be routed to technical and marked urgent, and the invoice request should be routed to billing and not urgent.",
+ "The vague account ticket may be sent to human review when the department confidence is low.",
+ "The last part should show a tool result from the local-keyword-classifier model for the invoice ticket, followed by an agent reply.",
+ "The output should not contain error messages or stack traces.",
+ ],
+ },
+
// ── AgentSkills ─────────────────────────────────────────────────────
new SampleDefinition
diff --git a/dotnet/samples/02-agents/AgentProviders/README.md b/dotnet/samples/02-agents/AgentProviders/README.md
index 407c4e8e4d9..982e35874cd 100644
--- a/dotnet/samples/02-agents/AgentProviders/README.md
+++ b/dotnet/samples/02-agents/AgentProviders/README.md
@@ -93,6 +93,13 @@ covering basics, function tools, structured output, middleware, MCP, code interp
| [Conversation](./openai/Agent_OpenAI_Step05_Conversation/) | Multi-turn conversations with OpenAI |
| [Code Interpreter](./openai/Agent_OpenAI_Step06_CodeInterpreterFileDownload/) | Code interpreter with file downloads |
+### [TypeSafe](./typesafe/)
+
+| Sample | Description |
+| --- | --- |
+| [Agent with TypeSafe](./typesafe/Agent_With_TypeSafe/) | Use Jev, the TypeSafe System One model, as an IChatClient that answers typed questions and calls closed-set tools |
+| [Loop judge](./typesafe/Agent_TypeSafe_Step01_LoopJudge/) | Use Jev as the judge of a LoopAgent, with one yes or no question per criterion |
+
## Running the samples
Navigate to a sample directory and run:
diff --git a/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/Agent_TypeSafe_Step01_LoopJudge.csproj b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/Agent_TypeSafe_Step01_LoopJudge.csproj
new file mode 100644
index 00000000000..48a53996f27
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/Agent_TypeSafe_Step01_LoopJudge.csproj
@@ -0,0 +1,20 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/Program.cs b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/Program.cs
new file mode 100644
index 00000000000..5310dbff117
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/Program.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample uses Jev, the TypeSafe System One model, as the judge of a LoopAgent. A Foundry agent writes the
+// answer. After each answer, Jev checks every criterion with its own yes or no question, and the loop sends the agent
+// the criteria that are still missing, until the answer meets all of them.
+
+#pragma warning disable MAAI001 // LoopAgent and DelegateLoopEvaluator are experimental.
+
+using System.ClientModel;
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.TypeSafe;
+using Microsoft.Extensions.AI;
+
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
+var typeSafeApiKey = Environment.GetEnvironmentVariable("TYPESAFE_API_KEY") ?? throw new InvalidOperationException("TYPESAFE_API_KEY is not set.");
+
+// 1. The criteria. Each one becomes a Noul question, whose answer is the probability that the answer meets it, so the
+// judge reports exactly which criteria are missing instead of a single verdict. The questions describe what a good
+// answer contains; wording that steers the judge, such as "only say yes if", tends to push every answer one way.
+Dictionary criteria = new()
+{
+ ["blue_sky"] = "explains why the daytime sky appears blue",
+ ["red_sunsets"] = "explains why sunsets appear red or orange",
+ ["plain_language"] = "uses clear language suitable for a general audience",
+};
+Dictionary questions = criteria.ToDictionary(
+ criterion => criterion.Key,
+ criterion => (JevQuestion)new JevNoulQuestion { Instructions = $"Does the assistant's latest response {criterion.Value}?" });
+
+// A criterion is met when its probability is above this threshold. 0.5 is not calibrated for any task: a higher value
+// accepts fewer answers that still miss a criterion, at the cost of more loop runs. Choose it on labeled examples of
+// your own answers.
+const double CriterionThreshold = 0.5;
+
+// 2. The agent that writes the answers. Its instructions hold back part of the first answer, so the judge has a gap
+// to find and the loop runs more than once.
+// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
+// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
+AIAgent answerer = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential()).AsAIAgent(
+ model: deploymentName,
+ name: "Answerer",
+ instructions: """
+ You answer science questions for a general audience in one short paragraph.
+ In your first answer, explain only why the daytime sky is blue, even if the user asks for more.
+ When feedback lists missing points, rewrite the whole answer so that it covers them too.
+ """);
+
+// 3. The judge. AIJudgeLoopEvaluator asks its chat client for JSON structured output, which Jev cannot write, so a
+// DelegateLoopEvaluator asks Jev the criteria questions instead. Jev reads the original request and the latest answer
+// as a conversation.
+using var judge = new JevChatClient(new ApiKeyCredential(typeSafeApiKey));
+bool accepted = false;
+var evaluator = new DelegateLoopEvaluator(async (context, cancellationToken) =>
+{
+ Console.WriteLine($"\nAnswer {context.Iteration}:\n{context.LastResponse.Text}");
+ List missing = await JudgeAsync(context.InitialMessages, context.LastResponse.Text, cancellationToken);
+ accepted = missing.Count == 0;
+
+ // The feedback names the missing criteria, which is the gap analysis that a generative judge would write.
+ return accepted
+ ? LoopEvaluation.Stop()
+ : LoopEvaluation.Continue($"Your answer does not yet: {string.Join("; ", missing)}. Rewrite the whole answer so that it does.");
+});
+
+// 4. The loop re-runs the agent until the judge stops it. MaxIterations bounds the cost if the answer never passes.
+const int MaxIterations = 3;
+const string Request = "Explain why the sky is blue and why sunsets are red.";
+AIAgent loop = new LoopAgent(answerer, evaluator, new LoopAgentOptions { MaxIterations = MaxIterations, NonStreamingReturnsLastResponseOnly = true });
+AgentResponse response = await loop.RunAsync(Request);
+
+// LoopAgent stops at MaxIterations before it calls the evaluator, so an answer from the last allowed run has not been
+// judged yet. Judging it here keeps the final answer from being presented as accepted when it may miss a criterion.
+if (!accepted)
+{
+ Console.WriteLine($"\nAnswer {MaxIterations} (the last run the loop allows):\n{response.Text}");
+ accepted = (await JudgeAsync([new ChatMessage(ChatRole.User, Request)], response.Text, CancellationToken.None)).Count == 0;
+}
+
+Console.WriteLine(accepted ? "\nFinal answer:" : "\nFinal answer (some criteria are still missing):");
+Console.WriteLine(response.Text);
+
+// Asks Jev whether the answer meets each criterion, prints the probabilities, and returns the missing criteria.
+async Task> JudgeAsync(IEnumerable request, string answer, CancellationToken cancellationToken)
+{
+ List conversation = [.. request, new ChatMessage(ChatRole.Assistant, answer)];
+ ChatResponse verdict = await judge.GetResponseAsync(conversation, new ChatOptions().WithJevQuestions(questions), cancellationToken);
+ JevResult result = verdict.GetJevResult()!;
+
+ Console.WriteLine("Judge:");
+ List missing = [];
+ foreach ((string id, string description) in criteria)
+ {
+ double probability = ((JevNoulResponse)result.Answers[id]).Noul;
+ bool met = probability > CriterionThreshold;
+ Console.WriteLine($" {id}: {(met ? "met" : "missing")} (probability {probability:F2})");
+ if (!met)
+ {
+ missing.Add(description);
+ }
+ }
+
+ return missing;
+}
diff --git a/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/README.md b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/README.md
new file mode 100644
index 00000000000..12c563f986b
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_TypeSafe_Step01_LoopJudge/README.md
@@ -0,0 +1,88 @@
+# Jev as the judge of a loop
+
+This sample uses `JevChatClient` from `Microsoft.Agents.AI.TypeSafe` as the judge of a `LoopAgent`. A Foundry agent
+writes the answer, and Jev, the TypeSafe System One model, decides whether the answer is complete.
+
+A generative judge writes a verdict and a gap analysis. Jev writes no text, so the sample asks it one `JevNoulQuestion`
+per criterion instead. Each answer is the probability, from 0 to 1, that the answer meets that criterion, so the judge
+reports exactly which criteria are missing:
+
+```text
+Judge:
+ blue_sky: met (probability 0.99)
+ red_sunsets: missing (probability 0.02)
+ plain_language: met (probability 0.92)
+```
+
+The loop turns the missing criteria into the feedback for the next run, and stops when every criterion is met or after
+three runs. The agent's instructions hold back part of the first answer on purpose, so the loop visibly runs twice.
+
+`LoopAgent` checks `MaxIterations` before it calls the evaluator, so when the loop reaches its limit, the answer of the
+last run has not been judged. The sample judges that answer after the loop and labels the final answer when some
+criteria are still missing, instead of presenting it as accepted.
+
+## Why a delegate evaluator
+
+`AIJudgeLoopEvaluator` asks its chat client for a `JudgeVerdict` as JSON structured output, which Jev cannot write;
+`JevChatClient` rejects the JSON response format with a `NotSupportedException`. The sample wraps the judge in a
+`DelegateLoopEvaluator` instead:
+
+```csharp
+var evaluator = new DelegateLoopEvaluator(async (context, cancellationToken) =>
+{
+ List missing = await JudgeAsync(context.InitialMessages, context.LastResponse.Text, cancellationToken);
+ accepted = missing.Count == 0;
+ return accepted
+ ? LoopEvaluation.Stop()
+ : LoopEvaluation.Continue($"Your answer does not yet: {string.Join("; ", missing)}. ...");
+});
+```
+
+`JudgeAsync` sends Jev the original request and the answer as a conversation, with
+`new ChatOptions().WithJevQuestions(questions)`, and returns the criteria whose Noul is at or below
+`CriterionThreshold`.
+
+Jev reads the original request and the latest answer as a conversation, and each question refers to "the assistant's
+latest response". The same pattern works with a `JevScoreQuestion` for a graded rubric, or a `JevChoiceQuestion` to
+classify what kind of revision is needed.
+
+## Writing criteria for a judge
+
+- **Choose the threshold on your own data.** The sample treats a criterion as met when its probability is above
+ `CriterionThreshold`, which is 0.5. That value is not calibrated for any task. The threshold trades two errors
+ against each other: a higher value accepts fewer answers that still miss a criterion, and costs more loop runs, while
+ a lower value stops sooner and lets more incomplete answers through. Pick it from labeled examples of the answers your
+ agent writes, based on which error costs you more.
+- **Describe the criterion, do not steer the judge.** Ask what a good answer contains, as the sample does ("Does the
+ assistant's latest response explain why sunsets appear red?"). Directional wording such as "only say yes if" or
+ "lean towards yes" tends to push every answer the same way instead of making the judge more accurate. When a
+ criterion is hard to judge, a neutral description of what is being judged helps more than stronger wording.
+- **Ask about the response, not the topic.** When the conversation contains harmful or sensitive content, a question
+ such as "Is this safe?" can be read as a question about that content rather than about the answer. A correct
+ refusal could then look like a missing criterion, and the loop would keep revising an answer that was already right.
+ Ask how the response handles the request, for example "Does the assistant's latest response decline to give
+ dangerous instructions?", and check such criteria on labeled examples, including refusals, before relying on them.
+
+## Prerequisites
+
+- .NET 10 SDK or later
+- A Microsoft Foundry project with a deployed chat model
+- Azure CLI installed and authenticated (`az login`)
+- A TypeSafe API key
+
+Set the environment variables:
+
+```powershell
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project"
+$env:FOUNDRY_MODEL="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
+$env:TYPESAFE_API_KEY="your-typesafe-api-key"
+```
+
+## Running the sample
+
+```powershell
+dotnet run
+```
+
+The sample prints each answer with the judge's probabilities, then the final answer, labeled when some criteria are
+still missing. The wording of the answers and the exact probabilities vary between runs.
diff --git a/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/Agent_With_TypeSafe.csproj b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/Agent_With_TypeSafe.csproj
new file mode 100644
index 00000000000..b83c02d799f
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/Agent_With_TypeSafe.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/Program.cs b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/Program.cs
new file mode 100644
index 00000000000..367db584b33
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/Program.cs
@@ -0,0 +1,159 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// This sample shows how to use Jev, the TypeSafe System One model, as an IChatClient and as an AIAgent.
+// Jev does not write text: it answers typed questions about the conversation and returns calibrated probabilities.
+
+using System.ClientModel;
+using System.ComponentModel;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.TypeSafe;
+using Microsoft.Extensions.AI;
+
+var apiKey = Environment.GetEnvironmentVariable("TYPESAFE_API_KEY") ?? throw new InvalidOperationException("TYPESAFE_API_KEY is not set.");
+
+// 1. The questions to answer. Each answer comes back under the ID of its question.
+Dictionary questions = new()
+{
+ ["department"] = new JevChoiceQuestion
+ {
+ Instructions = "Which team should handle the support request?",
+ Criteria = new Dictionary
+ {
+ ["billing"] = "Payments, subscriptions, invoices, or refunds",
+ ["technical"] = "Bugs, outages, integrations, or account access",
+ ["sales"] = "Pricing, upgrades, or new account questions",
+ },
+ },
+ ["frustration"] = new JevScoreQuestion
+ {
+ Instructions = "How frustrated does the customer appear?",
+ Criteria = ["Calm and factual", "Frustrated but civil", "Very angry or threatening to leave"],
+ },
+ ["is_urgent"] = new JevNoulQuestion
+ {
+ Instructions = "Does the request require urgent attention?",
+ Criteria = new JevNoulCriteria
+ {
+ True = "The customer describes ongoing harm, lost revenue, or an immediate deadline",
+ False = "The request can wait for the normal support queue",
+ },
+ },
+};
+
+// 2. Call the chat client directly. WithJevQuestions passes the questions through ChatOptions.
+using IChatClient chatClient = new JevChatClient(new ApiKeyCredential(apiKey));
+ChatResponse direct = await chatClient.GetResponseAsync(
+ "I was charged twice for the same subscription. Please refund the duplicate charge.",
+ new ChatOptions().WithJevQuestions(questions));
+PrintEvaluation("Direct JevChatClient result:", direct.GetJevResult()!);
+
+// 3. Use the same client through an agent. ChatClientAgentRunOptions carries the questions for one run.
+AIAgent agent = chatClient.AsAIAgent(name: "JevTicketEvaluator", instructions: "Evaluate the support request.");
+AgentResponse agentResponse = await agent.RunAsync(
+ "Our checkout integration has failed for three days and we are losing sales. Please help ASAP.",
+ options: new ChatClientAgentRunOptions(new ChatOptions().WithJevQuestions(questions)));
+PrintEvaluation("Agent result:", agentResponse.GetJevResult()!);
+
+// 4. Default questions. A client created with DefaultQuestions answers requests that pass no questions, so code that
+// knows nothing about Jev can use it like any other IChatClient; here, an agent run without Jev options.
+using IChatClient triageClient = new JevChatClient(new ApiKeyCredential(apiKey), new JevChatClientOptions { DefaultQuestions = questions });
+AgentResponse triaged = await triageClient.AsAIAgent(name: "JevTriage").RunAsync(
+ "Hi, could you send me a copy of last month's invoice when you get a chance? Thanks!");
+PrintEvaluation("Default questions result:", triaged.GetJevResult()!);
+
+// 5. Function calling. Jev cannot write arguments, so it chooses them from closed sets: an enum, a Boolean, and an
+// optional enum that is passed only when Jev decides the user asked for it. The user asks about two cities, so the
+// client allows two tool calls per turn instead of the default one. Jev reads the tool's name and description when
+// it chooses a tool, so the local function gets a readable name instead of its compiler-generated one.
+using IChatClient weatherClient = new JevChatClient(new ApiKeyCredential(apiKey), new JevChatClientOptions { MaximumToolCallsPerTurn = 2 });
+AIAgent weatherAgent = weatherClient.AsAIAgent(name: "WeatherEvaluator", tools: [AIFunctionFactory.Create(GetWeather, "get_weather")]);
+var comparison = new Dictionary
+{
+ ["better_city"] = new JevChoiceQuestion
+ {
+ Instructions = "Which city has better weather based on the tool results?",
+ Criteria = new Dictionary
+ {
+ ["Seattle"] = "Seattle has the better weather.",
+ ["Amsterdam"] = "Amsterdam has the better weather.",
+ },
+ },
+};
+AgentResponse weather = await weatherAgent.RunAsync(
+ "Give me a detailed weather report for Seattle and Amsterdam in Fahrenheit and tell me where the weather is better.",
+ options: new ChatClientAgentRunOptions(new ChatOptions().WithJevQuestions(comparison)));
+
+// After tool calls, the text is the tool results followed by one line per Choice and Score answer.
+Console.WriteLine("\nFunction calling result:");
+Console.WriteLine(weather.Text);
+Console.WriteLine($"Comparison confidence: {((JevChoiceResponse)weather.GetJevResult()!.Answers["better_city"]).Confidence:F3}");
+
+// 6. Tool approval. A tool wrapped in ApprovalRequiredAIFunction runs only after the user approves the call that Jev
+// chose. The session keeps the pending call, so the approval is sent in a second run with the same questions.
+AIAgent refundAgent = chatClient.AsAIAgent(name: "RefundAgent", tools: [new ApprovalRequiredAIFunction(AIFunctionFactory.Create(IssueRefund, "issue_refund"))]);
+AgentSession session = await refundAgent.CreateSessionAsync();
+var refundOptions = new ChatClientAgentRunOptions(new ChatOptions().WithJevQuestions(questions));
+AgentResponse refund = await refundAgent.RunAsync(
+ "I was charged twice for my Pro plan this month. Please refund the duplicate charge.",
+ session,
+ refundOptions);
+
+Console.WriteLine("\nTool approval:");
+if (refund.Messages.SelectMany(message => message.Contents).OfType().FirstOrDefault() is { ToolCall: FunctionCallContent call } approval)
+{
+ string arguments = string.Join(", ", call.Arguments?.Select(argument => $"{argument.Key}: {argument.Value}") ?? []);
+ Console.Write($"Jev wants to call {call.Name}({arguments}). Approve? (Y/N): ");
+ bool approved = Console.ReadLine()?.Trim().Equals("Y", StringComparison.OrdinalIgnoreCase) ?? false;
+ refund = await refundAgent.RunAsync(new ChatMessage(ChatRole.User, [approval.CreateResponse(approved)]), session, refundOptions);
+}
+
+Console.WriteLine(refund.Text);
+
+static void PrintEvaluation(string label, JevResult result)
+{
+ var department = (JevChoiceResponse)result.Answers["department"];
+ var frustration = (JevScoreResponse)result.Answers["frustration"];
+ var urgent = (JevNoulResponse)result.Answers["is_urgent"];
+
+ Console.WriteLine($"\n{label}");
+ Console.WriteLine($"Department: {department.Choice} (confidence {department.Confidence:F3})");
+ Console.WriteLine($"Frustration score: {frustration.Score:F3} of {frustration.Legend.Count - 1}");
+ Console.WriteLine($"Urgency probability: {urgent.Noul:F3}");
+ Console.WriteLine($"Tokens: {result.Usage.InputTokens} in, {result.Usage.OutputTokens} out");
+}
+
+[Description("Gets the weather forecast.")]
+static string GetWeather(
+ [Description("The city to get the weather for")] City city,
+ [Description("Whether to include a detailed hourly forecast; true when the user asks for a detailed report")] bool detailed,
+ [Description("The temperature unit; leave it out to use Celsius")] TemperatureUnit? unit = null)
+{
+ int celsius = Random.Shared.Next(15, 31);
+ string temperature = unit == TemperatureUnit.Fahrenheit ? $"{(celsius * 9 / 5) + 32} F" : $"{celsius} C";
+ string suffix = detailed ? " with a detailed hourly forecast" : string.Empty;
+ return $"{city} is sunny and {temperature}{suffix}.";
+}
+
+[Description("Refunds a duplicate charge for a subscription plan.")]
+static string IssueRefund([Description("The plan that was charged twice")] Plan plan) =>
+ $"Refunded the duplicate {plan} plan charge.";
+
+internal enum City
+{
+ Seattle,
+ Amsterdam,
+ Paris,
+}
+
+internal enum TemperatureUnit
+{
+ Celsius,
+ Fahrenheit,
+}
+
+internal enum Plan
+{
+ Basic,
+ Pro,
+ Enterprise,
+}
diff --git a/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/README.md b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/README.md
new file mode 100644
index 00000000000..0d5dd3a98e4
--- /dev/null
+++ b/dotnet/samples/02-agents/AgentProviders/typesafe/Agent_With_TypeSafe/README.md
@@ -0,0 +1,114 @@
+# Agent with TypeSafe (Jev)
+
+This sample uses `JevChatClient` from `Microsoft.Agents.AI.TypeSafe`, an `IChatClient` for Jev, the TypeSafe System
+One model. Jev does not write text. It answers typed questions about the conversation and returns calibrated
+probabilities:
+
+| Question type | What it answers | Answer |
+| --- | --- | --- |
+| `JevChoiceQuestion` | Which of these named alternatives? | The selected label, the probability of each label, and a confidence |
+| `JevScoreQuestion` | Which score on an ordered rubric (2 to 10 entries)? | An expected score that can fall between rubric entries, the probability of each score, and a confidence |
+| `JevNoulQuestion` | Yes or no? | The probability, from 0 to 1, of yes |
+
+The client follows the TypeSafe connector for Python (`agent-framework-typesafe`), so both languages behave alike.
+
+## What the sample shows
+
+1. **The chat client directly.** `WithJevQuestions` puts the questions in `ChatOptions`, and `GetJevResult` reads the
+ typed answers from the response:
+
+ ```csharp
+ IChatClient client = new JevChatClient(new ApiKeyCredential(apiKey));
+ ChatResponse response = await client.GetResponseAsync(ticket, new ChatOptions().WithJevQuestions(questions));
+ JevResult result = response.GetJevResult()!;
+ ```
+
+ The response text is the same result as JSON, so code that reads only text still gets every answer.
+
+2. **The client behind an agent.** A `ChatClientAgentRunOptions` carries the questions for one run. Questions in the
+ agent's `ChatClientAgentOptions.ChatOptions` apply to every run, and a run's questions replace them.
+
+3. **Default questions.** A client created with `JevChatClientOptions.DefaultQuestions` answers requests that pass no
+ questions, so code that knows nothing about Jev can use it like any other `IChatClient`. The sample runs an agent
+ without any Jev options.
+
+4. **Function calling.** Jev cannot write arguments, so the client adds internal questions that choose the tool and
+ each argument from a closed set: a constant, an enum, a Boolean, or an array of enum values. Optional arguments
+ are passed only when Jev decides the user asked for them; here, the temperature unit is passed because the user
+ asks for Fahrenheit. The agent runs the tool, and once the turn has used
+ `JevChatClientOptions.MaximumToolCallsPerTurn` calls (one by default, two in this sample), the client answers the
+ caller's questions with the tool results in the state. The response text is then the tool results followed by one
+ `id: answer` line per Choice and Score answer:
+
+ ```text
+ Seattle is sunny and 75 F with a detailed hourly forecast.
+ Amsterdam is sunny and 69 F with a detailed hourly forecast.
+ better_city: Seattle
+ ```
+
+ Jev reads each tool's name and description, and each argument's description, to make these choices, so write them
+ for Jev as you would for a person. Give local functions an explicit name, because their compiler-generated names
+ mean nothing. In this sample, saying that `detailed` is true "when the user asks for a detailed report" is what makes
+ Jev choose it reliably. Tools with free-form arguments, such as text or numbers, are left out of the request with a
+ warning in the log.
+ Constant and enum values must satisfy their other schema constraints too, including array item constraints;
+ tools with conflicting constraints are excluded rather than called with invalid arguments.
+
+5. **Tool approval.** A tool wrapped in `ApprovalRequiredAIFunction` runs only after the user approves the call that
+ Jev chose. The first run returns a `ToolApprovalRequestContent`; the sample asks for approval in the console and
+ sends the answer in a second run on the same session, with the same questions.
+
+## Limits
+
+Jev answers all questions at once, so the client rejects what needs generated text: streaming (`RunStreamingAsync`),
+sampling and output options such as `Temperature` or a JSON `ResponseFormat`, server-side conversations, tools other
+than functions, and message content other than text, reasoning text, and function calls and results. Each fails with a
+`NotSupportedException` that names the problem. Tool approval requests and responses are allowed but left out of what
+Jev reads, because the call and result they lead to carry the information.
+
+Because streaming is not supported, run agents with `RunAsync`. A function-invoking client runs approved tools before
+it calls the chat client, so a streaming run that resumes a tool approval would run the tool and then fail.
+
+Agents tag stored messages as chat history, which lets Jev distinguish a new user request from text moved after a
+resumed approval's tool result. When managing history yourself with `FunctionInvokingChatClient`, tag prior messages
+with `message.WithAgentRequestMessageSource(AgentRequestMessageSourceType.ChatHistory)` before appending new input.
+Without that attribution, a user message starts a new turn.
+
+## Configuring the client
+
+`JevChatClientOptions` follows the `ClientPipelineOptions` pattern of the OpenAI and Azure AI client libraries, with the
+retry and timeout defaults of the official TypeSafe SDK:
+
+```csharp
+var options = new JevChatClientOptions
+{
+ ModelId = "jev-latest", // ChatOptions.ModelId overrides it per request
+ MaximumToolCallsPerTurn = 2,
+ Transport = new HttpClientPipelineTransport(httpClientFactory.CreateClient("jev")),
+};
+IChatClient client = new JevChatClient(new ApiKeyCredential(apiKey), options);
+```
+
+To use Jev as a tool of an agent whose own model writes the replies, see
+[Classification with Jev](../../../Agents/Agent_Step25_ClassificationWithJev/). To use it as the judge of a
+`LoopAgent`, see [Jev as the judge of a loop](../Agent_TypeSafe_Step01_LoopJudge/).
+
+## Prerequisites
+
+- .NET 10 SDK or later
+- A TypeSafe API key
+
+Set the environment variable:
+
+```powershell
+$env:TYPESAFE_API_KEY="your-typesafe-api-key"
+```
+
+## Running the sample
+
+```powershell
+dotnet run
+```
+
+The tool approval step asks for `Y` or `N` in the console. Probabilities, scores, and the temperatures in the tool
+results vary between runs.
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/Agent_Step25_ClassificationWithJev.csproj b/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/Agent_Step25_ClassificationWithJev.csproj
new file mode 100644
index 00000000000..1c976a48330
--- /dev/null
+++ b/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/Agent_Step25_ClassificationWithJev.csproj
@@ -0,0 +1,21 @@
+
+
+
+ Exe
+ net10.0
+
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/KeywordClassifier.cs b/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/KeywordClassifier.cs
new file mode 100644
index 00000000000..338dd1e7dc6
--- /dev/null
+++ b/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/KeywordClassifier.cs
@@ -0,0 +1,60 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Diagnostics.CodeAnalysis;
+
+///
+/// A local classifier that stands in for another classification client. It has its own request and response types,
+/// knows nothing about Jev, and needs no network: it scores each option by the words it shares with the text.
+///
+[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Instantiated by the service provider")]
+internal sealed class KeywordClassifier
+{
+ private static readonly char[] s_separators = [' ', ',', '.', '!', '?', ';', ':', '\'', '"', '(', ')', '-', '\n', '\r', '\t'];
+
+ public Task ClassifyAsync(ClassificationRequest request, CancellationToken cancellationToken)
+ {
+ HashSet words = Words(request.Text);
+ var results = new Dictionary();
+ foreach (ClassificationTask task in request.Tasks)
+ {
+ // One point per shared word, plus one so that every option keeps some probability.
+ double[] points = [.. task.Options.Select(option => Words(option.Description).Count(words.Contains) + 1.0)];
+ double total = points.Sum();
+ results[task.Id] = new ClassificationResult(
+ task.Kind,
+ [.. task.Options.Select((option, index) => new ScoredOption(option.Key, option.Description, points[index] / total))]);
+ }
+
+ return Task.FromResult(new ClassificationResponse(results));
+ }
+
+ // Words are compared in upper case and without a plural "S", so "invoice" matches "invoices".
+ private static HashSet Words(string text) =>
+ [.. text.ToUpperInvariant().Split(s_separators, StringSplitOptions.RemoveEmptyEntries).Where(word => word.Length > 3).Select(word => word.TrimEnd('S'))];
+}
+
+/// What the classifier decides for one task.
+internal enum ClassificationKind
+{
+ PickOne,
+ Rate,
+ YesNo,
+}
+
+/// The classifier's request: the text and the tasks to decide about it.
+internal sealed record ClassificationRequest(string Text, IReadOnlyList Tasks);
+
+/// One task: pick one option, rate on ordered options, or decide yes or no.
+internal sealed record ClassificationTask(string Id, ClassificationKind Kind, IReadOnlyList Options);
+
+/// An option of a task and the words that describe it.
+internal sealed record ClassificationOption(string Key, string Description);
+
+/// The classifier's response: one result per task, by task ID.
+internal sealed record ClassificationResponse(IReadOnlyDictionary Results);
+
+/// The result of one task: every option with its probability.
+internal sealed record ClassificationResult(ClassificationKind Kind, IReadOnlyList Options);
+
+/// An option with the probability that the classifier gives it.
+internal sealed record ScoredOption(string Key, string Description, double Probability);
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/Program.cs b/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/Program.cs
new file mode 100644
index 00000000000..a228d7573d2
--- /dev/null
+++ b/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/Program.cs
@@ -0,0 +1,147 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+// Classification with Jev — Give an agent a fast, calibrated classifier as a tool
+//
+// Jev, the TypeSafe System One model, does not write text: it answers typed questions
+// (choice, score, noul) about a state and returns calibrated probabilities. This sample gives
+// a support agent a Jev tool built with JevAIToolBuilder. The agent's model decides which
+// questions to ask, Jev answers them, and the model writes the routing decision, sending
+// uncertain tickets to a human. The second part answers the same tool with another client.
+
+using System.ClientModel;
+using System.Globalization;
+using Azure.AI.Projects;
+using Azure.Identity;
+using Microsoft.Agents.AI;
+using Microsoft.Agents.AI.TypeSafe;
+using Microsoft.Extensions.AI;
+using Microsoft.Extensions.DependencyInjection;
+
+var endpoint = Environment.GetEnvironmentVariable("FOUNDRY_PROJECT_ENDPOINT") ?? throw new InvalidOperationException("FOUNDRY_PROJECT_ENDPOINT is not set.");
+var deploymentName = Environment.GetEnvironmentVariable("FOUNDRY_MODEL") ?? "gpt-5.4-mini";
+var typeSafeApiKey = Environment.GetEnvironmentVariable("TYPESAFE_API_KEY") ?? throw new InvalidOperationException("TYPESAFE_API_KEY is not set.");
+
+//
+// The tool's arguments are a Jev request (the state plus typed questions) and its result is the Jev response.
+AIFunction jev = new JevAIToolBuilder(new ApiKeyCredential(typeSafeApiKey)).Build();
+//
+
+const string Instructions = """
+ You triage customer support tickets. For every ticket, call ask_system_one once with the ticket text as the
+ state and these questions:
+ - "department": a choice between billing (payments, invoices, refunds), technical (bugs, outages, integrations),
+ and sales (pricing, upgrades, new accounts).
+ - "urgent": a noul asking whether the customer needs help urgently.
+ - "frustration": a score with the rubric calm, annoyed, and angry.
+ Then reply in one line with the department, whether it is urgent, and the frustration level. When the department
+ confidence is below 0.6, say the ticket needs human review instead of naming a department.
+ """;
+
+// WARNING: DefaultAzureCredential is convenient for development but requires careful consideration in production.
+// In production, consider using a specific credential (e.g., ManagedIdentityCredential) to avoid
+// latency issues, unintended credential probing, and potential security risks from fallback mechanisms.
+var projectClient = new AIProjectClient(new Uri(endpoint), new DefaultAzureCredential());
+AIAgent agent = projectClient.AsAIAgent(model: deploymentName, instructions: Instructions, name: "TicketTriage", tools: [jev]);
+
+string[] tickets =
+[
+ "I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP.",
+ "Hi, could you send me a copy of last month's invoice when you get a chance? Thanks!",
+ "I'd like to talk to someone about my account.",
+];
+
+foreach (string ticket in tickets)
+{
+ await TriageAsync(agent, ticket);
+}
+
+//
+// The same tool can be answered by another client. UseClient takes the client's operation and two mappers: one from
+// the JevRequest that the model writes to the client's request, and one from the client's response to a JevResult.
+// The tool resolves the client from the agent's services, in a new scope on every call, and checks the mapped result
+// against the request before the model sees it.
+AIFunction localJev = new JevAIToolBuilder()
+ .UseClient(
+ (classifier, request, cancellationToken) => classifier.ClassifyAsync(request, cancellationToken),
+ inputMapper: ToClassificationRequest,
+ outputMapper: ToJevResult)
+ .Build();
+
+using ServiceProvider services = new ServiceCollection()
+ .AddSingleton()
+ .BuildServiceProvider();
+AIAgent localAgent = projectClient.AsAIAgent(
+ model: deploymentName,
+ instructions: Instructions,
+ name: "LocalTicketTriage",
+ tools: [localJev],
+ services: services);
+//
+
+Console.WriteLine("The same tool, answered by a local keyword classifier:");
+Console.WriteLine();
+await TriageAsync(localAgent, tickets[1]);
+
+static async Task TriageAsync(AIAgent agent, string ticket)
+{
+ AgentResponse response = await agent.RunAsync(ticket);
+
+ Console.WriteLine($"Ticket: {ticket}");
+ foreach (FunctionResultContent result in response.Messages.SelectMany(static m => m.Contents).OfType())
+ {
+ Console.WriteLine($"Tool result: {result.Result}");
+ }
+
+ Console.WriteLine($"Agent: {response.Text}");
+ Console.WriteLine();
+}
+
+//
+// Converts each Jev question into a classifier task. The options carry the words the classifier matches.
+static ClassificationRequest ToClassificationRequest(JevRequest request) =>
+ new(
+ request.State.ToString(),
+ [.. request.Questions.Select(question => question.Value switch
+ {
+ JevChoiceQuestion choice => new ClassificationTask(
+ question.Key,
+ ClassificationKind.PickOne,
+ [.. choice.Criteria.Select(criterion => new ClassificationOption(criterion.Key, $"{criterion.Key} {criterion.Value}"))]),
+ JevScoreQuestion score => new ClassificationTask(
+ question.Key,
+ ClassificationKind.Rate,
+ [.. score.Criteria.Select((level, index) => new ClassificationOption(index.ToString(CultureInfo.InvariantCulture), level.ToString()))]),
+ JevNoulQuestion noul => new ClassificationTask(
+ question.Key,
+ ClassificationKind.YesNo,
+ [new ClassificationOption("yes", $"{noul.Instructions} {noul.Criteria?.True}"), new ClassificationOption("no", $"{noul.Criteria?.False}")]),
+ _ => throw new NotSupportedException($"Question '{question.Key}' has an unknown type."),
+ })]);
+
+// Converts each classifier result back into the Jev answer of the same kind.
+static JevResult ToJevResult(ClassificationResponse response) => new()
+{
+ Model = "local-keyword-classifier",
+ Answers = response.Results.ToDictionary(result => result.Key, result => ToJevResponse(result.Value)),
+ Usage = new JevUsage { InputTokens = 0, OutputTokens = 0 },
+};
+
+static JevResponse ToJevResponse(ClassificationResult result)
+{
+ ScoredOption best = result.Options.MaxBy(option => option.Probability)!;
+ Dictionary probabilities = result.Options.ToDictionary(option => option.Key, option => option.Probability);
+ return result.Kind switch
+ {
+ ClassificationKind.PickOne => new JevChoiceResponse { Choice = best.Key, Confidence = best.Probability, Probabilities = probabilities },
+ ClassificationKind.Rate => new JevScoreResponse
+ {
+ // Like Jev, the score is the expected value over the rubric, so it can fall between levels.
+ Score = result.Options.Sum(option => int.Parse(option.Key, CultureInfo.InvariantCulture) * option.Probability),
+ Confidence = best.Probability,
+ Legend = result.Options.ToDictionary(option => option.Key, option => (JevEntry)option.Description),
+ Probabilities = probabilities,
+ },
+ _ => new JevNoulResponse { Noul = probabilities["yes"] },
+ };
+}
+//
diff --git a/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/README.md b/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/README.md
new file mode 100644
index 00000000000..e4753e04d0e
--- /dev/null
+++ b/dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev/README.md
@@ -0,0 +1,128 @@
+# Classification with Jev
+
+This sample gives an agent a classifier tool built with `JevAIToolBuilder` from `Microsoft.Agents.AI.TypeSafe`.
+
+Jev, the TypeSafe System One model, does not write text. It answers typed questions about a state and returns
+calibrated probabilities. An agent's model writes the replies; Jev makes the fast, structured decisions:
+
+1. The agent's model calls `ask_system_one` with a Jev request: the `state` and the `questions` to
+ answer about it.
+2. Jev answers each question:
+
+ | Question type | What it answers | Answer |
+ | --- | --- | --- |
+ | `choice` | Which of these named alternatives? | The selected label, the probability of each label, and a confidence |
+ | `score` | Which score on an ordered rubric (2 to 10 entries)? | An expected score that can fall between rubric entries, the probability of each score, and a confidence |
+ | `noul` | Yes or no? | The probability, from 0 to 1, of yes |
+
+ The state, the instructions, and every criterion can be text or JSON, like `EntryType` in the official TypeSafe SDK
+ ([`@typesafe-ai/sdk`](https://github.com/typesafe-ai/typesafe-sdk-js)). The Jev contract types mirror the SDK's type
+ names: `JevRequest`, `JevChoiceQuestion`, `JevResult`, `JevChoiceResponse`, and so on.
+
+3. The model reads the answers and writes the reply. Here it routes each support ticket and sends uncertain tickets to
+ human review.
+4. The same tool is answered by a local keyword classifier instead of the TypeSafe API, through `UseClient` and the
+ agent's services (see [Using another client](#using-another-client)).
+
+To use Jev as the agent's chat client instead, answering questions about the whole conversation, see
+[Agent with TypeSafe](../../AgentProviders/typesafe/Agent_With_TypeSafe/).
+
+## Configuring the client
+
+`JevAIToolBuilder` hides the HTTP client. The API key is an `ApiKeyCredential`, and `JevAIToolOptions` follows the
+`ClientPipelineOptions` pattern of the OpenAI and Azure AI client libraries:
+
+```csharp
+var options = new JevAIToolOptions
+{
+ ModelId = "jev-latest", // default
+ Transport = new HttpClientPipelineTransport(httpClient), // for example from IHttpClientFactory
+ NetworkTimeout = TimeSpan.FromSeconds(30),
+};
+options.AddPolicy(myPolicy, PipelinePosition.PerCall); // custom pipeline policies
+
+AIFunction jev = new JevAIToolBuilder(new ApiKeyCredential(apiKey), options).Build();
+```
+
+Unset pipeline settings get the defaults of the official SDK: two retries on 408, 429, and 5xx responses (529 means
+overloaded), honoring `retry-after-ms` or `Retry-After` up to one minute and otherwise backing off from half a second
+to five seconds, and a 10-second timeout per attempt. `Name` and `Description` set what the agent's model sees.
+
+## Using another client
+
+Any other client can answer the requests instead of the TypeSafe API. The second part of the sample shows it with
+`KeywordClassifier`, a local classifier in [KeywordClassifier.cs](./KeywordClassifier.cs) that has its own request and
+response types, knows nothing about Jev, and needs no network. `UseClient` takes the client's operation and two
+mappers between the Jev types and the client's types:
+
+```csharp
+AIFunction localJev = new JevAIToolBuilder()
+ .UseClient(
+ (classifier, request, cancellationToken) => classifier.ClassifyAsync(request, cancellationToken),
+ inputMapper: ToClassificationRequest,
+ outputMapper: ToJevResult)
+ .Build();
+```
+
+`ToClassificationRequest` turns each Jev question into a classifier task, and `ToJevResult` turns each classifier
+result back into the Jev answer of the same kind (see the `mappers` region of [Program.cs](./Program.cs)). The tool
+checks the mapped result before the model sees it: every question needs an answer of the same type, and a `choice`
+answer and its probabilities may only use the labels the question offered. The output mapper receives only the
+client's response, so the client's result says which kind of answer it is.
+
+The keyword classifier is far less certain than Jev, so for the same invoice ticket the agent's confidence rule
+usually sends it to human review. Use `UseMapping` instead of `UseClient` for a mapping that is a single delegate from
+a `JevRequest` to a `JevResult`.
+
+## Dependency injection
+
+Every mapping receives the `IServiceProvider` of the agent that calls the tool. `FunctionInvokingChatClient` passes
+it in `AIFunctionArguments.Services`, from the services the agent was created with. `UseClient` resolves
+the client from a new scope on every call and disposes the scope afterwards, so scoped and disposable clients work
+even though hosted agents are singletons that receive the root provider. The sample registers the classifier in a
+`ServiceCollection` and passes the provider to the agent:
+
+```csharp
+using ServiceProvider services = new ServiceCollection()
+ .AddSingleton()
+ .BuildServiceProvider();
+AIAgent localAgent = projectClient.AsAIAgent(model: deploymentName, instructions: Instructions, tools: [localJev], services: services);
+```
+
+In a host, register the client and build the tool in the agent's tool factory:
+
+```csharp
+builder.Services.AddScoped();
+builder.Services.AddAIAgent("triage", Instructions)
+ .WithAITool(sp => new JevAIToolBuilder()
+ .UseClient(
+ (classifier, request, cancellationToken) => classifier.ClassifyAsync(request, cancellationToken),
+ inputMapper: ToClassificationRequest,
+ outputMapper: ToJevResult)
+ .Build());
+```
+
+With the TypeSafe API, build the tool in the same `WithAITool` factory and take the credential and the transport from
+the container or configuration.
+
+## Prerequisites
+
+- .NET 10 SDK or later
+- A Microsoft Foundry project with a model deployment
+- Azure CLI installed and authenticated (for Azure credential authentication)
+- A TypeSafe API key (see the [TypeSafe quickstart](https://docs.typesafe.ai/introduction/quickstart))
+
+Set the following environment variables:
+
+```powershell
+$env:FOUNDRY_PROJECT_ENDPOINT="https://your-foundry-service.services.ai.azure.com/api/projects/your-foundry-project" # Replace with your Foundry project endpoint
+$env:FOUNDRY_MODEL="gpt-5.4-mini" # Optional, defaults to gpt-5.4-mini
+$env:TYPESAFE_API_KEY=""
+```
+
+## Run the sample
+
+```powershell
+cd dotnet/samples/02-agents/Agents/Agent_Step25_ClassificationWithJev
+dotnet run
+```
diff --git a/dotnet/samples/02-agents/Agents/README.md b/dotnet/samples/02-agents/Agents/README.md
index 487e7f269a2..356cf1116bf 100644
--- a/dotnet/samples/02-agents/Agents/README.md
+++ b/dotnet/samples/02-agents/Agents/README.md
@@ -51,6 +51,7 @@ Before you begin, ensure you have the following prerequisites:
|[Switching agent operating mode](./Agent_Step22_AgentMode/)|This sample demonstrates how to use the AgentModeProvider to track and switch an agent's operating mode at runtime, including the built-in plan/execute modes and custom modes, with a simple input loop that switches mode using a slash command.|
|[Tracking work with a todo list](./Agent_Step23_TodoList/)|This sample demonstrates how to use the TodoProvider to let an agent plan and track multi-step work using a todo list that persists across turns, printing the evolving todo list after each turn.|
|[Routing turns across multiple models](./Agent_Step24_MultiModelRouting/)|This sample demonstrates how to use the RoutePersistingRoutingChatClient to route each agent turn to one of several named chat clients, switching the active model mid-conversation while preserving the conversation history.|
+|[Classification with Jev](./Agent_Step25_ClassificationWithJev/)|This sample demonstrates how to give an agent a Jev tool built with JevAIToolBuilder, so the agent's model asks Jev typed choice, score, and noul questions and uses the calibrated answers to route support tickets.|
## Running the samples from the console
diff --git a/dotnet/samples/AGENTS.md b/dotnet/samples/AGENTS.md
index 7c56735e4c7..417f3434bcc 100644
--- a/dotnet/samples/AGENTS.md
+++ b/dotnet/samples/AGENTS.md
@@ -26,7 +26,8 @@ dotnet/samples/
│ │ ├── google-gemini/ # Google Gemini provider sample
│ │ ├── ollama/ # Ollama provider sample
│ │ ├── onnx/ # ONNX Runtime provider sample
-│ │ └── openai/ # OpenAI provider samples
+│ │ ├── openai/ # OpenAI provider samples
+│ │ └── typesafe/ # TypeSafe (Jev) chat client samples
│ ├── AgentOpenTelemetry/ # OpenTelemetry integration
│ ├── AgentSkills/ # Agent skills patterns
│ ├── AgentWithMemory/ # Memory providers (chat history, Mem0, Valkey, Foundry, AgentMemory)
diff --git a/dotnet/src/Microsoft.Agents.AI.TypeSafe/Core/JevAIFunction.cs b/dotnet/src/Microsoft.Agents.AI.TypeSafe/Core/JevAIFunction.cs
new file mode 100644
index 00000000000..45fb819c118
--- /dev/null
+++ b/dotnet/src/Microsoft.Agents.AI.TypeSafe/Core/JevAIFunction.cs
@@ -0,0 +1,89 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System;
+using System.Collections.Generic;
+using System.Text.Json;
+using System.Text.Json.Nodes;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.AI;
+using Microsoft.Shared.Diagnostics;
+
+namespace Microsoft.Agents.AI.TypeSafe;
+
+///
+/// The created by : its arguments are a
+/// and its result is the of the configured mapping or the TypeSafe API.
+///
+internal sealed class JevAIFunction : AIFunction
+{
+ private readonly Func> _systemOneAsync;
+
+ public JevAIFunction(string name, string description, Func> systemOneAsync)
+ {
+ this.Name = name;
+ this.Description = description;
+ this._systemOneAsync = systemOneAsync;
+ }
+
+ public override string Name { get; }
+
+ public override string Description { get; }
+
+ public override JsonElement JsonSchema => JevJsonUtilities.RequestSchema;
+
+ public override JsonElement? ReturnJsonSchema => JevJsonUtilities.ResultSchema;
+
+ public override JsonSerializerOptions JsonSerializerOptions => JevJsonUtilities.Options;
+
+ protected override async ValueTask