Skip to content

Latest commit

 

History

History
185 lines (155 loc) · 9.78 KB

session.md

File metadata and controls

185 lines (155 loc) · 9.78 KB

Local agent sessions

agentgo/session records one logical agent conversation. A session may span sequential loop runs; independently running loops use separate sessions. The package owns JSONL recording and reading, not application scheduling, quality judgments, retention, or a global timeline.

A typical analysis starts with an application-owned timeline, locates a loop, then reads its session to inspect requests, responses, tool use, context changes, and completion. No workflow recovery is required for this path.

Identity and ownership

  • A Session has a random, persistent session_id.
  • BeginRun creates a fresh run_id for one loop invocation, including a later invocation after reopening the file. Only one run may be open per Session.
  • Execution.ID remains the kernel's run-local logical call ID; Attempt distinguishes retries. run.Ref(execution) combines all four coordinates.
  • (session_id, seq) identifies one JSONL record. Sequence is append order, not causality; event timestamps come from the source, not file write time.

The application chooses file paths and keeps the session-ID-to-path association. An external timeline stores call references plus its own stage hierarchy, times and status. Prompt and tool-output bodies stay in the Session. Neither package must depend on the other's storage format.

One Session handle owns each file. Writes through that handle are serialized; the host must prevent a second handle or process from opening the same file for writing. The package does not implement distributed ownership or retention.

Recording

stateCodec, err := agentgo.NewCodec(agentcontext.CodecOptions()...)
if err != nil { return err }
s, err := session.Create(path, stateCodec)
if err != nil { return err }
defer func() { err = errors.Join(err, s.Close()) }() // named return err
run, err := s.BeginRun()
if err != nil { return err }

var recordErr error
for event := range agentgo.AgentLoop(ctx, input, agentContext, config) {
    if recordErr == nil {
        recordErr = run.Record(event)
    }
    if event.Execution != nil {
        ref := run.Ref(*event.Execution)
        // Supply ref and event to the application's timeline adapter.
        _ = ref
    }
    // Continue draining even if recording fails. The host may instead cancel
    // the loop, then drain it. A recording failure must remain visible.
}
return errors.Join(recordErr, run.Close())

For a stateful Agent, feed the same run.Record(event) from Agent.Subscribe. Unsubscribe and close the recording scope after WaitForIdle, before recording a subsequent run. Capture and report recording errors in the callback; they do not alter the loop's execution result automatically.

The file records native event types with an EventData payload:

Fact Recorded content
model_request Messages, tool declarations and resolved per-call Config after middleware, including summary and standalone calls
model_exec_end Successful response in Message, or failed response evidence in PartialMessage alongside the error, including reported usage and provider state
message_end Committed, codec-encoded AgentMessage, preserving application types
tool_exec_start Requested tool and arguments
tool_invoke_start Actual arguments after middleware and authorization
tool_exec_end Complete result, rich content, details, error flag and disposition
Context, retry, turn and agent lifecycle Native facts and summary without copying the full AgentState

model_request describes the AgentGo executor boundary, not a provider's HTTP wire body or effective provider defaults. It excludes CallOption functions and credentials. Middleware short-circuits have a result without an executor request. Message containers and mutable Config fields are detached for publication; opaque message metadata values and tool declarations' schemas remain read-only. APIKey is also removed from the in-memory request event. JSON-valued Config fields such as ToolChoice and ResponseFormat.JSONSchema.Schema are frozen as json.RawMessage; unencodable values carry a snapshot_error diagnostic without retaining the input. message_end and model_exec_end have distinct meaning: a model result is not proof that the corresponding conversation message committed.

Token deltas, partial message snapshots, tool progress/preview updates, and the redundant model_response notification are not written. A model_response with a length recovery decision records that decision without duplicating the message content. Failed stream partial content is recorded only once at the failed model boundary. A run does not implicitly store a full snapshot at any lifecycle event.

The default codec knows built-in AgentGo messages. Supply a JSON-compatible codec with custom message/artifact registrations, and context.CodecOptions() when context-owned representations are used. Unknown typed messages cause an explicit recording error rather than silently lowering away their original content.

Standalone model calls

A recording run can also scope standalone calls. Use an explicit executor and event observer for model work; the host supplies IDs unique within that run.

var recordErr error
executor := agentgo.NewModelExecutor(agentgo.ModelExecutorConfig{
    Middlewares: middlewares,
    OnEvent: func(event agentgo.Event) {
        // Events are serialized within a call. Synchronize across concurrent calls.
        recordErr = errors.Join(recordErr, run.Record(event))
    },
})
result, callErr := executor.Call(ctx, model, agentgo.ModelExecution{
    Execution: agentgo.Execution{
        ID: "classify", Kind: agentgo.ExecutionKindModel, Attempt: 1,
    },
    Request: agentgo.LLMRequest{
        Messages: messages,
        Config: &agentgo.CallConfig{ThinkingLevel: agentgo.ThinkingMedium},
    },
})
// result.Message may contain diagnostic content even when callErr != nil.
// Handle callErr and recordErr independently, then close the recording run.

The executor always consumes GenerateStream, returning the final result while publishing transient model_stream events to OnEvent. It emits start, the final request after middleware, routed attempts when using a Router, and end. A middleware short-circuit omits the request and provider attempts. Call adds no retries and does not commit messages. Partial responses never create message_end or recovery checkpoints. Standalone completion is determined by model_exec_end and its error; no agent_end is manufactured.

Share an executor with an Agent via WithModelExecutor(executor), or set LoopConfig.ModelExecutor for a bare Loop. Context preparation receives it in TransformContext.ModelExecutor; summaries pass staged Artifacts explicitly. Tools that call models receive the executor through their own constructor or closure. Context carries cancellation, parent execution coordinates and internal run event routing, never middleware configuration. A nil executor uses the default policy. Shared middleware and observers must be safe for concurrent calls.

When a call belongs to a Loop, its model facts also appear on that Loop's event stream. Record either that stream or the executor observer to avoid duplicates. Only the Loop's conversation call produces message_start / message_update; auxiliary model progress is model_stream. Session recording omits both forms of transient progress and stores completion or failure at model_exec_end.

Reading and interrupted files

session.Read(reader, visit) streams Record values without retaining the whole file. Decode event Data into session.EventData, and decode its Message or PartialMessage with the same codec registry. Match call records using Session, Run, Execution ID and Attempt; use Execution.ParentID for nested work.

The first session_start record declares format version 1. Complete lines must have contiguous sequence numbers and the same session identity. Unsupported versions and complete corrupt lines fail. Only a final line without a newline is ignored, even if that line happens to contain syntactically valid JSON.

Open validates the file and removes only that unfinished tail before appending. An interrupted old run remains incomplete; a new run gets a fresh ID. run_close means the recording scope was closed, not that execution succeeded. Success or failure comes from agent_end for loops and model_exec_end for standalone calls. A missing terminal event remains a gap. Kernel event delivery is best-effort after cancellation, so continuously drain events.

Record writes directly to the file without an application buffer, but does not fsync every event. Run.Close, Session.Close, and explicit checkpoints sync. A write/encoding error is sticky and is returned on later writes and Close; the host decides whether to cancel execution or continue with a reported evidence gap. JSONL recording does not make tool side effects transactional.

Optional checkpoints

Observation works without checkpoints. If a caller needs to save a local loop's state, run.SaveCheckpoint(snapshot) appends a codec-encoded snapshot and syncs it together with prior records. s.LoadCheckpoint() returns only the latest explicit checkpoint; subsequent request/result records never advance it.

Choose checkpoints at valid committed boundaries, not on a timer. For a bare loop, a completed turn_end.State can be wrapped in AgentSnapshot{State: ...}. For a stateful Agent, capture agent.Snapshot() in a boundary listener to retain accepted input queues. A terminal listener also includes AfterRun artifact writes. Large checkpoints contain full history, so their cadence belongs to the caller.

Loading does not start a loop. Rebind model, tools and configuration before installing the snapshot via the existing Agent/Loop APIs. No Review/workflow state, automatic replay of tool effects, durable inbox or global resume protocol is part of Session.