model

package
v0.2.0-alpha.1 Latest Latest
Warning

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

Go to latest
Published: Aug 23, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// ArtifactSourceAgent is an agent that chose to publish a file. SourceID is
	// its run or session ID where one is known.
	ArtifactSourceAgent = "agent"
	// ArtifactSourceTaskRun is a worker's run output. SourceID is the task run.
	ArtifactSourceTaskRun = "task_run"
	// ArtifactSourceUserUpload is a member uploading a file directly.
	ArtifactSourceUserUpload = "user_upload"
	// ArtifactSourceSystem is BuildMax generating a file with no agent call.
	ArtifactSourceSystem = "system"
)

Artifact source types record which operation produced the file. They are persisted, so they are permanent in the same way audit actions are.

View Source
const (
	ArtifactCreatorUser   = "user"
	ArtifactCreatorAgent  = "agent"
	ArtifactCreatorWorker = "worker"
	ArtifactCreatorSystem = "system"
)

Artifact creator kinds. These answer "what kind of actor", not "which user": automated work does not get a person's ID invented for it, which is the same rule the audit trail follows.

View Source
const (
	// AuditUserLogin records a successful login. Failures are not recorded
	// here — a failed login says nothing about who the actor was, and
	// recording attempts keyed by a supplied email would turn the audit log
	// into a place to write arbitrary strings.
	AuditUserLogin = "user.login"
	// AuditUserLogout records a session being revoked on purpose. It is the
	// counterpart to AuditUserLogin: together they bound when a session could
	// have been used.
	AuditUserLogout = "user.logout"
	// AuditRefreshReuse records a refresh token presented after it had already
	// been exchanged. It means the credential existed in two places, and the
	// session was revoked in response. Unlike the actions above this is not a
	// user's intent — it is the server reporting what it saw.
	AuditRefreshReuse = "auth.refresh_reuse"
	// AuditPasswordSet records an account's password being set or changed. The
	// event says that it happened, never what it became.
	AuditPasswordSet = "user.password_set"
	// AuditUserCreated records an account coming into existence, and
	// AuditLoginCodeIssued records a way into one being minted. They are
	// separate because they are separate decisions: creating an account gives
	// nobody access until a code or a password follows.
	AuditUserCreated     = "user.created"
	AuditLoginCodeIssued = "user.login_code_issued"
	// AuditUserDisabled and AuditUserEnabled record an account's access being
	// stopped and restored. Disabling is not deletion: nothing is removed, and
	// enabling reverses the state and nothing else.
	AuditUserDisabled = "user.disabled"
	AuditUserEnabled  = "user.enabled"
	// AuditSessionsRevoked records every live session of one account being
	// retired at once. It is separate from user.logout, which is a person
	// ending their own.
	AuditSessionsRevoked = "user.sessions_revoked"
	// AuditTeamMemberAdded and AuditTeamMemberRemoved record changes to who
	// can reach a team's resources.
	AuditTeamMemberAdded   = "team.member_added"
	AuditTeamMemberRemoved = "team.member_removed"
	// AuditModelCreated, AuditModelEnabled, and AuditModelDisabled record
	// changes to which models a deployment will call. The catalog holds
	// provider credentials, so a change to it is a change to what the
	// deployment can spend and where prompts go.
	AuditModelCreated  = "llm_model.created"
	AuditModelEnabled  = "llm_model.enabled"
	AuditModelDisabled = "llm_model.disabled"
	// AuditAccessDenied records a refused request. This is the one action
	// written on failure rather than success: a denial is what shows someone
	// probing at a boundary. TeamID is empty when the refused route was
	// deployment-scoped rather than team-scoped.
	AuditAccessDenied = "access.denied"
	// AuditSystemAdminGranted and AuditSystemAdminRevoked record deployment
	// authority changing hands. They are not team-scoped, so TeamID is empty.
	//
	// These are the two actions in this list where a dropped write costs the
	// most: a grant that was made and not recorded is exactly what an
	// investigation needs. The write is still best-effort, for the reason in
	// internal/service/audit — see docs/design/system-administration.md
	// section 9.
	AuditSystemAdminGranted = "system.admin_granted"
	AuditSystemAdminRevoked = "system.admin_revoked"
	// AuditArtifactCreated and AuditArtifactDeleted record a durable file
	// entering and leaving a team's keeping. They are metadata-only by
	// construction: the target is the ar_ ID, and neither the storage key, the
	// content, nor an uploader-supplied description belongs in the trail.
	AuditArtifactCreated = "artifact.created"
	AuditArtifactDeleted = "artifact.deleted"
	// The plugin actions record changes to what a deployment's members can
	// install. A release is instructions that cause tool use, processes that
	// start with someone's credentials, and hooks that run local programs, so
	// publishing one is a change to what every machine that installs it will
	// do. The detail names the version and a digest prefix — never package
	// contents or configuration values.
	AuditPluginCreated    = "plugin.created"
	AuditPluginUpdated    = "plugin.updated"
	AuditPluginArchived   = "plugin.archived"
	AuditPluginUnarchived = "plugin.unarchived"
	AuditPluginPublished  = "plugin.published"
	AuditPluginYanked     = "plugin.yanked"

	// A team activating a release is the record that answers "why did this run
	// have this capability". The pin moves and the suspension are separate
	// actions because each is a different decision about a team's runs.
	AuditPluginActivated    = "plugin.activated"
	AuditPluginPinMoved     = "plugin.pin_moved"
	AuditPluginSuspended    = "plugin.suspended"
	AuditPluginResumed      = "plugin.resumed"
	AuditTeamPluginCuration = "team.plugin_curation_set"
	// AuditEventsExported records the trail itself being read out in bulk.
	// Reading every recorded action is a sensitive action, and an export that
	// left no trace would be the one way to consult the record without
	// appearing in it.
	AuditEventsExported = "audit.exported"
	// AuditEventsPruned records events expiring under the deployment's
	// retention window, naming the cutoff and how many rows went.
	//
	// It is what separates a gap that policy created from evidence that was
	// lost: without it, a trail that starts on a Tuesday is indistinguishable
	// from a trail somebody truncated. Recording the deletion in the same
	// table it deletes from is deliberate — the event survives its own sweep
	// until the window moves past it, and by then a later one says the same.
	AuditEventsPruned = "audit.pruned"
	// AuditQuotaThresholdReached records a team crossing a share of its quota,
	// and AuditQuotaExceeded records work being refused because the limit was
	// reached. The first is a warning nobody was blocked by; the second is the
	// block. They are separate actions because they call for different
	// responses — one is a heads-up, the other is work not happening.
	//
	// Both are written at most once per limit per period, so a team that keeps
	// submitting does not turn its own trail into a log of retries.
	AuditQuotaThresholdReached = "quota.threshold_reached"
	AuditQuotaExceeded         = "quota.exceeded"
)

Audit actions. These strings are persisted, so they are permanent: renaming one rewrites history for every reader that filters on it.

View Source
const (
	AuditActorUser   = "user"
	AuditActorWorker = "worker"
	AuditActorSystem = "system"
)

Audit actor kinds.

View Source
const (
	// ChannelWorkflow and ChannelIssueAgent mark a conversation nobody holds. A
	// workflow step and an issue agent run each need one because Task requires
	// a conversation, not because anyone is talking through it.
	//
	// They live here rather than with the transports in
	// service/conversation/channel because they are not transports: nothing
	// sends or receives through them, and the store has to know them to keep
	// them out of a list of conversations people hold.
	ChannelWorkflow   = "workflow"
	ChannelIssueAgent = "issue_agent"
)
View Source
const (
	IssueStatusTodo       = "todo"
	IssueStatusInProgress = "in_progress"
	IssueStatusDone       = "done"
	IssueAssigneePerson   = "person"
	IssueAssigneeAgent    = "agent"
	IssueAssigneeWorkflow = "workflow"
)
View Source
const (
	IssueCommentAuthorUser   = "user"
	IssueCommentAuthorAgent  = "agent"
	IssueCommentAuthorSystem = "system"
)

Issue comment author kinds. A comment is written by a person, reported by an agent run, or stated by the server itself.

View Source
const (
	LLMCallStatusAccepted  = "ACCEPTED"
	LLMCallStatusSucceeded = "SUCCEEDED"
	LLMCallStatusFailed    = "FAILED"
	LLMCallStatusCanceled  = "CANCELED"
)

Managed LLM call lifecycle statuses.

View Source
const (
	LLMUsageSourceReported    = "reported"
	LLMUsageSourceEstimated   = "estimated"
	LLMUsageSourceUnavailable = "unavailable"
)

Where a call's token counts came from. Recording this keeps accounting honest when a provider reports no usage: an absent number and a zero are different facts, and only one of them may be billed.

View Source
const (
	LLMCallSurfaceServer  = "server"
	LLMCallSurfaceCLI     = "cli"
	LLMCallSurfaceDesktop = "desktop"
	LLMCallSurfaceWorker  = "worker"
)

Surfaces a managed call can originate from.

View Source
const (
	AccessTokenTTLDefault       = 7 * 24 * time.Hour
	RefreshTokenTTLDefault      = 30 * 24 * time.Hour
	RefreshRotationGraceDefault = 30 * time.Second
)

Token lifetimes used when a deployment does not choose. The access token is a signed JWT the server never stores, so the only way to retire one early is to wait for it to expire; the refresh token is a stored row and can be revoked at any time. That asymmetry is why the long-lived half is the stored one.

View Source
const (
	RunCreatedByTypeUser    = "user"
	RunCreatedByTypeWebhook = "webhook"
	RunCreatedByTypeSystem  = "system"
)
View Source
const (
	RunTriggerSourceTaskCreate = "task_create"
	RunTriggerSourceTaskRerun  = "task_rerun"
	// RunTriggerSourceTaskRetry marks a run that repeats an earlier one's
	// input rather than carrying new instructions. It is distinct from a rerun
	// because "this was run again unchanged" and "someone asked for something
	// else" are different answers to why a run exists.
	RunTriggerSourceTaskRetry          = "task_retry"
	RunTriggerSourcePortalConversation = "portal_conversation"
	RunTriggerSourcePortalTaskCreate   = "portal_task_create"
	RunTriggerSourcePortalTaskRerun    = "portal_task_rerun"
	RunTriggerSourceIssueAgentRun      = "issue_agent_run"
	RunTriggerSourceWorkflowStep       = "workflow_step"
	RunTriggerSourceWebhook            = "webhook"
)
View Source
const (
	// DeliveryPending is a report that is owed and has not been made.
	DeliveryPending = "PENDING"
	// DeliveryDelivered is a report that reached its conversation.
	DeliveryDelivered = "DELIVERED"
	// DeliveryAbandoned is a report that will not be attempted again. The run's
	// outcome is not lost with it — a task's card reads the run directly.
	DeliveryAbandoned = "ABANDONED"
)

Task result delivery statuses.

View Source
const (
	// TeamRoleOwner is the initial role for the user who creates a team.
	TeamRoleOwner = "owner"
	// TeamRoleAdmin can manage shared automation assets but not membership ownership.
	TeamRoleAdmin = "admin"
	// TeamRoleMember is the basic collaboration role for invited members.
	TeamRoleMember = "member"
	// DefaultPersonalTeamName is the initial UX-facing name for a user's own space.
	DefaultPersonalTeamName = "My Space"
)
View Source
const (
	WorkflowStatusDraft     = "draft"
	WorkflowStatusPublished = "published"
	WorkflowStatusArchived  = "archived"

	WorkflowRunStatusPending   = "pending"
	WorkflowRunStatusRunning   = "running"
	WorkflowRunStatusSucceeded = "succeeded"
	WorkflowRunStatusFailed    = "failed"
	WorkflowRunStatusCanceled  = "canceled"

	WorkflowStepTypeAgentTask = "agent_task"

	WorkflowStepRunStatusPending   = "pending"
	WorkflowStepRunStatusRunning   = "running"
	WorkflowStepRunStatusSucceeded = "succeeded"
	WorkflowStepRunStatusFailed    = "failed"
	WorkflowStepRunStatusCanceled  = "canceled"
	WorkflowStepRunStatusBlocked   = "blocked"
)
View Source
const AuditActorOperator = "buildmax-server"

AuditActorOperator is the ActorID for an action taken by an operator command rather than by a signed-in user. The command runs on the machine that holds the database credentials and has no session to name, so the record names the binary. That is less than naming a person and more than recording nothing.

View Source
const LoginCodeTTLDefault = time.Hour

LoginCodeTTLDefault is how long an issued code stays valid when the caller does not choose. Long enough to hand a code to someone over a chat message, short enough that a leaked one expires before it is useful.

View Source
const PasswordMaxLength = 1024

PasswordMaxLength bounds what will be hashed. Argon2 has no length limit of its own, but accepting unbounded input means accepting unbounded work.

View Source
const PasswordMinLength = 12

PasswordMinLength is the shortest password BuildMax accepts.

It is longer than the usual eight because BuildMax has no login throttling yet: an attacker who can reach the server can guess as fast as the server will hash. Length is the only defense that does not need infrastructure, so it carries more weight here than it would elsewhere. See docs/deploy/authentication.md.

View Source
const (
	// SystemRoleAdmin may manage accounts, read deployment status, and search
	// the audit trail across teams. It grants no access to any team's issues,
	// conversations, artifacts, files, or run traces: those stay behind team
	// membership, which a system grant never substitutes for.
	SystemRoleAdmin = "system_admin"
)

System roles are deployment-scoped: they are held by a user and attached to no team. A grant is an authority to operate the deployment, not a key to its contents — see docs/design/system-administration.md.

Variables

View Source
var (
	ErrPasswordTooShort = fmt.Errorf("password must be at least %d characters", PasswordMinLength)
	ErrPasswordTooLong  = fmt.Errorf("password must be at most %d characters", PasswordMaxLength)
)

ErrPasswordTooShort and ErrPasswordTooLong report an unusable password. They are separate from a failed login: these mean "choose another", not "wrong".

ActiveRunStatuses are the statuses a run passes through before it finishes. One task may hold at most one run in these statuses at a time.

View Source
var ErrDuplicateLLMCall = errors.New("llm call already exists for this client call id")

ErrDuplicateLLMCall is returned when a team reuses a client call ID. The unique index is what actually decides it, so two concurrent requests with one key cannot both open a record.

View Source
var ErrEmailExists = errors.New("email already exists")

ErrEmailExists is returned by CreateUser when the email is already registered.

View Source
var ErrLLMModelNameTaken = errors.New("a model with this name already exists")

ErrLLMModelNameTaken is returned when an operator reuses a model name.

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound is returned when an operation's only return is an error and the row it names does not exist. Callers that can get a nil value keep using that.

View Source
var ErrPluginAlreadyActivated = errors.New("this team has already activated this plugin")

ErrPluginAlreadyActivated means the team already has an activation for this plugin. Moving to another release is a pin change, not a second activation, which is why the two are different calls.

View Source
var ErrPluginArchived = errors.New("this plugin is archived and accepts no new releases")

ErrPluginArchived is returned when a release is published against a plugin an administrator has retired.

View Source
var ErrPluginNameTaken = errors.New("a plugin with this name already exists")

ErrPluginNameTaken is returned when a catalog entry already claims a name.

View Source
var ErrPluginVersionExists = errors.New("this plugin version has already been published")

ErrPluginVersionExists is returned when a version has already been published.

It is returned for identical bytes too. A release is what someone reviewed and what someone else downloaded, so replacing one would leave both of those facts describing something that is no longer there.

View Source
var ErrRefreshTokenInvalid = errors.New("refresh token invalid")

ErrRefreshTokenInvalid means the token is unknown, expired, or belongs to a session that has been revoked. The three are deliberately indistinguishable to the caller.

View Source
var ErrRefreshTokenReused = errors.New("refresh token reused")

ErrRefreshTokenReused means a token that had already been rotated was presented again after the grace window. Either the client is replaying a credential it should have discarded, or someone else has a copy — and there is no way to tell which. The session is revoked before this is returned.

View Source
var ErrRunCanceled = errors.New("task run canceled")

ErrRunCanceled is the reason a canceled run's context carries, and what RunTask returns for a run that stopped because someone asked it to. It marks an outcome, not a fault: a worker that returns it did what it was told.

View Source
var ErrRunInProgress = errors.New("task has a run already in progress")

ErrRunInProgress is returned by CreateTaskRun when the task already has a run in PENDING, SCHEDULED, or RUNNING.

View Source
var ErrRunInterrupted = errors.New("task run interrupted: the worker was shut down")

ErrRunInterrupted is the reason a run's context carries when the process executing it was asked to stop — SIGTERM from a node drain, an eviction, or an operator restarting the deployment.

It is deliberately distinct from ErrRunCanceled: nobody asked this run to stop, and it is equally not the run failing at its work. What it buys is a run that says what happened while it still can, instead of staying RUNNING until the stale-run reaper closes it hours later. See docs/design/graceful-shutdown.md §6.2.

View Source
var ErrSystemGrantExists = errors.New("user already holds this system role")

ErrSystemGrantExists is returned when the user already holds an active grant for the role.

View Source
var ErrSystemRoleUnknown = errors.New("unknown system role")

ErrSystemRoleUnknown is returned when a grant names a role this build does not define. The role column exists so a second role can be added without a migration, but only roles with a caller are accepted.

View Source
var ErrUserDisabled = errors.New("account is disabled")

ErrUserDisabled is returned when a credential belongs to an account an administrator has disabled. It is deliberately distinguishable from a wrong credential: someone who can prove the account is theirs should be told why they are being refused, while a wrong password still gets the generic answer.

View Source
var ErrUserNotFound = errors.New("user not found")

ErrUserNotFound is returned when an operation names an account that is not there.

View Source
var SyntheticChannels = []string{ChannelWorkflow, ChannelIssueAgent}

SyntheticChannels are the channels of conversations that exist only to satisfy the schema.

They are kept out of the conversation list rather than deleted: a workflow run and an issue agent run still hold their transcript, and a link straight to one still opens it. What they must not do is push a person's own conversations off a page of the list.

It is a deny-list rather than "anything that is not a transport" so that adding a real channel does not silently hide it.

Functions

func DummyVerifyPassword

func DummyVerifyPassword(plaintext string) bool

DummyVerifyPassword performs the same work as VerifyPassword and always fails.

Login calls it when the address has no account, so that a request for an unknown address costs the same as one for a known address with the wrong password. Without it the response time alone answers "does this person have an account here".

func HashPassword

func HashPassword(plaintext string) (string, error)

HashPassword returns a PHC-encoded argon2id hash of plaintext.

Argon2id rather than a plain SHA family: this is the one value in BuildMax that a person chose and may have reused elsewhere, so a leaked database must not turn into a list of working passwords for other services. Memory-hard hashing is what makes an offline attack on the dump expensive.

func RunStatusTerminal

func RunStatusTerminal(status string) bool

RunStatusTerminal reports whether a run in this status has finished. A run leaves a non-terminal status only through its worker, the scheduler, or a cancel; a terminal one never changes again.

func ValidPluginCuration

func ValidPluginCuration(s PluginCuration) bool

ValidPluginCuration reports whether s is a mode that may be stored. The write path checks it so the read path has only two values to consider.

func ValidSystemRole

func ValidSystemRole(role string) bool

ValidSystemRole reports whether role is one this build authorizes.

func ValidatePassword

func ValidatePassword(plaintext string) error

ValidatePassword reports whether plaintext may be used as a password.

Length only. A composition rule — a digit, a symbol, a capital — pushes people toward short predictable passwords that satisfy it, which is the opposite of what the length minimum is for.

func VerifyPassword

func VerifyPassword(encodedHash, plaintext string) bool

VerifyPassword reports whether plaintext produced encodedHash.

A malformed or empty hash is a mismatch rather than an error. Callers use this to decide whether to authenticate, and a stored value that cannot be parsed must not become a way in.

Types

type ActivatePluginInput

type ActivatePluginInput struct {
	TeamID     string
	PluginName string
	Version    string
	Digest     string
	Origin     PluginActivationOrigin
	ActorID    string
}

ActivatePluginInput pins one release for one team.

Version and Digest are supplied rather than resolved here. The caller has already read the release to decide it may be activated at all, and a store that resolved again could pin bytes nobody checked.

type Agent

type Agent struct {
	ID           string `json:"id"`
	UserID       string `json:"user_id"`
	TeamID       string `json:"team_id,omitempty"`
	Name         string `json:"name"`
	Description  string `json:"description"`
	Instructions string `json:"instructions"`
	// Plugins names the catalog plugins this agent loads for a background run.
	// Nothing is inherited from the team's activations: an agent that names
	// none loads none. See docs/design/plugin-team-distribution.md §5.3.
	Plugins []string `json:"plugins,omitempty"`
	// Revision numbers the agent_revision row holding this content. It starts
	// at 1 and advances every time the definition changes.
	Revision int `json:"revision"`
	// DeletedAt is set when the agent was deleted. The row stays because work
	// that already refers to it — a task's agent, a step run's target, a
	// revision's subject — would otherwise point at nothing. A deleted agent is
	// invisible to every path that would start new work with it.
	DeletedAt *time.Time `json:"deleted_at,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
}

Agent is a user-defined Portal agent stored in the database.

type AgentDefinition

type AgentDefinition struct {
	Name         string
	Description  string
	Instructions string
	// Plugins names catalog plugins, never releases. The version and digest
	// come from the team's activation, so moving a plugin to a new release
	// stays one edit in one place.
	Plugins []string
}

AgentDefinition is the content of one agent: what a revision records and what a write replaces.

A write carries the whole definition rather than the fields that changed, because a revision holding only a delta could not answer what the agent was at that point, which is the question revisions exist for.

type AgentRevision

type AgentRevision struct {
	AgentID      string `json:"agent_id"`
	Revision     int    `json:"revision"`
	Name         string `json:"name"`
	Description  string `json:"description"`
	Instructions string `json:"instructions"`
	// Plugins is the selection this revision recorded. It versions with the
	// rest of the definition, so an old revision still answers what that agent
	// named.
	Plugins []string `json:"plugins,omitempty"`
	// CreatedBy is the user who wrote this revision, which is not necessarily
	// the agent's owner.
	CreatedBy string    `json:"created_by"`
	CreatedAt time.Time `json:"created_at"`
}

AgentRevision is one recorded version of an agent definition.

Revisions are append-only: an edit adds one, nothing rewrites or deletes one, and restoring an older revision is itself an edit that appends a new one. They outlive the agent, so a deleted agent's history stays readable.

type AgentStore

type AgentStore interface {
	ListAgentsByUser(ctx context.Context, userID string) ([]Agent, error)
	ListAgentsByTeam(ctx context.Context, teamID string) ([]Agent, error)
	// GetAgent returns a live agent. A deleted one reads as not found, so no
	// caller can start new work with it by forgetting to check.
	GetAgent(ctx context.Context, agentID string) (*Agent, error)
	// GetAgentIncludingDeleted resolves an agent a record already refers to,
	// deleted or not. Use it to finish or describe work that named the agent
	// before it was deleted, never to start work with it.
	GetAgentIncludingDeleted(ctx context.Context, agentID string) (*Agent, error)
	CreateAgentInTeam(ctx context.Context, in CreateAgentInput) (*Agent, error)
	UpdateAgentInTeam(ctx context.Context, in UpdateAgentInput) (*Agent, error)
	// DeleteAgent and DeleteAgentInTeam mark the agent deleted rather than
	// removing the row. Deleting an agent a published workflow still names is
	// refused above this layer; see the delete handler.
	// Both return ErrNotFound when there is no such live agent for that owner.
	DeleteAgent(ctx context.Context, agentID, userID string) error
	DeleteAgentInTeam(ctx context.Context, agentID, teamID string) error
	// ListAgentRevisions returns an agent's revisions, newest first, with the
	// total count.
	ListAgentRevisions(ctx context.Context, agentID string, limit, offset int) ([]AgentRevision, int, error)
	// GetAgentRevision returns one revision, or nil when the agent has no such
	// revision number.
	GetAgentRevision(ctx context.Context, agentID string, revision int) (*AgentRevision, error)
}

AgentStore provides persistence for Portal agents.

type AppendMessageInput

type AppendMessageInput struct {
	ConversationID string
	Role           string
	Content        string
	Channel        *string
	ToolCallID     *string
	ToolCallsJSON  *string
	// ProviderStateJSON is set only for an assistant message from a protocol
	// that produced reasoning state.
	ProviderStateJSON *string
	// PartsJSON is set when the message carries non-text content.
	PartsJSON *string
}

AppendMessageInput is one message to store.

It is a struct because the column set grows as the LLM contract does, and a positional list this long stops saying which nil means what.

type Artifact

type Artifact struct {
	ID        string `json:"id"`
	TeamID    string `json:"team_id"`
	Filename  string `json:"filename"`
	MediaType string `json:"media_type"`
	SizeBytes int64  `json:"size_bytes"`
	SHA256    string `json:"sha256"`
	// StorageKey is where the object store put the bytes. It is never
	// serialized: an API response, tool output, trace, or audit event that
	// carried it would leak deployment layout and outlive the layout's freedom
	// to change.
	StorageKey    string `json:"-"`
	CreatedByType string `json:"created_by_type"`
	CreatedByID   string `json:"created_by_id"`
	SourceType    string `json:"source_type"`
	SourceID      string `json:"source_id,omitempty"`
	Title         string `json:"title,omitempty"`
	// DeletedAt tombstones the artifact. Metadata and content stop being
	// served the moment it is set; removing the object itself is a later,
	// separate step under retention policy.
	DeletedAt *time.Time `json:"deleted_at,omitempty"`
	ExpiresAt *time.Time `json:"expires_at,omitempty"`
	CreatedAt time.Time  `json:"created_at"`
}

Artifact is a durable file BuildMax holds on a team's behalf.

It is a first-class object rather than a by-product of whatever produced it: an agent, a background run, or a person uploading a file all create the same record, and the producer is kept as provenance rather than as a parent. That is the whole difference from the earlier artifact/artifact_item tables, which a task run owned and which migration 0001 removed. See docs/design/unified-artifacts.md.

Content is immutable. There is no update path: a changed file is a new Artifact, because a reference someone saved must not quietly come to mean something else.

func (*Artifact) Deleted

func (a *Artifact) Deleted() bool

Deleted reports whether the artifact has been tombstoned.

type ArtifactStore

type ArtifactStore interface {
	CreateArtifact(ctx context.Context, in CreateArtifactInput) (*Artifact, error)
	// GetArtifact returns the artifact by its ar_ ID, or (nil, nil) when there
	// is none. A tombstoned artifact is returned, not hidden: the caller has to
	// tell "never existed" from "deleted" to answer either one correctly.
	GetArtifact(ctx context.Context, artifactID string) (*Artifact, error)
	// ListArtifactsByTeam returns live artifacts newest first, with the total.
	ListArtifactsByTeam(ctx context.Context, teamID string, limit, offset int) ([]Artifact, int, error)
	// ListArtifactsBySource returns live artifacts produced by any of the given
	// operations, newest first, keyed by source ID. It is how a work object
	// finds what its runs published without owning them.
	ListArtifactsBySource(ctx context.Context, sourceIDs []string) (map[string][]Artifact, error)
	// SoftDeleteArtifact tombstones the artifact and reports whether it changed
	// anything, so a repeat delete is distinguishable from a first one.
	SoftDeleteArtifact(ctx context.Context, artifactID string, deletedAt time.Time) (bool, error)
}

ArtifactStore persists artifact metadata.

It knows nothing about task runs, issues, or conversations. Provenance reaches it as two opaque strings, which is what keeps an artifact from acquiring an owner it should not have.

type ArtifactWithTask

type ArtifactWithTask struct {
	ArtifactID       string    `json:"artifact_id"`
	TaskID           string    `json:"task_id"`
	TaskRunID        string    `json:"task_run_id"`
	ConversationID   string    `json:"conversation_id"`
	UserID           string    `json:"user_id"`
	CreatedAt        time.Time `json:"created_at"`
	TaskInputSnippet string    `json:"task_input_snippet"`
}

ArtifactWithTask is a DTO for listing run outputs (artifacts) with task/run context. ArtifactID holds task_run_id for API compatibility.

type AuditCursor

type AuditCursor struct {
	CreatedAt time.Time
	ID        string
}

AuditCursor is a position in the trail, used to walk it in bounded pages.

It exists because offset paging is wrong for an export. An export reads the table over many round trips while rows are still being appended at one end and, under a retention window, removed at the other — and either shifts every offset behind it, so a page boundary silently skips records. A keyset cursor names where the last page stopped, so the next one continues from that record no matter what the table did meanwhile.

The zero value starts at the newest event. `CreatedAt` alone is not enough to resume from: microsecond resolution narrows collisions but does not remove them, so the event's own identity breaks the tie.

That identity is the public one. The row key that actually orders a tie lives below the store boundary, so a store resolves this handle to it rather than handing a database key to a caller.

func (AuditCursor) Zero

func (c AuditCursor) Zero() bool

Zero reports whether the cursor is a fresh start rather than a resumption.

type AuditEvent

type AuditEvent struct {
	ID string `json:"id"`
	// TeamID is empty for actions that are not team-scoped, such as a login.
	TeamID     string `json:"team_id,omitempty"`
	ActorType  string `json:"actor_type"`
	ActorID    string `json:"actor_id"`
	Action     string `json:"action"`
	TargetType string `json:"target_type,omitempty"`
	TargetID   string `json:"target_id,omitempty"`
	// Detail is a short, non-sensitive note — a role name, a model alias. It
	// is not a place for request bodies.
	Detail    string    `json:"detail,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

AuditEvent is one recorded action.

It deliberately carries no prompts, no generated content, no tool output, and no credentials — only who did what to which object. Run diagnostics live in the durable run trace and per-call accounting in the llm_call ledger; this is the record that a meaningful action occurred, which is a different question with a different retention answer.

type AuditFilter

type AuditFilter struct {
	// TeamID matches events scoped to one team. It does not match the events
	// that have no team, such as a login or a grant — see WithoutTeam.
	TeamID string
	// WithoutTeam matches only the deployment-scoped events. It exists because
	// an empty TeamID already means "any team", so there would otherwise be no
	// way to ask for the ones a team-scoped reader can never see.
	WithoutTeam bool
	ActorID     string
	Action      string
	// Since and Until bound created_at, inclusive and exclusive respectively.
	// Zero means unbounded.
	Since time.Time
	Until time.Time
}

AuditFilter narrows an audit search. Every field is optional; the zero value matches everything.

There is no free-text field and there will not be one. The trail holds who did what to which object, and a search across it is a search over those — adding a text query would invite a `Detail LIKE` scan over a column whose whole purpose is to stay small and structured.

type AuditPruneStore

type AuditPruneStore interface {
	// PruneAuditEvents deletes events recorded before the cutoff, at most
	// limit of them, and returns how many went. A bounded delete keeps one
	// sweep from holding the table while a backlog of an old deployment's
	// events is removed; the caller repeats until it returns fewer than limit.
	PruneAuditEvents(ctx context.Context, before time.Time, limit int) (int64, error)
	// OldestAuditEventAt reports the timestamp of the oldest event, or zero
	// when the table is empty. The sweep uses it to say what a prune actually
	// removed rather than only what it was allowed to.
	OldestAuditEventAt(ctx context.Context) (time.Time, error)
}

AuditPruneStore expires audit events under a retention window.

It is deliberately not part of AuditStore. Every reader and writer of the trail holds that interface, and none of them should be able to remove a record; the one caller that legitimately can is the retention sweep, which takes this narrower one.

type AuditStore

type AuditStore interface {
	AuditWriter
	// ListAuditEvents returns a team's events, newest first.
	//
	// It stays alongside SearchAuditEvents rather than being replaced by it:
	// a team owner asks a narrower question, and giving that reader the wider
	// method is how a team-scoped route acquires a deployment-scoped answer.
	ListAuditEvents(ctx context.Context, teamID string, limit, offset int) ([]AuditEvent, int, error)
	// SearchAuditEvents returns events across every team, newest first. It is
	// the deployment-scoped read, and only /api/admin routes may reach it.
	SearchAuditEvents(ctx context.Context, filter AuditFilter, limit, offset int) ([]AuditEvent, int, error)
	// ExportTeamAuditEvents returns one page of a team's events, newest first,
	// continuing from after. It answers the same question as ListAuditEvents
	// and differs only in how it pages, because an export walks the whole
	// trail rather than showing the first screen of it.
	ExportTeamAuditEvents(ctx context.Context, teamID string, after AuditCursor, limit int) ([]AuditEvent, error)
	// ExportAuditEvents is the deployment-scoped counterpart, and the same rule
	// applies as for SearchAuditEvents: only /api/admin routes may reach it.
	// The pair stays split for the reason ListAuditEvents gives — a team-scoped
	// route that can name its own filter is a route that can widen it.
	ExportAuditEvents(ctx context.Context, filter AuditFilter, after AuditCursor, limit int) ([]AuditEvent, error)
}

AuditStore persists audit events.

Record takes no error-returning contract the caller must handle at the call site by design: see the recorder in internal/service/audit for why a failed write is logged rather than propagated, and what that costs.

Nothing here updates or deletes an event. Retention expiry is the one thing that removes rows, and it lives in AuditPruneStore rather than in this interface: it is a policy applied uniformly by age, not an edit anyone can make to a particular record, and every reader of this interface should stay unable to reach it.

type AuditWriter

type AuditWriter interface {
	// RecordAuditEvent appends one event. Events are append-only; nothing here
	// updates or deletes one, because a record that can be edited is not
	// evidence.
	RecordAuditEvent(ctx context.Context, in AuditEvent) error
}

AuditWriter appends audit events.

Most callers only ever write: an operator command, a handler recording a grant, the quota service noticing a limit. They take this rather than the full store so that recording an action never carries the ability to read the trail back, which is a wider permission than any of them needs.

type ClaimTaskInput

type ClaimTaskInput struct {
	TaskID         string
	ExpectedStatus string
	NewStatus      string
	StartedAt      *time.Time
	EndedAt        *time.Time
	Output         *string
	ErrorMessage   *string
	SessionID      *string
}

ClaimTaskInput atomically transitions a task from ExpectedStatus to NewStatus.

type ClaimTaskRunInput

type ClaimTaskRunInput struct {
	TaskRunID      string
	ExpectedStatus RunStatus
	NewStatus      RunStatus
	StartedAt      *time.Time
	EndedAt        *time.Time
	Output         *string
	ErrorMessage   *string
	SessionID      *string
}

ClaimTaskRunInput atomically transitions a run from ExpectedStatus to NewStatus.

type Conversation

type Conversation struct {
	ID        string    `json:"id"`
	UserID    string    `json:"user_id"`
	TeamID    string    `json:"team_id,omitempty"`
	Channel   string    `json:"channel"`
	Title     string    `json:"title,omitempty"`
	CreatedBy string    `json:"created_by"`
	CreatedAt time.Time `json:"created_at"`
}

Conversation is the Tier 1 conversation container.

type ConversationMessage

type ConversationMessage struct {
	ID             string  `json:"id"`
	ConversationID string  `json:"conversation_id"`
	Role           string  `json:"role"`
	Content        string  `json:"content"`
	Channel        *string `json:"channel,omitempty"`
	ToolCallID     *string `json:"tool_call_id,omitempty"`
	ToolCallsJSON  *string `json:"tool_calls,omitempty"`
	// ProviderStateJSON is opaque reasoning state for an assistant message,
	// stored and replayed but never read here. See core/llm.ProviderState.
	ProviderStateJSON *string `json:"provider_state,omitempty"`
	// PartsJSON is non-text content on the message, stored as the canonical
	// part list. Content remains the text describing it.
	PartsJSON *string   `json:"parts,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

ConversationMessage is one message in a Tier 1 conversation.

type ConversationMessageStore

type ConversationMessageStore interface {
	AppendMessage(ctx context.Context, in AppendMessageInput) (*ConversationMessage, error)
	ListMessages(ctx context.Context, conversationID string) ([]ConversationMessage, error)
	// GetMessage returns one message by handle, or (nil, nil) when there is no
	// such row. It exists because a run names the message that asked for it,
	// and reading a whole transcript to resolve one handle is the wrong shape
	// for a question about one run.
	GetMessage(ctx context.Context, messageID string) (*ConversationMessage, error)
}

ConversationMessageStore provides Tier 1 conversation message persistence. For role=assistant with tool calls, toolCallsJSON should be the JSON-encoded array of tool calls (id, name, arguments).

type ConversationStore

type ConversationStore interface {
	CreateConversation(ctx context.Context, userID, channel, createdBy string) (*Conversation, error)
	CreateConversationInTeam(ctx context.Context, teamID, userID, channel, createdBy string) (*Conversation, error)
	GetConversation(ctx context.Context, conversationID string) (*Conversation, error)
	ListConversationsByUser(ctx context.Context, userID string, limit, offset int) ([]Conversation, int, error)
	ListConversationsByTeam(ctx context.Context, teamID string, limit, offset int) ([]Conversation, int, error)
	UpdateConversationTitle(ctx context.Context, conversationID, title string) error
}

ConversationStore provides Tier 1 conversation persistence. Conversations are user-scoped.

type CreateAgentInput

type CreateAgentInput struct {
	TeamID string
	UserID string
	Def    AgentDefinition
}

CreateAgentInput and UpdateAgentInput carry a whole definition plus who it belongs to. They are structs rather than positional arguments because the definition grew past the point where an argument list said which value was which.

type CreateArtifactInput

type CreateArtifactInput struct {
	TeamID        string
	ArtifactID    string
	Filename      string
	MediaType     string
	SizeBytes     int64
	SHA256        string
	StorageKey    string
	CreatedByType string
	CreatedByID   string
	SourceType    string
	SourceID      string
	Title         string
	ExpiresAt     *time.Time
}

CreateArtifactInput is everything the store needs to record one artifact. The caller has already stored the content and measured it.

type CreateIssueCommentInput

type CreateIssueCommentInput struct {
	IssueID         string
	AuthorKind      string
	AuthorID        string
	Body            string
	SourceTaskID    *string
	SourceTaskRunID *string
}

type CreateIssueInput

type CreateIssueInput struct {
	Title         string
	Description   string
	ParentIssueID *string
}

type CreateLLMModelInput

type CreateLLMModelInput struct {
	Name              string
	ProviderType      string
	APIURL            string
	APIKey            string
	Model             string
	ContextWindow     int
	CallTimeout       int
	MaxTokens         int
	Reasoning         string
	CacheMode         string
	CacheTTL          string
	Vision            bool
	Currency          string
	InputPerMTok      int64
	CacheReadPerMTok  int64
	CacheWritePerMTok int64
	OutputPerMTok     int64
	Capabilities      []string
}

CreateLLMModelInput is a new catalog row, including the credential that the record itself never carries afterwards.

type CreatePluginInput

type CreatePluginInput struct {
	Name        string
	DisplayName string
	Description string
	CreatedBy   string
}

CreatePluginInput creates a catalog entry.

type CreatePluginReleaseInput

type CreatePluginReleaseInput struct {
	PluginName         string
	Version            string
	MinBuildmaxVersion string
	Digest             string
	ObjectKey          string
	SizeBytes          int64
	Inspection         PluginInspection
	Source             PluginReleaseSource
	PublishedBy        string
}

CreatePluginReleaseInput publishes one version. Digest, ObjectKey, and SizeBytes describe bytes the server has already stored.

type CreateTaskInput

type CreateTaskInput struct {
	ConversationID          string
	TeamID                  string
	Input                   string
	Title                   string
	CreatedBy               string
	InitialRunCreatedBy     string
	InitialRunCreatedByType string
	InitialRunTriggerSource string
	// InitialRunSourceMessageID names the message that asked for this task.
	InitialRunSourceMessageID *string
	TitlePromptTokens         int
	TitleCompletionTokens     int
	AgentID                   *string
	IssueID                   *string
}

CreateTaskInput is the input for CreateTask.

type CreateTaskRunInput

type CreateTaskRunInput struct {
	TaskID        string
	Input         string
	CreatedBy     string
	CreatedByType string
	TriggerSource string
	// RetryOfTaskRunID names the run this one repeats, when it repeats one.
	RetryOfTaskRunID *string
	// SourceMessageID names the conversation message that asked for this run.
	SourceMessageID *string
}

CreateTaskRunInput describes a new run on an existing task.

type CreateWorkflowRunInput

type CreateWorkflowRunInput struct {
	WorkflowID       string
	WorkflowRevision int
	IssueID          *string
	ConversationID   string
	Status           string
	CreatedBy        string
	StartedAt        *time.Time
}

type CreateWorkflowStepRunInput

type CreateWorkflowStepRunInput struct {
	StepID            string
	StepIndex         int
	StepType          string
	TargetAgentID     *string
	AgentName         string
	AgentDescription  string
	AgentInstructions string
	AgentRevision     int
	Prompt            string
	Status            string
}

type Issue

type Issue struct {
	ID            string    `json:"id"`
	UserID        string    `json:"user_id"`
	TeamID        string    `json:"team_id,omitempty"`
	ParentIssueID *string   `json:"parent_issue_id,omitempty"`
	Title         string    `json:"title"`
	Description   string    `json:"description"`
	Status        string    `json:"status"`
	AssigneeKind  *string   `json:"assignee_kind,omitempty"`
	AssigneeID    *string   `json:"assignee_id,omitempty"`
	CreatedBy     string    `json:"created_by"`
	CreatedAt     time.Time `json:"created_at"`
	UpdatedAt     time.Time `json:"updated_at"`
}

Issue is the user-facing work-management object. It is intentionally separate from low-level task/task_run execution records.

ParentIssueID makes the issue a sub-issue of another issue in the same team. The hierarchy is capped at two levels — a parent must itself have no parent — which is enforced in internal/service/issue, not by the schema. See docs/design/issue-model.md.

type IssueChildStats

type IssueChildStats struct {
	Total int `json:"total"`
	Done  int `json:"done"`
}

IssueChildStats is a parent's sub-issue progress. It is always computed, never stored: a persisted counter is a second source of truth that drifts the first time a status write fails between the two rows.

type IssueComment

type IssueComment struct {
	ID              string    `json:"id"`
	IssueID         string    `json:"issue_id"`
	AuthorKind      string    `json:"author_kind"`
	AuthorID        string    `json:"author_id"`
	Body            string    `json:"body"`
	SourceTaskID    *string   `json:"source_task_id,omitempty"`
	SourceTaskRunID *string   `json:"source_task_run_id,omitempty"`
	CreatedAt       time.Time `json:"created_at"`
	// EditedAt is nil until the body is changed. Its absence is meaningful:
	// it means the text is as first written.
	EditedAt *time.Time `json:"edited_at,omitempty"`
}

IssueComment is one statement about an issue, addressed to people.

It is deliberately not a conversation_message: that table is LLM turn history carrying roles and tool traffic, replayed into a model context. A comment outlives any particular conversation and is never replayed. See docs/design/issue-model.md.

type IssueCommentStore

type IssueCommentStore interface {
	CreateIssueComment(ctx context.Context, in CreateIssueCommentInput) (*IssueComment, error)
	// ListIssueComments returns comments oldest first — a thread reads in the
	// order it was written.
	ListIssueComments(ctx context.Context, issueID string, limit, offset int) ([]IssueComment, int, error)
	GetIssueComment(ctx context.Context, commentID string) (*IssueComment, error)
	UpdateIssueComment(ctx context.Context, commentID, body string) (*IssueComment, error)
	DeleteIssueComment(ctx context.Context, commentID string) error
	// CountIssueComments returns comment totals keyed by issue ID. Issues with
	// no comments are absent from the map.
	CountIssueComments(ctx context.Context, issueIDs []string) (map[string]int, error)
}

IssueCommentStore provides issue comment persistence. A comment's team is its issue's team; the row carries no team_id of its own, so every caller authorizes through the issue.

type IssueStore

type IssueStore interface {
	CreateIssue(ctx context.Context, userID string, in CreateIssueInput) (*Issue, error)
	CreateIssueInTeam(ctx context.Context, teamID, createdBy string, in CreateIssueInput) (*Issue, error)
	ListIssuesByUser(ctx context.Context, userID string, limit, offset int) ([]Issue, int, error)
	ListIssuesByTeam(ctx context.Context, teamID string, filter ListIssuesFilter, limit, offset int) ([]Issue, int, error)
	ListIssueChildren(ctx context.Context, parentIssueID string) ([]Issue, error)
	// ChildStatsForIssues returns sub-issue progress keyed by parent issue ID.
	// Parents with no children are absent from the map rather than present with
	// a zero value, so callers must treat a miss as "no sub-issues".
	ChildStatsForIssues(ctx context.Context, issueIDs []string) (map[string]IssueChildStats, error)
	GetIssue(ctx context.Context, issueID string) (*Issue, error)
	UpdateIssue(ctx context.Context, issueID, userID string, in UpdateIssueInput) (*Issue, error)
	UpdateIssueInTeam(ctx context.Context, issueID, teamID string, in UpdateIssueInput) (*Issue, error)
}

IssueStore provides issue persistence. Issues are user-scoped.

type LLMCall

type LLMCall struct {
	ID string `json:"id"`
	// ClientCallID is the caller's idempotency key, unique within one user's
	// calls. It is absent for calls the server makes on its own behalf.
	ClientCallID *string `json:"client_call_id,omitempty"`

	// Identity — derived from authentication, never from the request body.
	//
	// A call is attributed to a person. There is no team column: a foreground
	// call belongs to no team, and a run's team is reached through TaskRunID.
	// See docs/design/client-modes.md section 9.
	UserID    *string `json:"user_id,omitempty"`
	TaskRunID *string `json:"task_run_id,omitempty"`

	// Correlation — context for investigation, not authorization input.
	Surface   string  `json:"surface,omitempty"`
	SessionID *string `json:"session_id,omitempty"`
	TaskID    *string `json:"task_id,omitempty"`

	// Model — what the caller asked for and what it resolved to.
	Model         string `json:"model,omitempty"`
	TargetID      string `json:"target_id"`
	ProviderType  string `json:"provider_type"`
	UpstreamModel string `json:"upstream_model"`
	Streaming     bool   `json:"streaming"`

	// Timing, in unix seconds like every other table.
	AcceptedAt        time.Time  `json:"accepted_at"`
	UpstreamStartedAt *time.Time `json:"upstream_started_at,omitempty"`
	FirstDeltaAt      *time.Time `json:"first_delta_at,omitempty"`
	CompletedAt       *time.Time `json:"completed_at,omitempty"`

	// Outcome.
	Status string `json:"status"`
	// ErrorClass is the stable BuildMax error classification, never an upstream
	// error body.
	ErrorClass *string `json:"error_class,omitempty"`
	// Attempts counts upstream attempts, so a retry does not read as two calls.
	Attempts int `json:"attempts,omitempty"`

	// Usage.
	PromptTokens     *int   `json:"prompt_tokens,omitempty"`
	CompletionTokens *int   `json:"completion_tokens,omitempty"`
	TotalTokens      *int   `json:"total_tokens,omitempty"`
	CacheReadTokens  *int   `json:"cache_read_tokens,omitempty"`
	CacheWriteTokens *int   `json:"cache_write_tokens,omitempty"`
	UsageSource      string `json:"usage_source,omitempty"`

	// Pricing is the rate snapshot taken when the call was accepted, in
	// nano-currency-units per million tokens. It is a snapshot rather than a
	// reference: a model's price changes, and recomputing an old call from the
	// new rates would rewrite what a team already spent. An empty Currency
	// means the model was unpriced then, which is not the same fact as a call
	// that cost nothing.
	Currency              string `json:"currency,omitempty"`
	RateInputPerMTok      *int64 `json:"rate_input_per_mtok,omitempty"`
	RateCacheReadPerMTok  *int64 `json:"rate_cache_read_per_mtok,omitempty"`
	RateCacheWritePerMTok *int64 `json:"rate_cache_write_per_mtok,omitempty"`
	RateOutputPerMTok     *int64 `json:"rate_output_per_mtok,omitempty"`
}

LLMCall is one logical managed inference call.

It is an accounting and diagnostic record, not a transcript: prompts, tool arguments, tool results, and generated content are deliberately absent. Run detail belongs to durable local traces. See docs/design/llm-gateway.md.

type LLMCallOutcome

type LLMCallOutcome struct {
	Status            string
	ErrorClass        *string
	Attempts          int
	UpstreamStartedAt *time.Time
	FirstDeltaAt      *time.Time
	CompletedAt       time.Time
	// Usage is nil when the provider reported none; the record then keeps
	// LLMUsageSourceUnavailable rather than zero counts.
	Usage *LLMCallUsage
}

LLMCallOutcome is the terminal state written when a call finishes.

type LLMCallStore

type LLMCallStore interface {
	// OpenLLMCall records an accepted call before the upstream request starts.
	// It assigns the call ID and returns the stored record.
	OpenLLMCall(ctx context.Context, call *LLMCall) (*LLMCall, error)
	// CompleteLLMCall writes the terminal outcome of an open call.
	CompleteLLMCall(ctx context.Context, llmCallID string, outcome LLMCallOutcome) error
	// GetLLMCall returns one call by ID, or (nil, nil) when not found.
	GetLLMCall(ctx context.Context, llmCallID string) (*LLMCall, error)
	// GetLLMCallByClientID returns one user's call by their idempotency key, or
	// (nil, nil) when not found. The key is scoped to the user who sent it, so
	// one caller's key can never resolve another's call.
	GetLLMCallByClientID(ctx context.Context, userID, clientCallID string) (*LLMCall, error)
	// ListLLMCallsByTaskRun returns a run's calls, oldest first, so a reader
	// follows the run in the order it happened.
	//
	// A run belongs to exactly one team, so authorizing the run authorizes its
	// ledger; the caller must have established that before asking.
	ListLLMCallsByTaskRun(ctx context.Context, taskRunID string) ([]LLMCall, error)
}

LLMCallStore persists the managed call ledger.

type LLMCallUsage

type LLMCallUsage struct {
	PromptTokens     int `json:"prompt_tokens"`
	CompletionTokens int `json:"completion_tokens"`
	TotalTokens      int `json:"total_tokens"`
	// CacheReadTokens and CacheWriteTokens are the cached parts of the prompt.
	// They break PromptTokens down rather than adding to it, so a spend report
	// must not sum them alongside it.
	CacheReadTokens  int    `json:"cache_read_tokens"`
	CacheWriteTokens int    `json:"cache_write_tokens"`
	Source           string `json:"source"`
}

LLMCallUsage is the token usage reported for one call.

type LLMModel

type LLMModel struct {
	ID string `json:"id"`
	// Name is the operator-facing name, unique within a deployment.
	Name string `json:"name"`
	// ProviderType selects the client implementation.
	ProviderType string `json:"provider_type"`
	// APIURL is the upstream base URL.
	APIURL string `json:"api_url"`
	// Model is the provider's own model identifier.
	Model string `json:"model"`
	// ContextWindow is the usable context size; 0 uses the client default.
	ContextWindow int `json:"context_window,omitempty"`
	// CallTimeout bounds one upstream call in seconds; 0 uses the client default.
	CallTimeout int `json:"call_timeout,omitempty"`
	// MaxTokens caps one response; 0 uses the client default. The Anthropic
	// protocol requires the field, so a target speaking it always sends one.
	MaxTokens int `json:"max_tokens,omitempty"`
	// Reasoning is the effort level the upstream is asked for; empty means off.
	Reasoning string `json:"reasoning,omitempty"`
	// CacheMode and CacheTTL are the prompt-cache policy: which calls ask the
	// upstream to cache the stable prefix of a request, and for how long. Empty
	// means unset, which takes the default policy.
	CacheMode string `json:"cache_mode,omitempty"`
	CacheTTL  string `json:"cache_ttl,omitempty"`
	// Pricing is what this upstream charges, in nano-currency-units per
	// million tokens. The four rates are separate because caching prices them
	// differently; an empty currency means the model is unpriced and its calls
	// report cost as unavailable rather than as zero.
	//
	// These are the *current* rates. What a past call cost is not recomputed
	// from them — the ledger row keeps the rates that applied when it ran, so
	// a price change does not rewrite history.
	Currency          string `json:"currency,omitempty"`
	InputPerMTok      int64  `json:"input_per_mtok,omitempty"`
	CacheReadPerMTok  int64  `json:"cache_read_per_mtok,omitempty"`
	CacheWritePerMTok int64  `json:"cache_write_per_mtok,omitempty"`
	OutputPerMTok     int64  `json:"output_per_mtok,omitempty"`
	// Vision says the upstream accepts image input.
	Vision bool `json:"vision,omitempty"`
	// Capabilities is what this model supports, e.g. "text_chat".
	Capabilities []string `json:"capabilities,omitempty"`
	// Enabled lets an operator retire a model without deleting it.
	Enabled   bool      `json:"enabled"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

LLMModel is one operator-approved upstream the managed gateway may call.

The record deliberately has no credential field. The key lives in the same table but is read only by the component that opens a provider connection, so listing, resolving, and diagnosing models can never carry it by accident. See docs/design/llm-gateway.md.

type LLMModelStore

type LLMModelStore interface {
	// CreateLLMModel stores a new model and returns it without its credential.
	CreateLLMModel(ctx context.Context, in CreateLLMModelInput) (*LLMModel, error)
	// GetLLMModel returns one model by ID, or (nil, nil) when not found.
	GetLLMModel(ctx context.Context, llmModelID string) (*LLMModel, error)
	// GetLLMModelByName returns one model by its operator-facing name, or
	// (nil, nil) when not found. Name is unique across the deployment and is
	// what a client names a model by, so this is the lookup on the call path.
	GetLLMModelByName(ctx context.Context, name string) (*LLMModel, error)
	// ListLLMModels returns every model, enabled or not, oldest first.
	ListLLMModels(ctx context.Context) ([]LLMModel, error)
	// SetLLMModelEnabled retires or restores a model.
	SetLLMModelEnabled(ctx context.Context, llmModelID string, enabled bool) error
	// LLMModelCredential returns the upstream key for a model. It is the only
	// way a credential leaves the store.
	LLMModelCredential(ctx context.Context, llmModelID string) (string, error)
}

LLMModelStore persists the managed model catalog.

Reading a model and reading its credential are separate operations on purpose: everything that lists, resolves, or reports a model uses the first, and only the client factory uses the second.

type ListIssuesFilter

type ListIssuesFilter struct {
	// TopLevelOnly restricts the listing to issues with no parent.
	TopLevelOnly bool
	// ParentIssueID restricts the listing to one parent's children. It is
	// ignored when TopLevelOnly is set.
	ParentIssueID string
}

ListIssuesFilter narrows a team issue listing. The zero value lists every issue in the team, which is what callers predating sub-issues expect.

type LoginCodeStore

type LoginCodeStore interface {
	// CreateLoginCode issues a single-use code for userID and returns the
	// plaintext, which is never stored and cannot be recovered afterwards.
	CreateLoginCode(ctx context.Context, userID string, ttl time.Duration) (plaintext string, expiresAt time.Time, err error)

	// ConsumeLoginCode redeems a code that was issued to userID. A code that
	// is unknown, already used, expired, or issued to somebody else returns
	// (false, nil) — the caller cannot tell which, and neither can an
	// attacker. Redemption is atomic: concurrent calls with the same code
	// produce exactly one winner.
	//
	// The account is named by the caller rather than reported back, so that a
	// code submitted with the wrong address is left untouched. A redemption
	// that spent the code first and checked the account afterwards burned it
	// on a typo, and the person retrying with the right address was then
	// refused for a reason nobody could see.
	ConsumeLoginCode(ctx context.Context, plaintext, userID string, now time.Time) (redeemed bool, err error)
}

LoginCodeStore issues and redeems single-use login codes.

This is BuildMax's answer to having no mail channel: an operator issues a code out of band (`buildmax-server user login-code`) and delivers it however they already talk to the person.

It is not the everyday credential — a password is. A code is what claims a new account and what recovers a forgotten password, which is why it is single-use and short-lived: it exists to be spent once, on the way to setting a password.

type MovePluginActivationPinInput

type MovePluginActivationPinInput struct {
	TeamID     string
	PluginName string
	Version    string
	Digest     string
	ActorID    string
}

MovePluginActivationPinInput repoints an existing activation at another release. It is separate from activation because it is the action that needs a person to have read the new release's capability report.

type NewRefreshToken

type NewRefreshToken struct {
	UserID string
	// SessionID names one login chain. Every rotation keeps it, so revoking a
	// session retires the whole chain rather than one link of it.
	SessionID string
	// Platform records which surface logged in ("portal", "cli", "desktop").
	// It is a label for the operator reading the session list, not something
	// the server enforces.
	Platform string
	TTL      time.Duration
}

NewRefreshToken describes a token to issue.

type PasswordStore

type PasswordStore interface {
	// PasswordHash returns the stored hash for userID, or "" when the account
	// has no password and can only sign in with a login code.
	PasswordHash(ctx context.Context, userID string) (string, error)
	// SetPassword stores an already-hashed password. Hashing belongs to the
	// caller — this interface must not be a place where a plaintext password
	// can be passed by mistake.
	SetPassword(ctx context.Context, userID, encodedHash string, setAt time.Time) error
}

PasswordStore reads and writes the one credential a person chose themselves.

It is deliberately separate from UserStore. A password hash is the only value in the system whose exposure would reach beyond BuildMax — people reuse passwords — so it is fetched only by the code that verifies a login, and never rides along on a User that some handler might serialize.

type Plugin

type Plugin struct {
	// Name is the manifest name, unique in the deployment, and the slug every
	// route addresses the plugin by.
	Name        string `json:"name"`
	DisplayName string `json:"display_name,omitempty"`
	Description string `json:"description,omitempty"`
	// ArchivedAt hides the entry from the default catalog and refuses new
	// releases. It never deletes anything: a local copy someone installed keeps
	// working, and the record still explains where that copy came from.
	ArchivedAt *time.Time `json:"archived_at,omitempty"`
	CreatedBy  string     `json:"created_by"`
	CreatedAt  time.Time  `json:"created_at"`
	UpdatedAt  time.Time  `json:"updated_at"`
}

Plugin is a catalog entry: the stable identity releases are published under.

The entry belongs to the deployment rather than to a team, so it carries no team. Publishing is a System Administrator action; see docs/design/plugin-marketplace.md §7.1.

func (Plugin) Archived

func (p Plugin) Archived() bool

Archived reports whether the entry has been retired.

type PluginActivation

type PluginActivation struct {
	ID         string `json:"id"`
	TeamID     string `json:"team_id"`
	PluginName string `json:"plugin_name"`
	Version    string `json:"version"`
	Digest     string `json:"digest"`
	// Enabled false suspends the activation without losing the pin. A suspended
	// activation fails the runs of the agents that name it rather than quietly
	// dropping the plugin from them.
	Enabled bool                   `json:"enabled"`
	Origin  PluginActivationOrigin `json:"origin"`

	ActivatedBy string    `json:"activated_by"`
	ActivatedAt time.Time `json:"activated_at"`
	UpdatedBy   string    `json:"updated_by,omitempty"`
	UpdatedAt   time.Time `json:"updated_at"`
}

PluginActivation is one team's pinned use of one catalog plugin.

The pin is the point. A release published after this row was written cannot change what a run loads — in either curation mode — until a person moves it.

type PluginActivationOrigin

type PluginActivationOrigin string

PluginActivationOrigin says who put an activation there.

const (
	// PluginActivationCurated is an activation an admin made deliberately.
	PluginActivationCurated PluginActivationOrigin = "curated"
	// PluginActivationAutomatic is one created because an agent named the
	// plugin in an open-mode team. It is labelled so a team's list reads as the
	// history it is rather than as one somebody curated.
	PluginActivationAutomatic PluginActivationOrigin = "automatic"
)

type PluginActivationStore

type PluginActivationStore interface {
	// ActivatePlugin records a new activation, or returns
	// ErrPluginAlreadyActivated when the team has one for that plugin.
	ActivatePlugin(ctx context.Context, in ActivatePluginInput) (*PluginActivation, error)
	// GetPluginActivation returns one team's activation of one plugin, or
	// (nil, nil) when there is none.
	GetPluginActivation(ctx context.Context, teamID, pluginName string) (*PluginActivation, error)
	// ListPluginActivations returns a team's activations, oldest first,
	// suspended ones included: a suspended activation still explains why a run
	// failed.
	ListPluginActivations(ctx context.Context, teamID string) ([]PluginActivation, error)
	// MovePluginActivationPin repoints an activation, or returns ErrNotFound.
	MovePluginActivationPin(ctx context.Context, in MovePluginActivationPinInput) (*PluginActivation, error)
	// SetPluginActivationEnabled suspends or resumes an activation without
	// losing the pin, or returns ErrNotFound.
	SetPluginActivationEnabled(ctx context.Context, teamID, pluginName string, enabled bool, actorID string) (*PluginActivation, error)
}

PluginActivationStore persists which releases a team's background runs may use. It is separate from PluginStore because the catalog belongs to the deployment and an activation belongs to a team.

type PluginCuration

type PluginCuration string

PluginCuration is a team's answer to who fills its activation list.

The modes differ in that and nothing else: both produce a pinned activation with the same digest, audit event, and trace provenance. See docs/design/plugin-team-distribution.md §4.1.

const (
	// PluginCurationOpen lets an agent name any catalog plugin and creates the
	// activation the first time one does. It is the default because the gate
	// that crosses teams is operator eligibility, not a team's housekeeping.
	PluginCurationOpen PluginCuration = "open"
	// PluginCurationCurated requires an admin to activate a plugin before an
	// agent may name it.
	PluginCurationCurated PluginCuration = "curated"
)

func NormalizePluginCuration

func NormalizePluginCuration(s string) PluginCuration

NormalizePluginCuration reads a stored value. Empty is open: a team that has never set the mode has not asked to be restricted.

type PluginInspection

type PluginInspection struct {
	Skills      []string            `json:"skills,omitempty"`
	Subagents   []inspect.Subagent  `json:"subagents,omitempty"`
	MCP         []inspect.MCPServer `json:"mcp,omitempty"`
	Hooks       []inspect.Hook      `json:"hooks,omitempty"`
	EnvRefs     []string            `json:"env_refs,omitempty"`
	PluginPaths []string            `json:"plugin_paths,omitempty"`
	// Warnings are the findings that did not stop publication, kept so an
	// installer can show what a publisher chose to accept.
	Warnings []string `json:"warnings,omitempty"`
}

PluginInspection is what a release says it contributes.

It reuses the sanitized shapes from the inspector, so what a catalog stores is exactly what that inspection was allowed to carry: names, transports, executables, and hosts — never arguments, header values, environment values, prompts, or file contents.

type PluginPin

type PluginPin struct {
	PluginName string `json:"plugin_name"`
	Version    string `json:"version"`
	Digest     string `json:"digest"`
}

PluginPin is one resolved activation as a run receives it.

It is the activation reduced to what materializing needs — which package, and the digest to check it against — because a worker has no business holding a team's activation record.

type PluginRelease

type PluginRelease struct {
	// PluginName is denormalised so a release can be reported without a second
	// read; the catalog entry remains the owner of the name.
	PluginName string `json:"plugin_name"`
	Version    string `json:"version"`
	// MinBuildmaxVersion is the release's own lower bound, kept as a column
	// because default install selection filters on it.
	MinBuildmaxVersion string `json:"min_buildmax_version,omitempty"`

	// Digest is the labelled SHA-256 of the stored bytes, calculated by the
	// server rather than accepted from the publisher.
	Digest    string `json:"digest"`
	ObjectKey string `json:"object_key"`
	SizeBytes int64  `json:"size_bytes"`

	// Inspection is the sanitized capability report shown before an install.
	Inspection PluginInspection `json:"inspection"`
	// Source is where the publisher says the bytes came from. Unlike the
	// digest, the server cannot verify it, so it is presented as a claim.
	Source PluginReleaseSource `json:"source"`

	PublishedBy string    `json:"published_by"`
	PublishedAt time.Time `json:"published_at"`

	// YankedAt removes the release from default selection without deleting it.
	// An existing local copy keeps working, and an exact version can still be
	// recovered by someone who acknowledges the state.
	YankedAt     *time.Time `json:"yanked_at,omitempty"`
	YankedBy     string     `json:"yanked_by,omitempty"`
	YankedReason string     `json:"yanked_reason,omitempty"`
}

PluginRelease is one immutable published version.

func (PluginRelease) Yanked

func (r PluginRelease) Yanked() bool

Yanked reports whether the release has been withdrawn from default selection.

type PluginReleaseSource

type PluginReleaseSource struct {
	RemoteURL string `json:"remote_url,omitempty"`
	Commit    string `json:"commit,omitempty"`
	Branch    string `json:"branch,omitempty"`
	// Dirty says the working tree held uncommitted changes when it was packed,
	// which means the commit above does not describe these bytes.
	Dirty bool `json:"dirty,omitempty"`
}

PluginReleaseSource is the publisher's claim about where the bytes came from.

A package assembled by hand rather than committed is a legitimate case, so an empty record is not an error — it is the absence of a claim.

type PluginStore

type PluginStore interface {
	// CreatePlugin adds a catalog entry, or returns ErrPluginNameTaken.
	CreatePlugin(ctx context.Context, in CreatePluginInput) (*Plugin, error)
	// GetPlugin returns one entry by name, or (nil, nil) when there is none.
	GetPlugin(ctx context.Context, name string) (*Plugin, error)
	// ListPlugins returns entries oldest first, archived ones only when asked.
	ListPlugins(ctx context.Context, includeArchived bool) ([]Plugin, error)
	// UpdatePlugin changes display metadata, or returns ErrNotFound.
	UpdatePlugin(ctx context.Context, name string, in UpdatePluginInput) (*Plugin, error)
	// SetPluginArchived retires or restores an entry, or returns ErrNotFound.
	SetPluginArchived(ctx context.Context, name string, archived bool) error

	// CreatePluginRelease publishes one version. It returns
	// ErrPluginVersionExists when that version is already published,
	// ErrPluginArchived when the entry is retired, and ErrNotFound when there
	// is no such entry.
	CreatePluginRelease(ctx context.Context, in CreatePluginReleaseInput) (*PluginRelease, error)
	// GetPluginRelease returns one version, or (nil, nil) when there is none.
	GetPluginRelease(ctx context.Context, name, version string) (*PluginRelease, error)
	// ListPluginReleases returns every release of one plugin, oldest first,
	// including yanked ones: choosing between them is the caller's job.
	ListPluginReleases(ctx context.Context, name string) ([]PluginRelease, error)
	// YankPluginRelease withdraws a release from default selection, or returns
	// ErrNotFound.
	YankPluginRelease(ctx context.Context, name, version, actor, reason string) error
}

PluginStore persists the deployment's plugin catalog.

Package bytes are not here: they live behind the object store, so a query that lists or inspects releases can never carry one.

type QuotaTier

type QuotaTier struct {
	TierName           string `json:"tier_name"`
	MaxRunsPerPeriod   int    `json:"max_runs_per_period"`
	MaxTokensPerPeriod int    `json:"max_tokens_per_period"`
	PeriodDays         int    `json:"period_days"`
}

QuotaTier defines limits for a tier (e.g. free_trial, pro).

type QuotaTierStore

type QuotaTierStore interface {
	// GetQuotaTier returns the tier limits by tier name, or (nil, nil) when not found.
	GetQuotaTier(ctx context.Context, tierName string) (*QuotaTier, error)
}

QuotaTierStore provides quota tier limits by tier name.

type RefreshTokenStore

type RefreshTokenStore interface {
	// CreateRefreshToken issues a token and returns the plaintext, which is
	// never stored — the row holds a hash, so a database backup yields no
	// usable credentials.
	CreateRefreshToken(ctx context.Context, in NewRefreshToken) (plaintext string, expiresAt time.Time, err error)

	// RotateRefreshToken exchanges plaintext for a fresh token in the same
	// session, spending the presented one.
	//
	// Within grace of having been spent, a token may be exchanged again. That
	// window is not a concession to sloppy clients: BuildMax's CLI and Desktop
	// share one credentials file across independent processes, and two of them
	// refreshing at the same moment is normal rather than suspicious. Both
	// receive a usable token; both stay in the same session.
	//
	// Past the grace window a spent token means ErrRefreshTokenReused, and the
	// whole session is revoked first — logging out the legitimate holder is the
	// correct response when a credential may be in two hands. That error comes
	// back with UserID and SessionID populated and Plaintext empty, so the
	// caller can record what was revoked.
	RotateRefreshToken(ctx context.Context, plaintext string, now time.Time, ttl, grace time.Duration) (RotatedRefreshToken, error)

	// RevokeRefreshTokenSession revokes the session the token belongs to and
	// reports whose it was. An unknown token is not an error: logging out
	// something already gone is a success.
	RevokeRefreshTokenSession(ctx context.Context, plaintext string, now time.Time) (userID, sessionID string, err error)

	// RevokeSession revokes every live token in one session and returns how
	// many it retired.
	RevokeSession(ctx context.Context, sessionID string, now time.Time) (int64, error)

	// RevokeUserSessions revokes every live session the user has and returns
	// how many tokens it retired. This is what "sign them out everywhere"
	// means, and it is the strongest thing disabling an account can do to a
	// credential the server actually stores.
	RevokeUserSessions(ctx context.Context, userID string, now time.Time) (int64, error)

	// CountUserSessions counts the user's live sessions — distinct login
	// chains, not tokens, since a chain is what a person would recognise as
	// "signed in on my laptop".
	CountUserSessions(ctx context.Context, userID string, now time.Time) (int, error)

	// DeleteExpiredRefreshTokens removes rows that can no longer be exchanged.
	DeleteExpiredRefreshTokens(ctx context.Context, before time.Time) (int64, error)
}

RefreshTokenStore issues, rotates, and revokes the stored half of a login.

Rotation is what makes a stolen refresh token detectable: each exchange spends the presented token and hands back a new one, so the same token appearing twice means two holders. See RotateRefreshToken for what the store does about that, and why a short grace window has to exist.

type RotatedRefreshToken

type RotatedRefreshToken struct {
	UserID    string
	SessionID string
	Plaintext string
	ExpiresAt time.Time
}

RotatedRefreshToken is the result of exchanging one refresh token for the next. Plaintext is returned once and never recoverable afterwards.

type RunStatus

type RunStatus string

RunStatus is the canonical lifecycle status for task runs.

const (
	RunStatusPending   RunStatus = "PENDING"
	RunStatusScheduled RunStatus = "SCHEDULED"
	RunStatusRunning   RunStatus = "RUNNING"
	RunStatusSucceeded RunStatus = "SUCCEEDED"
	RunStatusFailed    RunStatus = "FAILED"
	// RunStatusCanceled is terminal and distinct from FAILED: nothing went
	// wrong, someone stopped the run. A canceled run keeps whatever output and
	// artifacts it had produced by then.
	RunStatusCanceled RunStatus = "CANCELED"
)

type SchemaMigration

type SchemaMigration struct {
	ID        string    `json:"id"`
	AppliedAt time.Time `json:"applied_at"`
}

SchemaMigration is one applied schema step.

The set of applied migrations is what tells an operator whether a database matches the binary talking to it — the question behind "we upgraded and something is wrong".

type SchemaStore

type SchemaStore interface {
	// AppliedMigrations returns applied migrations, oldest first.
	AppliedMigrations(ctx context.Context) ([]SchemaMigration, error)
}

SchemaStore reports what has been done to the database.

type SystemGrant

type SystemGrant struct {
	ID     string `json:"id"`
	UserID string `json:"user_id"`
	Role   string `json:"role"`
	// GrantedBy is the user_id of the admin who made the grant, or
	// AuditActorOperator when it came from the operator command, which runs
	// with database credentials and no signed-in identity. It is deliberately
	// the same string the matching audit event carries in ActorID: one act
	// should not have two names across two tables.
	GrantedBy string    `json:"granted_by"`
	GrantedAt time.Time `json:"granted_at"`
	// RevokedAt is nil while the grant is active. Revoking sets it rather than
	// deleting the row: who held authority and when is the question an
	// investigation asks, and a deleted row cannot answer it.
	RevokedAt *time.Time `json:"revoked_at,omitempty"`
}

SystemGrant is one deployment-scoped authority held by one user.

It is a row rather than a flag on User because a flag has no granting actor, no timestamp, and no history — and those three are the point. Authority that cannot be attributed or revoked is the thing this model exists to avoid.

func (SystemGrant) Active

func (g SystemGrant) Active() bool

Active reports whether the grant is currently in force.

type SystemGrantStore

type SystemGrantStore interface {
	// ActiveSystemRoles returns the roles userID currently holds, empty for
	// almost every caller. It is on the path of every authenticated admin
	// request, so it must stay a single indexed read.
	ActiveSystemRoles(ctx context.Context, userID string) ([]string, error)
	// ListSystemGrants returns grants newest first. includeRevoked adds the
	// retired ones, which is how the trail of who held authority is read.
	ListSystemGrants(ctx context.Context, includeRevoked bool) ([]SystemGrant, error)
	// GrantSystemRole grants role to userID. It returns ErrSystemGrantExists
	// when an active grant is already there, so a caller can report "already
	// an admin" rather than silently creating a second row.
	GrantSystemRole(ctx context.Context, userID, role, grantedBy string, now time.Time) (*SystemGrant, error)
	// RevokeSystemRole revokes the active grant and reports whether one was
	// found. Revoking an absent grant is not an error: the end state is what
	// was asked for.
	RevokeSystemRole(ctx context.Context, userID, role string, now time.Time) (bool, error)
	// CountActiveSystemGrants counts live grants for role. It is what the API
	// checks before revoking the last one — see
	// docs/design/system-administration.md section 6.
	CountActiveSystemGrants(ctx context.Context, role string) (int, error)
}

SystemGrantStore persists deployment-scoped role grants.

type Task

type Task struct {
	ID                    string     `json:"id"`
	ConversationID        string     `json:"conversation_id"`
	TeamID                string     `json:"team_id,omitempty"`
	IssueID               *string    `json:"issue_id,omitempty"`
	Status                string     `json:"status"`
	Input                 string     `json:"input"`
	Title                 string     `json:"title,omitempty"`
	TitlePromptTokens     int        `json:"title_prompt_tokens,omitempty"`
	TitleCompletionTokens int        `json:"title_completion_tokens,omitempty"`
	Output                *string    `json:"output,omitempty"`
	CreatedBy             string     `json:"created_by"`
	CreatedAt             time.Time  `json:"created_at"`
	StartedAt             *time.Time `json:"started_at,omitempty"`
	EndedAt               *time.Time `json:"ended_at,omitempty"`
	ErrorMessage          *string    `json:"error_message,omitempty"`
	SessionID             *string    `json:"session_id,omitempty"`
	LastRunID             *string    `json:"last_run_id,omitempty"`
	AgentID               *string    `json:"agent_id,omitempty"`
}

Task holds the user-visible state for a background task.

type TaskResultDelivery

type TaskResultDelivery struct {
	TaskRunID      string
	ConversationID string
	Status         string
	// Attempts counts claims, not successes. It is incremented when a delivery
	// is claimed rather than when one fails, so an attempt that dies mid-flight
	// still counts against the cap.
	Attempts  int
	LastError *string
	// NextAttemptAt is both the backoff and the lease: claiming pushes it out,
	// so a second sweeper does not pick up a delivery already in flight.
	NextAttemptAt time.Time
	CreatedAt     time.Time
}

TaskResultDelivery is one owed report: a run that finished and a conversation that has not yet been told.

It exists because the report is a Tier 1 turn, and a turn is a model call that can fail, be refused, or be interrupted by a restart. Without a record of the obligation, a report that does not happen simply does not happen, and nothing afterwards knows one was owed. What the report says is not stored: it is derived from the run each attempt, so a retry reports the run as it is rather than as it was when it finished.

type TaskResultDeliveryStore

type TaskResultDeliveryStore interface {
	// EnqueueTaskResultDelivery records that a run's outcome is owed to a
	// conversation. It is idempotent per run: a run reported twice — by its
	// worker and then by the reaper that gave up on it — owes one report.
	EnqueueTaskResultDelivery(ctx context.Context, taskRunID, conversationID string, now time.Time) error
	// ListDueTaskResultDeliveries returns pending reports whose next attempt is
	// due, oldest first.
	ListDueTaskResultDeliveries(ctx context.Context, now time.Time, limit int) ([]TaskResultDelivery, error)
	// ClaimTaskResultDelivery takes one pending delivery that is due, counting
	// the attempt and pushing its next one to nextAttemptAt. It returns nil
	// when the delivery is not pending, not due, or was claimed by someone
	// else — which is what keeps one run from being reported twice.
	ClaimTaskResultDelivery(ctx context.Context, taskRunID string, now, nextAttemptAt time.Time) (*TaskResultDelivery, error)
	// FinishTaskResultDelivery closes a delivery as DELIVERED or ABANDONED.
	FinishTaskResultDelivery(ctx context.Context, taskRunID, status string, lastError *string) error
	// RecordTaskResultDeliveryFailure keeps a delivery pending, records why the
	// last attempt did not succeed, and brings its next attempt forward. The
	// claim pushed that time out far enough to protect a turn still running;
	// an attempt that has already failed no longer needs protecting.
	RecordTaskResultDeliveryFailure(ctx context.Context, taskRunID, lastError string, nextAttemptAt time.Time) error
}

TaskResultDeliveryStore persists owed reports.

type TaskRun

type TaskRun struct {
	ID               string     `json:"id"`
	TaskID           string     `json:"task_id"`
	Input            string     `json:"input"`
	CreatedBy        string     `json:"created_by,omitempty"`
	CreatedByType    string     `json:"created_by_type,omitempty"`
	TriggerSource    string     `json:"trigger_source,omitempty"`
	Status           string     `json:"status"`
	Output           *string    `json:"output,omitempty"`
	ErrorMessage     *string    `json:"error_message,omitempty"`
	StartedAt        *time.Time `json:"started_at,omitempty"`
	EndedAt          *time.Time `json:"ended_at,omitempty"`
	SessionID        *string    `json:"session_id,omitempty"`
	WorkerType       string     `json:"worker_type,omitempty"`
	K8sJobName       *string    `json:"k8s_job_name,omitempty"`
	K8sJobCreatedAt  *time.Time `json:"k8s_job_created_at,omitempty"`
	PromptTokens     *int       `json:"prompt_tokens,omitempty"`
	CompletionTokens *int       `json:"completion_tokens,omitempty"`
	// TracePath locates this run's durable trace inside run-global storage,
	// e.g. "traces/<session>/rt_….jsonl". Nil when no trace was written — the
	// run failed before an agent started, or tracing was disabled.
	TracePath *string `json:"trace_path,omitempty"`
	// CancelRequestedAt is when someone asked this run to stop. A cancel is
	// recorded rather than applied because the only thing that can stop a
	// started run is its own worker: the server states the intent, the worker
	// honors it and reports CANCELED. Nil means nobody has asked.
	CancelRequestedAt *time.Time `json:"cancel_requested_at,omitempty"`
	// CancelRequestedBy is the user who asked. A team's runs can be stopped by
	// anyone on the team, so "why did this stop" needs a name to answer.
	CancelRequestedBy *string `json:"cancel_requested_by,omitempty"`
	// RetryOfTaskRunID names the run this one repeats. Nil for every run that
	// carries its own instructions. The lineage is one level deep by record but
	// unbounded by use: retrying a retry points at the run it repeated, not at
	// the first of the chain.
	RetryOfTaskRunID *string `json:"retry_of_task_run_id,omitempty"`
	// AgentRevision numbers the agent definition this run was actually given.
	//
	// The definition is resolved when a worker asks for its run, not when the
	// task was created, so an edit takes effect on the next run. That is what
	// someone editing the field expects and it is also why this is recorded: a
	// run's instructions are otherwise whatever the agent says today, and no
	// record says which text produced this outcome. Nil for a run with no agent
	// and for runs that predate the column.
	AgentRevision *int `json:"agent_revision,omitempty"`
	// PluginPins are the releases this run was given, resolved when its worker
	// claimed it and fixed from that moment.
	//
	// Recorded for the reason AgentRevision is: afterwards nothing else can say
	// which versions this run actually had. The trace says so too, but a trace
	// is fail-open and lives in run-global storage, while this is the queryable
	// fact and what a retry reads. Nil for a run that resolved no plugins.
	PluginPins []PluginPin `json:"plugin_pins,omitempty"`
	// SourceMessageID names the conversation message this run was asked for in.
	//
	// Input is what Tier 1 decided to send a worker; this is what the person
	// actually said. They are not the same text and the difference is the point:
	// without it, nobody can tell a constraint the model dropped from one the
	// user never gave. Nil for a run with no message behind it — a workflow
	// step, an issue agent run, a retry, or a task created straight from the API.
	SourceMessageID *string   `json:"source_message_id,omitempty"`
	CreatedAt       time.Time `json:"created_at"`
}

TaskRun is one execution (initial or follow-up) of a task.

type TaskRunArtifact

type TaskRunArtifact struct {
	TaskRunID    string `json:"task_run_id"`
	RelativePath string `json:"relative_path"`
}

TaskRunArtifact is one output file (artifact) for a task run.

type TaskRunStore

type TaskRunStore interface {
	// CreateTaskRun creates a new run (PENDING). Returns ErrRunInProgress if the task has any run in PENDING/SCHEDULED/RUNNING.
	CreateTaskRun(ctx context.Context, in CreateTaskRunInput) (*TaskRun, error)
	// CountTaskRunsByStatus returns how many runs are in each status. It is
	// the one number that answers "is work flowing through this deployment",
	// and it carries no team, input, or output — only counts.
	CountTaskRunsByStatus(ctx context.Context) (map[string]int, error)
	// GetNextPendingTaskRun returns the oldest run with status PENDING (by created_at), or (nil, nil) if none.
	GetNextPendingTaskRun(ctx context.Context) (*TaskRun, error)
	GetTaskRun(ctx context.Context, taskRunID string) (*TaskRun, error)
	// GetTaskRunWithTask returns the run and its task, or (nil, nil, nil) if run not found.
	GetTaskRunWithTask(ctx context.Context, taskRunID string) (*TaskRun, *Task, error)
	// ListTaskRunIDsByTasks returns each task's run IDs, newest first, keyed by
	// task ID. Tasks with no runs are absent from the map.
	//
	// It exists because a task's last run is not its only run: a retried task
	// has earlier ones, and what those produced did not stop existing.
	ListTaskRunIDsByTasks(ctx context.Context, taskIDs []string) (map[string][]string, error)
	// GetActiveTaskRunByTask returns the task's run in PENDING, SCHEDULED, or
	// RUNNING, or (nil, nil) when the task has none. A task holds at most one.
	GetActiveTaskRunByTask(ctx context.Context, taskID string) (*TaskRun, error)
	// RequestTaskRunCancel records who asked a run to stop, and when, on a run
	// that has not reached a terminal status. Returns false when the run is
	// already terminal or already carries a request, so a second cancel
	// neither resets the clock the backstop measures nor overwrites the name
	// of whoever asked first.
	RequestTaskRunCancel(ctx context.Context, taskRunID, requestedBy string, requestedAt time.Time) (bool, error)
	// ClaimTaskRun atomically updates a run when current status matches ExpectedStatus.
	ClaimTaskRun(ctx context.Context, in ClaimTaskRunInput) (bool, error)
	// UpdateRun updates a run's status and optional fields.
	UpdateRun(ctx context.Context, in UpdateTaskRunInput) error
	UpdateTaskRunWorkerInfo(ctx context.Context, taskRunID, workerType string, k8sJobName *string, k8sJobCreatedAt *time.Time) error
	// RecordTaskRunAgentRevision stores which agent definition a run was given.
	// The first write wins: a run executes under the instructions it was handed
	// at dispatch, and a later edit does not retroactively change what ran.
	RecordTaskRunAgentRevision(ctx context.Context, taskRunID string, revision int) error
	// RecordTaskRunPluginPins stores the releases a run was given. Like the
	// agent revision, the first write wins: a worker polls its run, and a
	// team's activation edited mid-run must not rewrite what actually ran.
	RecordTaskRunPluginPins(ctx context.Context, taskRunID string, pins []PluginPin) error
	// OnRunComplete creates task_run_artifact rows (one per relativePath) and updates task denormalized fields. Use for SUCCEEDED runs.
	OnRunComplete(ctx context.Context, taskRunID string, relativePaths []string) error
	// SyncTaskFromRun updates task denormalized fields and last_run_id from the run (no output). Use for FAILED runs.
	SyncTaskFromRun(ctx context.Context, taskRunID string) error
}

TaskRunStore provides task run persistence.

type TaskRunTerminalInfo

type TaskRunTerminalInfo struct {
	TaskRunID      string
	TaskID         string
	ConversationID string
	// TeamID is the team that owns the task. Empty on a task created before
	// tasks carried one, which is why UserID is still here to fall back to.
	TeamID       string
	UserID       string
	Status       string
	Output       *string
	ErrorMessage *string
}

TaskRunTerminalInfo describes a task run that reached a terminal state. Used by the workflow service to advance or finalize workflow step runs.

type TaskStore

type TaskStore interface {
	// ListTasksByConversation returns tasks in the conversation. order is "asc" (oldest first) or "desc" (latest first); default "desc".
	ListTasksByConversation(ctx context.Context, conversationID string, order string) ([]Task, error)
	// ListTasksByConversationPaginated returns tasks with optional executed_only filter, ordered by created_at DESC. total is total matching count.
	ListTasksByConversationPaginated(ctx context.Context, conversationID string, executedOnly bool, limit, offset int) ([]Task, int, error)
	ListTasksByIssue(ctx context.Context, issueID string, limit, offset int) ([]Task, int, error)
	GetTask(ctx context.Context, taskID string) (*Task, error)
	GetTaskBySessionID(ctx context.Context, sessionID string) (*Task, error)
	// CreateTask creates a new task and its first TaskRun (input, title, PENDING). Returns the task with last_run_id set.
	CreateTask(ctx context.Context, in *CreateTaskInput) (*Task, error)
	UpdateTask(ctx context.Context, in UpdateTaskInput) error
	ClaimTask(ctx context.Context, in ClaimTaskInput) (updated bool, err error)
}

TaskStore provides task persistence. Tasks belong to a conversation. CreateTask creates a task plus its first TaskRun (both in one transaction).

type Team

type Team struct {
	ID                string  `json:"id"`
	Name              string  `json:"name"`
	PersonalForUserID *string `json:"personal_for_user_id,omitempty"`
	QuotaTier         string  `json:"quota_tier,omitempty"`
	// PluginCuration is who fills this team's plugin activation list; empty
	// reads as PluginCurationOpen. See plugin_activation.go.
	PluginCuration PluginCuration `json:"plugin_curation,omitempty"`
	CreatedBy      string         `json:"created_by"`
	CreatedAt      time.Time      `json:"created_at"`
	UpdatedAt      time.Time      `json:"updated_at"`
}

Team is the ownership and collaboration boundary for working resources. A user's default personal team is represented by personal_for_user_id.

type TeamMember

type TeamMember struct {
	TeamID    string    `json:"team_id"`
	UserID    string    `json:"user_id"`
	Role      string    `json:"role"`
	CreatedAt time.Time `json:"created_at"`
}

TeamMember is one user's membership in a team.

type TeamStore

type TeamStore interface {
	// GetTeam returns the team by team_id, or (nil, nil) when not found.
	GetTeam(ctx context.Context, teamID string) (*Team, error)
	// GetPersonalTeamByUser returns the default personal team for the user, or (nil, nil) when not found.
	GetPersonalTeamByUser(ctx context.Context, userID string) (*Team, error)
	// ListTeamsByUser returns all teams the user belongs to, ordered by created_at ASC.
	ListTeamsByUser(ctx context.Context, userID string) ([]Team, error)
	// CreateTeam creates a new team and owner membership.
	CreateTeam(ctx context.Context, name, createdBy, quotaTier string) (*Team, error)
	// AddTeamMember adds or updates a team membership.
	AddTeamMember(ctx context.Context, teamID, userID, role string) (*TeamMember, error)
	// RemoveTeamMember removes one membership from a team.
	RemoveTeamMember(ctx context.Context, teamID, userID string) error
	// ListTeamMembers returns members of the team ordered by created_at ASC.
	ListTeamMembers(ctx context.Context, teamID string) ([]TeamMember, error)
	// ListAllTeams returns every team newest first, with the total count. A
	// non-empty query filters on name as a substring.
	//
	// It is the one method here that ignores membership, so only
	// deployment-scoped callers may reach it. It returns teams, never their
	// contents: an administrator learns that a team exists and how large it is,
	// not what is in it.
	ListAllTeams(ctx context.Context, query string, limit, offset int) ([]Team, int, error)
	// CountTeamMembers returns member counts for the given teams, keyed by
	// team id. It exists so listing teams is two queries rather than one per
	// row.
	CountTeamMembers(ctx context.Context, teamIDs []string) (map[string]int, error)
	// SetTeamPluginCuration records who fills the team's plugin activation
	// list, or returns ErrNotFound. The value is validated above this layer.
	SetTeamPluginCuration(ctx context.Context, teamID string, mode PluginCuration) error
}

TeamStore provides team persistence and membership lookup.

type UpdateAgentInput

type UpdateAgentInput struct {
	AgentID string
	TeamID  string
	// UpdatedBy is taken because a team agent is edited by whoever holds the
	// permission, not only by its owner, and a revision that cannot name its
	// author is not much of a record.
	UpdatedBy string
	Def       AgentDefinition
}

type UpdateIssueInput

type UpdateIssueInput struct {
	Title         *string
	Description   *string
	Status        *string
	AssigneeKind  *string
	AssigneeID    *string
	ParentIssueID *string
}

type UpdatePluginInput

type UpdatePluginInput struct {
	DisplayName string
	Description string
}

UpdatePluginInput changes the display metadata of an entry. The name is not here: it identifies the plugin every installed copy came from.

type UpdateTaskInput

type UpdateTaskInput struct {
	TaskID       string
	Status       string
	StartedAt    *time.Time
	EndedAt      *time.Time
	Output       *string
	ErrorMessage *string
	SessionID    *string
}

UpdateTaskInput updates a task to the given status with optional fields.

type UpdateTaskRunInput

type UpdateTaskRunInput struct {
	TaskRunID        string
	Status           RunStatus
	StartedAt        *time.Time
	EndedAt          *time.Time
	Output           *string
	ErrorMessage     *string
	SessionID        *string
	PromptTokens     *int
	CompletionTokens *int
	TracePath        *string
}

UpdateTaskRunInput updates a run to the given status with optional fields.

type UpdateWorkflowInput

type UpdateWorkflowInput struct {
	Name        *string
	Description *string
	Definition  *string
	Status      *string
	// UpdatedBy is recorded as the author of the revision this update appends.
	UpdatedBy string
}

type UpdateWorkflowRunInput

type UpdateWorkflowRunInput struct {
	Status       string
	StartedAt    *time.Time
	EndedAt      *time.Time
	ErrorMessage *string
}

type UpdateWorkflowStepRunInput

type UpdateWorkflowStepRunInput struct {
	Status        *string
	TaskID        *string
	TaskRunID     *string
	OutputSummary *string
	ErrorMessage  *string
	StartedAt     *time.Time
	EndedAt       *time.Time
}

type UsageInWindowReader

type UsageInWindowReader interface {
	// TeamUsageInWindow returns run count and total tokens for the team in [sinceUnix, untilUnix].
	TeamUsageInWindow(ctx context.Context, teamID string, since, until time.Time) (runCount, totalTokens int, err error)
}

UsageInWindowReader provides usage aggregation for a team in a time window.

type User

type User struct {
	ID                string     `json:"id"`
	Email             string     `json:"email"`
	Name              string     `json:"name"`
	QuotaTier         string     `json:"quota_tier,omitempty"`
	LastLoginAt       *time.Time `json:"last_login_at,omitempty"`
	LastLoginPlatform *string    `json:"last_login_platform,omitempty"`
	CreatedAt         time.Time  `json:"created_at"`
	// HasPassword reports whether this account can sign in with a password. The
	// hash itself never travels on this struct — see PasswordStore — so that no
	// handler can serialize it into a response by accident.
	HasPassword bool `json:"has_password"`
	// DisabledAt is nil for an ordinary account. Non-nil means every credential
	// this account holds is refused: password, login code, refresh token, the
	// access token it already has, and its webhook keys. Disabling is not
	// deletion — nothing is removed, and enabling reverses the state and
	// nothing else. See docs/design/system-administration.md section 8.
	DisabledAt *time.Time `json:"disabled_at,omitempty"`
}

User is the user model. JSON uses snake_case per project convention. Internal numeric ID is retained for compatibility but is not part of the public API.

func (User) Disabled

func (u User) Disabled() bool

Disabled reports whether the account is currently refused.

type UserStore

type UserStore interface {
	// UserByEmail matches the address without regard to case, and returns
	// (nil, nil) when nobody has it. Login resolves the account this way and
	// compares nothing afterwards, so a case-sensitive implementation would
	// refuse people whose address is stored in another case.
	UserByEmail(ctx context.Context, email string) (*User, error)
	// GetUser returns the user by user_id, or (nil, nil) when not found.
	GetUser(ctx context.Context, userID string) (*User, error)
	// CreateUser creates a user with the given email. defaultQuotaTier is applied when non-empty. Returns ErrEmailExists if the email is already registered.
	CreateUser(ctx context.Context, email string, defaultQuotaTier string) (*User, error)
	// UpdateLoginMeta records the last login timestamp and platform for the user.
	UpdateLoginMeta(ctx context.Context, userID string, loginAt time.Time, platform string) error
	// ListUsers returns accounts newest first with the total count. A non-empty
	// query filters on email as a substring.
	ListUsers(ctx context.Context, query string, limit, offset int) ([]User, int, error)
	// SetUserDisabled disables the account at the given time, or enables it
	// when disabledAt is nil. Returns ErrUserNotFound when there is no such
	// account.
	SetUserDisabled(ctx context.Context, userID string, disabledAt *time.Time) error
}

UserStore looks up users by email and creates new users.

type UserWebhookKey

type UserWebhookKey struct {
	ID        string    `json:"id"`
	UserID    string    `json:"user_id"`
	KeyHash   string    `json:"-"` // SHA256 hex of plaintext key
	Name      string    `json:"name,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

UserWebhookKey is a webhook API key for a user. Plaintext key is returned only at creation; only key_hash is stored. JSON uses snake_case per project convention.

type UserWebhookKeyStore

type UserWebhookKeyStore interface {
	// CreateKey creates a new webhook key for the user. Returns plaintext key (e.g. whsec_...) and key_id. Caller must store plaintext securely; it is not persisted.
	CreateKey(ctx context.Context, userID, name string) (plaintextKey, keyID string, err error)
	// GetUserIDByKey looks up the user_id for the given plaintext key. Returns empty string if not found.
	GetUserIDByKey(ctx context.Context, plaintextKey string) (userID string, err error)
	// ListKeys returns key metadata for the user (no plaintext).
	ListKeys(ctx context.Context, userID string) ([]WebhookKeyMeta, error)
	// RevokeKey deletes the key by keyID if it belongs to the user.
	RevokeKey(ctx context.Context, userID, keyID string) error
}

UserWebhookKeyStore provides per-user webhook API key persistence. Keys are stored by hash; plaintext is returned only from CreateKey.

type WebhookKeyMeta

type WebhookKeyMeta struct {
	KeyID     string    `json:"key_id"`
	Name      string    `json:"name,omitempty"`
	CreatedAt time.Time `json:"created_at"`
}

WebhookKeyMeta is key metadata returned by ListKeys (no plaintext).

type Workflow

type Workflow struct {
	ID          string `json:"id"`
	TeamID      string `json:"team_id"`
	Name        string `json:"name"`
	Description string `json:"description"`
	Definition  string `json:"definition"`
	Status      string `json:"status"`
	// Revision numbers the workflow_revision row holding this content. It
	// starts at 1 and advances every time the name, description, definition,
	// or status changes.
	Revision  int       `json:"revision"`
	CreatedBy string    `json:"created_by"`
	CreatedAt time.Time `json:"created_at"`
	UpdatedAt time.Time `json:"updated_at"`
}

Workflow is a reusable team-scoped execution plan.

type WorkflowDefinition

type WorkflowDefinition struct {
	Steps []WorkflowDefinitionStep `json:"steps"`
}

WorkflowDefinition is the parsed structure of a workflow definition JSON.

type WorkflowDefinitionStep

type WorkflowDefinitionStep struct {
	StepID        string `json:"step_id"`
	Type          string `json:"type"`
	TargetAgentID string `json:"target_agent_id"`
	Prompt        string `json:"prompt"`
}

WorkflowDefinitionStep describes one step in a workflow definition.

type WorkflowRevision

type WorkflowRevision struct {
	WorkflowID  string    `json:"workflow_id"`
	Revision    int       `json:"revision"`
	Name        string    `json:"name"`
	Description string    `json:"description"`
	Definition  string    `json:"definition"`
	Status      string    `json:"status"`
	CreatedBy   string    `json:"created_by"`
	CreatedAt   time.Time `json:"created_at"`
}

WorkflowRevision is one recorded version of a workflow.

Revisions are append-only: an edit adds one, nothing rewrites or deletes one, and restoring an older revision is itself an edit that appends a new one.

type WorkflowRun

type WorkflowRun struct {
	ID         string `json:"id"`
	WorkflowID string `json:"workflow_id"`
	// WorkflowRevision is the workflow revision this run expanded. It is 0 for
	// runs started before workflows recorded revisions.
	WorkflowRevision int        `json:"workflow_revision,omitempty"`
	IssueID          *string    `json:"issue_id,omitempty"`
	ConversationID   string     `json:"conversation_id"`
	Status           string     `json:"status"`
	CreatedBy        string     `json:"created_by"`
	CreatedAt        time.Time  `json:"created_at"`
	StartedAt        *time.Time `json:"started_at,omitempty"`
	EndedAt          *time.Time `json:"ended_at,omitempty"`
	ErrorMessage     *string    `json:"error_message,omitempty"`
}

WorkflowRun is one execution attempt of a workflow.

type WorkflowStepRun

type WorkflowStepRun struct {
	ID            string  `json:"id"`
	WorkflowRunID string  `json:"workflow_run_id"`
	StepID        string  `json:"step_id"`
	StepIndex     int     `json:"step_index"`
	StepType      string  `json:"step_type"`
	TargetAgentID *string `json:"target_agent_id,omitempty"`
	// AgentName, AgentDescription, and AgentInstructions capture the target agent
	// definition as it was when the run started, so later edits to the agent cannot
	// change what a step in flight sends to the model.
	AgentName         string     `json:"agent_name,omitempty"`
	AgentDescription  string     `json:"agent_description,omitempty"`
	AgentInstructions string     `json:"agent_instructions,omitempty"`
	AgentRevision     int        `json:"agent_revision,omitempty"`
	Prompt            string     `json:"prompt"`
	Status            string     `json:"status"`
	TaskID            *string    `json:"task_id,omitempty"`
	TaskRunID         *string    `json:"task_run_id,omitempty"`
	OutputSummary     *string    `json:"output_summary,omitempty"`
	ErrorMessage      *string    `json:"error_message,omitempty"`
	CreatedAt         time.Time  `json:"created_at"`
	StartedAt         *time.Time `json:"started_at,omitempty"`
	EndedAt           *time.Time `json:"ended_at,omitempty"`
}

WorkflowStepRun is one durable step execution record under a workflow run.

type WorkflowStore

type WorkflowStore interface {
	ListWorkflowsByTeam(ctx context.Context, teamID string) ([]Workflow, error)
	CreateWorkflow(ctx context.Context, teamID, createdBy, name, description, definition string) (*Workflow, error)
	GetWorkflow(ctx context.Context, workflowID string) (*Workflow, error)
	UpdateWorkflow(ctx context.Context, workflowID, teamID string, in UpdateWorkflowInput) (*Workflow, error)
	CreateWorkflowRun(ctx context.Context, in CreateWorkflowRunInput) (*WorkflowRun, error)
	ListWorkflowRunsByWorkflow(ctx context.Context, workflowID string, limit, offset int) ([]WorkflowRun, int, error)
	ListWorkflowRunsByIssue(ctx context.Context, issueID string, limit, offset int) ([]WorkflowRun, int, error)
	GetWorkflowRun(ctx context.Context, workflowRunID string) (*WorkflowRun, error)
	ListWorkflowStepRuns(ctx context.Context, workflowRunID string) ([]WorkflowStepRun, error)
	CreateWorkflowStepRuns(ctx context.Context, workflowRunID string, steps []CreateWorkflowStepRunInput) ([]WorkflowStepRun, error)
	UpdateWorkflowRun(ctx context.Context, workflowRunID string, in UpdateWorkflowRunInput) (*WorkflowRun, error)
	UpdateWorkflowStepRun(ctx context.Context, stepRunID string, in UpdateWorkflowStepRunInput) (*WorkflowStepRun, error)
	GetWorkflowStepRunByTaskID(ctx context.Context, taskID string) (*WorkflowStepRun, error)
	GetWorkflowStepRunByTaskRunID(ctx context.Context, taskRunID string) (*WorkflowStepRun, error)
	// ListWorkflowRevisions returns a workflow's revisions, newest first, with
	// the total count.
	ListWorkflowRevisions(ctx context.Context, workflowID string, limit, offset int) ([]WorkflowRevision, int, error)
	// GetWorkflowRevision returns one revision, or nil when the workflow has no
	// such revision number.
	GetWorkflowRevision(ctx context.Context, workflowID string, revision int) (*WorkflowRevision, error)
}

WorkflowStore provides workflow and workflow execution persistence.

Jump to

Keyboard shortcuts

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