Files
kandev/apps/backend/internal/office/models/models.go
T
2026-08-24 11:09:03 +00:00

760 lines
37 KiB
Go

// Package models defines data types for the office domain.
package models
import (
"encoding/json"
"time"
settingsmodels "github.com/kandev/kandev/internal/agent/settings/models"
)
// AgentRole and AgentStatus are aliases for the canonical types declared
// in internal/agent/settings/models. ADR 0005 Wave D moved the typed
// constants to the settings package because the agent_profiles row owns
// these attributes after the unification; the aliases here keep ~270
// office callsites compiling without an import-flag-day rename.
//
// New code should import settingsmodels directly. Office-only callers
// reading models.AgentRoleCEO etc. continue to work via the const
// re-exports below.
type (
AgentRole = settingsmodels.AgentRole
AgentStatus = settingsmodels.AgentStatus
)
// Re-exported AgentRole values. These are package-level constant aliases —
// `models.AgentRoleCEO` evaluates to the same untyped string as
// `settingsmodels.AgentRoleCEO`, so callers can mix references across
// packages freely.
const (
AgentRoleCEO = settingsmodels.AgentRoleCEO
AgentRoleWorker = settingsmodels.AgentRoleWorker
AgentRoleSpecialist = settingsmodels.AgentRoleSpecialist
AgentRoleAssistant = settingsmodels.AgentRoleAssistant
AgentRoleSecurity = settingsmodels.AgentRoleSecurity
AgentRoleQA = settingsmodels.AgentRoleQA
AgentRoleDevOps = settingsmodels.AgentRoleDevOps
)
// Re-exported AgentStatus values.
const (
AgentStatusIdle = settingsmodels.AgentStatusIdle
AgentStatusWorking = settingsmodels.AgentStatusWorking
AgentStatusPaused = settingsmodels.AgentStatusPaused
AgentStatusStopped = settingsmodels.AgentStatusStopped
AgentStatusPendingApproval = settingsmodels.AgentStatusPendingApproval
)
// AgentInstance is an alias for settings.AgentProfile. ADR 0005 Wave G
// collapsed the two structs into one row representation: every office
// agent is an agent_profiles row with workspace_id != ”. Office code
// continues to import models.AgentInstance for ergonomic naming, but the
// underlying type is identical to settingsmodels.AgentProfile.
//
// The legacy AgentProfileID field is gone — under the unified model the
// row's ID is the profile id. Callsites that previously read .AgentProfileID
// have been migrated to .ID.
type AgentInstance = settingsmodels.AgentProfile
// Skill represents a reusable skill definition.
//
// System skills (is_system = true) are kandev-owned: they are
// bundled with the binary, upserted into office_skills at startup,
// and refreshed in place on every kandev release. Per-agent
// `desired_skills` references are preserved across updates because
// the row id stays stable. User skills (is_system = false) are
// imported by users (`source_type = git | inline | local_path |
// skills_sh | user_home`) and are never touched by the startup sync.
//
// `default_for_roles` is a JSON-encoded `[]string` of agent roles
// that should receive this system skill auto-attached on agent
// create (e.g. ["ceo"]). Empty array for "no auto-attach". Ignored
// for is_system = false.
type Skill struct {
ID string `json:"id" db:"id"`
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
Name string `json:"name" db:"name"`
Slug string `json:"slug" db:"slug"`
Description string `json:"description" db:"description"`
SourceType SkillSourceType `json:"source_type" db:"source_type"`
SourceLocator string `json:"source_locator" db:"source_locator"`
Content string `json:"content" db:"content"`
FileInventory string `json:"file_inventory" db:"file_inventory"`
Version string `json:"version" db:"version"`
ContentHash string `json:"content_hash" db:"content_hash"`
ApprovalState SkillApprovalState `json:"approval_state" db:"approval_state"`
CreatedByAgentProfileID string `json:"created_by_agent_profile_id" db:"created_by_agent_profile_id"`
IsSystem bool `json:"is_system" db:"is_system"`
SystemVersion string `json:"system_version" db:"system_version"`
DefaultForRoles string `json:"default_for_roles" db:"default_for_roles"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// RunSkillSnapshot records the exact skill package attached to a run.
type RunSkillSnapshot struct {
RunID string `json:"run_id" db:"run_id"`
SkillID string `json:"skill_id" db:"skill_id"`
Version string `json:"version" db:"version"`
ContentHash string `json:"content_hash" db:"content_hash"`
MaterializedPath string `json:"materialized_path" db:"materialized_path"`
}
// ProjectStatus represents the status of a project.
type ProjectStatus string
const (
ProjectStatusActive ProjectStatus = "active"
ProjectStatusCompleted ProjectStatus = "completed"
ProjectStatusOnHold ProjectStatus = "on_hold"
ProjectStatusArchived ProjectStatus = "archived"
)
// Project represents an office project.
//
// Repositories is stored in the DB and YAML as a JSON-encoded array
// string (e.g. `["github.com/foo/bar"]`). MarshalJSON/UnmarshalJSON
// convert it to/from a real `[]string` on the API wire so the
// frontend (and any other JSON consumer) sees an array.
type Project struct {
ID string `json:"id" db:"id"`
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
Status ProjectStatus `json:"status" db:"status"`
LeadAgentProfileID string `json:"lead_agent_profile_id" db:"lead_agent_profile_id"`
Color string `json:"color" db:"color"`
BudgetCents int `json:"budget_cents" db:"budget_cents"`
Repositories string `json:"-" db:"repositories"`
ExecutorConfig string `json:"executor_config" db:"executor_config"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// projectWire is the on-the-wire JSON representation of Project. It
// matches Project field-for-field except `repositories` is a real
// []string instead of a JSON-encoded string. Kept private so callers
// only ever construct Project directly.
type projectWire struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
Name string `json:"name"`
Description string `json:"description"`
Status ProjectStatus `json:"status"`
LeadAgentProfileID string `json:"lead_agent_profile_id"`
Color string `json:"color"`
BudgetCents int `json:"budget_cents"`
Repositories []string `json:"repositories"`
ExecutorConfig string `json:"executor_config"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// MarshalJSON emits Project with `repositories` as a []string. The
// stored value is a JSON-encoded array string; empty values normalise
// to an empty array so the frontend never has to guard against null.
func (p Project) MarshalJSON() ([]byte, error) {
repos, err := DecodeRepositories(p.Repositories)
if err != nil {
return nil, err
}
return json.Marshal(projectWire{
ID: p.ID,
WorkspaceID: p.WorkspaceID,
Name: p.Name,
Description: p.Description,
Status: p.Status,
LeadAgentProfileID: p.LeadAgentProfileID,
Color: p.Color,
BudgetCents: p.BudgetCents,
Repositories: repos,
ExecutorConfig: p.ExecutorConfig,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
})
}
// UnmarshalJSON accepts `repositories` as a []string and stores it
// back as the canonical JSON-encoded string. Other consumers
// (config sync, YAML) treat the field as a string so we preserve the
// internal shape.
func (p *Project) UnmarshalJSON(data []byte) error {
var w projectWire
if err := json.Unmarshal(data, &w); err != nil {
return err
}
encoded, err := EncodeRepositories(w.Repositories)
if err != nil {
return err
}
p.ID = w.ID
p.WorkspaceID = w.WorkspaceID
p.Name = w.Name
p.Description = w.Description
p.Status = w.Status
p.LeadAgentProfileID = w.LeadAgentProfileID
p.Color = w.Color
p.BudgetCents = w.BudgetCents
p.Repositories = encoded
p.ExecutorConfig = w.ExecutorConfig
p.CreatedAt = w.CreatedAt
p.UpdatedAt = w.UpdatedAt
return nil
}
// DecodeRepositories parses the stored JSON-encoded array string into
// a []string. Empty input returns an empty (non-nil) slice so callers
// never serialise `null`.
func DecodeRepositories(raw string) ([]string, error) {
if raw == "" || raw == "[]" {
return []string{}, nil
}
var repos []string
if err := json.Unmarshal([]byte(raw), &repos); err != nil {
return nil, err
}
if repos == nil {
return []string{}, nil
}
return repos, nil
}
// EncodeRepositories renders a []string as the canonical JSON-encoded
// array string used by the DB and YAML layers. A nil/empty slice
// becomes "[]" so the validator's empty-check stays consistent.
func EncodeRepositories(repos []string) (string, error) {
if len(repos) == 0 {
return "[]", nil
}
b, err := json.Marshal(repos)
if err != nil {
return "", err
}
return string(b), nil
}
// TaskCounts holds aggregated task status counts for a project.
type TaskCounts struct {
Total int `json:"total" db:"total"`
InProgress int `json:"in_progress" db:"in_progress"`
Done int `json:"done" db:"done"`
Blocked int `json:"blocked" db:"blocked"`
}
// ProjectWithCounts is a project with aggregated task counts.
type ProjectWithCounts struct {
Project
TaskCounts TaskCounts `json:"task_counts"`
}
// MarshalJSON inlines the embedded Project alongside `task_counts`.
// Required because Project has a custom MarshalJSON and Go would
// otherwise use it for the whole struct, dropping TaskCounts.
func (p ProjectWithCounts) MarshalJSON() ([]byte, error) {
repos, err := DecodeRepositories(p.Repositories)
if err != nil {
return nil, err
}
type wire struct {
projectWire
TaskCounts TaskCounts `json:"task_counts"`
}
return json.Marshal(wire{
projectWire: projectWire{
ID: p.ID,
WorkspaceID: p.WorkspaceID,
Name: p.Name,
Description: p.Description,
Status: p.Status,
LeadAgentProfileID: p.LeadAgentProfileID,
Color: p.Color,
BudgetCents: p.BudgetCents,
Repositories: repos,
ExecutorConfig: p.ExecutorConfig,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
},
TaskCounts: p.TaskCounts,
})
}
// ValidProjectStatuses contains valid project status values.
var ValidProjectStatuses = map[ProjectStatus]bool{
ProjectStatusActive: true,
ProjectStatusCompleted: true,
ProjectStatusOnHold: true,
ProjectStatusArchived: true,
}
// CostContractVersion is the in-band activation point for the cache-split /
// cost-provenance / turn-attribution columns (docs/specs/office/requirements/costs.md).
// The Rill cost extract has no schema versioning of its own, so a row
// written under a prior contract is distinguished by comparing
// cost_contract_version, not by a date an analyst has to be told out of
// band. Both production writers (the prompt-usage subscriber's buildCostEvent
// and the manual-entry RecordCostEvent) use this value. The test harness uses
// it too. Thus, the writers cannot disagree about which contract applies to a
// row. Bump only if the contract's meaning changes again.
//
// v1 → v2: on the CostSourceUnpriced path, v1 forced Estimated=true
// regardless of the caller's own Estimated flag, conflating "we could not
// resolve a price" with "the token counts themselves were synthesised" —
// two different signals this same contract introduced CostSource
// specifically to keep separate. v2 preserves the caller's Estimated value
// verbatim on every path; cost_source=unpriced alone now carries the
// pricing-failure signal.
//
// v2 → v3: v2 wrote TokensOut = OutputTokens unconditionally, so a
// synthesised-usage turn (Estimated=true) with no observed output token
// count stored a plain 0 — indistinguishable from a genuine zero-output
// turn, and a row with real dollars attached to it. v3 makes TokensOut
// nullable and uses the normalized output-token presence signal to leave it
// NULL when no output count was observed, so "never measured" is
// unrepresentable as a number.
const CostContractVersion int64 = 3
// CostEvent represents a cost tracking event.
//
// CostSubcents is stored as hundredths of a cent (int64) to keep token-rate
// math integer-only. UI divides by 10000 when rendering dollars. Estimated
// is true when token counts are not authoritative for a complete turn, such
// as adapter synthesis or a provider frame that covers only part of a turn;
// the row still counts toward budget totals at face value.
//
// TokensCachedRead / TokensCachedWrite / TokensOut / TurnID / UsageEventID /
// CostSource / the Rate*PerMillion columns / PricingCatalogVersion /
// CostContractVersion are all nullable (pointer fields): NULL means "not
// recorded" — a legacy row written before the column existed, or (for the
// cache split specifically) an adapter that did not report cache data.
// TokensOut is NULL specifically when no output count was observed: a 0
// would assert a measurement that was never taken, and a downstream
// per-output-token measure must see "unknown" rather than a fake zero-output
// turn. The JSON cost-list representation includes this field as null so API
// consumers can make the same distinction. NULL is never backfilled to 0;
// TokensCachedIn keeps its original read+write sum semantics on every row so
// existing consumers of that column are unaffected. See docs/specs/office/requirements/costs.md.
type CostEvent struct {
ID string `json:"id" db:"id"`
SessionID string `json:"session_id" db:"session_id"`
TaskID string `json:"task_id" db:"task_id"`
AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"`
ProjectID string `json:"project_id" db:"project_id"`
Model string `json:"model" db:"model"`
Provider string `json:"provider" db:"provider"`
TokensIn int64 `json:"tokens_in" db:"tokens_in"`
TokensCachedIn int64 `json:"tokens_cached_in" db:"tokens_cached_in"`
TokensCachedRead *int64 `json:"tokens_cached_read,omitempty" db:"tokens_cached_read"`
TokensCachedWrite *int64 `json:"tokens_cached_write,omitempty" db:"tokens_cached_write"`
TokensOut *int64 `json:"tokens_out" db:"tokens_out"`
CostSubcents int64 `json:"cost_subcents" db:"cost_subcents"`
Estimated bool `json:"estimated" db:"estimated"`
TurnID *string `json:"turn_id,omitempty" db:"turn_id"`
UsageEventID *string `json:"usage_event_id,omitempty" db:"usage_event_id"`
CostSource *CostSource `json:"cost_source,omitempty" db:"cost_source"`
RateInputPerMillion *int64 `json:"rate_input_per_million,omitempty" db:"rate_input_per_million"`
RateCachedReadPerMillion *int64 `json:"rate_cached_read_per_million,omitempty" db:"rate_cached_read_per_million"`
RateCachedWritePerMillion *int64 `json:"rate_cached_write_per_million,omitempty" db:"rate_cached_write_per_million"`
RateOutputPerMillion *int64 `json:"rate_output_per_million,omitempty" db:"rate_output_per_million"`
PricingCatalogVersion *string `json:"pricing_catalog_version,omitempty" db:"pricing_catalog_version"`
CostContractVersion *int64 `json:"cost_contract_version,omitempty" db:"cost_contract_version"`
OccurredAt time.Time `json:"occurred_at" db:"occurred_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// BudgetPolicy represents a budget limit policy. LimitSubcents is
// hundredths of a cent (matches CostEvent.CostSubcents); the UI divides
// by 10000 to render dollars via apps/web/lib/utils.ts:formatDollars.
type BudgetPolicy struct {
ID string `json:"id" db:"id"`
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
ScopeType BudgetScopeType `json:"scope_type" db:"scope_type"`
ScopeID string `json:"scope_id" db:"scope_id"`
LimitSubcents int64 `json:"limit_subcents" db:"limit_subcents"`
Period BudgetPeriod `json:"period" db:"period"`
AlertThresholdPct int `json:"alert_threshold_pct" db:"alert_threshold_pct"`
ActionOnExceed BudgetActionOnExceed `json:"action_on_exceed" db:"action_on_exceed"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Run represents a run queue entry.
type Run struct {
ID string `json:"id" db:"id"`
AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"`
Reason string `json:"reason" db:"reason"`
Payload string `json:"payload" db:"payload"`
Status RunStatus `json:"status" db:"status"`
CoalescedCount int `json:"coalesced_count" db:"coalesced_count"`
IdempotencyKey *string `json:"idempotency_key" db:"idempotency_key"`
ContextSnapshot string `json:"context_snapshot" db:"context_snapshot"`
Capabilities string `json:"capabilities" db:"capabilities"`
InputSnapshot string `json:"input_snapshot" db:"input_snapshot"`
OutputSummary string `json:"output_summary" db:"output_summary"`
FailureReason string `json:"failure_reason" db:"failure_reason"`
SessionID string `json:"session_id" db:"session_id"`
RetryCount int `json:"retry_count" db:"retry_count"`
ScheduledRetryAt *time.Time `json:"scheduled_retry_at" db:"scheduled_retry_at"`
CancelReason *string `json:"cancel_reason,omitempty" db:"cancel_reason"`
// ErrorMessage is set when the run transitions to status=failed.
// Stored verbatim from the agent error event for inbox + chat use.
ErrorMessage string `json:"error_message,omitempty" db:"error_message"`
// ResultJSON is the structured adapter output captured at run
// completion. The continuation-summary builder reads this to
// populate the "Recent decisions" / "Recent actions" sections.
// Defaults to "{}".
ResultJSON string `json:"result_json,omitempty" db:"result_json"`
// AssembledPrompt is the final prompt string the agent received.
// Persisted at dispatch so the run-detail UI can render exactly
// what the agent saw (independent of session replay).
AssembledPrompt string `json:"assembled_prompt,omitempty" db:"assembled_prompt"`
// SummaryInjected is the continuation-summary content prepended
// to the prompt at dispatch time, snapshot for inspection. Empty
// when no summary was injected (today: every run, until PR 2).
SummaryInjected string `json:"summary_injected,omitempty" db:"summary_injected"`
RequestedAt time.Time `json:"requested_at" db:"requested_at"`
ClaimedAt *time.Time `json:"claimed_at" db:"claimed_at"`
FinishedAt *time.Time `json:"finished_at" db:"finished_at"`
// Provider-routing columns (office-provider-routing spec). All
// optional and ignored when workspace routing is disabled. The TEXT
// columns are pointer-typed so SELECT * StructScan handles the NULL
// rows existing runs ship with after the ADD COLUMN migration.
//
// LogicalProviderOrder is a JSON snapshot of the effective provider
// order at launch time; remains stable across post-start fallbacks
// within the same run.
LogicalProviderOrder *string `json:"logical_provider_order,omitempty" db:"logical_provider_order"`
// RequestedTier is the tier the resolver consumed (override > workspace
// default) when the run was first dispatched.
RequestedTier *string `json:"requested_tier,omitempty" db:"requested_tier"`
// ResolvedExecutionProfileID/ProviderID/Model identify the candidate
// that actually launched; empty until a launch succeeds. Provider and
// model are audit snapshots derived from the concrete profile.
ResolvedExecutionProfileID *string `json:"resolved_execution_profile_id,omitempty" db:"resolved_execution_profile_id"`
ResolvedProviderID *string `json:"resolved_provider_id,omitempty" db:"resolved_provider_id"`
ResolvedModel *string `json:"resolved_model,omitempty" db:"resolved_model"`
// CurrentRouteAttemptSeq tracks the in-flight attempt so post-start
// fallback can find the right row to update and exclude already-tried
// providers when re-resolving.
CurrentRouteAttemptSeq int `json:"current_route_attempt_seq" db:"current_route_attempt_seq"`
// RouteCycleBaselineSeq marks the seq floor for the current retry
// cycle: prior attempts with seq <= baseline are NOT counted toward
// the dispatcher's exclude-set. Bumped to CurrentRouteAttemptSeq
// whenever a parked run is lifted (auto wake-up or manual retry) so
// the run can re-try every provider in its order. Post-start fallback
// does NOT bump it — within a single cycle, providers that fail still
// stay excluded.
RouteCycleBaselineSeq int `json:"route_cycle_baseline_seq" db:"route_cycle_baseline_seq"`
// RoutingBlockedStatus is set when every provider candidate is
// unavailable; values: 'waiting_for_provider_capacity' |
// 'blocked_provider_action_required'.
RoutingBlockedStatus *RoutingBlockedStatus `json:"routing_blocked_status,omitempty" db:"routing_blocked_status"`
// EarliestRetryAt is the earliest moment a parked run should be re-
// resolved. Set only when at least one degraded route is auto-retryable.
EarliestRetryAt *time.Time `json:"earliest_retry_at,omitempty" db:"earliest_retry_at"`
}
// RouteAttempt records one provider attempt inside a Run. Each fallback
// (resolver candidate tried) appends a new row keyed by (run_id, seq).
// Persisted by the routing scheduler dispatcher in
// internal/office/scheduler/dispatch_routing.go.
type RouteAttempt struct {
RunID string `json:"run_id" db:"run_id"`
Seq int `json:"seq" db:"seq"`
ExecutionProfileID string `json:"execution_profile_id" db:"execution_profile_id"`
ProviderID string `json:"provider_id" db:"provider_id"`
Model string `json:"model" db:"model"`
Tier string `json:"tier" db:"tier"`
// TierSource names the precedence level that supplied Tier: one of
// "wake_reason", "override", "role", "workspace". Empty means "not
// recorded" — either a pre-migration row (Tier non-empty) or an
// attempt that never resolved a tier (both empty, e.g. a
// max-attempts-exceeded row) — never interpreted as "workspace".
TierSource string `json:"tier_source,omitempty" db:"tier_source"`
Outcome RouteAttemptOutcome `json:"outcome" db:"outcome"`
ErrorCode string `json:"error_code,omitempty" db:"error_code"`
ErrorConfidence ErrorConfidence `json:"error_confidence,omitempty" db:"error_confidence"`
AdapterPhase AdapterPhase `json:"adapter_phase,omitempty" db:"adapter_phase"`
ClassifierRule string `json:"classifier_rule,omitempty" db:"classifier_rule"`
ExitCode *int `json:"exit_code,omitempty" db:"exit_code"`
RawExcerpt string `json:"raw_excerpt,omitempty" db:"raw_excerpt"`
ResetHint *time.Time `json:"reset_hint,omitempty" db:"reset_hint"`
StartedAt time.Time `json:"started_at" db:"started_at"`
FinishedAt *time.Time `json:"finished_at,omitempty" db:"finished_at"`
}
// ProviderHealth records the health state of one (workspace, provider,
// scope) tuple. Scope is one of 'provider' (whole provider), 'model'
// (a specific model on this provider), or 'tier' (a specific workspace
// tier mapping on this provider). The resolver checks scopes in order
// provider → tier → model before considering a candidate eligible.
type ProviderHealth struct {
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
ProviderID string `json:"provider_id" db:"provider_id"`
Scope ProviderHealthScope `json:"scope" db:"scope"`
ScopeValue string `json:"scope_value" db:"scope_value"`
State ProviderHealthState `json:"state" db:"state"`
ErrorCode string `json:"error_code,omitempty" db:"error_code"`
RetryAt *time.Time `json:"retry_at,omitempty" db:"retry_at"`
BackoffStep int `json:"backoff_step" db:"backoff_step"`
LastFailure *time.Time `json:"last_failure,omitempty" db:"last_failure"`
LastSuccess *time.Time `json:"last_success,omitempty" db:"last_success"`
RawExcerpt string `json:"raw_excerpt,omitempty" db:"raw_excerpt"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// RunEvent is one row in office_run_events. Each row is a discrete
// lifecycle event for an office run: init, adapter.invoke, step,
// complete, error. The frontend renders these in the run detail
// page's Events log.
type RunEvent struct {
RunID string `json:"run_id" db:"run_id"`
Seq int `json:"seq" db:"seq"`
EventType RunEventType `json:"event_type" db:"event_type"`
Level RunEventLevel `json:"level" db:"level"`
Payload string `json:"payload" db:"payload"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// Routine represents a recurring task definition.
type Routine struct {
ID string `json:"id" db:"id"`
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
Name string `json:"name" db:"name"`
Description string `json:"description" db:"description"`
TaskTemplate string `json:"task_template" db:"task_template"`
AssigneeAgentProfileID string `json:"assignee_agent_profile_id" db:"assignee_agent_profile_id"`
Status string `json:"status" db:"status"`
ConcurrencyPolicy RoutineConcurrencyPolicy `json:"concurrency_policy" db:"concurrency_policy"`
CatchUpPolicy RoutineCatchUpPolicy `json:"catch_up_policy" db:"catch_up_policy"`
CatchUpMax int `json:"catch_up_max" db:"catch_up_max"`
Variables string `json:"variables" db:"variables"`
LastRunAt *time.Time `json:"last_run_at" db:"last_run_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// RoutineTrigger represents a trigger for a routine.
type RoutineTrigger struct {
ID string `json:"id" db:"id"`
RoutineID string `json:"routine_id" db:"routine_id"`
Kind string `json:"kind" db:"kind"`
CronExpression string `json:"cron_expression" db:"cron_expression"`
Timezone string `json:"timezone" db:"timezone"`
PublicID string `json:"public_id" db:"public_id"`
SigningMode string `json:"signing_mode" db:"signing_mode"`
Secret string `json:"secret" db:"secret"`
NextRunAt *time.Time `json:"next_run_at" db:"next_run_at"`
LastFiredAt *time.Time `json:"last_fired_at" db:"last_fired_at"`
Enabled bool `json:"enabled" db:"enabled"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// RoutineRun represents a single run of a routine.
type RoutineRun struct {
ID string `json:"id" db:"id"`
RoutineID string `json:"routine_id" db:"routine_id"`
TriggerID string `json:"trigger_id" db:"trigger_id"`
Source string `json:"source" db:"source"`
Status RoutineRunStatus `json:"status" db:"status"`
TriggerPayload string `json:"trigger_payload" db:"trigger_payload"`
LinkedTaskID string `json:"linked_task_id" db:"linked_task_id"`
CoalescedIntoRunID string `json:"coalesced_into_run_id" db:"coalesced_into_run_id"`
DispatchFingerprint string `json:"dispatch_fingerprint" db:"dispatch_fingerprint"`
StartedAt *time.Time `json:"started_at" db:"started_at"`
CompletedAt *time.Time `json:"completed_at" db:"completed_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// ApprovalType constants for approval request types.
const (
ApprovalTypeHireAgent = "hire_agent"
ApprovalTypeBudgetIncrease = "budget_increase"
ApprovalTypeBoardApproval = "board_approval"
ApprovalTypeTaskReview = "task_review"
ApprovalTypeSkillCreation = "skill_creation"
)
// Approval represents a pending or resolved approval request.
type Approval struct {
ID string `json:"id" db:"id"`
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
Type string `json:"type" db:"type"`
RequestedByAgentProfileID string `json:"requested_by_agent_profile_id" db:"requested_by_agent_profile_id"`
Status ApprovalStatus `json:"status" db:"status"`
Payload string `json:"payload" db:"payload"`
DecisionNote string `json:"decision_note" db:"decision_note"`
DecidedBy string `json:"decided_by" db:"decided_by"`
DecidedAt *time.Time `json:"decided_at" db:"decided_at"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// ActivityEntry represents an entry in the activity log.
type ActivityEntry struct {
ID string `json:"id" db:"id"`
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
ActorType ActivityActorType `json:"actor_type" db:"actor_type"`
ActorID string `json:"actor_id" db:"actor_id"`
Action ActivityAction `json:"action" db:"action"`
TargetType ActivityTargetType `json:"target_type" db:"target_type"`
TargetID string `json:"target_id" db:"target_id"`
Details string `json:"details" db:"details"`
// RunID + SessionID let the run detail page join activity rows
// back to the originating run for the "Tasks Touched" surface.
// Empty string for activity not produced under a run (manual
// user actions, etc.).
RunID string `json:"run_id,omitempty" db:"run_id"`
SessionID string `json:"session_id,omitempty" db:"session_id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// AgentMemory represents a memory entry for an agent.
type AgentMemory struct {
ID string `json:"id" db:"id"`
AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"`
Layer string `json:"layer" db:"layer"`
Key string `json:"key" db:"key"`
Content string `json:"content" db:"content"`
Metadata string `json:"metadata" db:"metadata"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Channel represents a communication channel for an agent.
type Channel struct {
ID string `json:"id" db:"id"`
WorkspaceID string `json:"workspace_id" db:"workspace_id"`
AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"`
Platform ChannelPlatform `json:"platform" db:"platform"`
Config string `json:"config" db:"config"`
WebhookSecret string `json:"webhook_secret,omitempty" db:"webhook_secret"`
Status ChannelStatus `json:"status" db:"status"`
TaskID string `json:"task_id" db:"task_id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// TaskBlocker represents a blocker relationship between tasks.
type TaskBlocker struct {
TaskID string `json:"task_id" db:"task_id"`
BlockerTaskID string `json:"blocker_task_id" db:"blocker_task_id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// Participant role values for the office_task_participants table.
// The DB has a CHECK constraint that pins these to two literals.
const (
ParticipantRoleReviewer = "reviewer"
ParticipantRoleApprover = "approver"
)
// Decision values recorded by the office approval flow. ADR 0005 Wave E
// moved storage from the legacy office_task_approval_decisions table to
// workflow_step_decisions; the literals below are still the canonical
// values office writes for `decision` and `decider_type`.
const (
DecisionApproved = "approved"
DecisionChangesRequested = "changes_requested"
DeciderTypeUser = "user"
DeciderTypeAgent = "agent"
)
// TaskComment represents an asynchronous comment on a task.
type TaskComment struct {
ID string `json:"id" db:"id"`
TaskID string `json:"task_id" db:"task_id"`
AuthorType string `json:"author_type" db:"author_type"`
AuthorID string `json:"author_id" db:"author_id"`
Body string `json:"body" db:"body"`
Source string `json:"source" db:"source"`
ReplyChannelID string `json:"reply_channel_id" db:"reply_channel_id"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}
// InboxItem represents a computed inbox entry for the user.
type InboxItem struct {
ID string `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Description string `json:"description,omitempty"`
Status string `json:"status"`
EntityID string `json:"entity_id,omitempty"`
EntityType string `json:"entity_type,omitempty"`
Payload map[string]interface{} `json:"payload,omitempty"`
CreatedAt time.Time `json:"created_at"`
}
// RunActivityDay holds aggregated run outcome counts for a single calendar day.
type RunActivityDay struct {
Date string
Succeeded int
Failed int
Other int
}
// TaskBreakdown holds task counts bucketed by status category.
type TaskBreakdown struct {
Open int
InProgress int
Blocked int
Done int
}
// RecentTask holds minimal fields for a recently-updated task.
type RecentTask struct {
ID string
Identifier string
Title string
Status string
AssigneeAgentProfileID string
UpdatedAt string
}
// DashboardData represents aggregated dashboard information.
type DashboardData struct {
AgentCount int `json:"agent_count"`
RunningCount int `json:"running_count"`
PausedCount int `json:"paused_count"`
ErrorCount int `json:"error_count"`
TasksInProgress int `json:"tasks_in_progress"`
OpenTasks int `json:"open_tasks"`
BlockedTasks int `json:"blocked_tasks"`
MonthSpendSubcents int64 `json:"month_spend_subcents"`
PendingApprovals int `json:"pending_approvals"`
RecentActivity []*ActivityEntry `json:"recent_activity"`
RecentIssues []interface{} `json:"recent_issues,omitempty"`
TaskCount int `json:"task_count"`
SkillCount int `json:"skill_count"`
RoutineCount int `json:"routine_count"`
RunActivity []RunActivityDay
TaskBreakdown TaskBreakdown
RecentTasks []RecentTask
}
// InstructionFile represents an instruction file for an agent instance.
type InstructionFile struct {
ID string `json:"id" db:"id"`
AgentProfileID string `json:"agent_profile_id" db:"agent_profile_id"`
Filename string `json:"filename" db:"filename"`
Content string `json:"content" db:"content"`
IsEntry bool `json:"is_entry" db:"is_entry"`
CreatedAt string `json:"created_at" db:"created_at"`
UpdatedAt string `json:"updated_at" db:"updated_at"`
}
// CostBreakdown represents an aggregated cost entry. TotalSubcents stores
// hundredths of a cent (UI divides by 10000 for dollars). GroupKey is the
// stable id used by the breakdown grouping (agent_profile_id, project_id,
// or model). GroupLabel is the human-readable label resolved by the query
// (e.g. agent profile name, project name); empty when no lookup applies
// or when the id has no row in the source table.
type CostBreakdown struct {
GroupKey string `json:"group_key" db:"group_key"`
GroupLabel string `json:"group_label" db:"group_label"`
TotalSubcents int64 `json:"total_subcents" db:"total_subcents"`
Count int `json:"count" db:"count"`
}