package models import ( "database/sql" "encoding/json" "time" ) // DefaultMaxConcurrentTasks mirrors the workspaces.max_concurrent_tasks // schema default. Handlers that resolve a 0/omitted payload value write // this constant so the read-side (scheduler capacity check) sees a // guaranteed non-zero column on every row. const DefaultMaxConcurrentTasks = 1 type Workspace struct { ID string `json:"id" db:"id"` Name string `json:"name" db:"name"` Role sql.NullString `json:"role" db:"role"` Tier int `json:"tier" db:"tier"` AwarenessNamespace sql.NullString `json:"awareness_namespace" db:"awareness_namespace"` Status string `json:"status" db:"status"` SourceBundleID sql.NullString `json:"source_bundle_id" db:"source_bundle_id"` AgentCard json.RawMessage `json:"agent_card" db:"agent_card"` URL sql.NullString `json:"url" db:"url"` ParentID *string `json:"parent_id" db:"parent_id"` ForwardedTo *string `json:"forwarded_to" db:"forwarded_to"` LastHeartbeatAt *time.Time `json:"last_heartbeat_at" db:"last_heartbeat_at"` LastErrorRate float64 `json:"last_error_rate" db:"last_error_rate"` LastSampleError sql.NullString `json:"last_sample_error" db:"last_sample_error"` ActiveTasks int `json:"active_tasks" db:"active_tasks"` MaxConcurrentTasks int `json:"max_concurrent_tasks" db:"max_concurrent_tasks"` UptimeSeconds int `json:"uptime_seconds" db:"uptime_seconds"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` // DeliveryMode: "push" (synchronous to URL — default) or "poll" (logged // to activity_logs, agent reads via GET /activity?since_id=). See // migration 045 + RFC #2339. DeliveryMode string `json:"delivery_mode" db:"delivery_mode"` // Canvas layout fields (from JOIN) X float64 `json:"x"` Y float64 `json:"y"` Collapsed bool `json:"collapsed"` } // Delivery mode constants. Matches the CHECK constraint in migration 045. const ( DeliveryModePush = "push" DeliveryModePoll = "poll" ) // IsValidDeliveryMode reports whether s is one of the recognised // delivery modes. Empty string is NOT valid here — callers must // resolve the default ("push") before calling. func IsValidDeliveryMode(s string) bool { return s == DeliveryModePush || s == DeliveryModePoll } type RegisterPayload struct { ID string `json:"id" binding:"required"` // URL is required for push-mode workspaces; optional / unused for // poll-mode (the platform never dispatches to it). The handler // enforces the conditional requirement based on the resolved // delivery mode (payload value, falling back to the row's existing // value, falling back to "push"). URL string `json:"url"` AgentCard json.RawMessage `json:"agent_card" binding:"required"` // DeliveryMode is optional. Empty string means "keep the existing // value on the workspace row, or default to push for new rows". // When set, must be one of DeliveryModePush / DeliveryModePoll. DeliveryMode string `json:"delivery_mode,omitempty"` } type HeartbeatPayload struct { WorkspaceID string `json:"workspace_id" binding:"required"` ErrorRate float64 `json:"error_rate"` SampleError string `json:"sample_error"` ActiveTasks int `json:"active_tasks"` UptimeSeconds int `json:"uptime_seconds"` CurrentTask string `json:"current_task"` // MonthlySpend is cumulative USD spend for the current calendar month, // denominated in cents (e.g. 1500 = $15.00). Zero means "no update" — // the heartbeat handler never writes zero to avoid accidentally clearing // a previously-reported spend value. Any non-zero value is clamped to // [0, maxMonthlySpend] before the DB write. (#615) MonthlySpend int64 `json:"monthly_spend"` // RuntimeState is a self-reported runtime health flag separate from // "is the heartbeat task firing at all". The heartbeat task lives in // its own asyncio task and keeps pinging even when the agent runtime // is wedged (e.g. claude_agent_sdk's `Control request timeout: // initialize` leaves the SDK in a permanent error state for the // process lifetime). RuntimeState is how the workspace tells the // platform "I'm alive but my Claude runtime is broken — flip me to // degraded so the canvas can show a Restart hint." // // Empty string = healthy / no signal. The only currently-recognised // non-empty value is "wedged"; future values can extend this without // migration. RuntimeState string `json:"runtime_state"` // RuntimeMetadata is the adapter-declared capability map + per- // capability override values. The Python runtime builds this from // BaseAdapter.capabilities() + per-hook methods (e.g. // idle_timeout_override()) — see workspace/heartbeat.py: // _runtime_metadata_payload. Optional: missing means "use platform // defaults for everything", matching pre-2026-04 behavior. // // Pointer (not value) so a missing JSON field is nil rather than a // zero-value RuntimeMetadata{} that would falsely claim "all caps = // false declared explicitly". Lets the platform distinguish "adapter // said no native ownership" from "old runtime version, didn't say". RuntimeMetadata *RuntimeMetadata `json:"runtime_metadata,omitempty"` } // RuntimeMetadata is the adapter-declared capability + override block // the Python runtime sends in the heartbeat payload. New fields can be // added with `omitempty` without breaking older runtime versions. // // See project memory `project_runtime_native_pluggable.md` for the // principle and workspace/adapter_base.py:RuntimeCapabilities for the // Python source of truth. type RuntimeMetadata struct { // Capabilities maps capability name → "adapter owns it natively". // Keys (heartbeat, scheduler, session, status_mgmt, retry, // activity_decoration, channel_dispatch) match // RuntimeCapabilities.to_dict() in adapter_base.py — keep in sync. Capabilities map[string]bool `json:"capabilities,omitempty"` // IdleTimeoutSeconds, when set, overrides the per-dispatch silence // window in a2a_proxy.go for this workspace's A2A traffic. Pointer // so nil means "no override; use the global default". Zero / negative // is treated as nil by the consumer (a2a_proxy.go). IdleTimeoutSeconds *int `json:"idle_timeout_seconds,omitempty"` } type UpdateCardPayload struct { WorkspaceID string `json:"workspace_id" binding:"required"` AgentCard json.RawMessage `json:"agent_card" binding:"required"` } // MemorySeed represents an initial memory to seed into a workspace at creation time. // Used by both the POST /workspaces API and org template import to pre-populate // agent memories from config (issue #1050). type MemorySeed struct { Content string `json:"content" yaml:"content"` Scope string `json:"scope" yaml:"scope"` // LOCAL, TEAM, GLOBAL } type CreateWorkspacePayload struct { Name string `json:"name" binding:"required"` Role string `json:"role"` Template string `json:"template"` // workspace-configs-templates folder name Tier int `json:"tier"` Model string `json:"model"` Runtime string `json:"runtime"` // "langgraph" (default), "claude-code", etc. External bool `json:"external"` // true = no Docker container, just a registered URL URL string `json:"url"` // for external workspaces: the A2A endpoint URL (push mode only — omit for poll) // DeliveryMode: "push" (default) sends inbound A2A to URL synchronously; // "poll" records inbound to activity_logs for the agent to consume via // GET /activity?since_id=. Poll mode does not require a URL. See #2339. DeliveryMode string `json:"delivery_mode,omitempty"` WorkspaceDir string `json:"workspace_dir"` // host path to mount as /workspace (empty = isolated volume) WorkspaceAccess string `json:"workspace_access"` // "none" (default), "read_only", or "read_write" — see #65 ParentID *string `json:"parent_id"` // BudgetLimit is the optional monthly spend ceiling in USD cents. // NULL (omitted) means no limit. budget_limit=500 means $5.00/month. BudgetLimit *int64 `json:"budget_limit"` // Secrets is an optional map of key→plaintext-value pairs to persist as // workspace secrets at creation time. Stored encrypted (same path as // POST /workspaces/:id/secrets). Nil/empty map is a no-op. Secrets map[string]string `json:"secrets"` // MaxConcurrentTasks caps parallel A2A + cron dispatch. 0 means use // DefaultMaxConcurrentTasks. Leaders typically set 3. MaxConcurrentTasks int `json:"max_concurrent_tasks"` Canvas struct { X float64 `json:"x"` Y float64 `json:"y"` } `json:"canvas"` // InitialMemories is an optional list of memories to seed into the // workspace immediately after creation. Each entry is inserted into // agent_memories with the workspace's awareness namespace. Issue #1050. InitialMemories []MemorySeed `json:"initial_memories"` } type CheckAccessPayload struct { CallerID string `json:"caller_id" binding:"required"` TargetID string `json:"target_id" binding:"required"` }