Memory & Sessions
This guide covers Goa-AI’s transcript model, memory persistence, and how to model multi-turn conversations and long-running workflows.
Why Transcripts Matter
Goa-AI treats the transcript as the single source of truth for a run: an ordered sequence of messages and tool interactions that is sufficient to:
- Reconstruct provider payloads (Bedrock/OpenAI) for every model call
- Drive planners (including retries and tool repair)
- Power UIs with accurate history
Because the transcript is authoritative, you do not need to hand-manage:
- Separate lists of prior tool calls and tool results
- Ad-hoc “conversation state” structures
- Per-turn copies of previous user/assistant messages
You persist and pass the transcript only; Goa-AI and its provider adapters rebuild everything they need from that.
Messages and Parts
At the model boundary, Goa-AI uses model.Message values to represent the transcript. Each message has a role (user, assistant) and an ordered list of parts:
| Part Type | Description |
|---|---|
ThinkingPart | Provider reasoning content (plaintext + signature or redacted bytes). Not user-facing; used for audit/replay and optional “thinking” UIs. |
TextPart | Visible text shown to the user (questions, answers, explanations). |
ToolUsePart | Assistant-initiated tool call with ID, Name (canonical tool ID), and Input (JSON payload). |
ToolResultPart | User/tool result correlated with a prior tool_use via ToolUseID and Content (JSON payload). |
Order is sacred:
- A tool-using assistant message typically looks like:
ThinkingPart, then one or moreToolUseParts, then optionalTextPart - A user/tool result message typically contains one or more
ToolResultParts referencing previous tool_use IDs, plus optional user text
Goa-AI’s provider adapters (e.g., Bedrock Converse) re-encode these parts into provider-specific blocks without reordering.
The Transcript Contract
The high-level transcript contract in Goa-AI is:
- The application (or runtime) persists every event for a run in order: assistant thinking, text, tool_use (ID + args), user tool_result (tool_use_id + content), subsequent assistant messages, and so on
- Before each model call, the caller supplies the entire transcript for that run as
[]*model.Message, with the last element being the new delta (user text or tool_result) - Goa-AI re-encodes that transcript into the provider’s chat format in the same order
There is no separate “tool history” API; the transcript is the history.
How This Simplifies Planners and UIs
- Planners: Receive the current transcript in
planner.PlanInput.Messagesandplanner.PlanResumeInput.Messages. Can decide what to do based purely on the messages, without threading extra state. - UIs: Can render chat history, tool ribbons, and agent cards from the same underlying transcript they persist for the model. No separate “tool log” structures needed.
- Provider adapters: Never guess which tools were called or which results belong where; they simply map transcript parts → provider blocks.
Transcript Ledger
The transcript ledger is a provider-precise record that maintains conversation history in the exact format required by model providers. It ensures deterministic replay and provider fidelity without leaking provider SDK types into workflow state.
Provider Fidelity
Different model providers (Bedrock, OpenAI, etc.) have strict requirements about message ordering and structure. The ledger enforces these constraints:
| Provider Requirement | Ledger Guarantee |
|---|---|
| Thinking must precede tool_use in assistant messages | Ledger orders parts: thinking → text → tool_use |
| Tool results must follow their corresponding tool_use | Ledger correlates tool_result via ToolUseID |
| Message alternation (assistant → user → assistant) | Ledger flushes assistant before appending user results |
For Bedrock specifically, when thinking is enabled:
- Assistant messages containing tool_use must start with a thinking block
- User messages with tool_result must immediately follow the assistant message declaring the tool_use
- Tool result count cannot exceed the prior tool_use count
Ordering Requirements
The ledger stores parts in the canonical order required by providers:
Assistant Message:
1. ThinkingPart(s) - provider reasoning (text + signature or redacted bytes)
2. TextPart(s) - visible assistant text
3. ToolUsePart(s) - tool invocations (ID, name, args)
User Message:
1. ToolResultPart(s) - tool results correlated via ToolUseID
This ordering is sacred — the ledger never reorders parts, and provider adapters re-encode them into provider-specific blocks in the same sequence.
Automatic Ledger Maintenance
The runtime automatically maintains the transcript ledger. You do not need to manage it manually:
Event Capture: As the run progresses, the runtime persists memory events (
EventThinking,EventAssistantMessage,EventToolCall,EventToolResult) in orderLedger Reconstruction: The
BuildMessagesFromEventsfunction rebuilds provider-ready messages from stored events:
// Reconstruct messages from persisted events
events := loadEventsFromStore(agentID, runID)
messages := transcript.BuildMessagesFromEvents(events)
// Messages are now in canonical provider order
// Ready to pass to model.Client.Complete() or Stream()
- Validation: Before sending to providers, the runtime can validate message structure:
// Validate Bedrock constraints when thinking is enabled
if err := transcript.ValidateBedrock(messages, thinkingEnabled); err != nil {
// Handle constraint violation
}
Ledger API
For advanced use cases, you can interact with the ledger directly. The ledger provides these key methods:
| Method | Description |
|---|---|
NewLedger() | Creates a new empty ledger |
AppendThinking(part) | Appends a thinking part to the current assistant message |
AppendText(text) | Appends visible text to the current assistant message |
DeclareToolUse(id, name, args) | Declares a tool invocation in the current assistant message |
FlushAssistant() | Finalizes the current assistant message and prepares for user input |
AppendUserToolResults(results) | Appends tool results as a user message |
BuildMessages() | Returns the complete transcript as []*model.Message |
Example usage:
import "goa.design/goa-ai/runtime/agent/transcript"
// Create a new ledger
l := transcript.NewLedger()
// Record assistant turn
l.AppendThinking(transcript.ThinkingPart{
Text: "Let me search for that...",
Signature: "provider-sig",
Index: 0,
Final: true,
})
l.AppendText("I'll search the database.")
l.DeclareToolUse("tu-1", "search_db", map[string]any{"query": "status"})
l.FlushAssistant()
// Record user tool results
l.AppendUserToolResults([]transcript.ToolResultSpec{{
ToolUseID: "tu-1",
Content: map[string]any{"results": []string{"item1", "item2"}},
IsError: false,
}})
// Build provider-ready messages
messages := l.BuildMessages()
Note: Most users don’t need to interact with the ledger directly. The runtime automatically maintains the ledger through event capture and reconstruction. Use the ledger API only for advanced scenarios like custom planners or debugging tools.
Why This Matters
- Deterministic Replay: Stored events can rebuild the exact transcript for debugging, auditing, or re-running failed turns
- Provider Agnostic Storage: The ledger stores JSON-friendly parts without provider SDK dependencies
- Simplified Planners: Planners receive correctly ordered messages without managing provider constraints
- Validation: Catch ordering violations before they reach the provider and cause cryptic errors
Sessions, Runs, and Transcripts
Goa-AI separates conversation state into three layers:
Session (
SessionID) – a conversation or workflow over time:- e.g., a chat session, a remediation ticket, a research task
- Multiple runs can belong to the same session
Run (
RunID) – one execution of an agent:- Each call to an agent client (
Run/Start) creates a run - Runs have status, phases, and labels
- Each call to an agent client (
Transcript – the full history of messages and tool interactions for a run:
- Represented as
[]*model.Message - Persisted via
memory.Storeas ordered memory events
- Represented as
SessionID & TurnID in Practice
When calling an agent:
client := chat.NewClient(rt)
out, err := client.Run(ctx, "chat-session-123", messages,
runtime.WithTurnID("turn-1"), // optional but recommended for chat
)
SessionID: Groups all runs for a conversation; often used as a search key in run stores and dashboardsTurnID: Groups events for a single user → assistant interaction; optional but helpful for UIs and logs
Memory Store vs Run Store
Goa-AI’s feature modules provide complementary stores:
Memory Store (memory.Store)
Persists per-run event history:
- User/assistant messages
- Tool calls and results
- Planner notes and thinking
type Store interface {
LoadRun(ctx context.Context, agentID, runID string) (memory.Snapshot, error)
AppendEvents(ctx context.Context, agentID, runID string, events ...memory.Event) error
}
Key types:
memory.Snapshot– immutable view of a run’s stored history (AgentID,RunID,Events []memory.Event)memory.Event– single persisted entry withType(user_message,assistant_message,tool_call,tool_result,planner_note,thinking),Timestamp,Data, andLabels
Run Store (run.Store)
Persists coarse-grained run metadata:
RunID,AgentID,SessionID,TurnID- Status, timestamps, labels
type Store interface {
Upsert(ctx context.Context, record run.Record) error
Load(ctx context.Context, runID string) (run.Record, error)
}
run.Record captures:
AgentID,RunID,SessionID,TurnIDStatus(pending,running,completed,failed,canceled,paused)StartedAt,UpdatedAtLabels(tenant, priority, etc.)
Wiring Stores
With the MongoDB-backed implementations:
import (
memorymongo "goa.design/goa-ai/features/memory/mongo"
runmongo "goa.design/goa-ai/features/run/mongo"
"goa.design/goa-ai/runtime/agent/runtime"
)
mongoClient := newMongoClient()
memStore, err := memorymongo.NewStore(memorymongo.Options{Client: mongoClient})
if err != nil {
log.Fatal(err)
}
runStore, err := runmongo.NewStore(runmongo.Options{Client: mongoClient})
if err != nil {
log.Fatal(err)
}
rt := runtime.New(
runtime.WithMemoryStore(memStore),
runtime.WithRunStore(runStore),
)
Once configured:
- Default subscribers persist memory and run metadata automatically
- You can rebuild transcripts from
memory.Storeat any time to re-call models, power UIs, or run offline analysis
Custom Stores
Implement the memory.Store and run.Store interfaces for custom backends:
// Memory store
type Store interface {
LoadRun(ctx context.Context, agentID, runID string) (memory.Snapshot, error)
AppendEvents(ctx context.Context, agentID, runID string, events ...memory.Event) error
}
// Run store
type Store interface {
Upsert(ctx context.Context, record run.Record) error
Load(ctx context.Context, runID string) (run.Record, error)
}
Common Patterns
Chat Sessions
- Use one
SessionIDper chat session - Start a new run per user turn or per “task”
- Persist transcripts per run; use session metadata to stitch the conversation
Long-Running Workflows
- Use a single run per logical workflow (potentially with pause/resume)
- Use
SessionIDto group related workflows (e.g., per ticket or incident) - Rely on
run.PhaseandRunCompletedevents for status tracking
Search and Dashboards
- Query
run.StorebySessionID, labels, status - Load transcripts from
memory.Storeon demand for selected runs
Best Practices
Always correlate tool results: Make sure tool implementations and planners preserve tool_use IDs and map tool results back to the correct
ToolUsePartviaToolResultPart.ToolUseIDUse strong, descriptive schemas: Rich
Args/Returntypes, descriptions, and examples in your Goa design produce clearer tool payloads/results in the transcriptLet the runtime own state: Avoid maintaining parallel “tool history” arrays or “previous messages” slices in your planner. Read from
PlanInput.Messages/PlanResumeInput.Messagesand rely on the runtime to append new partsPersist transcripts once, reuse everywhere: Whatever store you choose, treat the transcript as reusable infrastructure—same transcript backing model calls, chat UI, debug UI, and offline analysis
Index frequently queried fields: Session ID, run ID, status for efficient queries
Archive old transcripts: Reduce storage costs by archiving completed runs
Next Steps
- Production - Deploy with Temporal, streaming UI, and model integration
- Runtime - Understand the plan/execute loop
- Agent Composition - Build complex agent graphs