Documentation
¶
Overview ¶
Package chatstate owns the durable execution-state transitions for the chatd subsystem. It implements the chat execution state model.
The package exposes two top-level entry points:
- CreateChat creates a brand new chat with its initial history in a single transaction. It is standalone because no chat-scoped state machine instance can exist before the chat row is written.
- ChatMachine wraps an existing chat. Callers use it to apply one or more transitions atomically via ChatMachine.Update, or to read related rows while holding the chat row lock via ChatMachine.Lock.
Every successful ChatMachine.Update call locks the chat row, advances `snapshot_version` exactly once, applies transition methods in order, and (on commit) publishes a single typed `chat:update` pubsub message describing the post-transition snapshot. Optional `chat:ownership` hints are published only when the post-transition state is runnable and ownership is missing or stale. Stream side effects are handled by `chat:update` consumers, and ownership hints wake chat workers.
Transition methods are explicit, typed wrappers around the durable mutations needed to move between states. Each transition reads the current chat row and queue cardinality, classifies the resulting execution state, validates it against the transition model, and rejects with an *TransitionError wrapping ErrTransitionNotAllowed when the transition is not legal from that state. The package owns transition validation, durable chat row and queue mutations, and post-commit pubsub publication.
Index ¶
- Constants
- Variables
- func LinkFiles(ctx context.Context, store database.Store, chatID uuid.UUID, ...) error
- func ReplaceInlineMCPServers(ctx context.Context, store database.Store, chatID uuid.UUID, ...) error
- func SetFamilyArchived(ctx context.Context, store database.Store, publisher Publisher, ...) ([]database.Chat, error)
- type AbandonInput
- type AbandonResult
- type AcquireInput
- type AcquireResult
- type BusyBehavior
- type CancelRequiresActionInput
- type CancelRequiresActionResult
- type ChatMachine
- type ClearContextInput
- type ClearContextResult
- type CommitStepInput
- type CommitStepResult
- type CompleteRequiresActionInput
- type CompleteRequiresActionResult
- type CreateChatInput
- type CreateChatResult
- type DeleteQueuedMessageInput
- type DeleteQueuedMessageResult
- type EditMessageInput
- type EditMessageResult
- type EnterRequiresActionInput
- type EnterRequiresActionResult
- type ExecutionState
- type FinishErrorInput
- type FinishErrorResult
- type FinishInterruptionInput
- type FinishInterruptionResult
- type FinishTurnInput
- type FinishTurnResult
- type InterruptInput
- type InterruptResult
- type Message
- type MessageQueueFullError
- type PromoteQueuedMessageInput
- type PromoteQueuedMessageResult
- type PublishBuffer
- type Publisher
- type ReconcileInvalidStateInput
- type ReconcileInvalidStateResult
- type RecordGenerationAttemptInput
- type RecordGenerationAttemptResult
- type RecordRetryStateInput
- type RecordRetryStateResult
- type RequestCompactionInput
- type RequestCompactionResult
- type SendMessageInput
- type SendMessageResult
- type SetArchivedInput
- type SetArchivedResult
- type SetFamilyArchivedInput
- type ToolResultInput
- type ToolResultValidationError
- type Transition
- type TransitionError
- type Tx
- func (tx *Tx) Abandon(_ AbandonInput) (AbandonResult, error)
- func (tx *Tx) Acquire(input AcquireInput) (AcquireResult, error)
- func (tx *Tx) CancelRequiresAction(input CancelRequiresActionInput) (CancelRequiresActionResult, error)
- func (tx *Tx) ChatID() uuid.UUID
- func (tx *Tx) ClearContext(input ClearContextInput) (ClearContextResult, error)
- func (tx *Tx) CommitStep(input CommitStepInput) (CommitStepResult, error)
- func (tx *Tx) CompleteRequiresAction(input CompleteRequiresActionInput) (CompleteRequiresActionResult, error)
- func (tx *Tx) Ctx() context.Context
- func (tx *Tx) DeleteQueuedMessage(input DeleteQueuedMessageInput) (DeleteQueuedMessageResult, error)
- func (tx *Tx) EditMessage(input EditMessageInput) (EditMessageResult, error)
- func (tx *Tx) EnterRequiresAction(_ EnterRequiresActionInput) (EnterRequiresActionResult, error)
- func (tx *Tx) FinishError(input FinishErrorInput) (FinishErrorResult, error)
- func (tx *Tx) FinishInterruption(input FinishInterruptionInput) (FinishInterruptionResult, error)
- func (tx *Tx) FinishTurn(_ FinishTurnInput) (FinishTurnResult, error)
- func (tx *Tx) Interrupt(input InterruptInput) (InterruptResult, error)
- func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueuedMessageResult, error)
- func (tx *Tx) ReconcileInvalidState(input ReconcileInvalidStateInput) (ReconcileInvalidStateResult, error)
- func (tx *Tx) RecordGenerationAttempt(_ RecordGenerationAttemptInput) (RecordGenerationAttemptResult, error)
- func (tx *Tx) RecordRetryState(input RecordRetryStateInput) (RecordRetryStateResult, error)
- func (tx *Tx) RequestCompaction(_ RequestCompactionInput) (RequestCompactionResult, error)
- func (tx *Tx) SendMessage(input SendMessageInput) (SendMessageResult, error)
- func (tx *Tx) SetArchived(input SetArchivedInput) (SetArchivedResult, error)
- func (tx *Tx) Store() database.Store
Constants ¶
const HeartbeatStaleSeconds = 30
HeartbeatStaleSeconds is the threshold chatstate uses when deciding whether to publish a `chat:ownership` hint for a runnable chat. A heartbeat older than this many seconds (by database time) counts as stale and triggers a hint so an idle worker can attempt a takeover.
Variables ¶
var ( // ErrTransitionNotAllowed is returned when a transition is applied // to a chat whose current execution state does not permit it. The // concrete error returned by transition methods is a // *TransitionError that wraps this sentinel. ErrTransitionNotAllowed = xerrors.New("chat state transition not allowed") // ErrInvalidState is returned when the chat row, queue, and // archive flag together produce a combination outside the chat // execution state model. ErrInvalidState = xerrors.New("chat is in an invalid execution state") // ErrQueuedMessageNotFound is returned by queue-targeting // transitions (delete, promote) when the supplied queued message // ID does not match a row on the chat. ErrQueuedMessageNotFound = xerrors.New("queued message not found") // ErrMessageNotFound is returned by [Tx.EditMessage] when the // target chat_messages row is missing or belongs to another chat. ErrMessageNotFound = xerrors.New("chat message not found") // ErrChatNotFound is returned when a non-create transition is // applied to a chat row that does not exist (or has been deleted // since the transition started). ErrChatNotFound = xerrors.New("chat not found") // ErrChatNotRoot is returned by family-archive helpers when the // supplied chat is not a root chat (its parent_chat_id is set). ErrChatNotRoot = xerrors.New("chat is not a root chat") // ErrEditedMessageNotUser is returned by [Tx.EditMessage] when the // targeted chat_messages row exists but its role is not user. ErrEditedMessageNotUser = xerrors.New("only user messages can be edited") // ErrMessageQueueFull is returned by queue-appending transitions // when the per-chat queue cap has been reached. The concrete // error returned by transitions is a *MessageQueueFullError that // wraps this sentinel. ErrMessageQueueFull = xerrors.New("chat message queue is full") // ErrNoDefaultChatModelConfig is returned when queued-message // promotion cannot find an enabled default model in the chat organization. ErrNoDefaultChatModelConfig = xerrors.New("no default chat model config is configured") // ErrChatFileCapExceeded reports a [LinkFiles] cap rejection. ErrChatFileCapExceeded = xerrors.New("chat attachment cap exceeded") // missing or already attached to another chat. ErrChatFileUnavailable = xerrors.New("chat attachment unavailable") // ErrToolResultDuplicate is returned by [Tx.CompleteRequiresAction] // when the same tool_call_id appears more than once in the // submitted results. ErrToolResultDuplicate = xerrors.New("duplicate tool result") // ErrToolResultUnexpected is returned by // [Tx.CompleteRequiresAction] when a submitted tool_call_id does // not correspond to a pending dynamic tool call. ErrToolResultUnexpected = xerrors.New("unexpected tool result") // ErrToolResultMissing is returned by [Tx.CompleteRequiresAction] // when a pending dynamic tool call has no submitted result. ErrToolResultMissing = xerrors.New("missing tool result") // ErrToolResultInvalidJSON is returned by // [Tx.CompleteRequiresAction] when a submitted tool result output // is not valid JSON. ErrToolResultInvalidJSON = xerrors.New("tool result output is not valid JSON") )
Sentinel errors returned by chatstate transitions and helpers. Callers should use errors.Is to test for these.
Functions ¶
func LinkFiles ¶ added in v2.37.0
func LinkFiles(ctx context.Context, store database.Store, chatID uuid.UUID, fileIDs []uuid.UUID, maxLinks int) error
LinkFiles links files, returning ErrChatFileCapExceeded for cap rejections and ErrChatFileUnavailable for files that are missing or attached to another chat. Use the caller's transaction so failures roll back related writes; existing links use no additional slots. maxLinks is the maximum number of files linked to the chat and must be positive.
func ReplaceInlineMCPServers ¶
func ReplaceInlineMCPServers(ctx context.Context, store database.Store, chatID uuid.UUID, servers []codersdk.InlineMCPServerRequest) error
ReplaceInlineMCPServers makes the chat's inline MCP servers equal to servers. Rows with a matching slug keep their id. An empty slice removes every server. store must be the transaction handle inside InTx.
func SetFamilyArchived ¶
func SetFamilyArchived( ctx context.Context, store database.Store, publisher Publisher, input SetFamilyArchivedInput, ) ([]database.Chat, error)
SetFamilyArchived runs Update for every chat in the root chat's family inside one transaction, applying SetArchived when the chat's archived flag differs from the requested value. It owns its transaction lifecycle and its PublishBuffer lifecycle: pubsub publications are buffered while the transaction is open and flushed only after a successful commit; the deferred Discard suppresses every buffered publication on failure.
On success SetFamilyArchived returns one database.Chat per family member in the order returned by GetChatFamilyIDsByRootID (root first, then children).
Family members that are already in the StateInvalid execution state cause SetFamilyArchived to return ErrInvalidState and roll back the cascade even when their archived flag already matches the desired value; invalid-state detection is never bypassed.
Family members that are valid and already match the desired archived value still run through Update, which increments their snapshot version and publishes a fresh snapshot without changing the archived flag. Advancing the snapshot version without a field change is safe, and it keeps publication behavior uniform while a partially archived family converges to the desired state.
Types ¶
type AbandonInput ¶
type AbandonInput struct{}
AbandonInput is intentionally empty. Ownership-fence checks belong outside the transition in caller code that reads the locked row before invoking Abandon.
type AcquireInput ¶
AcquireInput configures Tx.Acquire.
type BusyBehavior ¶
type BusyBehavior string
BusyBehavior controls how SendMessage behaves when the chat is currently busy (R*/I*/A*). From idle/error states the two behaviors are equivalent.
const ( BusyBehaviorQueue BusyBehavior = "queue" BusyBehaviorInterrupt BusyBehavior = "interrupt" )
type CancelRequiresActionInput ¶
type CancelRequiresActionInput struct {
Reason string
}
CancelRequiresActionInput configures Tx.CancelRequiresAction.
type CancelRequiresActionResult ¶
type CancelRequiresActionResult struct {
CancellationMessages []database.ChatMessage
}
CancelRequiresActionResult is returned by Tx.CancelRequiresAction.
type ChatMachine ¶
type ChatMachine struct {
// contains filtered or unexported fields
}
ChatMachine is a chat-scoped handle for state-machine operations on a single chat row. It captures the database store, the pubsub publisher, and the chat ID at construction time so callers do not have to thread them through Update, Lock, or any transition method.
ChatMachine values are cheap. Create one per chat for the lifetime of a request or worker turn; do not cache mutable chat state across calls.
func NewChatMachine ¶
NewChatMachine constructs a chat-scoped state machine handle. The store may be the root database handle or an existing transaction handle; publisher is the pubsub used for `chat:update` and `chat:ownership` emissions. Both are required and captured for the lifetime of the returned machine.
func (*ChatMachine) Lock ¶
Lock locks the chat row with FOR UPDATE and runs fn in a transaction without advancing snapshot_version. It uses the store captured by NewChatMachine. Use it when the caller needs a consistent chat snapshot plus related rows such as messages or queued messages but is NOT applying a transition.
Callers must not pass a store here; it belongs on the machine.
Lock publishes nothing. Callback errors roll back the transaction and propagate to the caller.
func (*ChatMachine) ReadLock ¶
ReadLock takes a shared lock on the chat row with FOR SHARE and runs fn in a transaction without advancing snapshot_version. It uses the store captured by NewChatMachine. Use it when the caller needs a consistent chat snapshot plus related rows such as messages or queued messages but is NOT applying a transition and does NOT need to block concurrent readers.
Unlike ChatMachine.Lock, the FOR SHARE lock permits other shared lockers to proceed concurrently while still blocking writers that take FOR UPDATE (such as ChatMachine.Update and ChatMachine.Lock) until the transaction commits.
Callers must not pass a store here; it belongs on the machine.
ReadLock publishes nothing. Callback errors roll back the transaction and propagate to the caller.
func (*ChatMachine) Update ¶
Update applies one or more transitions to the machine's chat.
Update opens a transaction on the captured store, atomically locks the chat row with FOR UPDATE and increments `snapshot_version` exactly once, then runs fn against a fresh *Tx and the active transaction store. It constructs a PublishBuffer, enqueues `chat:update` (and a `chat:ownership` hint when the post-transition state is worker-runnable and ownership is missing or stale) inside the transaction, and flushes the buffer only after the transaction function succeeds. If the transaction rolls back, the deferred Discard suppresses every buffered publication so subscribers never see uncommitted state.
If Update is called with a store that is already in a transaction, database.Store.InTx reuses the active transaction. In that case, callers that need outer-transaction publication semantics can pass a PublishBuffer as the machine publisher. The inner buffer flushes into the outer buffer, and the outer owner remains responsible for publishing only after the outer transaction commits.
If the chat row does not exist, Update returns ErrChatNotFound without mutating anything.
Callbacks that return an error roll back the transaction (rolling back the automatic snapshot bump) and publish nothing.
type ClearContextInput ¶
type ClearContextInput struct {
Messages []Message
}
ClearContextInput configures Tx.ClearContext. Messages carries the boundary rows built by chatd; chatstate requires a non-empty batch so the insert trigger grants the fresh history epoch.
type ClearContextResult ¶
type ClearContextResult struct {
Chat database.Chat
InsertedMessages []database.ChatMessage
}
ClearContextResult is returned by Tx.ClearContext.
type CommitStepInput ¶
type CommitStepInput struct {
Messages []Message
// ConsumeCompactionRequest clears the one-shot manual compaction
// marker atomically with the committed step. Compaction commits
// set this so a request is consumed exactly once and never
// replays on a later turn.
ConsumeCompactionRequest bool
}
CommitStepInput configures Tx.CommitStep.
type CommitStepResult ¶
type CommitStepResult struct {
InsertedMessages []database.ChatMessage
}
CommitStepResult is returned by Tx.CommitStep.
type CompleteRequiresActionInput ¶
type CompleteRequiresActionInput struct {
CreatedBy uuid.UUID
ModelConfigID uuid.UUID
Results []ToolResultInput
SuffixMessages []Message
}
CompleteRequiresActionInput configures Tx.CompleteRequiresAction.
type CompleteRequiresActionResult ¶
type CompleteRequiresActionResult struct {
InsertedMessages []database.ChatMessage
}
CompleteRequiresActionResult is returned by Tx.CompleteRequiresAction.
type CreateChatInput ¶
type CreateChatInput struct {
OrganizationID uuid.UUID
OwnerID uuid.UUID
WorkspaceID uuid.NullUUID
BuildID uuid.NullUUID
AgentID uuid.NullUUID
ParentChatID uuid.NullUUID
RootChatID uuid.NullUUID
LastModelConfigID uuid.UUID
Title string
Mode database.NullChatMode
PlanMode database.NullChatPlanMode
MCPServerIDs []uuid.UUID
InlineMCPServers []codersdk.InlineMCPServerRequest
Labels pqtype.NullRawMessage
DynamicTools pqtype.NullRawMessage
ClientType database.ChatClientType
InitialMessages []Message
// FileIDs are linked atomically with the initial messages.
FileIDs []uuid.UUID
// InitialStatus selects the chat's starting execution state:
// `running` (R0) when the initial history ends with a user turn
// the worker should process, or `waiting` (W) when the chat is
// created idle with no initial user message. Empty defaults to
// `running`.
InitialStatus database.ChatStatus
// MaxFileLinks is the maximum number of files linked to the chat. It
// must be positive when FileIDs is not empty.
MaxFileLinks int
}
CreateChatInput configures CreateChat.
type CreateChatResult ¶
type CreateChatResult struct {
Chat database.Chat
InitialMessages []database.ChatMessage
}
CreateChatResult is the value returned by CreateChat. It carries the new chat row and the inserted initial history.
func CreateChat ¶
func CreateChat( ctx context.Context, store database.Store, publisher Publisher, input CreateChatInput, ) (CreateChatResult, error)
CreateChat creates a brand new chat with initial history in a single transaction. It is package-level rather than a method on ChatMachine because no chat-scoped machine can exist before the chat row is written.
Validation:
- InitialMessages must be non-empty.
- InitialStatus must be `waiting`, `running`, or empty (`running`).
After commit CreateChat publishes a `chat:update` message describing the new chat snapshot. When the new chat is runnable (`running`), CreateChat also publishes an ownership hint so workers can race to acquire it. A `waiting` chat is idle, so no ownership hint is published until a later transition makes it runnable.
func CreateChatWithID ¶ added in v2.37.0
func CreateChatWithID( ctx context.Context, store database.Store, publisher Publisher, chatID uuid.UUID, input CreateChatInput, ) (CreateChatResult, error)
CreateChatWithID creates a chat using a caller-minted ID so admission-time work can reference the chat before it exists.
type DeleteQueuedMessageInput ¶
type DeleteQueuedMessageInput struct {
QueuedMessageID int64
}
DeleteQueuedMessageInput configures Tx.DeleteQueuedMessage.
type DeleteQueuedMessageResult ¶
type DeleteQueuedMessageResult struct {
DeletedQueuedMessage database.ChatQueuedMessage
}
DeleteQueuedMessageResult is returned by Tx.DeleteQueuedMessage.
type EditMessageInput ¶
type EditMessageInput struct {
MessageID int64
// SuffixMessages are inserted after the replacement message in the
// same transaction, so a later edit's suffix truncation cleans them
// up together with the rest of the discarded turn.
SuffixMessages []Message
CreatedBy uuid.UUID
Content pqtype.NullRawMessage
ModelConfigIDOverride uuid.NullUUID
ReasoningEffortOverride database.NullChatReasoningEffort
}
EditMessageInput configures Tx.EditMessage.
type EditMessageResult ¶
type EditMessageResult struct {
ReplacementMessage database.ChatMessage
DeletedMessageIDs []int64
DeletedQueuedMessageIDs []int64
CancellationMessages []database.ChatMessage
SuffixMessages []database.ChatMessage
}
EditMessageResult is returned by Tx.EditMessage.
type EnterRequiresActionInput ¶
type EnterRequiresActionInput struct{}
EnterRequiresActionInput is intentionally empty.
type EnterRequiresActionResult ¶
EnterRequiresActionResult is returned by Tx.EnterRequiresAction.
type ExecutionState ¶
type ExecutionState string
ExecutionState identifies a chat's current execution state. Values outside the chat execution state model are represented by StateInvalid.
const ( // StateN: chat does not exist. StateN ExecutionState = "N" // StateW: waiting, empty queue, not archived. StateW ExecutionState = "W" // StateE0: error, empty queue, not archived. StateE0 ExecutionState = "E0" // StateE1: error, non-empty queue, not archived. StateE1 ExecutionState = "E1" // StateR0: running, empty queue, not archived. StateR0 ExecutionState = "R0" // StateR1: running, non-empty queue, not archived. StateR1 ExecutionState = "R1" // StateI0: interrupting, empty queue, not archived. StateI0 ExecutionState = "I0" // StateI1: interrupting, non-empty queue, not archived. StateI1 ExecutionState = "I1" // StateA0: requires_action, empty queue, not archived. StateA0 ExecutionState = "A0" // StateA1: requires_action, non-empty queue, not archived. StateA1 ExecutionState = "A1" // StateXW: archived waiting, empty queue. StateXW ExecutionState = "XW" // StateXE0: archived error, empty queue. StateXE0 ExecutionState = "XE0" // StateXE1: archived error, non-empty queue. StateXE1 ExecutionState = "XE1" // StateInvalid groups every status/archive/queue combination that // is not one of the valid states above. The state machine refuses // non-reconciliation transitions on invalid states and exposes the // [Tx.ReconcileInvalidState] transition to recover. StateInvalid ExecutionState = "Invalid" )
func ClassifyExecutionState ¶
func ClassifyExecutionState(chat database.Chat, queueNonEmpty, exists bool) ExecutionState
ClassifyExecutionState turns the chat row, queue cardinality, and whether the chat row exists into an ExecutionState. The caller is responsible for loading the chat under the row lock and reading the queue count in the same transaction.
Callers that have no chat row (lookup returned sql.ErrNoRows) should pass exists=false; the chat, status, and archive arguments are then ignored.
The classifier is a single flat switch over the valid (status, archived, queue) tuples in the chat execution state model. Anything outside that set (archived busy states, waiting with a non-empty queue, future enum values) falls through to StateInvalid.
func (ExecutionState) IsRunnable ¶
func (s ExecutionState) IsRunnable() bool
IsRunnable returns true for the execution states that the chat worker is allowed to acquire and drive forward: R0, R1, I0, I1, A0, and A1. Requires-action states need worker ownership for timeout processing. Other states are idle (W, E*, XW, XE*), absent (N), or invalid.
func (ExecutionState) String ¶
func (s ExecutionState) String() string
String implements fmt.Stringer.
type FinishErrorInput ¶
type FinishErrorInput struct {
LastError pqtype.NullRawMessage
}
FinishErrorInput configures Tx.FinishError.
type FinishErrorResult ¶
type FinishErrorResult struct{}
FinishErrorResult is returned by Tx.FinishError.
type FinishInterruptionInput ¶
type FinishInterruptionInput struct {
PartialMessages []Message
}
FinishInterruptionInput configures Tx.FinishInterruption.
type FinishInterruptionResult ¶
type FinishInterruptionResult struct {
InsertedMessages []database.ChatMessage
PromotedMessage *database.ChatMessage
}
FinishInterruptionResult is returned by Tx.FinishInterruption.
type FinishTurnResult ¶
type FinishTurnResult struct {
Chat database.Chat
PromotedMessage *database.ChatMessage
}
FinishTurnResult is returned by Tx.FinishTurn.
type InterruptInput ¶
type InterruptInput struct {
Reason string
}
InterruptInput configures Tx.Interrupt.
type InterruptResult ¶
type InterruptResult struct {
CancellationMessages []database.ChatMessage
}
InterruptResult is returned by Tx.Interrupt.
type Message ¶
type Message struct {
Role database.ChatMessageRole
Content pqtype.NullRawMessage
Visibility database.ChatMessageVisibility
ModelConfigID uuid.NullUUID
ReasoningEffort database.NullChatReasoningEffort
CreatedBy uuid.NullUUID
ContentVersion int16
Compressed bool
InputTokens sql.NullInt64
OutputTokens sql.NullInt64
TotalTokens sql.NullInt64
ReasoningTokens sql.NullInt64
CacheCreationTokens sql.NullInt64
CacheReadTokens sql.NullInt64
ContextLimit sql.NullInt64
RuntimeMs sql.NullInt64
ProviderResponseID sql.NullString
// QueuedMessageID is the chat_queued_messages row this message was
// promoted from. Only messageFromQueuedRow sets it.
QueuedMessageID sql.NullInt64
}
Message is the durable message input shape used by chatstate transitions. It is intentionally lower level than the SDK message request types: callers must produce a fully materialized message (parsed parts, calculated cost, resolved model config) before passing it in.
The state machine never reshapes a Message except to attach the runtime `chat_id`.
type MessageQueueFullError ¶
type MessageQueueFullError struct {
Max int64
}
MessageQueueFullError carries the per-chat queue cap so HTTP endpoints can include the cap in their response detail. It wraps ErrMessageQueueFull so callers can match it with errors.Is.
func (*MessageQueueFullError) Error ¶
func (e *MessageQueueFullError) Error() string
Error implements the error interface.
func (*MessageQueueFullError) Unwrap ¶
func (*MessageQueueFullError) Unwrap() error
Unwrap returns ErrMessageQueueFull so callers can match the generic sentinel.
type PromoteQueuedMessageInput ¶
type PromoteQueuedMessageInput struct {
QueuedMessageID int64
}
PromoteQueuedMessageInput configures Tx.PromoteQueuedMessage.
type PromoteQueuedMessageResult ¶
type PromoteQueuedMessageResult struct {
QueuedMessage database.ChatQueuedMessage
InsertedMessage *database.ChatMessage
CancellationMessages []database.ChatMessage
}
PromoteQueuedMessageResult is returned by Tx.PromoteQueuedMessage.
type PublishBuffer ¶
type PublishBuffer struct {
// contains filtered or unexported fields
}
PublishBuffer is a Publisher that records each Publish call in order without forwarding it until PublishBuffer.Flush is called. It is an internal primitive used by chatstate entry points to hold pubsub messages until the surrounding transaction commits, and by tests that need to observe buffered output. Normal callers do not construct a PublishBuffer themselves and do not invoke Flush or Discard; chatstate's entry points own that lifecycle.
func NewPublishBuffer ¶
func NewPublishBuffer(inner Publisher) *PublishBuffer
NewPublishBuffer constructs a PublishBuffer that, when flushed, will forward messages in order to inner.
func (*PublishBuffer) Discard ¶
func (b *PublishBuffer) Discard()
Discard clears the buffered messages without forwarding them. It is safe to call multiple times and is harmless after PublishBuffer.Flush: once Flush has marked the buffer flushed and forwarded its pending messages, a subsequent Discard simply clears the (now empty) pending slice and sets the buffer to drop any future Publish calls. This makes `defer buf.Discard()` a safe pattern after a successful flush, including the one chatstate entry points use to own the buffer lifecycle.
func (*PublishBuffer) Flush ¶
func (b *PublishBuffer) Flush() error
Flush forwards every pending message to the inner publisher in the order it was buffered, then marks the buffer flushed. Joined publish errors are returned with channel names annotated after every pending message has been attempted.
func (*PublishBuffer) Publish ¶
func (b *PublishBuffer) Publish(channel string, payload []byte) error
Publish records a message. It never forwards to the inner publisher until PublishBuffer.Flush is called. Returns an error if Flush has already happened to make accidental reuse obvious.
type Publisher ¶
Publisher is the minimal interface chatstate needs to publish pubsub messages. It is intentionally compatible with database/pubsub.Pubsub: real callers pass the live pubsub directly and tests pass a recording fake.
type ReconcileInvalidStateInput ¶
type ReconcileInvalidStateInput struct {
LastError pqtype.NullRawMessage
CancellationReason string
}
ReconcileInvalidStateInput configures Tx.ReconcileInvalidState.
type ReconcileInvalidStateResult ¶
type ReconcileInvalidStateResult struct {
CancellationMessages []database.ChatMessage
}
ReconcileInvalidStateResult is returned by Tx.ReconcileInvalidState.
type RecordGenerationAttemptInput ¶
type RecordGenerationAttemptInput struct{}
RecordGenerationAttemptInput is intentionally empty.
type RecordGenerationAttemptResult ¶
type RecordGenerationAttemptResult struct {
GenerationAttempt int64
}
RecordGenerationAttemptResult is returned by Tx.RecordGenerationAttempt.
type RecordRetryStateInput ¶
type RecordRetryStateInput struct {
RetryState pqtype.NullRawMessage
}
RecordRetryStateInput configures Tx.RecordRetryState.
type RecordRetryStateResult ¶
RecordRetryStateResult is returned by Tx.RecordRetryState.
type RequestCompactionInput ¶ added in v2.36.0
type RequestCompactionInput struct{}
RequestCompactionInput is intentionally empty. The compaction turn derives AI Gateway attribution from the owner's synthetic API key, so the request carries no caller input.
type RequestCompactionResult ¶ added in v2.36.0
RequestCompactionResult is returned by Tx.RequestCompaction.
type SendMessageInput ¶
type SendMessageInput struct {
Message Message
BusyBehavior BusyBehavior
// MaxQueueSize is the maximum number of messages that can be queued
// in the chat. It must be positive when the message is queued.
MaxQueueSize int
}
SendMessageInput configures Tx.SendMessage.
type SendMessageResult ¶
type SendMessageResult struct {
InsertedMessages []database.ChatMessage
QueuedMessage *database.ChatQueuedMessage
}
SendMessageResult is returned by Tx.SendMessage.
type SetArchivedInput ¶
type SetArchivedInput struct {
Archived bool
}
SetArchivedInput configures Tx.SetArchived.
type SetArchivedResult ¶
type SetArchivedResult struct{}
SetArchivedResult is returned by Tx.SetArchived.
type SetFamilyArchivedInput ¶
type SetFamilyArchivedInput struct {
// RootID identifies the family root. SetFamilyArchived rejects
// calls for child chats with [ErrChatNotRoot] and unknown chats
// with [ErrChatNotFound].
RootID uuid.UUID
// Archived is the desired post-call archived value for every
// family member.
Archived bool
}
SetFamilyArchivedInput configures SetFamilyArchived. The struct shape avoids a boolean flag parameter at the API surface; callers build it explicitly with named fields for clarity.
type ToolResultInput ¶
type ToolResultInput struct {
ToolCallID string
Output json.RawMessage
IsError bool
}
ToolResultInput is one submitted dynamic-tool result.
type ToolResultValidationError ¶
ToolResultValidationError carries a structured tool-result validation failure. It always wraps a specific sentinel (ErrToolResultDuplicate, ErrToolResultMissing, ErrToolResultUnexpected, ErrToolResultInvalidJSON) so callers can match either the generic sentinel or the specific cause.
func ValidateToolResults ¶ added in v2.37.0
func ValidateToolResults(results []ToolResultInput, pending map[string]string) *ToolResultValidationError
ValidateToolResults returns the first validation error for results submitted against pending dynamic tool calls.
func (*ToolResultValidationError) Error ¶
func (e *ToolResultValidationError) Error() string
Error implements the error interface.
func (*ToolResultValidationError) Unwrap ¶
func (e *ToolResultValidationError) Unwrap() error
Unwrap returns the specific cause so callers can match it.
type Transition ¶
type Transition string
Transition is the enumeration of transitions implemented by the state machine. Values intentionally match the names of the public methods on Tx (and CreateChat). The transition matrix below declares the legal (from -> to) execution-state mappings used by each transition method for validation.
const ( TransitionCreateChat Transition = "CreateChat" TransitionSetArchived Transition = "SetArchived" TransitionSendMessage Transition = "SendMessage" TransitionEditMessage Transition = "EditMessage" TransitionRequestCompaction Transition = "RequestCompaction" TransitionClearContext Transition = "ClearContext" TransitionDeleteQueuedMessage Transition = "DeleteQueuedMessage" TransitionPromoteQueuedMessage Transition = "PromoteQueuedMessage" TransitionInterrupt Transition = "Interrupt" TransitionCompleteRequiresAction Transition = "CompleteRequiresAction" TransitionAbandon Transition = "Abandon" TransitionRecordGenerationAttempt Transition = "RecordGenerationAttempt" TransitionRecordRetryState Transition = "RecordRetryState" TransitionCommitStep Transition = "CommitStep" TransitionEnterRequiresAction Transition = "EnterRequiresAction" TransitionFinishInterruption Transition = "FinishInterruption" TransitionFinishTurn Transition = "FinishTurn" TransitionFinishError Transition = "FinishError" TransitionCancelRequiresAction Transition = "CancelRequiresAction" TransitionReconcileInvalidState Transition = "ReconcileInvalidState" )
type TransitionError ¶
type TransitionError struct {
Transition Transition
From ExecutionState
Reason string
Cause error
}
TransitionError carries the structured detail for a rejected transition. It always wraps ErrTransitionNotAllowed so callers can match with errors.Is without losing context. When a specific chatstate sentinel is the proximate cause, Cause is set and errors.Is will match that sentinel too.
func (*TransitionError) Error ¶
func (e *TransitionError) Error() string
Error implements the error interface.
func (*TransitionError) Unwrap ¶
func (e *TransitionError) Unwrap() error
Unwrap returns the error chain attached to this error. The chain always includes ErrTransitionNotAllowed, and may include a more specific cause through errors.Join, so callers can use errors.Is without custom matching logic on TransitionError.
type Tx ¶
type Tx struct {
// contains filtered or unexported fields
}
Tx is the per-transaction handle passed to ChatMachine.Update callbacks. It carries the active context, the transactional store, and the chat ID. Tx does not cache mutable chat state across calls: every transition method reads the chat row and queue cardinality from the database on entry, so a bundle of transitions inside one Update callback always validates against the latest committed state.
func (*Tx) Abandon ¶
func (tx *Tx) Abandon(_ AbandonInput) (AbandonResult, error)
Abandon clears worker_id and runner_id from the locked chat row. It rejects calls when the chat is not currently owned (worker_id IS NULL). Callers that need to verify their own identity before abandoning should read the locked row through the transactional store and compare values before invoking Abandon.
func (*Tx) Acquire ¶
func (tx *Tx) Acquire(input AcquireInput) (AcquireResult, error)
Acquire claims the chat for a worker/runner pair. Execution state is preserved.
Acquire never inspects the chat's current ownership: it simply overwrites worker_id/runner_id with the supplied identifiers and upserts a fresh heartbeat. Detecting and recovering from stale leases is a worker-side fence concern outside the state machine. Callers that need to coordinate takeovers with the previous owner must arrange that out-of-band before calling Acquire.
func (*Tx) CancelRequiresAction ¶
func (tx *Tx) CancelRequiresAction(input CancelRequiresActionInput) (CancelRequiresActionResult, error)
CancelRequiresAction synthesizes cancellation results for every pending dynamic tool call and returns the chat to running.
func (*Tx) ClearContext ¶
func (tx *Tx) ClearContext(input ClearContextInput) (ClearContextResult, error)
ClearContext commits a synchronous context reset: it inserts the caller-built boundary rows, clears any stored error and pending compaction request, preserves ownership, and lands in waiting. No worker turn is needed; the message insert trigger advances history_version and resets the retry budget.
func (*Tx) CommitStep ¶
func (tx *Tx) CommitStep(input CommitStepInput) (CommitStepResult, error)
CommitStep stores one durable message suffix while remaining running.
func (*Tx) CompleteRequiresAction ¶
func (tx *Tx) CompleteRequiresAction(input CompleteRequiresActionInput) (CompleteRequiresActionResult, error)
CompleteRequiresAction validates and stores user-submitted tool results that satisfy the chat's pending dynamic tool calls, then returns the chat to running.
func (*Tx) Ctx ¶
Ctx returns the context the surrounding ChatMachine.Update call is using.
func (*Tx) DeleteQueuedMessage ¶
func (tx *Tx) DeleteQueuedMessage(input DeleteQueuedMessageInput) (DeleteQueuedMessageResult, error)
DeleteQueuedMessage removes a single queued user message.
func (*Tx) EditMessage ¶
func (tx *Tx) EditMessage(input EditMessageInput) (EditMessageResult, error)
EditMessage replaces an earlier user message and discards the active-history suffix that followed it.
func (*Tx) EnterRequiresAction ¶
func (tx *Tx) EnterRequiresAction(_ EnterRequiresActionInput) (EnterRequiresActionResult, error)
EnterRequiresAction parks the chat in requires_action with a database-time deadline of now() + requiresActionTimeout.
func (*Tx) FinishError ¶
func (tx *Tx) FinishError(input FinishErrorInput) (FinishErrorResult, error)
FinishError parks the chat in error with the supplied last_error. It is allowed when an unarchived chat is waiting or running.
func (*Tx) FinishInterruption ¶
func (tx *Tx) FinishInterruption(input FinishInterruptionInput) (FinishInterruptionResult, error)
FinishInterruption commits an optional partial assistant/tool suffix and lands the chat in waiting (I0) or running with the next queued message promoted (I1).
func (*Tx) FinishTurn ¶
func (tx *Tx) FinishTurn(_ FinishTurnInput) (FinishTurnResult, error)
FinishTurn completes a running turn.
func (*Tx) Interrupt ¶
func (tx *Tx) Interrupt(input InterruptInput) (InterruptResult, error)
Interrupt requests interruption of an active or requires-action chat.
func (*Tx) PromoteQueuedMessage ¶
func (tx *Tx) PromoteQueuedMessage(input PromoteQueuedMessageInput) (PromoteQueuedMessageResult, error)
PromoteQueuedMessage promotes the target queued message to the queue head; from E1/A1 it also pops it into active history.
func (*Tx) ReconcileInvalidState ¶
func (tx *Tx) ReconcileInvalidState(input ReconcileInvalidStateInput) (ReconcileInvalidStateResult, error)
ReconcileInvalidState moves an invalid execution-state combination into a valid error state. Queued messages are preserved; pending dynamic-tool calls are closed with synthetic cancellation results.
func (*Tx) RecordGenerationAttempt ¶
func (tx *Tx) RecordGenerationAttempt(_ RecordGenerationAttemptInput) (RecordGenerationAttemptResult, error)
RecordGenerationAttempt durably records that the worker is attempting another generation under the current history version.
func (*Tx) RecordRetryState ¶
func (tx *Tx) RecordRetryState(input RecordRetryStateInput) (RecordRetryStateResult, error)
RecordRetryState stores the client-visible retry payload for the current generation attempt.
func (*Tx) RequestCompaction ¶ added in v2.36.0
func (tx *Tx) RequestCompaction(_ RequestCompactionInput) (RequestCompactionResult, error)
RequestCompaction records a manual compaction request, clears any prior error, and hands ownership off to a worker. The transition changes no history, so the previous runner cannot detect the work from its existing running snapshot. Clearing ownership makes ChatMachine.Update publish an ownership hint for worker acquisition.
The compaction turn gets the same fresh history epoch a history change would grant: a full retry budget regardless of how the previous turn spent its own, and message part episode keys that cannot collide with episodes the failed turn's replica retains.
func (*Tx) SendMessage ¶
func (tx *Tx) SendMessage(input SendMessageInput) (SendMessageResult, error)
SendMessage admits a new user message. Depending on input state and BusyBehavior, the message lands directly in history, in the queue, or replaces the queue head as part of a running-state promotion.
func (*Tx) SetArchived ¶
func (tx *Tx) SetArchived(input SetArchivedInput) (SetArchivedResult, error)
SetArchived sets or clears the chat's archived marker.
func (*Tx) Store ¶
Store exposes the active transaction store so callers can perform validation reads (for example loading the messages affected by an EditMessage transition) and metadata writes (for example updating title or labels) that must be atomic with the transition.
Callers MUST NOT use Store to mutate execution-state tables (chats.status, chat_messages, chat_queued_messages, chat_heartbeats, or the version fields on chats). Those mutations belong to the transition methods and are validated against the state machine matrix.