Skip to content

Checkpoints & Human Review ​

Three Persistence Concepts ​

MechanismWhat it keepsHow to use it
StoreA keyed state snapshotSaveState / LoadState
Session treeAppend-only entries and conversation branchessession.FileStore, optionally session.AgentStore
CheckpointSaverA sequence of execution snapshotsTurn checkpoints, SaveCheckpoint, RestoreLatestCheckpoint

None provides exactly-once execution of an external side effect. A snapshot may be written before or after a tool changes another system. Use application-level transactions, idempotency keys, or reconciliation for that boundary.

Automatic Checkpoints ​

go
saver := agentcore.NewMemoryCheckpointSaver()
saver.MaxCheckpointsPerThread = 20
cfg.Checkpoint = &agentcore.CheckpointSettings{
    Saver: saver,
    ThreadID: "conversation-123",
    SaveOnTurnStart: true,
}

By default, a configured saver receives checkpoints at turn end. SaveOnTurnStart also saves before the provider call, after steering and compaction. SkipSaveOnTurnEnd disables automatic turn-end saves.

The memory saver survives neither process exit nor machine failure. Implement CheckpointSaver with durable storage for restart recovery. Shared savers must be concurrency-safe; thread IDs must isolate users and conversations. An empty thread ID defaults to default, which is inappropriate for unrelated users.

Interrupt and Resume ​

A tool can return NewInterruptError or NewInterruptErrorWithData. The agent records a friendly tool result and interrupt reason, then returns control. Check both the returned error and Interrupted(); a nil error does not mean that the conversation reached a final answer.

The host decides what happens next. Inspect structured interrupt data, collect a human decision, queue a message with Steer, and call Resume. Resume requires an interrupt reason and continues from the stored tool result. It does not automatically execute the original operation after an approval.

Approval is a host policy

Authenticate the reviewer, bind the decision to a specific operation, and enforce that decision in the side-effecting tool. A model receiving the word "approved" is not sufficient authorization. Parallel batches may already have executed other tools when one requests an interrupt.

Offline Example ​

sh
go run ./example/recovery

No credentials are needed. A deterministic provider requests review, a checkpoint captures the interrupt, and another agent restores and resumes the conversation. Both agents share the memory saver; this is restoration within one process, not persistence across restarts.

go
package main

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"log"

	"github.com/covoyage/covonaut/agentcore"
)

type reviewProvider struct{}

func (reviewProvider) Complete(ctx context.Context, req *agentcore.ProviderRequest) (*agentcore.ProviderResponse, error) {
	for _, message := range req.Messages {
		if message.Role == agentcore.RoleTool {
			return &agentcore.ProviderResponse{Content: "Review recorded. No external action was performed."}, nil
		}
	}
	return &agentcore.ProviderResponse{ToolCalls: []agentcore.ToolCall{{
		ID: "review-1", Name: "request_review", Arguments: `{}`,
	}}}, nil
}

func (reviewProvider) Stream(ctx context.Context, req *agentcore.ProviderRequest) (<-chan agentcore.StreamDelta, error) {
	return nil, errors.New("this offline example does not support streaming")
}

func main() {
	if err := run(); err != nil {
		log.Fatal(err)
	}
}

func run() error {
	ctx := context.Background()
	saver := agentcore.NewMemoryCheckpointSaver()
	saver.MaxCheckpointsPerThread = 10
	cfg := agentcore.NewConfig(
		agentcore.WithModel("offline-review"),
		agentcore.WithProvider(reviewProvider{}),
		agentcore.WithMaxTurns(5),
		agentcore.WithCheckpoint(&agentcore.CheckpointSettings{
			Saver: saver, ThreadID: "review-thread",
		}),
		agentcore.WithTools(&agentcore.Tool{
			Name: "request_review", Description: "Pause for a host review",
			Parameters: map[string]any{"type": "object"},
			Func: func(ctx context.Context, args json.RawMessage) (any, error) {
				return nil, agentcore.NewInterruptErrorWithData("host review required", map[string]any{
					"request_id": "preview-1",
				})
			},
		}),
	)
	original, err := agentcore.NewWithError(cfg)
	if err != nil {
		return err
	}
	defer original.Close()
	if _, err := original.Run(ctx, "Prepare a request for review."); err != nil {
		return err
	}
	if original.Interrupted() == nil {
		return errors.New("expected a review interrupt")
	}
	fmt.Println("Paused:", original.Interrupted().Reason)

	restored, err := agentcore.NewWithError(cfg)
	if err != nil {
		return err
	}
	defer restored.Close()
	if err := restored.RestoreLatestCheckpoint(ctx, "review-thread"); err != nil {
		return err
	}
	if restored.Interrupted() == nil {
		return errors.New("checkpoint did not restore the interrupt")
	}
	fmt.Println("Restored:", restored.Interrupted().Data["request_id"])
	restored.Steer(agentcore.Message{Role: agentcore.RoleUser, Content: "I reviewed the preview. Continue without external actions."})
	output, err := restored.Resume(ctx)
	if err != nil {
		return err
	}
	fmt.Println(output)
	return nil
}

Choose the Next Entry Point ​

MethodBehavior
Run(ctx, input)Append a user message and run using existing history
Continue(ctx)Continue history without appending user input
Resume(ctx)Require an interrupt, clear it, then continue

RestoreLatestCheckpoint restores state and interrupt metadata, but does not itself call the provider. Choose the next entry point based on restored status.

Schema Migration ​

Snapshots carry a version. StateSnapshotMigrator advances one version at a time; a version jump or a snapshot newer than the supported version is an error. Legacy unversioned snapshots use version zero. Configure WithStateMigrators for ordinary snapshots and the checkpoint migration settings where required.

Test migration against persisted fixtures before upgrading a production application. Preserve copies until recovery has been validated.

Released under the MIT License.