Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 48 additions & 17 deletions docs/architecture/persistence-checkpoints-and-forks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@

## Durable interaction model

`AgentInstance` represents ephemeral compute. An A2A context durably owns its
`AgentInstance` represents ephemeral compute. An `agent_history` row durably owns its
tasks and ordered events, allowing interaction history to remain as an audit trail
after compute is removed. New instances allocate independent instance, wire A2A
context, and durable history IDs. `agent_instance.history_id` selects the history;
`agent_instance.context_id` binds its public context. A composite foreign key
ensures that binding agrees with `a2a_context`. A history belongs to at most one
live instance, while multiple fork authorities may use the same wire context.
ensures the instance ID, owner, and context agree with `agent_history`. The history
retains its unique originating `instance_id` after compute is deleted, so that ID
still resolves the complete checkpoint lineage. Multiple fork authorities may use
the same wire context.

Deleting an instance leaves a hidden tombstone for its owner and creation-request
identity, while releasing its runtime references and shares. A retry of that
creation request cannot recreate the deleted instance.

The core PostgreSQL records are:

Expand All @@ -18,7 +24,7 @@ The core PostgreSQL records are:
| `agent_template_harness_pair` | Pair status and latest successful revision |
| `agent_instance` | Compute identity, pinned revision, lifecycle phase, and Actor identity |
| `agent_instance_share` | Instance authorization grants |
| `a2a_context` | Durable history scope and its wire A2A context binding |
| `agent_history` | One durable history branch, its wire A2A context binding, and parent history/cutoff |
| `agent_instance_task` | Rebuildable current A2A task state and query indexes |
| `agent_instance_task_event` | Authoritative append-only task and message events, with creation and runtime-boundary metadata |
| `agent_instance_checkpoint` | Named immutable snapshot/history boundary |
Expand All @@ -40,15 +46,16 @@ without duplicating SQL. Transaction boundaries remain with the owning operation
flowchart TD
PAIR[Harness + AgentTemplate pair] --> REV[runtime revision]
REV --> INSTANCE[AgentInstance]
INSTANCE --> CONTEXT[history scope + wire context]
CONTEXT --> EVENT[immutable ordered task events]
INSTANCE -->|history_id| HISTORY[agent_history]
HISTORY --> EVENT[immutable ordered task events]
EVENT -->|replay| TASK[materialized task views]
CONTEXT --> CHECKPOINT[checkpoint boundary]
HISTORY --> CHECKPOINT[checkpoint boundary]
REV --> CHECKPOINT
CHECKPOINT --> TAG[Substrate snapshot tag]
CHECKPOINT --> FORK[forked AgentInstance]
FORK --> NEWCTX[new history scope, same wire context]
EVENT -->|copy through checkpoint cutoff| NEWCTX
FORK -->|history_id| NEWHISTORY[new agent_history, same wire context]
NEWHISTORY -->|parent history + cutoff| HISTORY
EVENT -->|copy through checkpoint cutoff| NEWHISTORY
```

## Checkpoint creation
Expand All @@ -74,23 +81,47 @@ The checkpoint retains source-instance provenance, source history, prepared
revision, name, head task, and history sequence. Reservation saves an event
cutoff in the same transaction as the runtime boundary reference. Later replies
append events beyond that cutoff and cannot change the saved task state.
The head identifies the task whose snapshot
covers the latest history event, including when an older paused task resumes. The source AgentInstance may be
deleted while its context and checkpoint remain.
The head identifies the task whose snapshot covers the latest history event,
including when an older paused task resumes and reaches a terminal state. The
source AgentInstance may be deleted while its history and checkpoint remain.

Deletion first hides the checkpoint, then deletes its snapshot tag, then removes
the row. A checkpoint referenced by a fork cannot be deleted. Substrate deletes the Tag's copied snapshot with the Tag.
the row. A checkpoint inside any retained fork history's inherited prefix cannot
start deletion. Deleting instances keeps those histories, so inherited checkpoints
and their Tags remain protected even after all related instances are removed.
There is currently no history garbage collection. A deletion already in progress
can finish or retry if a later fork inherits its boundary. Substrate deletes the
Tag's copied snapshot with the Tag.

## Forking

History ancestry is recorded directly on `agent_history`: `parent_history_id` and
`parent_history_sequence` identify the source history and the cutoff copied from
it. Roots have neither field. Fork creation sets both fields atomically with the
new instance and never changes them. Foreign keys require the parent to share the
child's owner and context. The fork transaction reads only parent-history events
and verifies the exact cutoff and its task/snapshot boundary before committing;
failure rolls back the new history and instance. There is no reverse foreign key
from history to an event. Events remain retained with their history; future event
garbage collection must preserve inherited boundaries and coordinate with forks.
Each new history points to an existing parent; ancestry traversal needs no
checkpoint rows. `agent_instance.source_checkpoint_id` separately retains
fork-request identity and runtime provenance, including on deletion tombstones.

Checkpoint listing follows parent histories and their cutoffs, returning owned
ready checkpoints with their original source provenance. Membership depends on the
history boundary, so a checkpoint created later at an already inherited boundary
also appears. Listing uses checkpoint-ID pagination across local and inherited
results. Listing starts from the history's retained instance ID, so deleting an
instance does not change its local or inherited checkpoint results.

Forking creates a new AgentInstance authority and durable history scope. It
preserves wire context, task, message, artifact IDs, and request deduplication
metadata while copying events through the saved cutoff and reconstructing task
views from those events. It never reads the source's current task views. It creates a
separate Actor from the checkpoint's snapshot tag. Private runtime session IDs and
opaque paused-tool references therefore remain valid without runtime-specific
rewriting. New work appends only to the fork's history; source history and the
checkpoint remain immutable. The copied head boundary
separate Actor from the checkpoint's snapshot tag. Private runtime session IDs
remain valid without runtime-specific rewriting. New work appends only to the
fork's history; it does not mutate the source history or checkpoint. The copied head boundary
uses the retained Tag URI, allowing a fresh fork to be checkpointed before its
first turn. Fork creation verifies the Tag UID and URI as well as the Actor's
source Tag, suspended state, template, and external snapshot.
Expand Down
6 changes: 3 additions & 3 deletions docs/plans/api-v2-execution-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ Public invariants:

- `Harness` and `AgentTemplate` are `kagent.dev/v1alpha3` CRDs.
- `AgentInstance` is a PostgreSQL-backed gRPC resource.
- One AgentInstance owns one rooted template tree and one A2A context.
- `A2A context_id == AgentInstance.id`.
- One AgentInstance owns one rooted template tree and one private history branch in `agent_history`.
- `AgentInstance.context_id` is the public A2A context ID; forks preserve it while using distinct instance and history IDs.
- A2A owns interaction and history; AgentInstanceService owns catalog, lifecycle, metadata, and sharing.
- Every quiescent A2A turn is durably suspended to an exact snapshot before its
final state is published; physical Actor suspension does not change a ready
Expand Down Expand Up @@ -444,7 +444,7 @@ Implement `ForkAgentInstance`:
- Create a new Actor identity from the checkpoint's retained snapshot tag.
- Create a new AgentInstance, A2A authority, and creator ownership.
- Keep source instance, history, and snapshots immutable.
- Represent inherited history through copy-on-write projections: deterministic fork-local Task IDs and the new context ID reference immutable source payloads and lineage without duplicating content.
- Copy events through the checkpoint cutoff and rebuild private task projections, preserving wire context and task IDs for runtime continuity. Record ancestry with a direct parent history and cutoff; sharing event storage is a separate future optimization.
- New Tasks append only to the fork.
- Return ready only after the cloned Actor resumes and passes health.

Expand Down
14 changes: 9 additions & 5 deletions go/api/gen/kagent/api/v1alpha1/checkpoints.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions go/api/gen/kagent/api/v1alpha1/checkpoints_grpc.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 14 additions & 4 deletions go/core/internal/database/agent_instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,15 +405,25 @@ func readAgentInstance(ctx context.Context, db dbExecutor, id string) (agentInst
// insertAgentInstanceRecords stores a new instance and its independent history together.
// Callers supply a transaction and a prepared CREATING/CREATE instance; a duplicate
// creator/requestID returns pgx.ErrNoRows so the caller can roll back and resolve it.
func insertAgentInstanceRecords(ctx context.Context, db dbExecutor, instance *apiv1alpha1.AgentInstance, requestID string, historyID uuid.UUID, sourceCheckpointID *uuid.UUID) (agentInstanceRow, error) {
// A fork records immutable parent history/cutoff metadata separately from the instance's
// source checkpoint, so ancestry does not depend on checkpoint rows or instance lifetime.
func insertAgentInstanceRecords(ctx context.Context, db dbExecutor, instance *apiv1alpha1.AgentInstance, requestID string, historyID uuid.UUID, source *agentInstanceCheckpointRow) (agentInstanceRow, error) {
data, err := marshalAgentInstance(instance)
if err != nil {
return agentInstanceRow{}, err
}
var sourceCheckpointID, parentHistoryID *uuid.UUID
var parentHistorySequence *int64
if source != nil {
sourceCheckpointID = &source.ID
parentHistoryID = &source.SourceHistoryID
parentHistorySequence = &source.HistorySequence
}
if err := execSQL(ctx, db, `
INSERT INTO a2a_context (id, user_id, context_id) VALUES ($1, $2, $3)
`, historyID, instance.Creator, instance.ContextId); err != nil {
return agentInstanceRow{}, fmt.Errorf("insert A2A context: %w", err)
INSERT INTO agent_history (id, instance_id, user_id, context_id, parent_history_id, parent_history_sequence)
VALUES ($1, $2, $3, $4, $5, $6)
`, historyID, instance.Id, instance.Creator, instance.ContextId, parentHistoryID, parentHistorySequence); err != nil {
return agentInstanceRow{}, fmt.Errorf("insert agent history: %w", err)
}
return queryOne(ctx, db, `
INSERT INTO agent_instance (id, user_id, request_id, context_id, history_id, prepared_revision,
Expand Down
Loading
Loading