model

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 28, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// ExecutorKindSingleLoop is a single gollem ReAct loop (Job
	// strategy=simple, and the casebound mention host, which runs gollem
	// directly).
	ExecutorKindSingleLoop = "single_loop"
	// ExecutorKindPlanexec is the plan-and-execute runtime (Job
	// strategy=planexec, and the threadcase mention host).
	ExecutorKindPlanexec = "plan_execute"
)

ExecutorKind values recorded on JobRunLog.ExecutorKind. They identify which runtime drove a run so a consumer can tell a single-loop turn from a plan-execute one without re-deriving it.

View Source
const (
	// MaxClaimLength is the maximum rune length of the Markdown claim body.
	MaxClaimLength = 8000
	// MaxTags is the maximum number of tag references on a single Knowledge entry.
	MaxTags = 50
)

Knowledge invariants. These are domain constants (not deployment-tunable configuration): they bound a single shared-knowledge entry so a runaway agent cannot write unbounded documents. The defaults live here as exported constants rather than hidden inside Validate so callers and tests reference the same source of truth.

View Source
const (
	FieldIDKey      = "field_id"
	ExpectedTypeKey = "expected_type"
	ActualTypeKey   = "actual_type"
	OptionIDKey     = "option_id"
	FieldValueKey   = "field_value"
)

Context keys for error values

View Source
const AgentAdditionalPromptMaxLen = 16384

AgentAdditionalPromptMaxLen caps the per-Case agent additional prompt length (UTF-8 bytes). The cap protects callers (LLM context window, Firestore doc size) and is enforced at the persistence boundary.

View Source
const CaseProposalTTL = 24 * time.Hour

CaseProposalTTL is the lifetime of a draft after creation.

View Source
const EmbeddingDimension = 768

EmbeddingDimension is the dimension of the embedding vector produced by the configured EmbedClient. The default model (Gemini text-embedding-004) emits 768-dimensional vectors. The constant is preserved for the upcoming similarity-search redesign that will reintroduce vector-backed features.

View Source
const EventTypeMention = "mention"

EventTypeMention is the JobRunLog.EventType provenance value for a mention-triggered run (a Slack mention in an existing case, handled by the casebound / threadcase hosts). Such runs are NOT TOML-configured Jobs, but they persist through the same JobRunLog schema so the case agent page lists them alongside real Job runs. EventType is the discriminator that tells a mention run apart from the "case" / "scheduled" JobEventDomain values the Job runner writes — each mention turn gets its own fresh JobID, so there is no reserved sentinel to match on.

View Source
const MaxInlineBytes = 800 * 1024

MaxInlineBytes is the soft cap on any single string/JSON payload field stored in Firestore (well under the 1 MiB doc limit). Payloads longer than this are truncated from the tail before persisting; this log is for rough flow tracing, not exact reproduction.

View Source
const MaxTagNameLength = 64

MaxTagNameLength is the maximum rune length of a Tag.Name.

View Source
const SystemActorID = "@system"

SystemActorID is the sentinel ActorUserID used when a Job's tool invokes a mutation on behalf of the system (no human user). The '@' prefix ensures the value never collides with Slack user IDs (which always start with 'U' or 'W').

Variables

View Source
var (
	ErrImportSessionMissingID            = goerr.New("import session has no ID")
	ErrImportSessionMissingWorkspace     = goerr.New("import session has no workspace ID")
	ErrImportSessionMissingCreator       = goerr.New("import session has no creator user ID")
	ErrImportSessionInvalidStatus        = goerr.New("import session has invalid status")
	ErrImportSessionInvalidSnapshot      = goerr.New("import session snapshot is invalid")
	ErrImportSessionInvalidIssueSeverity = goerr.New("import session issue has invalid severity")
	ErrImportSessionInvalidItemStatus    = goerr.New("import session item has invalid status")
)

Sentinel errors. Repositories MUST call Validate before every write so that an upstream bug (e.g. forgetting to populate CreatorUserID after auth.ContextWithToken) fails loudly at the persistence boundary instead of silently producing an unattributable record.

View Source
var (
	ErrInvalidFieldType = goerr.New("invalid field type")
	ErrInvalidOptionID  = goerr.New("invalid option ID")
	ErrMissingRequired  = goerr.New("required field is missing")
	// ErrCaseFieldValidation wraps the aggregated, non-fail-fast validation
	// result produced by ValidateCaseFieldsAll. Its message lists every
	// violation (one per line) so the caller can feed the complete set back
	// to an LLM in a single round.
	ErrCaseFieldValidation = goerr.New("case field validation failed")
	// ErrUnknownFieldID is reported by ValidateCaseFieldsAll when a supplied
	// field id is not present in the workspace schema. (The fail-fast
	// ValidateCaseFields preserves unknown fields for forward compatibility;
	// the create path rejects them instead of silently dropping data.)
	ErrUnknownFieldID = goerr.New("unknown field ID")
)

Validation errors

View Source
var ErrActionEventValidation = goerr.New("action event validation failed")

ErrActionEventValidation is returned when an ActionEvent fails its persistence-boundary invariants.

View Source
var ErrActionStepValidation = goerr.New("action step validation failed")

ErrActionStepValidation is returned when an ActionStep fails its persistence-boundary invariants.

View Source
var ErrActionValidation = goerr.New("action validation failed")

ErrActionValidation is returned when an Action fails its persistence-boundary invariants.

View Source
var ErrAssistLogValidation = goerr.New("assist log validation failed")

ErrAssistLogValidation is returned when an AssistLog fails its persistence-boundary invariants.

View Source
var ErrCaseAgentPromptTooLong = goerr.New("case agent additional prompt is too long")

ErrCaseAgentPromptTooLong is returned by Case.Validate when the Case-specific agent additional prompt exceeds AgentAdditionalPromptMaxLen.

View Source
var ErrCaseMissingReporter = goerr.New("case has no reporter")

ErrCaseMissingReporter is returned by Case.ValidateNew when a channel-mode case is about to be created without a reporter. A channel-mode case originates from some authenticated user (Web cookie, Slack interactivity callback, no-auth dev-mode token); losing that identity later means the Cases / Drafts UI shows an empty Reporter column and Slack invites have nobody to add as the channel creator, so repositories enforce it at the Create boundary. Thread-mode cases (SlackThreadTS set) are exempt: a channel-root intake post relayed by an integration bot may name no human, so an empty ReporterID is a legitimate state there.

View Source
var ErrCaseNotDraft = goerr.New("case is not a draft")

ErrCaseNotDraft is returned when an operation requires a case to be in DRAFT status but the case is already submitted (OPEN/CLOSED) or otherwise not a draft.

View Source
var ErrHomeMessageValidation = goerr.New("home message validation failed")

ErrHomeMessageValidation is returned when a HomeMessage fails validation.

View Source
var ErrInvalidGitHubRepo = goerr.New("invalid GitHub repository")

ErrInvalidGitHubRepo is returned when the input cannot be parsed as a GitHub repository

View Source
var ErrInvalidNotionID = goerr.New("invalid Notion ID")

ErrInvalidNotionID is returned when the input cannot be parsed as a Notion ID

View Source
var ErrInvalidWorkspaceGroup = goerr.New("invalid workspace group")

ErrInvalidWorkspaceGroup is returned when a group violates its core invariant (an empty ID). Richer validation (ID pattern, cross-file duplicate IDs, member existence) lives in the config layer; this is the last-line guard so a code path that builds a group with no identity fails loudly before Register.

View Source
var ErrKnowledgeValidation = goerr.New("knowledge validation failed")

ErrKnowledgeValidation is returned when a Knowledge fails its structural invariants.

View Source
var ErrMemoValidation = goerr.New("memo validation failed")

ErrMemoValidation is returned when a Memo fails its structural invariants.

View Source
var ErrNotificationSlotValidation = goerr.New("notification slot validation failed")

ErrNotificationSlotValidation is returned when a NotificationSlot fails its persistence-boundary invariants.

View Source
var ErrSessionValidation = goerr.New("session validation failed")

ErrSessionValidation is returned when a Session fails its persistence-boundary invariants.

View Source
var ErrSlackUserValidation = goerr.New("slack user validation failed")

ErrSlackUserValidation is returned when a SlackUser fails its persistence-boundary invariants.

View Source
var ErrSourceValidation = goerr.New("source validation failed")

ErrSourceValidation is returned when a Source fails its persistence-boundary invariants.

View Source
var ErrTagValidation = goerr.New("tag validation failed")

ErrTagValidation is returned when a Tag fails its structural invariants.

View Source
var ErrUserPreferenceValidation = goerr.New("user preference validation failed")

ErrUserPreferenceValidation is returned when a UserPreference fails validation.

View Source
var ErrWorkspaceGroupNotFound = goerr.New("workspace group not found")

ErrWorkspaceGroupNotFound is returned when a group is not found in the registry.

View Source
var ErrWorkspaceNotFound = goerr.New("workspace not found")

ErrWorkspaceNotFound is returned when a workspace is not found in the registry

Functions

func CoerceFieldInputs

func CoerceFieldInputs(schema *config.FieldSchema, in []FieldInput) (map[string]FieldValue, []string)

CoerceFieldInputs maps FieldInput entries into FieldValue keyed by field id, resolving each field's storage shape from the schema (multi -> []string, number -> float64). It performs TYPE COERCION ONLY, not business validation: an unknown field id is passed through with its raw value and Type unset so the downstream validator (ValidateCaseFieldsPartialStrict / ...All) reports the schema violation with a single consistent message. An unparseable number is the one shape the coercion cannot represent, so it is returned as a violation string (rather than written as a string the storage layer would reject) giving the caller a precise "must be a number" hint.

Empty scalar values are intentionally PRESERVED (as an explicit "set to empty") — callers that must instead drop empties (e.g. the materialize path guarding against clobbering an existing value) do their own filtering and do not route through this helper.

func IsCaseAccessible

func IsCaseAccessible(c *Case, userID string) bool

IsCaseAccessible checks if a user has access to a case. Non-private cases are always accessible. Private cases are accessible only if the userID is in ChannelUserIDs.

func IsSystemActor

func IsSystemActor(actorUserID string) bool

IsSystemActor reports whether the given actor user ID is the system sentinel. Permission checks that key off a Slack user identity must skip for system actor — there is no human to authenticate as.

func IsValidImportIssueSeverity

func IsValidImportIssueSeverity(s ImportIssueSeverity) bool

IsValidImportIssueSeverity reports whether s is a known severity.

func IsValidImportItemResultStatus

func IsValidImportItemResultStatus(s ImportItemResultStatus) bool

IsValidImportItemResultStatus reports whether s is a known per-item result status.

func IsValidImportSessionStatus

func IsValidImportSessionStatus(s ImportSessionStatus) bool

IsValidImportSessionStatus reports whether s is a known status value.

func ParseGitHubRepo

func ParseGitHubRepo(input string) (owner, repo string, err error)

ParseGitHubRepo extracts owner and repo from either "owner/repo" or a GitHub URL. Accepted formats:

func ParseNotionID

func ParseNotionID(input string) (string, error)

ParseNotionID extracts a Notion ID from either a raw ID or a Notion URL. The returned ID is always in UUID format (8-4-4-4-12) as required by the Notion API. Accepted formats:

Types

type Action

type Action struct {
	ID             int64
	CaseID         int64 // Required: Action must be associated with a Case (1:n relationship)
	Title          string
	Description    string
	AssigneeID     string // Slack User ID; empty string means unassigned
	SlackMessageTS string // Optional: Slack message ID (timestamp)
	Status         types.ActionStatus
	DueDate        *time.Time // Optional: deadline for the action
	ArchivedAt     *time.Time // nil = active; non-nil = archived at the given time
	CreatedAt      time.Time
	UpdatedAt      time.Time
}

Action represents a task/action associated with a case

func (*Action) IsArchived

func (a *Action) IsArchived() bool

IsArchived reports whether the action is currently archived.

func (*Action) Validate

func (a *Action) Validate() error

Validate enforces the invariants required before any persistence write. The repository assigns the storage ID; the caller sets CaseID (the 1:n link to the owning Case) before the write, so an orphan Action (CaseID == 0) fails loudly here instead of landing in storage.

type ActionEvent

type ActionEvent struct {
	ID        string                // unique within the action
	ActionID  int64                 // parent action id
	Kind      types.ActionEventKind // CREATED / TITLE_CHANGED / STATUS_CHANGED / ASSIGNEE_CHANGED
	ActorID   string                // Slack user id of the person who triggered the change ("" = system)
	OldValue  string                // old field value rendered as a string (empty for CREATED)
	NewValue  string                // new field value rendered as a string
	CreatedAt time.Time
}

ActionEvent is one entry in an Action's structural change history. Used to render the WebUI activity feed (creation, title / status / assignee edits). Messages-from-Slack go through ActionMessageRepository instead.

func (*ActionEvent) Validate

func (e *ActionEvent) Validate() error

Validate enforces the invariants required before any persistence write. The caller supplies both the entry ID (unique within the action) and the parent ActionID, so a history entry with no parent or no ID fails loudly here instead of landing in storage.

type ActionStatusColorPreset

type ActionStatusColorPreset string

ActionStatusColorPreset is a semantic preset name. The actual color value is resolved on the frontend; the backend only validates membership.

const (
	ActionStatusColorIdle        ActionStatusColorPreset = "idle"
	ActionStatusColorActive      ActionStatusColorPreset = "active"
	ActionStatusColorWaiting     ActionStatusColorPreset = "waiting"
	ActionStatusColorPaused      ActionStatusColorPreset = "paused"
	ActionStatusColorAttention   ActionStatusColorPreset = "attention"
	ActionStatusColorBlocked     ActionStatusColorPreset = "blocked"
	ActionStatusColorSuccess     ActionStatusColorPreset = "success"
	ActionStatusColorNeutralDone ActionStatusColorPreset = "neutral_done"
	ActionStatusColorFailure     ActionStatusColorPreset = "failure"
)

type ActionStatusDefinition

type ActionStatusDefinition struct {
	ID          string
	Name        string
	Description string
	Color       string // either a preset name (lowercase) or "#RRGGBB" / "#RGB"
	Emoji       string
}

ActionStatusDefinition describes a single status definable per workspace.

func (ActionStatusDefinition) SlackColor

func (d ActionStatusDefinition) SlackColor() string

SlackColor returns a Slack-compatible color string for this status (the value used by `attachment.color`). Hex values stored in `Color` pass through verbatim; preset names are resolved via `actionStatusPresetHex`. Empty or unrecognized values return `""`, which Slack treats as "no color bar".

type ActionStatusSet

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

ActionStatusSet is the resolved per-workspace status configuration. It is constructed once at startup and treated as immutable thereafter.

func DefaultActionStatusSet

func DefaultActionStatusSet() *ActionStatusSet

DefaultActionStatusSet returns the legacy 5-status set used when a workspace does not define its own statuses. Kept here so storage values from before configurable statuses (`"BACKLOG"`, `"TODO"`, ...) keep working unchanged.

func NewActionStatusSet

func NewActionStatusSet(initialID string, closedIDs []string, statuses []ActionStatusDefinition) (*ActionStatusSet, error)

NewActionStatusSet constructs an ActionStatusSet and validates the input.

func (*ActionStatusSet) ClosedIDs

func (s *ActionStatusSet) ClosedIDs() []string

ClosedIDs returns the IDs flagged as closed, in insertion order of `statuses`.

func (*ActionStatusSet) Emoji

func (s *ActionStatusSet) Emoji(id string) string

Emoji returns the emoji for the given id, or a fallback if unknown / empty.

func (*ActionStatusSet) Get

Get returns the definition for the given id.

func (*ActionStatusSet) IDs

func (s *ActionStatusSet) IDs() []string

IDs returns all status ids in declaration order.

func (*ActionStatusSet) Initial

Initial returns the initial status definition.

func (*ActionStatusSet) InitialID

func (s *ActionStatusSet) InitialID() string

InitialID returns the initial status ID.

func (*ActionStatusSet) IsClosed

func (s *ActionStatusSet) IsClosed(id string) bool

IsClosed reports whether the given status id is configured as closed.

func (*ActionStatusSet) IsValid

func (s *ActionStatusSet) IsValid(id string) bool

IsValid reports whether the given id matches a defined status.

func (*ActionStatusSet) Statuses

func (s *ActionStatusSet) Statuses() []ActionStatusDefinition

Statuses returns a copy of all status definitions in declaration order.

type ActionStep

type ActionStep struct {
	ID        string
	ActionID  int64
	Title     string
	DoneAt    *time.Time
	DoneBy    string
	CreatedBy string
	CreatedAt time.Time
	UpdatedAt time.Time
}

ActionStep is a small, binary-state work item that lives under an Action. Used to track granular progress toward completing an Action. The completion state is encoded by DoneAt: nil means ongoing, non-nil means done at the given time. There is no separate Done boolean to avoid the two-source-of- truth hazard that the Action.ArchivedAt / IsArchived pattern already established for archive state.

func (*ActionStep) IsDone

func (s *ActionStep) IsDone() bool

IsDone reports whether the step is currently marked done.

func (*ActionStep) Validate

func (s *ActionStep) Validate() error

Validate enforces the invariants required before any persistence write. The caller supplies both the step ID and the parent ActionID (the latter is the repository storage key), so a step with no parent or no ID fails loudly here instead of landing in storage.

type AssistLog

type AssistLog struct {
	ID        AssistLogID
	CaseID    int64
	Summary   string // One-line summary of this session
	Actions   string // What was done in this session (may be empty if nothing was done)
	Reasoning string // Rationale behind decisions made
	NextSteps string // Items to address in future sessions
	CreatedAt time.Time
}

AssistLog represents an execution log entry from the assist agent. After each assist session, the agent's actions are summarized and stored as a log for context in subsequent runs.

func (*AssistLog) Validate

func (l *AssistLog) Validate() error

Validate enforces the invariants required before any persistence write. The repository assigns the storage ID (NewAssistLogID when empty) and sets CaseID from its caseID argument, so callers MUST invoke this after that assignment; a log with no owning Case (CaseID == 0) fails loudly here instead of landing in storage.

type AssistLogID

type AssistLogID string

AssistLogID is a UUID-based identifier for AssistLog

func NewAssistLogID

func NewAssistLogID() AssistLogID

NewAssistLogID generates a new UUID v4 AssistLogID

type Case

type Case struct {
	ID             int64
	Title          string
	Description    string
	Status         types.CaseStatus
	ReporterID     string   // Slack User ID of the case reporter (immutable after creation)
	AssigneeIDs    []string // Slack User IDs
	SlackChannelID string

	// SlackThreadTS binds a thread-mode Case to its Slack thread. Empty for
	// channel-mode Cases (SlackChannelID is then a dedicated channel). For
	// thread-mode Cases SlackChannelID is the monitored channel and
	// SlackThreadTS is the thread parent timestamp. See CaseMode.
	SlackThreadTS string

	// BoardStatus is the configurable workflow status id (the Kanban column)
	// for thread-mode Cases, validated against the workspace's CaseStatusSet.
	// Empty for channel-mode Cases, where the configurable status attaches to
	// Actions instead. The lifecycle Status is kept in sync with BoardStatus
	// via SyncLifecycleFromBoardStatus (closed status id => CLOSED).
	BoardStatus string

	IsPrivate      bool                  // Private mode flag
	IsTest         bool                  // Marks a case filed for testing/verification, distinguished from production cases
	ChannelUserIDs []string              // Slack User IDs of channel members (synced for all cases)
	AccessDenied   bool                  // Runtime-only: set by UseCase when access is restricted (not persisted)
	FieldValues    map[string]FieldValue // key = FieldID
	RequestKey     string                // UUID for preventing duplicate case creation from Slack modals

	// AgentAdditionalPrompt is a Case-specific Markdown snippet that the
	// Job runner appends to the TOML-defined Job prompt at agent execution
	// time. Empty by default; capped at AgentAdditionalPromptMaxLen bytes
	// and enforced by Validate().
	AgentAdditionalPrompt string

	// AgentSourceIDs restricts which Sources the agent uses when running
	// against this Case. Empty (nil or len==0) means "use every Source
	// the agent would normally consider" — i.e. preserves the existing
	// default of all enabled Workspace Sources. Non-empty narrows the
	// set to exactly the listed IDs (unknown / disabled IDs are dropped
	// at use time, not at write time, so a Source toggled off later does
	// not invalidate the stored selection).
	AgentSourceIDs []SourceID

	CreatedAt time.Time
	UpdatedAt time.Time
}

Case represents a generic case/project entity.

Lifecycle (see types.CaseStatus): DRAFT → OPEN → CLOSED. A case in DRAFT is an "in-progress" entry saved from the Slack creation modal's Save as Draft button; it is visible only to its reporter and triggers none of the channel-binding / notification side effects of a real Case until SubmitDraft promotes it to OPEN.

func RestrictCase

func RestrictCase(c *Case) *Case

RestrictCase returns a copy of the case with sensitive fields removed. Only ID, Status, IsPrivate, CreatedAt, UpdatedAt are preserved. AccessDenied is set to true.

func (*Case) AssignUsers

func (c *Case) AssignUsers(userIDs []string) bool

AssignUsers adds the given Slack user IDs to AssigneeIDs as a set union: IDs already present are ignored, blank IDs are skipped, and genuinely new IDs are appended in input order so existing order is preserved. It reports whether the assignee set actually changed, letting callers skip a no-op write. This is the in-memory half of the atomic assign operation; the repository supplies the concurrency guarantee (transaction / lock).

func (*Case) IsDraft

func (c *Case) IsDraft() bool

IsDraft reports whether this Case is currently in the unsubmitted draft state.

func (*Case) IsThreadBound

func (c *Case) IsThreadBound() bool

IsThreadBound reports whether this Case is bound to a Slack thread (thread-mode). Channel-mode Cases have an empty SlackThreadTS.

func (*Case) SubmitDraft

func (c *Case) SubmitDraft() error

SubmitDraft transitions the case from DRAFT to OPEN in place. Returns an error if the case is not in DRAFT (callers must not silently no-op when promoting an already-submitted case). Persistence and any post-promotion side effects are the caller's responsibility.

func (*Case) SyncLifecycleFromBoardStatus

func (c *Case) SyncLifecycleFromBoardStatus(set *ActionStatusSet)

SyncLifecycleFromBoardStatus keeps the lifecycle Status consistent with the configurable BoardStatus for thread-mode Cases: a closed board status maps to CaseStatusClosed, any other to CaseStatusOpen. DRAFT cases are left untouched (thread-mode Cases are never drafts, but guard defensively). It is a no-op when set is nil or BoardStatus is empty.

func (*Case) UnassignUsers

func (c *Case) UnassignUsers(userIDs []string) bool

UnassignUsers removes the given Slack user IDs from AssigneeIDs, preserving the order of the IDs that remain. Removing an ID that is not present is a no-op. It reports whether the assignee set actually changed.

func (*Case) Validate

func (c *Case) Validate() error

Validate checks the basic invariants every persisted Case must satisfy. Repositories MUST call this before every write. For new cases (Create), call ValidateNew instead, which additionally enforces ReporterID.

func (*Case) ValidateNew

func (c *Case) ValidateNew() error

ValidateNew checks the invariants that must hold for a newly created Case. For channel-mode cases it additionally enforces ReporterID at creation time: that field is the channel creator and its silent loss (the canonical failure mode where a Slack handler forgot to inject auth.ContextWithToken) is caught at the persistence boundary. Thread-mode cases (SlackThreadTS set) are exempt — a channel-root intake post relayed by an integration bot may name no human, so an empty ReporterID is allowed (it is best-effort resolved from the post body; see usecase.HandleThreadCaseCreation). Repositories MUST call this instead of Validate for Create operations.

type CaseEventConfig

type CaseEventConfig struct {
	// On lists the lifecycle events the Job is subscribed to. Always a
	// normalised, non-empty slice produced by the config loader from the
	// TOML "on" field. The domain layer never accepts a single-string form
	// — see config/job.go for the parsing rules.
	On []CaseLifecycle
}

CaseEventConfig is the listen filter for the `case` event domain. A Job fires when the published Case event's CaseLifecycle is contained in On.

func (*CaseEventConfig) Matches

func (c *CaseEventConfig) Matches(lc CaseLifecycle) bool

Matches reports whether the given lifecycle event matches this config's On filter.

func (*CaseEventConfig) Validate

func (c *CaseEventConfig) Validate() error

Validate enforces invariants for the case event filter: - On must be non-empty - every value must be a known CaseLifecycle

type CaseLifecycle

type CaseLifecycle string

CaseLifecycle enumerates the case lifecycle events that a Job can listen to via `events.case.on` in TOML. Each value names a distinct, observable transition.

const (
	// CaseLifecycleCreated fires when a Case is newly created (regardless of
	// its initial status — DRAFT or OPEN both qualify).
	CaseLifecycleCreated CaseLifecycle = "created"
	// CaseLifecycleClosed fires when a Case's status transitions to CLOSED.
	CaseLifecycleClosed CaseLifecycle = "closed"
)

func AllCaseLifecycles

func AllCaseLifecycles() []CaseLifecycle

AllCaseLifecycles returns every valid CaseLifecycle for validation and documentation purposes.

func (CaseLifecycle) IsValid

func (l CaseLifecycle) IsValid() bool

IsValid reports whether the lifecycle value is one of the recognised enum members.

func (CaseLifecycle) String

func (l CaseLifecycle) String() string

String returns the string form for prompt rendering / logging.

type CaseMode

type CaseMode string

CaseMode selects how Cases bind to Slack within a workspace.

  • CaseModeChannel (default): one Case maps to a dedicated Slack channel created at case activation. This is the original behaviour.
  • CaseModeThread: the workspace monitors a single channel; each top-level human message in that channel becomes a Case bound to its thread. No dedicated channel is created, Actions and Drafts are not used, and the configurable status set ([case.status]) attaches to the Case itself.
const (
	// CaseModeChannel is the default channel-per-case mode.
	CaseModeChannel CaseMode = "channel"
	// CaseModeThread is the thread-per-case mode.
	CaseModeThread CaseMode = "thread"
)

func (CaseMode) IsThread

func (m CaseMode) IsThread() bool

IsThread reports whether this is the thread-per-case mode.

func (CaseMode) IsValid

func (m CaseMode) IsValid() bool

IsValid reports whether the mode is a recognised value. The empty string is not valid here; callers normalise empty to CaseModeChannel before use.

func (CaseMode) Normalize

func (m CaseMode) Normalize() CaseMode

Normalize maps the empty string to the default CaseModeChannel.

type CaseProposal

type CaseProposal struct {
	ID        CaseProposalID
	CreatedBy string // Slack user ID who triggered the mention
	CreatedAt time.Time
	ExpiresAt time.Time

	// (a) Source — workspace-agnostic, immutable after creation
	RawMessages []ProposalMessage
	MentionText string
	Source      ProposalSource

	// (b) Current AI materialization (overwritten on workspace switch)
	SelectedWorkspaceID string
	Materialization     *WorkspaceMaterialization

	// InferenceInProgress is the server-side guard against concurrent
	// Submit/Edit/WS-switch interactions while a Materializer call is running.
	InferenceInProgress bool

	// EphemeralChannelID and EphemeralMessageTS identify the originally posted
	// ephemeral message. Used by interaction handlers that lack a response_url
	// (e.g., view_submission flows) to update the ephemeral via chat APIs.
	EphemeralChannelID string
	EphemeralMessageTS string
}

CaseProposal holds the workspace-agnostic source material plus the current (single) AI-materialized Case payload for the currently-selected workspace. Switching workspace re-runs the Materializer and overwrites Materialization; no per-workspace cache is kept (intentional simplification).

func NewCaseProposal

func NewCaseProposal(now time.Time, createdBy string) *CaseProposal

NewCaseProposal constructs a fresh draft with a new ID, current timestamps, and a default 24h expiry. Caller must populate Source / RawMessages / MentionText / SelectedWorkspaceID before saving.

func (*CaseProposal) IsExpired

func (d *CaseProposal) IsExpired(now time.Time) bool

IsExpired reports whether the draft is past its ExpiresAt.

type CaseProposalID

type CaseProposalID string

CaseProposalID is a UUID-based identifier for CaseProposal

func NewCaseProposalID

func NewCaseProposalID() CaseProposalID

NewCaseProposalID generates a new UUID v4 CaseProposalID

func (CaseProposalID) String

func (id CaseProposalID) String() string

String returns the string representation

type CaseRef

type CaseRef struct {
	ID          int64
	Title       string
	Status      types.CaseStatus
	WorkspaceID string
}

CaseRef is the summary projection of a Case used by case_ref fields: the picker, the agent search tool, and value-label resolution. It carries only the fields safe to surface for a referenceable (non-private) Case and deliberately omits Description / Reporter / Assignees / FieldValues so a summary can never leak more than a title.

func NewCaseRef

func NewCaseRef(workspaceID string, c *Case) CaseRef

NewCaseRef builds a CaseRef summary for a Case in the given workspace.

type CaseTrigger

type CaseTrigger string

CaseTrigger selects what starts a Case in a thread-mode workspace.

  • CaseTriggerInstant (default): every channel-root post in the monitored channel starts a Case. This is the original thread-mode behaviour.
  • CaseTriggerMention: a Case is started only when the bot is @mentioned — either in a channel-root message or inside a thread that has no Case yet. A plain post (no mention) never starts a Case.

It is orthogonal to CaseMode and is only meaningful when CaseMode is thread; channel-mode workspaces ignore it.

const (
	// CaseTriggerInstant is the default: any channel-root post starts a Case.
	CaseTriggerInstant CaseTrigger = "instant"
	// CaseTriggerMention starts a Case only on an @mention of the bot.
	CaseTriggerMention CaseTrigger = "mention"
)

func (CaseTrigger) IsMention

func (t CaseTrigger) IsMention() bool

IsMention reports whether Cases are started only on an @mention.

func (CaseTrigger) IsValid

func (t CaseTrigger) IsValid() bool

IsValid reports whether the trigger is a recognised value. The empty string is not valid here; callers normalise empty to CaseTriggerInstant before use.

func (CaseTrigger) Normalize

func (t CaseTrigger) Normalize() CaseTrigger

Normalize maps the empty string to the default CaseTriggerInstant.

type FieldInput

type FieldInput struct {
	// FieldID references FieldDefinition.ID from the workspace schema.
	FieldID string
	// Value carries the scalar form (text / number / url / date / single
	// select option id / single user id).
	Value string
	// Values carries the multi form (multi-select option ids / multi-user ids).
	Values []string
}

FieldInput is the LLM-facing shape of one custom-field assignment: a field id plus either a scalar value or a list of values. It is the agent-boundary form that planners / tools emit before the value is coerced into a typed FieldValue.

type FieldValidator

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

FieldValidator validates field values against field schema

func NewFieldValidator

func NewFieldValidator(schema *config.FieldSchema) *FieldValidator

NewFieldValidator creates a new FieldValidator with the given schema

func (*FieldValidator) ValidateCaseFields

func (v *FieldValidator) ValidateCaseFields(fieldValues map[string]FieldValue) (map[string]FieldValue, error)

ValidateCaseFields validates field values for a Case and returns enriched values with Type injected from config. The input map is not modified; a new map is returned with Type set on each FieldValue.

func (*FieldValidator) ValidateCaseFieldsAll

func (v *FieldValidator) ValidateCaseFieldsAll(fieldValues map[string]FieldValue) (map[string]FieldValue, error)

ValidateCaseFieldsAll is the full-validation variant that does NOT fail fast. It walks every supplied value and every required field, accumulating all violations, and returns them as a single ErrCaseFieldValidation whose message lists every violation (one per line). Unlike the fail-fast ValidateCaseFields, an unknown field id is reported as a violation (not silently preserved) so the thread-mode create agent learns it referenced a field that does not exist. On success it returns the enriched values (Type injected) and a nil error.

func (*FieldValidator) ValidateCaseFieldsPartial

func (v *FieldValidator) ValidateCaseFieldsPartial(fieldValues map[string]FieldValue) (map[string]FieldValue, error)

ValidateCaseFieldsPartial is the partial-update variant: only the supplied field values are type-checked, and missing required fields do NOT fail. Use this for UpdateCase where the caller may submit a subset of fields and the remaining values are preserved untouched on the existing Case.

func (*FieldValidator) ValidateCaseFieldsPartialStrict

func (v *FieldValidator) ValidateCaseFieldsPartialStrict(fieldValues map[string]FieldValue) (map[string]FieldValue, error)

ValidateCaseFieldsPartialStrict is the agent-and-API boundary variant: like ValidateCaseFieldsPartial it does NOT require missing required fields (a partial update need not resubmit every field), but — unlike the lenient partial variant — an unknown field id is reported as a violation rather than silently preserved, and every violation is accumulated into a single ErrCaseFieldValidation so the caller can fix them in one shot. Use this for UpdateCase / MaterializeThreadCase where the submitted values come from an untrusted source (LLM / API client) that must not write a field id the workspace does not define.

type FieldValue

type FieldValue struct {
	FieldID types.FieldID   // References FieldDefinition.ID from configuration
	Type    types.FieldType // Field type (self-descriptive)
	Value   any             // Actual value (Go type depends on field type)
}

FieldValue represents a single custom field value embedded in a Case document. Each value carries its own Type for self-describing data.

func (FieldValue) IsValueInSet

func (fv FieldValue) IsValueInSet(fieldType types.FieldType, validSet map[string]bool) bool

IsValueInSet checks whether the field value is contained in the given valid set. For select fields, the value must be a string present in validSet. For multi-select fields, all elements must be strings present in validSet. For other field types, it always returns true.

type GitHubConfig

type GitHubConfig struct {
	Repositories []GitHubRepository
}

GitHubConfig holds GitHub specific configuration

type GitHubRepository

type GitHubRepository struct {
	Owner string
	Repo  string
}

GitHubRepository represents a GitHub repository reference

type HomeMessage

type HomeMessage struct {
	ID        HomeMessageID
	UserID    string // Slack User ID (auth token Sub). Required.
	Message   string // Generated one-liner. Required.
	Lang      string // Language at generation ("en"/"ja"); used for reuse matching.
	CreatedAt time.Time
}

HomeMessage is one LLM-generated home greeting for a user. Messages are append-only (never updated or TTL-deleted); freshness is judged in the usecase by comparing CreatedAt against a window. Keeping the history lets the generator avoid repeating recent phrasings.

func (*HomeMessage) Validate

func (m *HomeMessage) Validate() error

Validate enforces the invariants the repository relies on before every write.

type HomeMessageID

type HomeMessageID string

HomeMessageID identifies a stored home message. It is a UUID v7 so that document IDs and CreatedAt order agree (lexicographically time-ordered).

func NewHomeMessageID

func NewHomeMessageID() HomeMessageID

NewHomeMessageID mints a time-ordered home message ID.

func (HomeMessageID) String

func (id HomeMessageID) String() string

type ImportActionResult

type ImportActionResult struct {
	Status          ImportItemResultStatus
	CreatedActionID *int64
	Error           *ImportIssue
}

ImportActionResult is the per-Action execution outcome, parallel to ImportCaseResult.

type ImportCaseResult

type ImportCaseResult struct {
	Status        ImportItemResultStatus
	CreatedCaseID *int64
	Error         *ImportIssue
}

ImportCaseResult is the per-Case execution outcome. CreatedCaseID is set when Status == created; Error is set when Status == failed.

type ImportIssue

type ImportIssue struct {
	Path     string
	Message  string
	Severity ImportIssueSeverity
}

ImportIssue is a single validation or execution-time finding. Path is a JSONPath-like locator (e.g. "cases[0].fields.severity") used by the frontend to highlight the offending value. Message is the human- readable text, already translated to the user's language (the i18n boundary lives in the usecase that produces the issue).

type ImportIssueSeverity

type ImportIssueSeverity string

ImportIssueSeverity classifies a single ImportIssue. error blocks execute; warning is informational only.

const (
	ImportIssueError   ImportIssueSeverity = "error"
	ImportIssueWarning ImportIssueSeverity = "warning"
)

type ImportItemResultStatus

type ImportItemResultStatus string

ImportItemResultStatus represents the per-Case / per-Action execution result. pending = not yet executed; created = persisted successfully; failed = persistence attempted and failed; skipped = not attempted because an earlier item in the same execute call failed.

const (
	ImportItemPending ImportItemResultStatus = "pending"
	ImportItemCreated ImportItemResultStatus = "created"
	ImportItemFailed  ImportItemResultStatus = "failed"
	ImportItemSkipped ImportItemResultStatus = "skipped"
)

type ImportSession

type ImportSession struct {
	ID            ImportSessionID
	WorkspaceID   string
	CreatorUserID string // Slack User ID of the creator (immutable)
	Status        ImportSessionStatus

	Source   ImportSource   // Original file metadata
	Snapshot ImportSnapshot // Normalized payload (per-Case result is stored here)
	Issues   []ImportIssue  // Session-level issues (parse failure, version, schema stale, etc.)

	// FieldSchemaHash is a digest of the workspace field schema captured at
	// createCaseImport time. executeCaseImport compares against the current
	// hash and refuses to run if it differs (callers must create a new import).
	FieldSchemaHash string

	CreatedAt time.Time
	UpdatedAt time.Time

	// ExecutedAt is set when status transitions to applied or failed.
	ExecutedAt *time.Time

	// Aggregate counts over Snapshot.Cases[*].Result.Status. Recomputed
	// by the usecase on every Update so they stay consistent.
	CreatedCount int
	FailedCount  int
	SkippedCount int
}

ImportSession is the persistent record of one "import YAML → Case/Action" workflow. Created by createCaseImport (status=pending), advanced once by executeCaseImport (→ applied or failed). Stored at tenants/{tenantID}/imports/{importID} in Firestore. The owning user (CreatorUserID) is the only principal allowed to read or execute it.

func (*ImportSession) HasErrorIssues

func (s *ImportSession) HasErrorIssues() bool

HasErrorIssues reports whether the session has any error-severity issue, including those nested under per-Case / per-Action issues. Used to compute the GraphQL `valid` field and to gate executeCaseImport.

func (*ImportSession) RecomputeCounts

func (s *ImportSession) RecomputeCounts()

RecomputeCounts updates CreatedCount / FailedCount / SkippedCount from the current Snapshot. The usecase calls this whenever Snapshot.Cases is mutated so the cached counts stay consistent with reality.

Counting rules (matches the spec / Design Canvas i05):

  • "created" counts Cases with Result.Status == created.
  • "failed" counts Cases with Result.Status == failed AND Actions with Result.Status == failed (a Case can be created while one of its Actions fails — that Action contributes to FailedCount).
  • "skipped" counts Cases with Result.Status == skipped only. Action-level skipped (under a created Case whose earlier Action failed) is NOT counted, matching the summary line shown to the user (Design Canvas treats the summary as Case-level except for failed which sweeps in Action-level failures too).

func (*ImportSession) Valid

func (s *ImportSession) Valid() bool

Valid is the logical opposite of HasErrorIssues. Provided as a method so converters can mirror the GraphQL `valid: Boolean!` field directly.

func (*ImportSession) Validate

func (s *ImportSession) Validate() error

Validate checks the invariants every persisted ImportSession must satisfy. Repositories MUST call this before every write.

type ImportSessionID

type ImportSessionID string

ImportSessionID is a UUID-based identifier for ImportSession.

func NewImportSessionID

func NewImportSessionID() ImportSessionID

NewImportSessionID generates a new UUID v4 ImportSessionID.

func (ImportSessionID) String

func (id ImportSessionID) String() string

String returns the string representation.

type ImportSessionStatus

type ImportSessionStatus string

ImportSessionStatus is the lifecycle status of an ImportSession.

Allowed transitions: pending → applied | failed. Once an ImportSession has reached applied or failed, it is terminal; retrying requires creating a new ImportSession via createCaseImport.

const (
	ImportSessionPending ImportSessionStatus = "pending"
	ImportSessionApplied ImportSessionStatus = "applied"
	ImportSessionFailed  ImportSessionStatus = "failed"
)

func (ImportSessionStatus) IsTerminal

func (s ImportSessionStatus) IsTerminal() bool

IsTerminal reports whether the session is in a state that cannot be further mutated (applied or failed).

type ImportSnapshot

type ImportSnapshot struct {
	Version int
	Cases   []ImportSnapshotCase
}

ImportSnapshot is the workspace-normalized interpretation of the uploaded YAML. Per-Case results live inside Cases[*].Result; the snapshot itself is mutated in place as execute progresses.

type ImportSnapshotAction

type ImportSnapshotAction struct {
	Index       int
	Title       string
	Description string
	AssigneeID  string
	DueDate     *time.Time
	Issues      []ImportIssue
	Result      ImportActionResult
}

ImportSnapshotAction is one Action to be created under its parent Snapshot Case. AssigneeID is a single Slack User ID (matches the existing Action.AssigneeID semantics).

type ImportSnapshotCase

type ImportSnapshotCase struct {
	Index       int
	Title       string
	Description string
	IsPrivate   bool
	AssigneeIDs []string
	FieldValues map[string]FieldValue
	Actions     []ImportSnapshotAction
	Issues      []ImportIssue
	Result      ImportCaseResult
}

ImportSnapshotCase is one Case to be created. Issues holds per-Case validation findings detected at createCaseImport time (and at execute time when field schema has drifted). Result is the per-Case execution outcome.

type ImportSource

type ImportSource struct {
	OriginalFileName string
	ContentDigest    string // sha256 of the raw YAML, hex-encoded
	SizeBytes        int
}

ImportSource captures metadata about the uploaded YAML payload.

type Job

type Job struct {
	// ID is the workspace-unique identifier (kebab-case).
	ID string

	// Name and Description are human-facing labels used in logs and
	// system prompts.
	Name        string
	Description string

	// Prompt is the user prompt template (Go text/template). Job-specific
	// behaviour is fully expressed here; runtime context (Case / Workspace
	// / Event) is injected as template data.
	Prompt string

	// Disabled defaults to false (= active). Setting it true silently
	// excludes the Job from event matching without removing the TOML
	// entry.
	Disabled bool

	// Quiet defaults to false. When true, the Job runs without emitting
	// operational Slack notifications (the "starting..." marker, the
	// per-run session-log thread, and the completion/failure markers).
	// It does NOT silence the agent's own slack__post_message tool — that
	// is a deliberate agent action, not an operational log.
	Quiet bool

	// Strategy selects which execution runtime drives this Job. Empty
	// (the zero value) is equivalent to JobStrategySimple; the config
	// loader normalises before Validate runs.
	Strategy JobStrategy

	// Interactive enables mid-run user interaction. When true, the Job may
	// suspend a run to ask the user a question (planexec Question), persist
	// the pending interaction on its run state, post a Slack form, and
	// resume when the user answers. Defaults to false (= the run is fully
	// unattended). Only meaningful for JobStrategyPlanexec: the simple
	// single-loop runtime has no Question mechanism, so Validate rejects
	// interactive simple Jobs. Independent of Quiet — the question form is a
	// deliberate agent interaction, not an operational log, so Quiet does
	// not suppress it.
	Interactive bool

	// Reflection, when true, runs a post-execution reflection pass after a
	// successful run: a knowledge-only agent reviews the run's conversation
	// history and curates the workspace's shared Knowledge / Tags. Defaults
	// to false. Skipped for private cases and for failed runs.
	Reflection bool

	// Events maps the event domains this Job subscribes to. Validate
	// guarantees at least one non-nil entry.
	Events JobEvents
}

Job is a workspace-scoped, declaratively configured agent that runs in response to events. Jobs are loaded from workspace TOML and held in memory on the WorkspaceEntry — they are not persisted to a backend.

func (*Job) ListensCase

func (j *Job) ListensCase(lc CaseLifecycle) bool

ListensCase reports whether the Job subscribes to the given case lifecycle.

func (*Job) ListensScheduled

func (j *Job) ListensScheduled() bool

ListensScheduled reports whether the Job subscribes to the scheduled domain.

func (*Job) Validate

func (j *Job) Validate() error

Validate enforces the full Job invariant set: identity, prompt presence, event-map well-formedness, and strategy enum membership.

type JobEventDomain

type JobEventDomain string

JobEventDomain enumerates the event domains the dispatcher recognises. Each domain has a distinct filter schema (see CaseEventConfig / ScheduledEventConfig).

const (
	JobEventDomainCase      JobEventDomain = "case"
	JobEventDomainScheduled JobEventDomain = "scheduled"
)

type JobEvents

type JobEvents struct {
	Case      *CaseEventConfig
	Scheduled *ScheduledEventConfig
}

JobEvents collects every event filter a Job listens to. A nil pointer means the corresponding domain is not subscribed; a non-nil pointer indicates the Job listens to that domain with the given filter.

func (*JobEvents) Validate

func (e *JobEvents) Validate() error

Validate enforces invariants for the event map: - at least one of Case / Scheduled must be non-nil - each non-nil sub-config must itself be valid

type JobRun

type JobRun struct {
	WorkspaceID string
	CaseID      int64
	JobID       string

	LastRunAt   time.Time
	LastStatus  JobRunStatus
	LastError   string
	LastRunID   string
	LastTraceID string

	// LeaseUntil is the wall-clock time at which the current run lock
	// expires. Zero value means no live lease (= idle). A lease may be
	// reclaimed by another acquirer once LeaseUntil < now (clock
	// agreement is assumed; lease durations are large enough to absorb
	// minor skew).
	LeaseUntil time.Time

	// SuspendedRunID, when non-empty, names a Run that is currently
	// suspended awaiting user input (Stage=AWAITING_INPUT in its
	// JobRunLog). While set, the scheduler/dispatcher MUST NOT start a new
	// Run for this (workspace, case, job) — the in-flight interactive Run
	// owns the slot until the user answers (resume clears it) or the
	// unanswered-run sweep expires it. The lease is released while
	// suspended (a human wait can outlast any lease), so SuspendedRunID is
	// the durable "do not double-start" signal, not the lease.
	SuspendedRunID string

	// SuspendedAt is when the current suspension began; the unanswered-run
	// sweep uses it to expire stale AWAITING_INPUT runs. Zero when not
	// suspended.
	SuspendedAt time.Time
}

JobRun records the most recent state of a Job × Case pair: when it last ran, what happened, and (when in flight) the lease that prevents concurrent execution.

The same document doubles as the lock record so a lease can be atomically taken or released in the same transaction that updates run history.

Identifiers are flat top-level fields (no nested JobRunKey) so a Firestore doc viewed in isolation surfaces "which (workspace, case, job) is this?" without having to parse the document path, and a BigQuery export yields rows that JOIN directly on WorkspaceID / CaseID / JobID with JobRunLog / JobRunEvent.

func (*JobRun) IsLeased

func (r *JobRun) IsLeased(now time.Time) bool

IsLeased reports whether the JobRun currently holds a non-expired lease as of `now`. Used by acquirers to decide whether to take the lock or step aside.

func (*JobRun) IsSuspended

func (r *JobRun) IsSuspended() bool

IsSuspended reports whether a Run is currently suspended awaiting user input for this (workspace, case, job). New triggers must step aside while this is true.

func (*JobRun) Key

func (r *JobRun) Key() JobRunKey

Key returns the composite JobRunKey for this run, reconstructed from the flat identifier fields. Kept for callers that still want to thread the composite around (scanner / runner helpers); the storage layer relies on the flat fields directly.

type JobRunEvent

type JobRunEvent struct {
	// Identifiers (flat, all populated on every write).
	WorkspaceID string
	CaseID      int64
	JobID       string
	RunID       string
	TraceID     string

	// EventID is the doc ID of this event. It is a UUIDv7 string,
	// chosen for Firestore-console readability (the doc ID surfaces a
	// millisecond timestamp prefix) and global uniqueness. doc IDs are
	// NOT relied on for strict ordering — `Sequence` is the
	// authoritative monotonic order.
	EventID string

	// Position.
	// Sequence is the authoritative monotonic order within a Run. It is
	// int64 (not uint64) because the Firestore SDK rejects uint64. The
	// value space is still effectively unbounded for this workload
	// (max int64 = 9.2e18 events per Run). List queries MUST OrderBy
	// "Sequence" — doc ID order may diverge under clock skew.
	Sequence   int64
	OccurredAt time.Time
	Kind       JobRunEventKind

	// ParentSequence links a child event to its parent. Currently used
	// by ToolCall events to point back at the LLMResponse whose tool_use
	// they implement. Zero means top-level (LLMRequest / LLMResponse /
	// RunError are always top-level).
	ParentSequence int64

	// Forward-compatible runtime tagging. v1 SingleLoopJobExecutor emits
	// Phase="execute" and AgentLabel="" for every event. A future
	// plan-execute runtime may emit Phase="plan" / "execute" / "review"
	// and AgentLabel="planner" / "executor" / etc.
	Phase      string
	AgentLabel string

	// Payload (exactly one is non-nil and matches Kind).
	LLMRequest  *LLMRequestPayload
	LLMResponse *LLMResponsePayload
	ToolCall    *ToolCallPayload
	RunError    *RunErrorPayload
}

JobRunEvent is one entry in the per-Run timeline. Stored at jobRuns/{JobID}/logs/{RunID}/events/{Sequence}. Identifiers are flat for BigQuery-friendliness; TraceID is duplicated here so trace-level analytics can group events without joining back to the log.

func (*JobRunEvent) Validate

func (e *JobRunEvent) Validate() error

Validate enforces invariants on a JobRunEvent. The sink calls this before every Append so that a payload-kind mismatch surfaces as an error at the boundary instead of producing unreadable docs.

type JobRunEventKind

type JobRunEventKind string

JobRunEventKind enumerates the per-call event types appended during a Run. Each maps to a specific gollem trace.Handler hook boundary:

  • LLM_REQUEST : input snapshot captured at EndLLMCall
  • LLM_RESPONSE : output snapshot captured at EndLLMCall
  • TOOL_CALL : single tool execution captured at EndToolExec
  • RUN_ERROR : terminal failure emitted by JobRunner via EmitRunError
const (
	JobRunEventKindLLMRequest  JobRunEventKind = "LLM_REQUEST"
	JobRunEventKindLLMResponse JobRunEventKind = "LLM_RESPONSE"
	JobRunEventKindToolCall    JobRunEventKind = "TOOL_CALL"
	JobRunEventKindRunError    JobRunEventKind = "RUN_ERROR"
)

func (JobRunEventKind) IsValid

func (k JobRunEventKind) IsValid() bool

IsValid reports whether the kind is a known enum member.

func (JobRunEventKind) String

func (k JobRunEventKind) String() string

String returns the string form for logging.

type JobRunKey

type JobRunKey struct {
	WorkspaceID string
	CaseID      int64
	JobID       string
}

JobRunKey identifies a single (workspace, case, job) lock and run-record tuple. The tuple is the lock granularity for both lease acquisition and last-run bookkeeping.

func (JobRunKey) Validate

func (k JobRunKey) Validate() error

Validate enforces that all three components are populated. Empty components produce ambiguous storage paths and corrupt the lock map.

type JobRunLog

type JobRunLog struct {
	// Identifiers (all flat, all required, all populated on every write).
	WorkspaceID string
	CaseID      int64
	JobID       string
	RunID       string
	TraceID     string

	// Lifecycle.
	Stage     JobRunStage
	StartedAt time.Time
	EndedAt   time.Time // zero while RUNNING
	Error     string    // empty unless Stage == FAILED

	// Runtime identification (forward-compatible with plan-execute).
	// v1 always writes ExecutorKind="single_loop".
	ExecutorKind    string
	ExecutorVersion string

	// Provenance — copied from the triggering Event so a single Get tells
	// you why this Run happened.
	EventType      string
	EventTriggerAt time.Time

	// SystemPrompt is held once per Run, here, rather than inside every
	// LLMRequest event (it doesn't change turn-to-turn within a Run).
	// Truncated from the tail to MaxInlineBytes if longer.
	SystemPrompt string

	// PendingInteraction is set ONLY while Stage == AWAITING_INPUT: it holds
	// the question put to the user and the Slack message coordinates needed
	// to update the form in place on resume. It MUST be nil in every other
	// stage (the resume path clears it). It is the entire persisted state a
	// suspend needs — no planexec plan snapshot is stored, because the
	// gollem conversation history (keyed by RunID) already carries the
	// sub-agent observations and the resume re-enters planexec at the
	// replan branch with a fresh budget.
	PendingInteraction *PendingInteraction
}

JobRunLog records ONE invocation of an agent against a Case. Stored at workspaces/{WorkspaceID}/cases/{CaseID}/jobRuns/{JobID}/logs/{RunID}.

Despite the "Job" name it holds every case-scoped agent run, not only TOML-configured Job runs: mention-triggered runs (casebound / threadcase) persist here too, each under its own fresh per-turn JobID and with EventType=EventTypeMention, so the case agent page lists them alongside Job runs through the same read path.

Every identifier is held as a TOP-LEVEL scalar field (no nested Key struct) so that a Firestore -> BigQuery export yields a flat row that joins directly on WorkspaceID / CaseID / JobID / RunID / TraceID.

func (*JobRunLog) Validate

func (l *JobRunLog) Validate() error

Validate enforces invariants on a JobRunLog. The repository calls this before every write (Create / Finish) so that a usecase bug that forgets to populate an identifier fails loudly at the first write instead of silently producing unattributable data.

type JobRunStage

type JobRunStage string

JobRunStage is the lifecycle stage of an individual Run (one invocation of a Job against a Case). Unlike JobRunStatus on the JobRun lock doc, JobRunStage includes RUNNING so a Run record exists while the agent is still executing — that way a crash mid-Run leaves a Stage=RUNNING log with the events captured up to that point.

const (
	JobRunStageRunning JobRunStage = "RUNNING"
	JobRunStageSuccess JobRunStage = "SUCCESS"
	JobRunStageFailed  JobRunStage = "FAILED"
	// JobRunStageAwaitingInput marks an interactive Run that suspended to
	// ask the user a question. It is a non-terminal stage: EndedAt stays
	// zero and PendingInteraction carries the question so the resume path
	// can parse the answer and continue. Only interactive (planexec) Jobs
	// ever reach it.
	JobRunStageAwaitingInput JobRunStage = "AWAITING_INPUT"
)

func (JobRunStage) IsValid

func (s JobRunStage) IsValid() bool

IsValid reports whether the stage is a known enum member.

func (JobRunStage) String

func (s JobRunStage) String() string

String returns the string form for logging.

type JobRunStatus

type JobRunStatus string

JobRunStatus enumerates the terminal outcome of the most recent Job run for a (workspace, case, job) tuple. It is set by JobRunRepository.RecordRun after the agent finishes (or fails). RUNNING is reserved for a future extension if we ever expose mid-flight observability — v1 does not write RUNNING; the in-flight signal is the lease_until column instead.

const (
	JobRunStatusSuccess JobRunStatus = "SUCCESS"
	JobRunStatusFailed  JobRunStatus = "FAILED"
)

func (JobRunStatus) IsValid

func (s JobRunStatus) IsValid() bool

IsValid reports whether the status is a known enum member.

func (JobRunStatus) String

func (s JobRunStatus) String() string

String returns the string form for logging.

type JobStrategy

type JobStrategy string

JobStrategy selects which execution runtime drives a Job. The default (zero value) is JobStrategySimple, which preserves the v1 single-loop behaviour. JobStrategyPlanexec opts the Job into the planexec (plan-and-execute) runtime shared with proposal — useful when the Job needs to investigate, replan, and produce a structured summary rather than emit a single ReAct turn.

const (
	// JobStrategySimple drives the Job through the v1
	// SingleLoopJobExecutor (one gollem.Agent.Execute call with the
	// configured tool set).
	JobStrategySimple JobStrategy = "simple"

	// JobStrategyPlanexec drives the Job through the planexec runtime
	// (plan → parallel sub-agents → replan → final response). The host
	// disables Question because Jobs run unattended.
	JobStrategyPlanexec JobStrategy = "planexec"
)

func NormaliseJobStrategy

func NormaliseJobStrategy(s JobStrategy) JobStrategy

NormaliseJobStrategy collapses the empty value to JobStrategySimple so callers downstream of TOML loading can treat every Job uniformly. Unknown non-empty values pass through unchanged and are then rejected by Validate, surfacing the typo at config-load time.

func (JobStrategy) IsValid

func (s JobStrategy) IsValid() bool

IsValid reports whether s is one of the recognised strategy values. The zero value (empty string) is NOT considered valid here — callers MUST normalise via NormaliseJobStrategy first, which substitutes JobStrategySimple for the empty input. This split keeps Validate strict (any non-empty unknown value is rejected) while letting TOML omit the field entirely.

func (JobStrategy) String

func (s JobStrategy) String() string

String returns the canonical string form for prompt rendering / logs.

type Knowledge

type Knowledge struct {
	ID          KnowledgeID
	WorkspaceID string
	Title       string
	// Claim is a single Markdown text body (not an array). It is stored verbatim
	// and rendered as Markdown by the consumer (Web UI). The agent write tools
	// produce it directly.
	Claim string
	// TagIDs reference first-class Tag entities for classification and filtering.
	// At least one tag is required. Tags must be created (create_tag / GraphQL
	// createTag) before they can be referenced here; the usecase verifies every
	// ID exists in the workspace before persistence. The slice is de-duplicated
	// (order preserved) by the usecase.
	TagIDs []TagID
	// Embedding is the vector embedding of Title + Claim used for in-memory
	// cosine semantic search. It is empty when no embedding client is configured
	// (fail-open). It is never exposed through the GraphQL surface.
	Embedding []float64
	// CreatorID is the Slack user id of the human author. It is empty when the
	// entry was authored by an agent (system actor).
	CreatorID string
	CreatedAt time.Time
	UpdatedAt time.Time
}

Knowledge is a workspace-wide shared knowledge entry: organization-specific information that does not exist in the LLM's general knowledge (operating rules, internal proper nouns, past judgements, threat intel, ...) accumulated so it can be reused on future case processing. Unlike Memo it is NOT scoped to a case and carries no custom fields — only a Markdown claim body and tags.

All identifiers are flat top-level fields even though the Firestore path already encodes WorkspaceID, mirroring the Memo convention so a document inspected in isolation answers "which workspace is this?".

func (*Knowledge) Validate

func (k *Knowledge) Validate() error

Validate enforces the structural invariants required before any persistence write. Repositories MUST call it before every write so a usecase / handler bug that forgot to inject an identity field fails loudly at the first write.

TagIDs must already be normalized by the caller; Validate does not mutate. Validate only enforces structural invariants — the existence of each TagID is verified by the usecase (which has repository access).

type KnowledgeID

type KnowledgeID string

KnowledgeID is a UUID-based identifier for a Knowledge entry.

As with MemoID, a string UUID (v7) is used rather than an int64 counter: knowledge entries are created by agents during case processing as well as by humans, so a counter document would add transaction contention. UUID v7 is generated in-process (multi-instance safe) and is time-ordered, keeping CreatedAt-order listing naturally sortable.

func NewKnowledgeID

func NewKnowledgeID() KnowledgeID

NewKnowledgeID generates a new time-ordered UUID v7 KnowledgeID.

func (KnowledgeID) String

func (id KnowledgeID) String() string

String returns the raw string form of the KnowledgeID.

type LLMContentBlock

type LLMContentBlock struct {
	Type string // "text" / "tool_call" / "tool_response" / "reasoning" / "image" / etc.

	Text string // text / reasoning

	// tool_call
	ID            string
	Name          string
	ArgumentsJSON string

	// tool_response
	ToolCallID string
	ResultJSON string

	// image / pdf / document / file
	MediaType string
	URL       string
	Title     string
}

LLMContentBlock is one chunk inside a message. Mirrors gollem trace.MessageContent — fields are populated only when relevant to Type.

type LLMFunctionCall

type LLMFunctionCall struct {
	ID            string // provider-issued id
	Name          string
	ArgumentsJSON string // raw JSON of trace.FunctionCall.Arguments
}

LLMFunctionCall mirrors gollem trace.FunctionCall.

type LLMMessage

type LLMMessage struct {
	Role     string
	Contents []LLMContentBlock
}

LLMMessage models one turn in the conversation history sent to the LLM. Mirrors gollem trace.Message.

type LLMRequestPayload

type LLMRequestPayload struct {
	Model    string        // LLMCallData.Model
	Messages []LLMMessage  // LLMRequest.Messages
	Tools    []LLMToolSpec // LLMRequest.Tools (name + description only)
}

LLMRequestPayload captures the input sent to the LLM API in one call, except the system prompt (which lives on JobRunLog because it doesn't change turn-to-turn). Maps to gollem trace.LLMRequest + LLMCallData.Model.

type LLMResponsePayload

type LLMResponsePayload struct {
	Model         string            // echoed back (LLMCallData.Model)
	Texts         []string          // LLMResponse.Texts
	FunctionCalls []LLMFunctionCall // LLMResponse.FunctionCalls
	InputTokens   int64             // LLMCallData.InputTokens
	OutputTokens  int64             // LLMCallData.OutputTokens
	DurationMs    int64             // wall-clock time between Start/End hook
}

LLMResponsePayload captures one assistant response. gollem returns texts and function calls as separate arrays (no interleaving order info), so this payload mirrors that shape rather than fabricating an ordering we cannot observe.

type LLMToolSpec

type LLMToolSpec struct {
	Name        string
	Description string
}

LLMToolSpec mirrors gollem trace.ToolSpec.

type MaterializationValidationIssue

type MaterializationValidationIssue struct {
	FieldID types.FieldID // empty for issues on Title/Description
	Code    string        // machine-readable category, e.g. "missing_required"
	Message string
	Fatal   bool
}

MaterializationValidationIssue describes a single problem found by WorkspaceMaterialization.Validate. Issues are partitioned into "fatal" (the materialization is unusable as-is — must be re-generated or rejected) and "fixable" (the user can correct it through the Edit modal).

func (MaterializationValidationIssue) Error

Error implements the error interface for individual issues so callers may surface them via goerr.Wrap.

type Memo

type Memo struct {
	ID          MemoID
	WorkspaceID string
	CaseID      int64
	Title       string
	// FieldValues holds the custom field values keyed by field id, using the
	// same representation as Case (model.FieldValue, validated by the shared
	// FieldValidator against the workspace's memo field schema).
	FieldValues map[string]FieldValue
	// CreatorID is the Slack user id of the human author. It is empty when the
	// memo was authored by an agent (system actor).
	CreatorID string
	// ArchivedAt is nil for an active memo and non-nil once archived (soft
	// delete). Archive state is expressed by this single field; there is no
	// derived boolean.
	ArchivedAt *time.Time
	CreatedAt  time.Time
	UpdatedAt  time.Time
}

Memo is a Case-scoped "memory": a note attached to a single Case that records facts / observations / hypotheses / decisions accumulated while working the Case. Like a Case it carries workspace-defined custom field values; only ID and Title are fixed, everything else is a custom field.

All identifiers (ID / WorkspaceID / CaseID) are flat top-level fields even though the Firestore path already encodes WorkspaceID and CaseID. This mirrors the JobRunLog convention so a document inspected in isolation answers "which workspace / case is this?" and a Firestore -> BigQuery export yields rows that JOIN directly on WorkspaceID / CaseID.

func (*Memo) IsArchived

func (m *Memo) IsArchived() bool

IsArchived reports whether the memo is currently archived.

func (*Memo) Validate

func (m *Memo) Validate() error

Validate enforces the structural invariants required before any persistence write. Repositories MUST call it before every write so a usecase / handler bug that forgot to inject an identity field fails loudly at the first write.

type MemoID

type MemoID string

MemoID is a UUID-based identifier for a Memo.

A string UUID (v7) is used rather than the int64 counter scheme of Case / Action: memos are created frequently by agents and live in a per-Case subcollection, so a counter document would add transaction contention and per-Case counter bookkeeping. UUID v7 is generated in-process (no counter read, multi-instance safe) and is time-ordered, which keeps CreatedAt-order listing and BigQuery export naturally sortable. This matches the existing string-UUID identifiers used by Source / ActionStep / ActionEvent / Session.

func NewMemoID

func NewMemoID() MemoID

NewMemoID generates a new time-ordered UUID v7 MemoID.

func (MemoID) String

func (id MemoID) String() string

String returns the raw string form of the MemoID.

type MyDueAction

type MyDueAction struct {
	WorkspaceID   string
	WorkspaceName string
	Action        *Action
	CaseID        int64
	CaseTitle     string
}

MyDueAction is one row of the cross-workspace "my incomplete actions" home aggregation. CaseID / CaseTitle are pre-expanded from the parent Case so the frontend need not resolve the parent (avoids an N+1 per row).

type MyOpenCase

type MyOpenCase struct {
	WorkspaceID   string
	WorkspaceName string
	Case          *Case
	// Stalled is true when the Case is open but has not been updated within the
	// configured stale window. Always false when the window is disabled.
	Stalled bool
}

MyOpenCase is one row of the cross-workspace "my open cases" home aggregation. It carries the workspace identity alongside the Case because the GraphQL Case type has no workspaceId field (a Case is always workspace-scoped elsewhere).

type NotificationSlot

type NotificationSlot struct {
	ChannelID string                  // Slack channel id (primary key)
	MessageTS string                  // Timestamp of the aggregated channel message
	Entries   []NotificationSlotEntry // Recorded change events (oldest first)
	SlotStart time.Time               // UTC time when the slot's channel message was first posted
	ExpiresAt time.Time               // SlotStart + slotDuration
	UpdatedAt time.Time
}

NotificationSlot is a per-Slack-channel rolling aggregation window for action/step change notifications. While a slot is active, additional channel-side notifications are folded into the same channel message via chat.update (one entry per event). The renderer groups entries by ActionMessageTS so the channel reader sees one section per Action with all of that Action's recent changes underneath, rather than every line repeating "for action X". When the slot expires, the next event posts a fresh channel message and replaces the slot.

The struct is also the Firestore wire format: do NOT add `firestore:"..."` tags (see .claude/rules/firestore.md) and prefer Go-native field names.

func (*NotificationSlot) Validate

func (s *NotificationSlot) Validate() error

Validate enforces the invariants required before any persistence write. ChannelID is the primary key, so a slot with no channel fails loudly here instead of landing in storage under an empty key.

type NotificationSlotEntry

type NotificationSlotEntry struct {
	ActionMessageTS string    // Parent Action card timestamp — used as the grouping key
	ActionTitle     string    // Action title as of the event (most recent wins per group)
	ActionPermalink string    // Slack permalink to the Action card; empty when lookup failed
	Body            string    // Pre-rendered change line ("@user changed status: A → B")
	EventTime       time.Time // UTC time the event was recorded
}

NotificationSlotEntry captures one change event folded into a slot. ActionMessageTS is the grouping key (timestamp of the parent Action card in Slack); the renderer collects every entry sharing this key into a single section. ActionPermalink is resolved once at enqueue time and cached so subsequent updates don't re-hit Slack's chat.getPermalink.

type NotionDBConfig

type NotionDBConfig struct {
	DatabaseID    string
	DatabaseTitle string
	DatabaseURL   string
}

NotionDBConfig holds Notion DB specific configuration

type NotionPageConfig

type NotionPageConfig struct {
	PageID    string
	PageTitle string
	PageURL   string
	Recursive bool
	MaxDepth  int
}

NotionPageConfig holds Notion Page specific configuration

type PendingInteraction

type PendingInteraction struct {
	// PostedChannelID / PostedMessageTS locate the Slack question form so
	// the resume path can update it in place to an "answered" view.
	PostedChannelID string
	PostedMessageTS string

	// Reason is the shared rationale rendered once above the items.
	Reason string

	// Items is the ordered list of questions (1..5).
	Items []PendingInteractionItem
}

PendingInteraction is the persisted snapshot of a question put to the user while an interactive Run is suspended. It lives on the run's JobRunLog (Stage=AWAITING_INPUT). The shape mirrors the host-neutral interaction.Request, but is defined here (domain layer) so persistence does not depend on pkg/agent/interaction; the Job host converts at the boundary.

func (*PendingInteraction) Validate

func (p *PendingInteraction) Validate() error

Validate enforces the same invariants the host-neutral interaction.Request enforces, so a snapshot that round-trips through Firestore stays well formed. Slack coordinates are required because the resume path must locate the form to update it.

type PendingInteractionItem

type PendingInteractionItem struct {
	ID      string
	Text    string
	Type    string // select | multi_select | free_text
	Options []string
}

PendingInteractionItem is one question within a PendingInteraction.

type PendingQuestion

type PendingQuestion struct {
	// PostedChannelID / PostedMessageTS locate the Slack message hosting the
	// question form so the submit handler can update it in place into the
	// read-only "answered" view.
	PostedChannelID string
	PostedMessageTS string
	// Reason is the planner's single-rationale text shared across all items.
	Reason string
	// Items mirrors proposal.QuestionPayload.Items at the time the question was
	// posted. Stored here so the submit handler can label each answer back
	// against the original question text and option list, even after the
	// planner advances and the Slack message blocks have been rebuilt.
	Items []PendingQuestionItem
}

PendingQuestion is the persisted snapshot of a question turn while we wait for the user's submission. It is set when the planner emits a `question` terminal action and consumed when the Submit button fires.

type PendingQuestionItem

type PendingQuestionItem struct {
	ID      string
	Text    string
	Type    string // "select" | "multi_select"
	Options []string
}

PendingQuestionItem is a single question's persisted snapshot.

type ProposalMessage

type ProposalMessage struct {
	UserID    string
	Text      string
	TS        string
	Permalink string
}

ProposalMessage is a single Slack message captured for context.

type ProposalSource

type ProposalSource struct {
	TeamID    string
	ChannelID string
	ThreadTS  string // empty when the mention was not in a thread
	MentionTS string
}

ProposalSource describes where the mention happened.

type RunErrorPayload

type RunErrorPayload struct {
	Stage   string // "prepare" / "execute" / "finish"
	Message string
}

RunErrorPayload is the terminal log entry on a FAILED Run, emitted by JobRunner via the handler's EmitRunError API.

type ScheduledEventConfig

type ScheduledEventConfig struct {
	// Every is the duration since the last run after which the Job becomes
	// due. Mutually exclusive with Cron.
	Every time.Duration

	// Cron is the parsed schedule for cron-style triggers. Mutually
	// exclusive with Every. Parsed at config load time so a malformed
	// expression fails loud before any Job dispatch.
	Cron cron.Schedule

	// CronExpr is the original cron expression preserved for display and
	// system-prompt rendering. Empty when Every is in use.
	CronExpr string
}

ScheduledEventConfig is the listen filter for the `scheduled` event domain. Exactly one of Every / Cron must be populated.

func (*ScheduledEventConfig) Validate

func (s *ScheduledEventConfig) Validate() error

Validate enforces the mutual exclusion: exactly one of Every / Cron.

type Session

type Session struct {
	ID            string
	ChannelID     string
	ThreadTS      string
	LastMentionTS string
	LastAction    SessionEndReason

	// Kind discriminates the conversation that owns this thread. It is set when
	// the Session is created and never changed afterwards: the Slack dispatcher
	// reads it to decide whether an @mention in a case-less thread starts a Case
	// (SessionKindCase) or continues the workspace agent
	// (SessionKindWorkspaceAgent).
	Kind SessionKind

	// Case binding — zero values when the thread is not in a case-bound channel.
	WorkspaceID string
	CaseID      int64
	ActionID    int64

	// Open-mode metadata — zero values when case-bound.
	CreatorUserID string
	ProposalID    CaseProposalID

	// Reaction-origin metadata — set only for a cross-channel reaction-triggered
	// case creation, where the case root lives in the monitored channel but the
	// flagged message lives elsewhere. Persisted so a later resume turn (after a
	// question) can still link the exact source message. Zero for every other
	// creation path.
	ReactionSourceChannelID string
	ReactionSourceMessageTS string

	// Turn lock fields. Maintained by SessionRepository.AcquireTurnLock /
	// Heartbeat / ReleaseTurnLock. Heartbeat staleness (TurnHeartbeatAt vs now)
	// is the activity signal; TurnStartedAt is recorded for traces / UX only.
	TurnState       SessionTurnState
	TurnOwnerID     string
	TurnStartedAt   time.Time
	TurnHeartbeatAt time.Time
	TurnTriggerTS   string

	// Reserved for future interrupt support. Never read or written by Phase A
	// code paths; populated by RequestInterrupt in a later spec.
	InterruptRequestedAt time.Time
	InterruptByTriggerTS string

	// PendingQuestion mirrors the planner's most recent question payload when
	// LastAction == SessionEndedWithQuestion. It is the single source of
	// truth for the Slack-side question form: rendering it, parsing the
	// submission state back into typed answers, and rebuilding the read-only
	// "answered" view after the user clicks Submit. Cleared on the next
	// terminal action.
	PendingQuestion *PendingQuestion

	CreatedAt time.Time
	UpdatedAt time.Time
}

Session represents an ongoing agent conversation bound to a Slack thread. It unifies what was previously split between AgentSession (case-bound) and per-mention draft state (open mode). One Session per (channelID, threadTS).

Lookup keys are (ChannelID, ThreadTS). Case binding is detected via CaseID != 0; open mode (draft creation) is the zero-CaseID case.

func (*Session) IsCaseBound

func (s *Session) IsCaseBound() bool

IsCaseBound reports whether this Session belongs to a case-bound thread.

func (*Session) ResumeOnReply

func (s *Session) ResumeOnReply() bool

ResumeOnReply reports whether a thread reply (without @mention) should kick off a new turn. Currently only true when the previous turn ended on a post_question (open mode); case-bound resume is @mention-only.

func (*Session) Validate

func (s *Session) Validate() error

Validate enforces the invariants required before any persistence write. A Session is located by (ChannelID, ThreadTS) and carries a caller-supplied ID, so a record missing any of these three fails loudly here instead of landing in storage under an incomplete key.

type SessionEndReason

type SessionEndReason string

SessionEndReason captures the terminal plan action that ended the most recent turn for a Session. The dispatcher uses it to decide whether a thread reply (without an @mention) should resume the agent.

const (
	SessionEndedNone               SessionEndReason = ""
	SessionEndedWithMessage        SessionEndReason = "post_message"
	SessionEndedWithQuestion       SessionEndReason = "post_question"
	SessionEndedWithMaterialize    SessionEndReason = "materialize"
	SessionEndedWithCaseBoundReply SessionEndReason = "case_bound"
)

type SessionKind added in v0.2.0

type SessionKind string

SessionKind discriminates what kind of agent conversation owns a thread. The zero value is SessionKindCase so every Session persisted before this field existed keeps its original meaning without a migration.

const (
	// SessionKindCase is a thread owned by a Case: either a Case-bound thread
	// (CaseID != 0) or a thread whose Case is still being formed by the
	// creation agent (CaseID == 0).
	SessionKindCase SessionKind = ""
	// SessionKindWorkspaceAgent is a thread owned by the workspace agent. An
	// @mention in such a thread never starts a Case.
	SessionKindWorkspaceAgent SessionKind = "workspace_agent"
)

type SessionTurnState

type SessionTurnState string

SessionTurnState captures whether a turn is currently running on a Session. It is the CAS key used by the Firestore-backed turn lock and is updated by AcquireTurnLock / Heartbeat / ReleaseTurnLock atomically.

const (
	SessionTurnIdle        SessionTurnState = ""
	SessionTurnRunning     SessionTurnState = "running"
	SessionTurnInterrupted SessionTurnState = "interrupted"
)

type SlackChannel

type SlackChannel struct {
	ID   string // Slack Channel ID (e.g., C01234567)
	Name string // Fallback name for display when API is unavailable
}

SlackChannel represents a Slack channel configuration

type SlackConfig

type SlackConfig struct {
	Channels []SlackChannel
}

SlackConfig holds Slack specific configuration

type SlackUser

type SlackUser struct {
	ID        SlackUserID
	Name      string    // Slack username / handle (e.g., "john.doe")
	RealName  string    // User-facing display name (Profile.DisplayName, with Profile.RealName / RealName as fallback). Field is named "RealName" for legacy reasons.
	Email     string    // Email address (for future features)
	ImageURL  string    // Avatar URL (empty string = no image)
	UpdatedAt time.Time // Last synchronized from Slack
}

SlackUser represents a Slack workspace user stored in the database

func (*SlackUser) Validate

func (u *SlackUser) Validate() error

Validate enforces the invariants required before any persistence write. ID is the natural key (the Slack user id), so a user record with no ID fails loudly here instead of landing in storage under an empty key.

type SlackUserID

type SlackUserID string

SlackUserID represents a unique identifier for a Slack user

type SlackUserMetadata

type SlackUserMetadata struct {
	LastRefreshSuccess time.Time // Last successful refresh time
	LastRefreshAttempt time.Time // Last refresh attempt time (success or failure)
	UserCount          int       // Number of users at last successful refresh
}

SlackUserMetadata tracks the health and status of Slack user synchronization

type Source

type Source struct {
	ID               SourceID
	Name             string
	SourceType       SourceType
	Description      string
	Enabled          bool
	NotionDBConfig   *NotionDBConfig
	NotionPageConfig *NotionPageConfig
	SlackConfig      *SlackConfig
	GitHubConfig     *GitHubConfig
	CreatedAt        time.Time
	UpdatedAt        time.Time
}

Source represents an external data source for risk monitoring

func (*Source) Validate

func (s *Source) Validate() error

Validate enforces the invariants required before any persistence write. The repository assigns the storage ID (NewSourceID when empty), so ID is not checked here; Name and SourceType are the source's mandatory identity/kind fields (every create path supplies a non-empty default for both).

type SourceID

type SourceID string

SourceID is a UUID-based identifier for Source

func NewSourceID

func NewSourceID() SourceID

NewSourceID generates a new UUID v4 SourceID

type SourceType

type SourceType string

SourceType represents the type of source

const (
	SourceTypeNotionDB   SourceType = "notion_db"
	SourceTypeNotionPage SourceType = "notion_page"
	SourceTypeSlack      SourceType = "slack"
	SourceTypeGitHub     SourceType = "github"
)

type Tag

type Tag struct {
	ID          TagID
	WorkspaceID string
	// Name is an optional, human-facing label. It is mutable (update_tag) and
	// carries no uniqueness constraint — the ID is the stable key.
	Name      string
	CreatedAt time.Time
	UpdatedAt time.Time
}

Tag is a workspace-scoped, first-class classification label referenced by Knowledge entries via its immutable ID. Unlike the previous free-form string tags, a Tag must be created explicitly (the create_tag tool / GraphQL createTag) before any Knowledge can reference it.

All identifiers are flat top-level fields even though the Firestore path already encodes WorkspaceID, mirroring the Knowledge / Memo convention so a document inspected in isolation answers "which workspace is this?".

func (*Tag) Validate

func (t *Tag) Validate() error

Validate enforces the structural invariants required before any persistence write. The caller (usecase) assigns the ID via NewTagID and stamps the timestamps before the repository writes.

type TagID

type TagID string

TagID is the immutable identifier of a Tag. A random UUIDv4 is used rather than a sequential counter: tags are created by agents (the create_tag tool / the reflection agent) as well as by humans, so a per-workspace counter document would add transaction contention for no benefit. Tag ordering is driven by the explicit CreatedAt field, so the time-ordering of UUIDv7 buys nothing here.

func NewTagID

func NewTagID() TagID

NewTagID generates a fresh random TagID.

func NormalizeTagIDs

func NormalizeTagIDs(ids []TagID) []TagID

NormalizeTagIDs drops empty entries and de-duplicates while preserving first-seen order. The usecase applies this before existence verification / Validate / persistence.

func (TagID) String

func (id TagID) String() string

String returns the raw string form of the TagID.

type ToolCallPayload

type ToolCallPayload struct {
	ToolName string

	ArgumentsJSON string // raw JSON of trace.ToolExecData.Args
	ResultJSON    string // raw JSON of trace.ToolExecData.Result; empty on pure error

	IsError      bool
	ErrorMessage string // trace.ToolExecData.Error when IsError

	StartedAt time.Time
	EndedAt   time.Time
}

ToolCallPayload is the full record of one tool execution. Maps to gollem trace.ToolExecData plus wall-clock timestamps captured by the handler at Start/End boundaries.

type UserPreference

type UserPreference struct {
	// UserID is the Slack User ID (auth token Sub). Document key. Required.
	UserID string
	// FavoriteWorkspaceIDs are the workspace IDs the user starred, in the
	// order the caller supplies (deduplicated). May be empty.
	FavoriteWorkspaceIDs []string
	CreatedAt            time.Time
	UpdatedAt            time.Time
}

UserPreference holds per-user settings that span workspaces. Keyed by the user's Slack ID (the auth token's Sub). Currently it only carries the favorite workspace list surfaced on the home dashboard.

func (*UserPreference) Validate

func (p *UserPreference) Validate() error

Validate enforces the identity invariant the repository relies on before every write.

type Workspace

type Workspace struct {
	ID          string
	Name        string
	Description string // Human-readable description (e.g. for AI workspace estimation, UI tooltips)
	// Emoji is an optional display glyph for the workspace badge. Mutually
	// exclusive with Color (enforced at config load). Empty when unset.
	Emoji string
	// Color is an optional #RRGGBB hex used as the workspace badge background.
	// Mutually exclusive with Emoji (enforced at config load). Empty when unset.
	Color string
}

Workspace represents a workspace's identity

type WorkspaceEntry

type WorkspaceEntry struct {
	Workspace            Workspace
	FieldSchema          *config.FieldSchema
	MemoConfig           *config.MemoConfig // Memo "strong definition" + custom field schema (nil/empty when memos disabled)
	ActionStatusSet      *ActionStatusSet
	SlackChannelPrefix   string
	SlackTeamID          string // Slack Team ID for org-level app support (empty for WS-level apps)
	SlackInviteUsers     []string
	SlackInviteGroups    []string
	SlackWelcomeMessages []string // Go text/template strings posted to the case channel after creation
	CompilePrompt        string
	AssistPrompt         string
	AssistLanguage       string
	// CaseCreatePrompt is the workspace-specific additional prompt for the
	// thread-mode case initialization (create) agent, configured via TOML
	// [case.prompts].create. Empty when unset; appended to the ModeCreate
	// planner system prompt.
	CaseCreatePrompt string
	Jobs             []*Job // Event-driven agent jobs loaded from workspace TOML

	// CaseMode selects channel-per-case (default) or thread-per-case binding.
	CaseMode CaseMode
	// CaseTrigger selects what starts a Case in thread mode: instant (default,
	// every channel-root post) or mention (only an @mention of the bot). Only
	// meaningful when CaseMode is thread.
	CaseTrigger CaseTrigger
	// SlackMonitorChannelID is the channel watched for thread-mode case
	// creation. Required (and only meaningful) when CaseMode is thread.
	SlackMonitorChannelID string
	// AcceptBot opts the thread-mode monitored channel into
	// treating bot-authored channel-root posts (e.g. an intake-form app's
	// relayed request) as case-creation triggers. Default false: only human
	// channel-root posts start a case, so the channel is not flooded with a case
	// per bot notification. When true, every bot root post (bot_message /
	// bot_id) starts a case.
	AcceptBot bool
	// CaseStatusSet is the configurable workflow status set that attaches to
	// Cases in thread mode (the Kanban columns). Non-nil only for thread-mode
	// workspaces; reuses the generic ActionStatusSet value type.
	CaseStatusSet *ActionStatusSet
	// ReactionEmoji is the Slack reaction (emoji name, without surrounding
	// colons) that triggers case creation for this workspace. Empty when the
	// reaction trigger is disabled. Only meaningful in thread mode, and unique
	// across workspaces (enforced at config load).
	ReactionEmoji string
	// SlackWorkspaceChannelID is the workspace-level shared channel where the
	// cross-case workspace agent runs (and future notifications flow). Channel
	// mode only; empty when unset. Unique across workspaces / monitor channels
	// (enforced at config load).
	SlackWorkspaceChannelID string
	// WorkspaceAgentPrompt is the operator-supplied custom instruction for the
	// workspace agent (from [slack.workspace_agent] prompt/prompt_file). It is
	// appended to the host-owned base system prompt and cannot relax it. Empty
	// when unset.
	WorkspaceAgentPrompt string
}

WorkspaceEntry holds workspace identity and its field schema

func (*WorkspaceEntry) IsThreadMode

func (e *WorkspaceEntry) IsThreadMode() bool

IsThreadMode reports whether this workspace uses thread-per-case binding.

type WorkspaceGroup

type WorkspaceGroup struct {
	ID          string
	Name        string
	Description string
	// MemberIDs are workspace IDs in declared order. May be empty. Existence of
	// each ID in the workspace set is verified at config load, not here.
	MemberIDs []string
}

WorkspaceGroup is a deployment-wide, organizational grouping of workspaces. It is an orthogonal concept: membership never changes a workspace's own behavior, and a deployment may define zero groups. It is loaded from the global config (see pkg/cli/config) and held read-only in memory, exactly like WorkspaceEntry.

func (*WorkspaceGroup) Validate

func (g *WorkspaceGroup) Validate() error

Validate enforces the group's core identity invariant: a non-empty ID.

type WorkspaceGroupRegistry

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

WorkspaceGroupRegistry holds workspace group configurations. Like WorkspaceRegistry it carries settings only (no Repository / UseCase).

func NewWorkspaceGroupRegistry

func NewWorkspaceGroupRegistry() *WorkspaceGroupRegistry

NewWorkspaceGroupRegistry creates a new empty WorkspaceGroupRegistry.

func (*WorkspaceGroupRegistry) Get

Get retrieves a group by ID.

func (*WorkspaceGroupRegistry) List

List returns all registered groups in registration order. It never returns nil, so callers (and the GraphQL non-null list contract) can range freely.

func (*WorkspaceGroupRegistry) Register

func (r *WorkspaceGroupRegistry) Register(g *WorkspaceGroup)

Register adds a group to the registry. A repeated ID overwrites the existing entry while preserving its position in the registration order.

type WorkspaceMaterialization

type WorkspaceMaterialization struct {
	GeneratedAt       time.Time
	Title             string
	Description       string
	CustomFieldValues map[string]FieldValue // key = FieldID defined in workspace's FieldSchema
	// IsTest carries the agent's test-case suggestion through the preview so
	// the human can confirm or override it before the case is created.
	IsTest bool
}

WorkspaceMaterialization is the AI-generated Case payload for a specific workspace's FieldSchema. Generated lazily and overwritten on workspace switch.

func (*WorkspaceMaterialization) FilterToValid

FilterToValid returns a copy of the materialization with all fields that failed validation stripped out (so the human can fill them via Edit). The returned bool indicates whether the *Title* survived; callers should regenerate the materialization when title is missing.

func (*WorkspaceMaterialization) Validate

func (m *WorkspaceMaterialization) Validate(schema *config.FieldSchema) (issues []MaterializationValidationIssue, fatal bool)

Validate checks the materialization against the workspace's FieldSchema. It does NOT mutate the receiver — it only inspects.

Returned issues describe per-field / per-attribute problems; the boolean `fatal` is true iff at least one issue is marked fatal (currently: missing required Title/Description, or a fundamentally unusable structure).

Callers typically:

  • reject (regenerate) on fatal == true
  • drop or surface non-fatal field issues so the human fills them via Edit

type WorkspaceRegistry

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

WorkspaceRegistry holds workspace configurations. It does not hold Repository or UseCase instances (settings only).

func NewWorkspaceRegistry

func NewWorkspaceRegistry() *WorkspaceRegistry

NewWorkspaceRegistry creates a new empty WorkspaceRegistry

func (*WorkspaceRegistry) FindByMonitorChannel

func (r *WorkspaceRegistry) FindByMonitorChannel(channelID string) (*WorkspaceEntry, bool)

FindByMonitorChannel returns the thread-mode workspace entry whose monitored channel matches channelID. It only considers thread-mode workspaces; the boolean is false when no thread-mode workspace watches the channel.

func (*WorkspaceRegistry) FindByReactionEmoji

func (r *WorkspaceRegistry) FindByReactionEmoji(emoji string) (*WorkspaceEntry, bool)

FindByReactionEmoji returns the thread-mode workspace entry whose reaction emoji matches. It only considers thread-mode workspaces; the boolean is false when no thread-mode workspace uses the emoji. emoji must already be normalized (no surrounding colons); an empty emoji never matches.

func (*WorkspaceRegistry) FindByWorkspaceChannel

func (r *WorkspaceRegistry) FindByWorkspaceChannel(channelID string) (*WorkspaceEntry, bool)

FindByWorkspaceChannel returns the channel-mode workspace entry whose workspace channel matches channelID. Thread-mode workspaces are never matched (the workspace channel is a channel-mode feature). The boolean is false when no channel-mode workspace uses the channel.

func (*WorkspaceRegistry) Get

func (r *WorkspaceRegistry) Get(workspaceID string) (*WorkspaceEntry, error)

Get retrieves a workspace entry by ID

func (*WorkspaceRegistry) List

func (r *WorkspaceRegistry) List() []*WorkspaceEntry

List returns all registered workspace entries in registration order

func (*WorkspaceRegistry) Register

func (r *WorkspaceRegistry) Register(entry *WorkspaceEntry)

Register adds a workspace entry to the registry

func (*WorkspaceRegistry) Workspaces

func (r *WorkspaceRegistry) Workspaces() []Workspace

Workspaces returns all registered workspaces in registration order

Directories

Path Synopsis
Package authz holds the Rego input/output document shapes used to authenticate and authorize MCP requests, plus the context plumbing that carries per-request HTTP metadata from the controller middleware down to the point where a tool call is evaluated against the policy.
Package authz holds the Rego input/output document shapes used to authenticate and authorize MCP requests, plus the context plumbing that carries per-request HTTP metadata from the controller middleware down to the point where a tool call is evaluated against the policy.

Jump to

Keyboard shortcuts

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