turn

package
v0.0.0-...-2769864 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 6, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package turn registers the LLM-transcript Kind. Each Entry is one memory.Turn; the deterministic ID turn-<index>-<role> makes re-append idempotent and (index, role) order recoverable.

Index

Constants

View Source
const (
	KindName = "turn"
	IDPrefix = "turn-"
)
View Source
const (
	// VisibleRoleUser is a human message; VisibleRoleAgent is an agent reply.
	VisibleRoleUser  = "user"
	VisibleRoleAgent = "agent"
)

Chat-timeline roles a memory turn maps to, and the internal turn roles this mapping reasons about.

Variables

This section is empty.

Functions

func EntryID

func EntryID(index int, role string) string

EntryID is the deterministic ID for a turn. The zero-padded index keeps lexicographic ID order aligned with (index, role) order.

func EntryToTurn

func EntryToTurn(e memory.Entry) (memory.Turn, error)

EntryToTurn reconstructs a Turn from its Entry. Index/role come from the ID; content/usage from Entry.Content. Exported so cmd/oap/internal/memstream can decode HTTP responses without duplicating the conversion.

func HasOpeningTurn

func HasOpeningTurn(turns []memory.Turn) bool

HasOpeningTurn reports whether turns contains a session's durable opening turn: index 0, role user — the runner's copy of the AgentSession's spec.prompt.inline.

The runner appends it only once its pod is running, so this is false for the whole window between a session being created and its runner starting. A mirror replaying the transcript inside that window can use it to render the opening message from the session's own spec, rather than showing a conversation that appears to have begun with the agent speaking.

Deliberately the SAME test the runner's own replay uses for hadInitialPrompt, so "the opening turn is placed" means one thing on both sides of the transcript.

func ReadAll

func ReadAll(ctx context.Context, m memory.Memory, scope memory.Scope) ([]memory.Turn, error)

ReadAll is a package-level convenience wrapper around NewAppender(m, scope).ReadAll. Callers that do not need the full Appender can use this directly.

func RecordInbox

func RecordInbox(ctx context.Context, m memory.Memory, scope memory.Scope, idx int, text string, author identity.Subject) error

RecordInbox writes a Role:"inbox" turn at the given index, attributed to author (empty for non-human seeds). Mirrors channelsd's memory append; used by tests and channelsd pipelines that need to seed inbox turns without constructing a full Appender.

Types

type Appender

type Appender struct {
	// contains filtered or unexported fields
}

Appender reads and writes turns for one session scope through a memory.Memory. It is the runner's transcript client and satisfies the loop's MemoryAppender structurally (ReadAll/ReadAfter/Append).

func NewAppender

func NewAppender(mem memory.Memory, scope memory.Scope) *Appender

func (*Appender) Append

func (a *Appender) Append(ctx context.Context, t memory.Turn) error

Append writes t. Re-appending an identical (index, role) is a no-op; re-appending a different payload at the same (index, role) returns memory.ErrIndexConflict.

Conflict detection is a best-effort check-then-put: the Query/Put pair is not atomic, so a TOCTOU window exists, acceptable only because a single writer per session scope (the runner loop) means no two turn-writers race. A failed conflict-check Query is fatal — returned rather than falling through to a blind Put, which would drop both the error and the check.

func (*Appender) ReadAfter

func (a *Appender) ReadAfter(ctx context.Context, after int) ([]memory.Turn, error)

ReadAfter returns turns with Index > after.

func (*Appender) ReadAll

func (a *Appender) ReadAll(ctx context.Context) ([]memory.Turn, error)

ReadAll returns every turn in the scope, ascending (index, role). An entry that will not decode is skipped and logged, not returned as an error — see below for why that is not a silent drop and why the alternative is fatal.

type Kind

type Kind struct{}

func (Kind) ContentSchema

func (Kind) ContentSchema() reflect.Type

func (Kind) IDPrefix

func (Kind) IDPrefix() string

func (Kind) IndexedFields

func (Kind) IndexedFields() []string

func (Kind) Name

func (Kind) Name() string

func (Kind) NewScopeHooks

func (Kind) NewScopeHooks(_ memory.Scope) memory.ScopeHooks

func (Kind) Retention

func (Kind) Retention() memory.Retention

func (Kind) WriteAuthority

func (Kind) WriteAuthority() memory.WriteAuthority

WriteAuthority: the transcript is the agent session authoring its own conversation.

type PlanTimelineItem

type PlanTimelineItem struct {
	PlanName     string
	SnapshotJSON []byte
	Deleted      bool
	CreatedAt    time.Time
}

PlanTimelineItem is a plan card reconstructed from the "plans" system_notes: one per distinct plan name, positioned at first appearance, carrying the LATEST snapshot (opaque JSON — the memory layer does not import the plan types; a higher layer maps it to the channel card). Deleted is true when the name's latest op was "delete".

type TimelineItem

type TimelineItem struct {
	Kind    string // "message" | "plan"
	Message *VisibleMessage
	Plan    *PlanTimelineItem
}

TimelineItem is one ordered entry of the resumed conversation: a user/agent message or a plan-card snapshot. It is the superset VisibleMessages filters.

func VisibleTimeline

func VisibleTimeline(turns []memory.Turn) []TimelineItem

VisibleTimeline extracts the ordered user-facing timeline — messages AND plan cards — from a session's raw memory turns; VisibleMessages is this, filtered. "plans" system_notes are grouped by name: one card at its first-appearance index, updated in place by later notes for the same name.

The subtlety it encapsulates is the inbox lifecycle. A mid-session human message is first written as a roleInbox turn (channelsd/chat); the runner's drainInbox promotes it to a roleUser turn at a new index and writes a roleInboxDone marker at the inbox turn's index, but PRESERVES the raw roleInbox turn for audit (memory is append-only). A consumed inbox turn (one with a matching roleInboxDone) therefore duplicates its promoted roleUser turn and is dropped — rendering both showed every drained message twice — while an unconsumed one (not yet drained, the runner is parked) is kept, so a just-sent message still renders before it is promoted.

Other rules: roleUser/roleInbox collapse to VisibleRoleUser and roleAssistant to VisibleRoleAgent; roleInboxDone and any other role contribute nothing; an agent reply's text is pulled from its respond_to_user tool_use block (see visibleText); a tool_result the live surfaces already showed the user renders as an agent message (see renderedLiveText); turns with no user-facing text (a tool-only assistant turn) are dropped. Input need not be sorted — turns are ordered by (index, role) here.

type VisibleMessage

type VisibleMessage struct {
	Role      string    // VisibleRoleUser | VisibleRoleAgent
	Text      string    // user-facing text (respond_to_user text extracted)
	CreatedAt time.Time // when the underlying turn was written
	Via       string    // optional surface origin (e.g., "urn:ap:view:artifact:...")
}

VisibleMessage is one user-facing message extracted from a session's memory log — the conversation as a human should see it, with the runner's internal bookkeeping (inbox duplication, tool-only turns, markers) resolved away.

func VisibleMessages

func VisibleMessages(turns []memory.Turn) []VisibleMessage

VisibleMessages extracts the ordered user-facing MESSAGES (no plan cards): VisibleTimeline filtered to messages, so the extraction rules live in one place. See VisibleTimeline.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL