Dela via


Durable Task-tillägg för Microsoft Agent Framework (förhandsversion)

Tillägget Durable Task för Microsoft Agent Framework ger durable execution direkt till Microsoft Agent Framework. Du kan registrera agenter med tillägget för att göra dem automatiskt varaktiga med beständiga sessioner, inbyggda API-slutpunkter och distribuerad skalning – utan ändringar i agentlogik.

Tillägget implementerar internt entitetsbaserade agentloopar, där varje agentsession är en varaktig entitet som automatiskt hanterar konversationstillstånd och kontrollpunkter.

Tillägget stöder två värdmetoder:

  • Azure Functions med hjälp av Azure Functions integrationspaketet.
  • Använd din egen beräkningskapacitet med baspaketet.

Agentvärdtjänst

Definiera din agent med standardmönstret Microsoft Agent Framework och förbättra den sedan med tillägget Durable Task. Tillägget hanterar sessionspersistence, skapande av slutpunkter och tillståndshantering automatiskt.

var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
    ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
    ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");

AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
    .GetChatClient(deploymentName)
    .AsAIAgent(
        instructions: "You are a professional content writer who creates engaging, "
                    + "well-structured documents for any given topic.",
        name: "DocumentPublisher");

// One line to make the agent durable with serverless hosting
using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableAgents(options => options.AddAIAgent(agent))
    .Build();
app.Run();
var endpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT")
    ?? throw new InvalidOperationException("AZURE_OPENAI_ENDPOINT is not set.");
var deploymentName = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT_NAME")
    ?? throw new InvalidOperationException("AZURE_OPENAI_DEPLOYMENT_NAME is not set.");

AIAgent agent = new AzureOpenAIClient(new Uri(endpoint), new DefaultAzureCredential())
    .GetChatClient(deploymentName)
    .AsAIAgent(
        instructions: "You are a professional content writer who creates engaging, "
                    + "well-structured documents for any given topic.",
        name: "DocumentPublisher");

// Host the agent with Durable Task Scheduler
string connectionString = "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.ConfigureDurableAgents(
            options => options.AddAIAgent(agent),
            workerBuilder: builder => builder.UseDurableTaskScheduler(connectionString),
            clientBuilder: builder => builder.UseDurableTaskScheduler(connectionString));
    })
    .Build();

await host.StartAsync();

Orkestrering med flera agenter

Du kan samordna flera specialiserade agenter som steg i en varaktig orkestrering. Varje agentanrop är kontrollerad vid en kontrollpunkt, och orkestreringen återställer sig automatiskt om något steg misslyckas. Slutförda agentanrop körs inte igen vid återställning.

I följande exempel visas ett sekventiellt arbetsflöde med flera agenter där en forskningsagent samlar in information och en skrivagent skapar ett dokument.

[Function(nameof(DocumentPublishingOrchestration))]
public async Task<string> DocumentPublishingOrchestration(
    [OrchestrationTrigger] TaskOrchestrationContext context)
{
    var docRequest = context.GetInput<DocumentRequest>();

    DurableAIAgent researchAgent = context.GetAgent("ResearchAgent");
    DurableAIAgent writerAgent = context.GetAgent("DocumentPublisherAgent");

    // Step 1: Research the topic
    AgentResponse<ResearchResult> researchResult = await researchAgent
        .RunAsync<ResearchResult>(
            $"Research the following topic: {docRequest.Topic}");

    // Step 2: Write the document using the research findings
    AgentResponse<DocumentResponse> document = await writerAgent
        .RunAsync<DocumentResponse>(
            $"""Create a document about {docRequest.Topic}.
            Research findings: {researchResult.Result.Findings}""");

    // Step 3: Publish
    return await context.CallActivityAsync<string>(
        nameof(PublishDocument),
        new { docRequest.Topic, document.Result.Text });
}
static async Task<string> DocumentPublishingOrchestration(
    TaskOrchestrationContext context, DocumentRequest docRequest)
{
    DurableAIAgent researchAgent = context.GetAgent("ResearchAgent");
    DurableAIAgent writerAgent = context.GetAgent("DocumentPublisherAgent");

    // Step 1: Research the topic
    AgentResponse<ResearchResult> researchResult = await researchAgent
        .RunAsync<ResearchResult>(
            $"Research the following topic: {docRequest.Topic}");

    // Step 2: Write the document using the research findings
    AgentResponse<DocumentResponse> document = await writerAgent
        .RunAsync<DocumentResponse>(
            $"""Create a document about {docRequest.Topic}.
            Research findings: {researchResult.Result.Findings}""");

    // Step 3: Publish
    return await context.CallActivityAsync<string>(
        nameof(PublishDocument),
        new { docRequest.Topic, document.Result.Text });
}

Diagrambaserade arbetsflöden

Tillägget Durable Task stöder även Microsoft Agent Framework-arbetsflöden, som använder en deklarativ, grafbaserad programmeringsmodell (WorkflowBuilder) för att definiera pipelines med flera steg för utförare och agenter. Tillägget skapar automatiskt kontrollpunkter för varje steg i grafen och återhämtar sig från fel utan ändringar i arbetsflödesdefinitionen.

Sekventiellt arbetsflöde

I följande exempel kedjas tre utförare in i ett arbetsflöde för orderavbokning: leta upp ordern, avbryta den och skicka sedan ett bekräftelsemeddelande.

OrderLookup orderLookup = new();
OrderCancel orderCancel = new();
SendEmail sendEmail = new();

Workflow cancelOrder = new WorkflowBuilder(orderLookup)
    .WithName("CancelOrder")
    .WithDescription("Cancel an order and notify the customer")
    .AddEdge(orderLookup, orderCancel)
    .AddEdge(orderCancel, sendEmail)
    .Build();

using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableWorkflows(workflows => workflows.AddWorkflows(cancelOrder))
    .Build();
app.Run();

OrderLookup, OrderCancel och SendEmail executors är standardexekutorer Microsoft Agent Framework utan durable-specifik kod. Fullständiga implementeringar finns i samplar på GitHub.

string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
    ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";

OrderLookup orderLookup = new();
OrderCancel orderCancel = new();
SendEmail sendEmail = new();

Workflow cancelOrder = new WorkflowBuilder(orderLookup)
    .WithName("CancelOrder")
    .WithDescription("Cancel an order and notify the customer")
    .AddEdge(orderLookup, orderCancel)
    .AddEdge(orderCancel, sendEmail)
    .Build();

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.ConfigureDurableWorkflows(
            workflowOptions => workflowOptions.AddWorkflow(cancelOrder),
            workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
            clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
    })
    .Build();

await host.StartAsync();

IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
IAwaitableWorkflowRun run = (IAwaitableWorkflowRun)await workflowClient.RunAsync(cancelOrder, "ORD-12345");
string? result = await run.WaitForCompletionAsync<string>();

OrderLookup, OrderCancel och SendEmail executors är standardexekutorer Microsoft Agent Framework utan durable-specifik kod. Fullständiga implementeringar finns i samplar på GitHub.

Arbetsflöde för ut-/fläkt-in (samtidig)

Du kan fläkta ut till flera utförare eller agenter som körs parallellt och sedan fläkta in för att aggregera resultatet. I följande exempel skickas en vetenskapsfråga till en fysiker och kemistagent parallellt och sammanställer sedan deras svar.

ChatClient chatClient = new AzureOpenAIClient(
    new Uri(endpoint), new DefaultAzureCredential()).GetChatClient(deploymentName);

AIAgent physicist = chatClient.AsAIAgent(
    "You are a physics expert. Be concise (2-3 sentences).", "Physicist");
AIAgent chemist = chatClient.AsAIAgent(
    "You are a chemistry expert. Be concise (2-3 sentences).", "Chemist");

ParseQuestionExecutor parseQuestion = new();
AggregatorExecutor aggregator = new();

Workflow workflow = new WorkflowBuilder(parseQuestion)
    .WithName("ExpertReview")
    .AddFanOutEdge(parseQuestion, [physicist, chemist])
    .AddFanInBarrierEdge([physicist, chemist], aggregator)
    .Build();

using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableWorkflows(workflows => workflows.AddWorkflows(workflow))
    .Build();
app.Run();

ParseQuestionExecutor och AggregatorExecutor är standardexekutorer för Microsoft Agent Framework utan durable-specifik kod. Fullständiga implementeringar finns i samplar på GitHub.

string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
    ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";

ChatClient chatClient = new AzureOpenAIClient(
    new Uri(endpoint), new DefaultAzureCredential()).GetChatClient(deploymentName);

ParseQuestionExecutor parseQuestion = new();
AIAgent physicist = chatClient.AsAIAgent(
    "You are a physics expert. Be concise (2-3 sentences).", "Physicist");
AIAgent chemist = chatClient.AsAIAgent(
    "You are a chemistry expert. Be concise (2-3 sentences).", "Chemist");
AggregatorExecutor aggregator = new();

Workflow workflow = new WorkflowBuilder(parseQuestion)
    .WithName("ExpertReview")
    .AddFanOutEdge(parseQuestion, [physicist, chemist])
    .AddFanInBarrierEdge([physicist, chemist], aggregator)
    .Build();

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.ConfigureDurableOptions(
            options => options.Workflows.AddWorkflow(workflow),
            workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
            clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
    })
    .Build();

await host.StartAsync();

IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
IWorkflowRun run = await workflowClient.RunAsync(workflow, "Why is the sky blue?");

if (run is IAwaitableWorkflowRun awaitableRun)
{
    string? result = await awaitableRun.WaitForCompletionAsync<string>();
    Console.WriteLine(result);
}

ParseQuestionExecutor och AggregatorExecutor är standardexekutorer för Microsoft Agent Framework utan durable-specifik kod. Fullständiga implementeringar finns i samplar på GitHub.

Arbetsflöde för villkorsstyrd routning

Du kan dirigera körningen till olika grenar baserat på körningsresultat. I följande exempel används en agent för skräppostidentifiering för att klassificera inkommande e-post och dirigerar sedan till antingen en skräpposthanterare eller en e-postassistentagent.

AIAgent spamDetector = chatClient.AsAIAgent(
    "You are a spam detection assistant. Return JSON with is_spam (bool) and reason (string).",
    "SpamDetectionAgent");
AIAgent emailAssistant = chatClient.AsAIAgent(
    "You are an email assistant. Draft a professional response.",
    "EmailAssistantAgent");

SpamHandlerExecutor spamHandler = new();
EmailSenderExecutor emailSender = new();

Workflow workflow = new WorkflowBuilder(spamDetector)
    .WithName("EmailClassification")
    .AddSwitchCaseEdgeGroup(spamDetector, [
        new Case(condition: IsSpamDetected, target: spamHandler),
        new Default(target: emailAssistant),
    ])
    .AddEdge(emailAssistant, emailSender)
    .Build();

using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableWorkflows(workflows => workflows.AddWorkflows(workflow))
    .Build();
app.Run();

SpamHandlerExecutor och EmailSenderExecutor är standardexekutorer för Microsoft Agent Framework utan durable-specifik kod. Fullständiga implementeringar finns i samplar på GitHub.

string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
    ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";

ChatClient chatClient = new AzureOpenAIClient(
    new Uri(endpoint), new DefaultAzureCredential()).GetChatClient(deploymentName);

AIAgent spamDetector = chatClient.AsAIAgent(
    "You are a spam detection assistant. Return JSON with is_spam (bool) and reason (string).",
    "SpamDetectionAgent");
AIAgent emailAssistant = chatClient.AsAIAgent(
    "You are an email assistant. Draft a professional response.",
    "EmailAssistantAgent");

SpamHandlerExecutor spamHandler = new();
EmailSenderExecutor emailSender = new();

Workflow workflow = new WorkflowBuilder(spamDetector)
    .WithName("EmailClassification")
    .AddSwitchCaseEdgeGroup(spamDetector, [
        new Case(condition: IsSpamDetected, target: spamHandler),
        new Default(target: emailAssistant),
    ])
    .AddEdge(emailAssistant, emailSender)
    .Build();

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.ConfigureDurableWorkflows(
            workflowOptions => workflowOptions.AddWorkflow(workflow),
            workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
            clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
    })
    .Build();

await host.StartAsync();

IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
IAwaitableWorkflowRun run = (IAwaitableWorkflowRun)await workflowClient.RunAsync(workflow, "Check this email for spam");
string? result = await run.WaitForCompletionAsync<string>();

SpamHandlerExecutor och EmailSenderExecutor är standardexekutorer för Microsoft Agent Framework utan durable-specifik kod. Fullständiga implementeringar finns i samplar på GitHub.

HITL-arbetsflöde (Human-in-the-loop)

Du kan pausa arbetsflödeskörningen på avsedda platser för att vänta på externa indata innan du fortsätter. Arbetsflödesmodellen Microsoft Agent Framework använder RequestPort noder (i .NET) eller ctx.request_info() (i Python) för att definiera pauspunkter. I följande exempel implementeras ett arbetsflöde för kostnadsersättning med ett administratörsgodkännande följt av parallella budget- och efterlevnadsgodkännanden.

CreateApprovalRequest createRequest = new();
RequestPort<ApprovalRequest, ApprovalResponse> managerApproval =
    RequestPort.Create<ApprovalRequest, ApprovalResponse>("ManagerApproval");
PrepareFinanceReview prepareFinanceReview = new();
RequestPort<ApprovalRequest, ApprovalResponse> budgetApproval =
    RequestPort.Create<ApprovalRequest, ApprovalResponse>("BudgetApproval");
RequestPort<ApprovalRequest, ApprovalResponse> complianceApproval =
    RequestPort.Create<ApprovalRequest, ApprovalResponse>("ComplianceApproval");
ExpenseReimburse reimburse = new();

Workflow expenseApproval = new WorkflowBuilder(createRequest)
    .WithName("ExpenseReimbursement")
    .WithDescription("Expense reimbursement with manager and parallel finance approvals")
    .AddEdge(createRequest, managerApproval)
    .AddEdge(managerApproval, prepareFinanceReview)
    .AddFanOutEdge(prepareFinanceReview, [budgetApproval, complianceApproval])
    .AddFanInBarrierEdge([budgetApproval, complianceApproval], reimburse)
    .Build();

using IHost app = FunctionsApplication
    .CreateBuilder(args)
    .ConfigureFunctionsWebApplication()
    .ConfigureDurableWorkflows(workflows =>
        workflows.AddWorkflow(expenseApproval, exposeStatusEndpoint: true))
    .Build();
app.Run();

Ramverket genererar automatiskt tre HTTP-slutpunkter för HITL-interaktion.

  • POST /api/workflows/{name}/run : Starta arbetsflödet
  • GET /api/workflows/{name}/status/{id} : Kontrollera status och väntande godkännanden
  • POST /api/workflows/{name}/respond/{id} : Skicka godkännandesvar för att återuppta

Följande posttyper definierar data som flödar genom arbetsflödet:

public record ApprovalRequest(string ExpenseId, decimal Amount, string EmployeeName);
public record ApprovalResponse(bool Approved, string? Comments);

CreateApprovalRequest, PrepareFinanceReview och ExpenseReimburse executors är standardexekutorer Microsoft Agent Framework utan durable-specifik kod. Fullständiga implementeringar finns i samplar på GitHub.

string dtsConnectionString = Environment.GetEnvironmentVariable("DURABLE_TASK_SCHEDULER_CONNECTION_STRING")
    ?? "Endpoint=http://localhost:8080;TaskHub=default;Authentication=None";

CreateApprovalRequest createRequest = new();
RequestPort<ApprovalRequest, ApprovalResponse> managerApproval =
    RequestPort.Create<ApprovalRequest, ApprovalResponse>("ManagerApproval");
PrepareFinanceReview prepareFinanceReview = new();
RequestPort<ApprovalRequest, ApprovalResponse> budgetApproval =
    RequestPort.Create<ApprovalRequest, ApprovalResponse>("BudgetApproval");
RequestPort<ApprovalRequest, ApprovalResponse> complianceApproval =
    RequestPort.Create<ApprovalRequest, ApprovalResponse>("ComplianceApproval");
ExpenseReimburse reimburse = new();

Workflow expenseApproval = new WorkflowBuilder(createRequest)
    .WithName("ExpenseReimbursement")
    .AddEdge(createRequest, managerApproval)
    .AddEdge(managerApproval, prepareFinanceReview)
    .AddFanOutEdge(prepareFinanceReview, [budgetApproval, complianceApproval])
    .AddFanInBarrierEdge([budgetApproval, complianceApproval], reimburse)
    .Build();

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.ConfigureDurableWorkflows(
            options => options.AddWorkflow(expenseApproval),
            workerBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString),
            clientBuilder: builder => builder.UseDurableTaskScheduler(dtsConnectionString));
    })
    .Build();

await host.StartAsync();

IWorkflowClient workflowClient = host.Services.GetRequiredService<IWorkflowClient>();
IStreamingWorkflowRun run = await workflowClient.StreamAsync(expenseApproval, "EXP-2025-001");

await foreach (WorkflowEvent evt in run.WatchStreamAsync())
{
    switch (evt)
    {
        case DurableWorkflowWaitingForInputEvent requestEvent:
            Console.WriteLine($"Workflow paused at: {requestEvent.RequestPort.Id}");
            ApprovalResponse approval = new(Approved: true, Comments: "Approved.");
            await run.SendResponseAsync(requestEvent, approval);
            break;

        case DurableWorkflowCompletedEvent completedEvent:
            Console.WriteLine($"Workflow completed: {completedEvent.Result}");
            break;
    }
}

Följande posttyper definierar data som flödar genom arbetsflödet:

public record ApprovalRequest(string ExpenseId, decimal Amount, string EmployeeName);
public record ApprovalResponse(bool Approved, string? Comments);

CreateApprovalRequest, PrepareFinanceReview och ExpenseReimburse executors är standardexekutorer Microsoft Agent Framework utan durable-specifik kod. Fullständiga implementeringar finns i samplar på GitHub.

Durable Task Scheduler-kontrollpanel

Använd Durable Task Scheduler-instrumentpanelen för full översikt över dina hållbara agenter, orkestreringar och grafbaserade arbetsflöden:

  • Visa konversationshistorik för varje agentsession
  • Granska verktygsanrop och strukturerade utdata
  • Spåra orkestrerings- och arbetsflödeskörningsflöden
  • Övervaka prestandamått

Både lokal utveckling (via emulatorn) och produktionsdistributioner har samma instrumentpanelsupplevelse.

Följande skärmbild visar en agentsession med dess konversationshistorik och sessionsinformation:

Skärmbild av instrumentpanelen Durable Task Scheduler som visar agentens konversationshistorik och sessionsinformation.

Följande skärmbild visar en deterministisk orkestrering med aktivitetskörningsinformation:

Skärmbild av instrumentpanelen Durable Task Scheduler som visar en deterministisk agentisk orkestreringsvy.

Sessionens tidsintervall (TTL)

Varaktiga agentsessioner upprätthåller automatiskt konversationshistorik och tillstånd, vilket kan ackumuleras på obestämd tid. Funktionen time-to-live (TTL) ger automatisk rensning av inaktiva sessioner, vilket förhindrar förbrukning av lagringsresurser och ökade kostnader.

När en agentsession är inaktiv längre än den konfigurerade TTL-perioden tas sessionstillståndet bort automatiskt. Varje ny interaktion återställer TTL-timern, vilket förlänger sessionens livslängd.

Standardvärden

  • Standard-TTL: 14 dagar
  • Minsta TTL-borttagningsfördröjning: 5 minuter

Konfiguration

TTL kan konfigureras globalt eller per agent. När en agentsession upphör att gälla tas hela dess tillstånd bort, inklusive konversationshistorik och eventuella anpassade tillståndsdata. Om ett meddelande skickas till samma session efter borttagningen skapas en ny session med en ny konversationshistorik.

Anmärkning

TTL-konfigurationen är för närvarande endast tillgänglig i .NET.

services.ConfigureDurableAgents(
    options =>
    {
        // Set global default TTL to 7 days
        options.DefaultTimeToLive = TimeSpan.FromDays(7);

        // Agent with custom TTL of 1 day
        options.AddAIAgent(shortLivedAgent, timeToLive: TimeSpan.FromDays(1));

        // Agent with custom TTL of 90 days
        options.AddAIAgent(longLivedAgent, timeToLive: TimeSpan.FromDays(90));

        // Agent using global default (7 days)
        options.AddAIAgent(defaultAgent);

        // Agent with no TTL (never expires)
        options.AddAIAgent(permanentAgent, timeToLive: null);
    });

Kända begränsningar

  • Maximal konversationsstorlek.
    Agentsessionstillståndet, inklusive den fullständiga konversationshistoriken, omfattas av tillståndsstorleksgränserna för den varaktiga serverdelen. När du använder Durable Task Scheduler är den maximala entitetstillståndsstorleken 1 MB. Långvariga konversationer med stora svar på verktygsanrop kan nå den här gränsen. Komprimering av konversationshistorik måste göras manuellt, till exempel genom att starta en ny agentsession och sammanfatta den tidigare kontexten.

  • Latens.
    Alla agentinteraktioner dirigeras genom Durable Task Scheduler, vilket medför latens jämfört med minnesintern körning av agenter. Den här kompromissen ger hållbarhet och distribuerad skalning.

  • Streaming.
    Eftersom varaktiga agenter implementeras ovanpå varaktiga entiteter är den underliggande kommunikationsmodellen begäran/svar. Strömning stöds via återanrop för svar (till exempel för överföring av tokens till en Redis-ström för klientanvändning), medan entiteten returnerar det fullständiga svaret efter att strömningen har avslutats.

  • TTL-förfallodatum.
    TTL-timern baseras på wall-clock-tid sedan det senaste meddelandet, inte kumulativ aktivitetstid. När en session har tagits bort (via TTL-förfallotid eller manuell borttagning) kan dess konversationshistorik inte återställas.

Nästa steg