481359ddb2
Co-authored-by: Kandev Agent <agent@kandev.dev> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1264 lines
57 KiB
Go
1264 lines
57 KiB
Go
package dto
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/kandev/kandev/internal/authz"
|
|
"github.com/kandev/kandev/internal/task/models"
|
|
"github.com/kandev/kandev/internal/task/service"
|
|
"github.com/kandev/kandev/internal/task/statussummary"
|
|
v1 "github.com/kandev/kandev/pkg/api/v1"
|
|
)
|
|
|
|
type WorkflowDTO struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
Prompt *string `json:"prompt,omitempty"`
|
|
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
|
SortOrder int `json:"sort_order"`
|
|
Hidden bool `json:"hidden,omitempty"`
|
|
// Style is a Phase 2 (ADR-0004) UX hint read by the frontend ONLY.
|
|
// Allowed values: "kanban" | "office" | "custom".
|
|
Style string `json:"style,omitempty"`
|
|
// Source records where the workflow definition came from ("manual" |
|
|
// "github"); SourcePath is the repo-relative file for synced workflows.
|
|
Source string `json:"source,omitempty"`
|
|
SourcePath string `json:"source_path,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type WorkspaceDTO struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
OwnerID string `json:"owner_id"`
|
|
UnitID string `json:"unit_id,omitempty"`
|
|
DefaultExecutorID *string `json:"default_executor_id,omitempty"`
|
|
DefaultEnvironmentID *string `json:"default_environment_id,omitempty"`
|
|
DefaultAgentProfileID *string `json:"default_agent_profile_id,omitempty"`
|
|
DefaultConfigAgentProfileID *string `json:"default_config_agent_profile_id,omitempty"`
|
|
TaskPrefix string `json:"task_prefix,omitempty"`
|
|
TaskSequence int `json:"task_sequence,omitempty"`
|
|
OfficeWorkflowID string `json:"office_workflow_id,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
|
|
// ViewerRole and Scopes describe what the *requesting* user may do here.
|
|
// The frontend gates every control on Scopes rather than comparing the
|
|
// current user ID to OwnerID, so permission logic lives in one place.
|
|
ViewerRole string `json:"viewer_role,omitempty"`
|
|
Scopes []string `json:"scopes,omitempty"`
|
|
MemberCount int `json:"member_count,omitempty"`
|
|
}
|
|
|
|
type RepositoryDTO struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
SourceType string `json:"source_type"`
|
|
LocalPath string `json:"local_path"`
|
|
Provider string `json:"provider"`
|
|
ProviderRepoID string `json:"provider_repo_id"`
|
|
ProviderHost string `json:"provider_host"`
|
|
ProviderScope string `json:"provider_scope"`
|
|
ProviderOwner string `json:"provider_owner"`
|
|
ProviderName string `json:"provider_name"`
|
|
RemoteURL string `json:"remote_url"`
|
|
DefaultBranch string `json:"default_branch"`
|
|
WorktreeBranchPrefix string `json:"worktree_branch_prefix"`
|
|
WorktreeBranchTemplate string `json:"worktree_branch_template"`
|
|
PullBeforeWorktree bool `json:"pull_before_worktree"`
|
|
SetupScript string `json:"setup_script"`
|
|
CleanupScript string `json:"cleanup_script"`
|
|
DevScript string `json:"dev_script"`
|
|
CopyFiles string `json:"copy_files"`
|
|
SecretBindings []RepositorySecretBindingDTO `json:"secret_bindings,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Scripts []RepositoryScriptDTO `json:"scripts,omitempty"`
|
|
}
|
|
|
|
type RepositorySecretBindingDTO struct {
|
|
Key string `json:"key"`
|
|
SecretID string `json:"secret_id"`
|
|
}
|
|
|
|
// RepositorySetDTO is a named group of workspace repositories on the wire.
|
|
// Repositories is always an array, never null: the web store indexes it without
|
|
// a nil check, and a set whose members were all deleted is legitimately empty.
|
|
type RepositorySetDTO struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
Repositories []RepositorySetItemDTO `json:"repositories"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// RepositorySetItemDTO is one repository's membership, in apply order.
|
|
type RepositorySetItemDTO struct {
|
|
RepositoryID string `json:"repository_id"`
|
|
Position int `json:"position"`
|
|
}
|
|
|
|
type RepositoryBranchPolicyDTO struct {
|
|
ID string `json:"id"`
|
|
RepositoryID string `json:"repository_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
BaseBranch string `json:"base_branch"`
|
|
BranchTemplate string `json:"branch_template"`
|
|
PullRequestTarget string `json:"pull_request_target"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type RepositoryScriptDTO struct {
|
|
ID string `json:"id"`
|
|
RepositoryID string `json:"repository_id"`
|
|
Name string `json:"name"`
|
|
Command string `json:"command"`
|
|
Position int `json:"position"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// ShellOutputSnapshotResponse is the on-demand full output for one shell message.
|
|
type ShellOutputSnapshotResponse struct {
|
|
MessageID string `json:"message_id"`
|
|
Status string `json:"status"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Output models.ShellExecOutputSnapshot `json:"output"`
|
|
}
|
|
|
|
type ExecutorDTO struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Type models.ExecutorType `json:"type"`
|
|
Status models.ExecutorStatus `json:"status"`
|
|
IsSystem bool `json:"is_system"`
|
|
Resumable bool `json:"resumable"`
|
|
Config map[string]string `json:"config,omitempty"`
|
|
Profiles []ExecutorProfileDTO `json:"profiles,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type ExecutorProfileDTO struct {
|
|
ID string `json:"id"`
|
|
ExecutorID string `json:"executor_id"`
|
|
ExecutorType string `json:"executor_type,omitempty"`
|
|
ExecutorName string `json:"executor_name,omitempty"`
|
|
Name string `json:"name"`
|
|
McpPolicy string `json:"mcp_policy,omitempty"`
|
|
Config map[string]string `json:"config,omitempty"`
|
|
PrepareScript string `json:"prepare_script"`
|
|
CleanupScript string `json:"cleanup_script"`
|
|
EnvVars []models.ProfileEnvVar `json:"env_vars,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type ListExecutorProfilesResponse struct {
|
|
Profiles []ExecutorProfileDTO `json:"profiles"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type EnvironmentDTO struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Kind models.EnvironmentKind `json:"kind"`
|
|
IsSystem bool `json:"is_system"`
|
|
WorktreeRoot string `json:"worktree_root,omitempty"`
|
|
ImageTag string `json:"image_tag,omitempty"`
|
|
Dockerfile string `json:"dockerfile,omitempty"`
|
|
BuildConfig map[string]string `json:"build_config,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type TaskDTO struct {
|
|
ID string `json:"id"`
|
|
WorkspaceID string `json:"workspace_id"`
|
|
WorkflowID string `json:"workflow_id"`
|
|
WorkflowStepID string `json:"workflow_step_id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
State v1.TaskState `json:"state"`
|
|
Priority string `json:"priority"`
|
|
WIPAdmitted bool `json:"wip_admitted"`
|
|
QueuedForStepID string `json:"queued_for_step_id,omitempty"`
|
|
QueuedAt *time.Time `json:"queued_at,omitempty"`
|
|
Repositories []TaskRepositoryDTO `json:"repositories,omitempty"`
|
|
WorkspaceFolders []TaskWorkspaceFolderDTO `json:"workspace_folders,omitempty"`
|
|
Position int `json:"position"`
|
|
PrimarySessionID *string `json:"primary_session_id,omitempty"`
|
|
SessionCount *int `json:"session_count,omitempty"`
|
|
ReviewStatus models.ReviewStatus `json:"review_status,omitempty"`
|
|
PrimaryExecutorID *string `json:"primary_executor_id,omitempty"`
|
|
PrimaryExecutorType *string `json:"primary_executor_type,omitempty"`
|
|
PrimaryExecutorName *string `json:"primary_executor_name,omitempty"`
|
|
PrimaryAgentName *string `json:"primary_agent_name,omitempty"`
|
|
PrimaryAgentProfileID *string `json:"primary_agent_profile_id,omitempty"`
|
|
PrimaryWorkingDirectory *string `json:"primary_working_directory,omitempty"`
|
|
PrimarySessionState *string `json:"primary_session_state,omitempty"`
|
|
PrimarySessionPendingAction *string `json:"primary_session_pending_action"`
|
|
TaskPendingAction *string `json:"task_pending_action"`
|
|
// ForegroundActivity is the task-level MOST-ACTIVE-WINS activity aggregate
|
|
// across the task's sessions: "generating" when
|
|
// any session is generating, "background" when none is generating but at
|
|
// least one RUNNING session is holding a turn open for background work, and
|
|
// empty (omitted) when no session is running — in which case task-level
|
|
// surfaces fall through to the coarse task state (done / waiting / failed).
|
|
// Computed on the backend and carried on the task record so every task-level
|
|
// surface reads one authoritative value; stamped by EnrichTaskForegroundActivity.
|
|
ForegroundActivity v1.ForegroundActivity `json:"foreground_activity,omitempty"`
|
|
ActiveSubagentCount int `json:"active_subagent_count"`
|
|
IsRemoteExecutor bool `json:"is_remote_executor,omitempty"`
|
|
ParentID string `json:"parent_id,omitempty"`
|
|
Autopilot bool `json:"autopilot"`
|
|
ArchivedAt *time.Time `json:"archived_at,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
// Interrupted reports that the task's session was mid-turn when the backend
|
|
// died and has not been resumed since. Derived from the interrupted_at
|
|
// metadata key at DTO conversion time (see FromTaskWithSessionInfo).
|
|
Interrupted bool `json:"interrupted,omitempty"`
|
|
// AutoStartFailed reports that a workflow step's auto_start_agent on_enter
|
|
// action failed to launch a run for this task. Derived from the
|
|
// auto_start_failed metadata key at DTO conversion time (see
|
|
// FromTaskWithSessionInfo).
|
|
AutoStartFailed bool `json:"auto_start_failed,omitempty"`
|
|
|
|
// Dependency projection. Derived on every read from task_blockers plus each
|
|
// related task's own state — never persisted, because a stale copy would be
|
|
// read by the auto-start gate. Stamped by EnrichTaskDependencies.
|
|
Blocked bool `json:"blocked,omitempty"`
|
|
// BlockedReason is "pending", "failed", or "unknown"; omitted when the task
|
|
// is not blocked.
|
|
BlockedReason string `json:"blocked_reason,omitempty"`
|
|
// DependsOn lists direct predecessors, DependsOn/Blocks are not transitive.
|
|
DependsOn []TaskDependencyRefDTO `json:"depends_on,omitempty"`
|
|
// Blocks lists direct dependents so the dependency chip can render both
|
|
// directions without a second round trip.
|
|
Blocks []TaskDependencyRefDTO `json:"blocks,omitempty"`
|
|
// StartWhenUnblocked reports that a launch intent is waiting on dependency
|
|
// resolution. Read-only here; set through the create request or the picker.
|
|
StartWhenUnblocked bool `json:"start_when_unblocked,omitempty"`
|
|
|
|
// Office extensions
|
|
AssigneeAgentProfileID string `json:"assignee_agent_profile_id,omitempty"`
|
|
// AssigneeUserID is the human assignee, independent of the agent one.
|
|
AssigneeUserID string `json:"assignee_user_id,omitempty"`
|
|
Origin string `json:"origin,omitempty"`
|
|
ProjectID string `json:"project_id,omitempty"`
|
|
Labels string `json:"labels,omitempty"`
|
|
Identifier string `json:"identifier,omitempty"`
|
|
// ExternalID is a caller-supplied identity used for task create-
|
|
// idempotency (docs/specs/tasks/requirements/external-id-idempotency.md). Omitted
|
|
// when the task holds none.
|
|
ExternalID string `json:"external_id,omitempty"`
|
|
// IsFromOffice is the authoritative "this task is owned by office"
|
|
// flag. Computed by the task repo at read time as
|
|
// (project_id != '' OR workflow_id == workspace.office_workflow_id).
|
|
// True for office tasks even when they have no project yet; false for
|
|
// kanban-board tasks. Use this to gate office-only UI (e.g. the
|
|
// "Open in office view" topbar link).
|
|
IsFromOffice bool `json:"is_from_office,omitempty"`
|
|
|
|
// PRs lists the GitHub pull requests associated with this task, when the
|
|
// github service is wired and any association exists. Surfaced through the
|
|
// task-listing MCP tools so agents can reason about PR status (e.g. find
|
|
// tasks whose PRs are merged). Omitted when empty.
|
|
PRs []v1.TaskPRSummary `json:"prs,omitempty"`
|
|
|
|
// StatusSummary is the bounded task-level projection consumed by task rows.
|
|
// It is loaded in batches and is absent when no projection exists yet; the
|
|
// existing coarse fields above remain the compatibility fallback.
|
|
StatusSummary *statussummary.TaskStatusSummary `json:"status_summary,omitempty"`
|
|
// StatusSummaryInvalidated distinguishes a known-stale summary from an
|
|
// ordinarily omitted partial projection so clients clear their cache and
|
|
// expose the coarse compatibility fallback.
|
|
StatusSummaryInvalidated bool `json:"status_summary_invalidated,omitempty"`
|
|
}
|
|
|
|
type TaskRepositoryDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
RepositoryID string `json:"repository_id"`
|
|
BaseBranch string `json:"base_branch"`
|
|
CheckoutBranch string `json:"checkout_branch,omitempty"`
|
|
BranchPolicyID string `json:"branch_policy_id,omitempty"`
|
|
BranchPolicyName string `json:"branch_policy_name,omitempty"`
|
|
BranchPolicyBaseBranch string `json:"branch_policy_base_branch,omitempty"`
|
|
BranchPolicyBranchTemplate string `json:"branch_policy_branch_template,omitempty"`
|
|
BranchPolicyPullRequestTarget string `json:"branch_policy_pull_request_target,omitempty"`
|
|
Position int `json:"position"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// TaskWorkspaceFolderDTO is the API projection of a durable non-Git source.
|
|
type TaskWorkspaceFolderDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
LocalPath string `json:"local_path"`
|
|
DisplayName string `json:"display_name"`
|
|
Position int `json:"position"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
type TaskSessionDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
// Name is the user-supplied session tab label. Serialized without
|
|
// omitempty so a cleared name ("") overwrites stale client state.
|
|
Name string `json:"name"`
|
|
AgentExecutionID string `json:"agent_execution_id,omitempty"`
|
|
ContainerID string `json:"container_id,omitempty"`
|
|
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
|
ExecutorID string `json:"executor_id,omitempty"`
|
|
ExecutorProfileID string `json:"executor_profile_id,omitempty"`
|
|
EnvironmentID string `json:"environment_id,omitempty"`
|
|
RepositoryID string `json:"repository_id,omitempty"`
|
|
BaseBranch string `json:"base_branch,omitempty"`
|
|
BaseCommitSHA string `json:"base_commit_sha,omitempty"`
|
|
WorktreeID string `json:"worktree_id,omitempty"`
|
|
WorktreePath string `json:"worktree_path,omitempty"`
|
|
WorktreeBranch string `json:"worktree_branch,omitempty"`
|
|
// WorkspacePath is the effective task root used by Files and chat links;
|
|
// WorktreePath remains the flattened primary repository path.
|
|
WorkspacePath string `json:"workspace_path,omitempty"`
|
|
// Worktrees lists all session worktrees (one per repo on multi-repo tasks);
|
|
// the flattened Worktree* fields above carry only the first for backward
|
|
// compatibility. Entries use the stable session-worktree wire shape
|
|
// (session_id + worktree identity) shared with TaskSession.ToAPI.
|
|
Worktrees []map[string]interface{} `json:"worktrees,omitempty"`
|
|
State models.TaskSessionState `json:"state"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
AgentProfileSnapshot map[string]interface{} `json:"agent_profile_snapshot,omitempty"`
|
|
ExecutorSnapshot map[string]interface{} `json:"executor_snapshot,omitempty"`
|
|
EnvironmentSnapshot map[string]interface{} `json:"environment_snapshot,omitempty"`
|
|
RepositorySnapshot map[string]interface{} `json:"repository_snapshot,omitempty"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
// Workflow fields
|
|
IsPrimary bool `json:"is_primary"`
|
|
IsPassthrough bool `json:"is_passthrough"`
|
|
ReviewStatus models.ReviewStatus `json:"review_status,omitempty"`
|
|
TaskEnvironmentID string `json:"task_environment_id,omitempty"`
|
|
// ForegroundActivity mirrors the in-memory fine-grained busy substate so a
|
|
// fresh page-load / second tab sees live background work without waiting for
|
|
// a WS flip (ADR-0049). Generating is emitted only for RUNNING sessions;
|
|
// background may remain present after the foreground turn settles.
|
|
// Not persisted — populated at the serialization boundary by
|
|
// EnrichForegroundActivity, never by FromTaskSession.
|
|
ForegroundActivity v1.ForegroundActivity `json:"foreground_activity,omitempty"`
|
|
// CancellationPending mirrors the orchestrator's runtime cancellation
|
|
// projection. It is always serialized so false clears stale client state.
|
|
CancellationPending bool `json:"cancellation_pending"`
|
|
// CancellationRevision identifies the process-local cancellation transition
|
|
// generation that produced CancellationPending. It is always serialized so
|
|
// clients can reject delayed snapshots from older generations.
|
|
CancellationRevision uint64 `json:"cancellation_revision"`
|
|
// SupportsSteering is true when a send right now would be delivered into the
|
|
// still-generating turn (mid-turn steering) rather than blocked/queued.
|
|
// Derived live at serialization from the connected agent's negotiated
|
|
// capability plus the runtime flag; never persisted (see mid-turn-steering
|
|
// spec). The composer uses it to promise delivery, not folding.
|
|
SupportsSteering bool `json:"supports_steering,omitempty"`
|
|
// PendingAction is the compact per-session projection used when the
|
|
// session transcript is not loaded in the client.
|
|
PendingAction *string `json:"pending_action"`
|
|
PendingActionRevision *models.PendingActionRevision `json:"pending_action_revision,omitempty"`
|
|
ActiveSubagentCount int `json:"active_subagent_count"`
|
|
// LastReadMessageID is the session's Slack-style read cursor — the id of
|
|
// the newest message the frontend has marked as read. Used by the
|
|
// transcript to position the unread ("New") divider.
|
|
LastReadMessageID string `json:"last_read_message_id,omitempty"`
|
|
// Usage/cost rollup (docs/specs/task-cost-ledger/spec.md AC-28, AC-29).
|
|
// Deliberately not on TaskSessionSummaryDTO - the summary projection used
|
|
// by cross-task views is not widened by this card.
|
|
CostSubcents int64 `json:"cost_subcents"`
|
|
TokensIn int64 `json:"tokens_in"`
|
|
TokensCachedIn int64 `json:"tokens_cached_in"`
|
|
TokensOut int64 `json:"tokens_out"`
|
|
}
|
|
|
|
// TaskSessionSummaryDTO is a lightweight version of TaskSessionDTO without snapshot fields.
|
|
// Used for list endpoints where snapshots are not needed, reducing response size by ~40-60%.
|
|
type TaskSessionSummaryDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
// Name is the user-supplied session tab label. Serialized without
|
|
// omitempty so a cleared name ("") overwrites stale client state.
|
|
Name string `json:"name"`
|
|
AgentExecutionID string `json:"agent_execution_id,omitempty"`
|
|
ContainerID string `json:"container_id,omitempty"`
|
|
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
|
ExecutorID string `json:"executor_id,omitempty"`
|
|
ExecutorProfileID string `json:"executor_profile_id,omitempty"`
|
|
EnvironmentID string `json:"environment_id,omitempty"`
|
|
RepositoryID string `json:"repository_id,omitempty"`
|
|
BaseBranch string `json:"base_branch,omitempty"`
|
|
BaseCommitSHA string `json:"base_commit_sha,omitempty"`
|
|
WorktreeID string `json:"worktree_id,omitempty"`
|
|
WorktreePath string `json:"worktree_path,omitempty"`
|
|
WorktreeBranch string `json:"worktree_branch,omitempty"`
|
|
// WorkspacePath is the effective task root used by Files and chat links;
|
|
// WorktreePath remains the flattened primary repository path.
|
|
WorkspacePath string `json:"workspace_path,omitempty"`
|
|
// Worktrees lists all session worktrees (one per repo on multi-repo tasks);
|
|
// the flattened Worktree* fields above carry only the first for backward
|
|
// compatibility. Entries use the stable session-worktree wire shape.
|
|
Worktrees []map[string]interface{} `json:"worktrees,omitempty"`
|
|
State models.TaskSessionState `json:"state"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
StartedAt time.Time `json:"started_at"`
|
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
IsPrimary bool `json:"is_primary"`
|
|
IsPassthrough bool `json:"is_passthrough"`
|
|
ReviewStatus models.ReviewStatus `json:"review_status,omitempty"`
|
|
TaskEnvironmentID string `json:"task_environment_id,omitempty"`
|
|
// ForegroundActivity mirrors the in-memory fine-grained busy substate
|
|
// (ADR-0049); see TaskSessionDTO.
|
|
ForegroundActivity v1.ForegroundActivity `json:"foreground_activity,omitempty"`
|
|
// CancellationPending mirrors the runtime cancellation projection and is
|
|
// always serialized so false clears stale client state.
|
|
CancellationPending bool `json:"cancellation_pending"`
|
|
// CancellationRevision identifies the process-local cancellation transition
|
|
// generation represented by CancellationPending.
|
|
CancellationRevision uint64 `json:"cancellation_revision"`
|
|
// SupportsSteering mirrors TaskSessionDTO.SupportsSteering for list endpoints.
|
|
SupportsSteering bool `json:"supports_steering,omitempty"`
|
|
// PendingAction is the compact per-session projection used when the
|
|
// session transcript is not loaded in the client.
|
|
PendingAction *string `json:"pending_action"`
|
|
PendingActionRevision *models.PendingActionRevision `json:"pending_action_revision,omitempty"`
|
|
ActiveSubagentCount int `json:"active_subagent_count"`
|
|
LastReadMessageID string `json:"last_read_message_id,omitempty"`
|
|
// CommandCount is the number of tool_call messages on this session,
|
|
// surfaced inline in the timeline entry header ("ran N commands").
|
|
// Populated by ListTaskSessions; defaults to 0 for callers that don't
|
|
// resolve it.
|
|
CommandCount int `json:"command_count"`
|
|
}
|
|
|
|
// ListTaskSessionSummariesResponse is the list response using summary DTOs.
|
|
type ListTaskSessionSummariesResponse struct {
|
|
Sessions []TaskSessionSummaryDTO `json:"sessions"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type GetTaskSessionResponse struct {
|
|
Session TaskSessionDTO `json:"session"`
|
|
}
|
|
|
|
// MarkSessionReadResponse is intentionally narrow: it carries only the
|
|
// updated read cursor, never a full session snapshot. A full-session
|
|
// response would be frozen at request time and, if applied verbatim by a
|
|
// client, could overwrite a newer field (e.g. state) that arrived via a
|
|
// concurrent WebSocket update while this request was still in flight.
|
|
type MarkSessionReadResponse struct {
|
|
SessionID string `json:"session_id"`
|
|
LastReadMessageID string `json:"last_read_message_id"`
|
|
}
|
|
|
|
type ListTaskSessionsResponse struct {
|
|
Sessions []TaskSessionDTO `json:"sessions"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type WorkflowSnapshotDTO struct {
|
|
Workflow WorkflowDTO `json:"workflow"`
|
|
Steps []WorkflowStepDTO `json:"steps"`
|
|
Tasks []TaskDTO `json:"tasks"`
|
|
}
|
|
|
|
type ListMessagesResponse struct {
|
|
Messages []*v1.Message `json:"messages"`
|
|
Total int `json:"total"`
|
|
HasMore bool `json:"has_more"`
|
|
Cursor string `json:"cursor"`
|
|
}
|
|
|
|
// MessageSearchHit is a lightweight match returned by a session message search.
|
|
type MessageSearchHit struct {
|
|
ID string `json:"id"`
|
|
TurnID string `json:"turn_id,omitempty"`
|
|
AuthorType string `json:"author_type"`
|
|
Type string `json:"type"`
|
|
Snippet string `json:"snippet"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// SearchMessagesResponse contains hits from a session message search.
|
|
type SearchMessagesResponse struct {
|
|
Hits []MessageSearchHit `json:"hits"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type TurnDTO struct {
|
|
ID string `json:"id"`
|
|
SessionID string `json:"session_id"`
|
|
TaskID string `json:"task_id"`
|
|
StartedAt string `json:"started_at"`
|
|
CompletedAt *string `json:"completed_at,omitempty"`
|
|
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
|
CreatedAt string `json:"created_at"`
|
|
UpdatedAt string `json:"updated_at"`
|
|
}
|
|
|
|
type ListTurnsResponse struct {
|
|
Turns []TurnDTO `json:"turns"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListWorkflowsResponse struct {
|
|
Workflows []WorkflowDTO `json:"workflows"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListWorkspacesResponse struct {
|
|
Workspaces []WorkspaceDTO `json:"workspaces"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListRepositoriesResponse struct {
|
|
Repositories []RepositoryDTO `json:"repositories"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListRepositorySetsResponse struct {
|
|
RepositorySets []RepositorySetDTO `json:"repository_sets"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListRepositoryBranchPoliciesResponse struct {
|
|
Policies []RepositoryBranchPolicyDTO `json:"repository_branch_policies"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListRepositoryScriptsResponse struct {
|
|
Scripts []RepositoryScriptDTO `json:"scripts"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListExecutorsResponse struct {
|
|
Executors []ExecutorDTO `json:"executors"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type ListEnvironmentsResponse struct {
|
|
Environments []EnvironmentDTO `json:"environments"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type BranchDTO struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"` // "local" or "remote"
|
|
Remote string `json:"remote"` // remote name (e.g., "origin") for remote branches
|
|
}
|
|
|
|
type RepositoryBranchesResponse struct {
|
|
Branches []BranchDTO `json:"branches"`
|
|
Total int `json:"total"`
|
|
CurrentBranch string `json:"current_branch,omitempty"`
|
|
// FetchedAt is the timestamp of the most recent `git fetch` for this
|
|
// repository (RFC3339). Empty when no refresh has been requested in the
|
|
// process lifetime.
|
|
FetchedAt string `json:"fetched_at,omitempty"`
|
|
// FetchError is the human-readable error from the last fetch attempt for
|
|
// this request, if one was attempted and failed. Empty otherwise.
|
|
FetchError string `json:"fetch_error,omitempty"`
|
|
}
|
|
|
|
// LocalRepositoryStatusResponse reports current branch + dirty paths for a
|
|
// local repository on disk (no session required). Used by the task-create
|
|
// dialog to preflight the fresh-branch flow on local executors.
|
|
type LocalRepositoryStatusResponse struct {
|
|
CurrentBranch string `json:"current_branch"`
|
|
DirtyFiles []string `json:"dirty_files"`
|
|
}
|
|
|
|
type LocalRepositoryDTO struct {
|
|
Path string `json:"path"`
|
|
Name string `json:"name"`
|
|
DefaultBranch string `json:"default_branch,omitempty"`
|
|
}
|
|
|
|
type RepositoryDiscoveryResponse struct {
|
|
Roots []string `json:"roots"`
|
|
Repositories []LocalRepositoryDTO `json:"repositories"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type RepositoryPathValidationResponse struct {
|
|
Path string `json:"path"`
|
|
Exists bool `json:"exists"`
|
|
IsGitRepo bool `json:"is_git"`
|
|
Allowed bool `json:"allowed"`
|
|
DefaultBranch string `json:"default_branch,omitempty"`
|
|
Message string `json:"message,omitempty"`
|
|
}
|
|
|
|
type ListTasksResponse struct {
|
|
Tasks []TaskDTO `json:"tasks"`
|
|
Total int `json:"total"`
|
|
}
|
|
|
|
type SuccessResponse struct {
|
|
Success bool `json:"success"`
|
|
}
|
|
|
|
func FromWorkflow(workflow *models.Workflow) WorkflowDTO {
|
|
var description *string
|
|
if workflow.Description != "" {
|
|
description = &workflow.Description
|
|
}
|
|
var prompt *string
|
|
if workflow.Prompt != "" {
|
|
prompt = &workflow.Prompt
|
|
}
|
|
|
|
return WorkflowDTO{
|
|
ID: workflow.ID,
|
|
WorkspaceID: workflow.WorkspaceID,
|
|
Name: workflow.Name,
|
|
Description: description,
|
|
Prompt: prompt,
|
|
AgentProfileID: workflow.AgentProfileID,
|
|
SortOrder: workflow.SortOrder,
|
|
Hidden: workflow.Hidden,
|
|
Style: workflow.Style,
|
|
Source: workflow.Source,
|
|
SourcePath: workflow.SourcePath,
|
|
CreatedAt: workflow.CreatedAt,
|
|
UpdatedAt: workflow.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromWorkspace(workspace *models.Workspace) WorkspaceDTO {
|
|
var description *string
|
|
if workspace.Description != "" {
|
|
description = &workspace.Description
|
|
}
|
|
|
|
return WorkspaceDTO{
|
|
ID: workspace.ID,
|
|
Name: workspace.Name,
|
|
Description: description,
|
|
OwnerID: workspace.OwnerID,
|
|
UnitID: workspace.UnitID,
|
|
DefaultExecutorID: workspace.DefaultExecutorID,
|
|
DefaultEnvironmentID: workspace.DefaultEnvironmentID,
|
|
DefaultAgentProfileID: workspace.DefaultAgentProfileID,
|
|
DefaultConfigAgentProfileID: workspace.DefaultConfigAgentProfileID,
|
|
TaskPrefix: workspace.TaskPrefix,
|
|
TaskSequence: workspace.TaskSequence,
|
|
OfficeWorkflowID: workspace.OfficeWorkflowID,
|
|
CreatedAt: workspace.CreatedAt,
|
|
UpdatedAt: workspace.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// FromWorkspaceWithAccess projects a workspace together with the requesting
|
|
// user's resolved role and scopes.
|
|
func FromWorkspaceWithAccess(workspace *models.Workspace, decision authz.Decision, memberCount int) WorkspaceDTO {
|
|
out := FromWorkspace(workspace)
|
|
out.ViewerRole = string(decision.Role)
|
|
out.Scopes = decision.Scopes.Strings()
|
|
out.MemberCount = memberCount
|
|
return out
|
|
}
|
|
|
|
func FromRepository(repository *models.Repository) RepositoryDTO {
|
|
bindings := make([]RepositorySecretBindingDTO, 0, len(repository.SecretBindings))
|
|
for _, binding := range repository.SecretBindings {
|
|
bindings = append(bindings, RepositorySecretBindingDTO{Key: binding.Key, SecretID: binding.SecretID})
|
|
}
|
|
return RepositoryDTO{
|
|
ID: repository.ID,
|
|
WorkspaceID: repository.WorkspaceID,
|
|
Name: repository.Name,
|
|
SourceType: repository.SourceType,
|
|
LocalPath: repository.LocalPath,
|
|
Provider: repository.Provider,
|
|
ProviderRepoID: repository.ProviderRepoID,
|
|
ProviderHost: repository.ProviderHost,
|
|
ProviderScope: repository.ProviderScope,
|
|
ProviderOwner: repository.ProviderOwner,
|
|
ProviderName: repository.ProviderName,
|
|
RemoteURL: repository.RemoteURL,
|
|
DefaultBranch: repository.DefaultBranch,
|
|
WorktreeBranchPrefix: repository.WorktreeBranchPrefix,
|
|
WorktreeBranchTemplate: repository.WorktreeBranchTemplate,
|
|
PullBeforeWorktree: repository.PullBeforeWorktree,
|
|
SetupScript: repository.SetupScript,
|
|
CleanupScript: repository.CleanupScript,
|
|
DevScript: repository.DevScript,
|
|
CopyFiles: repository.CopyFiles,
|
|
SecretBindings: bindings,
|
|
CreatedAt: repository.CreatedAt,
|
|
UpdatedAt: repository.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromRepositorySet(set *models.RepositorySet) RepositorySetDTO {
|
|
items := make([]RepositorySetItemDTO, 0, len(set.Items))
|
|
for _, item := range set.Items {
|
|
items = append(items, RepositorySetItemDTO{
|
|
RepositoryID: item.RepositoryID,
|
|
Position: item.Position,
|
|
})
|
|
}
|
|
return RepositorySetDTO{
|
|
ID: set.ID,
|
|
WorkspaceID: set.WorkspaceID,
|
|
Name: set.Name,
|
|
Description: set.Description,
|
|
Repositories: items,
|
|
CreatedAt: set.CreatedAt,
|
|
UpdatedAt: set.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromRepositoryBranchPolicy(policy *models.RepositoryBranchPolicy) RepositoryBranchPolicyDTO {
|
|
return RepositoryBranchPolicyDTO{
|
|
ID: policy.ID, RepositoryID: policy.RepositoryID, Name: policy.Name,
|
|
Description: policy.Description, BaseBranch: policy.BaseBranch,
|
|
BranchTemplate: policy.BranchTemplate, PullRequestTarget: policy.PullRequestTarget,
|
|
CreatedAt: policy.CreatedAt, UpdatedAt: policy.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromRepositoryScript(script *models.RepositoryScript) RepositoryScriptDTO {
|
|
return RepositoryScriptDTO{
|
|
ID: script.ID,
|
|
RepositoryID: script.RepositoryID,
|
|
Name: script.Name,
|
|
Command: script.Command,
|
|
Position: script.Position,
|
|
CreatedAt: script.CreatedAt,
|
|
UpdatedAt: script.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromExecutor(executor *models.Executor) ExecutorDTO {
|
|
return ExecutorDTO{
|
|
ID: executor.ID,
|
|
Name: executor.Name,
|
|
Type: executor.Type,
|
|
Status: executor.Status,
|
|
IsSystem: executor.IsSystem,
|
|
Resumable: executor.Resumable,
|
|
Config: executor.Config,
|
|
CreatedAt: executor.CreatedAt,
|
|
UpdatedAt: executor.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromExecutorProfile(profile *models.ExecutorProfile) ExecutorProfileDTO {
|
|
return ExecutorProfileDTO{
|
|
ID: profile.ID,
|
|
ExecutorID: profile.ExecutorID,
|
|
Name: profile.Name,
|
|
McpPolicy: profile.McpPolicy,
|
|
Config: profile.Config,
|
|
PrepareScript: profile.PrepareScript,
|
|
CleanupScript: profile.CleanupScript,
|
|
EnvVars: profile.EnvVars,
|
|
CreatedAt: profile.CreatedAt,
|
|
UpdatedAt: profile.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// FromExecutorProfileWithExecutor converts an ExecutorProfile model to a DTO
|
|
// with executor type and name populated.
|
|
func FromExecutorProfileWithExecutor(profile *models.ExecutorProfile, executor *models.Executor) ExecutorProfileDTO {
|
|
d := FromExecutorProfile(profile)
|
|
if executor != nil {
|
|
d.ExecutorType = string(executor.Type)
|
|
d.ExecutorName = executor.Name
|
|
}
|
|
return d
|
|
}
|
|
|
|
func FromEnvironment(environment *models.Environment) EnvironmentDTO {
|
|
return EnvironmentDTO{
|
|
ID: environment.ID,
|
|
Name: environment.Name,
|
|
Kind: environment.Kind,
|
|
IsSystem: environment.IsSystem,
|
|
WorktreeRoot: environment.WorktreeRoot,
|
|
ImageTag: environment.ImageTag,
|
|
Dockerfile: environment.Dockerfile,
|
|
BuildConfig: environment.BuildConfig,
|
|
CreatedAt: environment.CreatedAt,
|
|
UpdatedAt: environment.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func FromLocalRepository(repo service.LocalRepository) LocalRepositoryDTO {
|
|
return LocalRepositoryDTO{
|
|
Path: repo.Path,
|
|
Name: repo.Name,
|
|
DefaultBranch: repo.DefaultBranch,
|
|
}
|
|
}
|
|
|
|
func FromTask(task *models.Task) TaskDTO {
|
|
return FromTaskWithPrimarySession(task, nil)
|
|
}
|
|
|
|
// FromTaskWithPrimarySession converts a task model to a TaskDTO, including the primary session ID.
|
|
func FromTaskWithPrimarySession(task *models.Task, primarySessionID *string) TaskDTO {
|
|
return FromTaskWithSessionInfo(task, primarySessionID, nil, models.ReviewStatusNone, nil, nil, nil, nil, nil, nil, nil, nil)
|
|
}
|
|
|
|
// FromTaskWithSessionInfo converts a task model to a TaskDTO, including session information.
|
|
func FromTaskWithSessionInfo(
|
|
task *models.Task,
|
|
primarySessionID *string,
|
|
sessionCount *int,
|
|
reviewStatus models.ReviewStatus,
|
|
primaryExecutorID *string,
|
|
primaryExecutorType *string,
|
|
primaryExecutorName *string,
|
|
primaryAgentName *string,
|
|
primaryAgentProfileID *string,
|
|
primaryWorkingDirectory *string,
|
|
primarySessionState *string,
|
|
primarySessionPendingAction *string,
|
|
) TaskDTO {
|
|
// Convert repositories
|
|
var repositories []TaskRepositoryDTO
|
|
for _, repo := range task.Repositories {
|
|
repositories = append(repositories, TaskRepositoryDTO{
|
|
ID: repo.ID,
|
|
TaskID: repo.TaskID,
|
|
RepositoryID: repo.RepositoryID,
|
|
BaseBranch: repo.BaseBranch,
|
|
CheckoutBranch: repo.CheckoutBranch,
|
|
BranchPolicyID: repo.BranchPolicyID,
|
|
BranchPolicyName: repo.BranchPolicyName,
|
|
BranchPolicyBaseBranch: repo.BranchPolicyBaseBranch,
|
|
BranchPolicyBranchTemplate: repo.BranchPolicyBranchTemplate,
|
|
BranchPolicyPullRequestTarget: repo.BranchPolicyPullRequestTarget,
|
|
Position: repo.Position,
|
|
Metadata: repo.Metadata,
|
|
CreatedAt: repo.CreatedAt,
|
|
UpdatedAt: repo.UpdatedAt,
|
|
})
|
|
}
|
|
var workspaceFolders []TaskWorkspaceFolderDTO
|
|
for _, folder := range task.WorkspaceFolders {
|
|
workspaceFolders = append(workspaceFolders, TaskWorkspaceFolderDTO{
|
|
ID: folder.ID,
|
|
TaskID: folder.TaskID,
|
|
LocalPath: folder.LocalPath,
|
|
DisplayName: folder.DisplayName,
|
|
Position: folder.Position,
|
|
CreatedAt: folder.CreatedAt,
|
|
UpdatedAt: folder.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
return TaskDTO{
|
|
ID: task.ID,
|
|
WorkspaceID: task.WorkspaceID,
|
|
WorkflowID: task.WorkflowID,
|
|
WorkflowStepID: task.WorkflowStepID,
|
|
Title: task.Title,
|
|
Description: task.Description,
|
|
State: task.State,
|
|
Priority: task.Priority,
|
|
WIPAdmitted: task.WIPAdmitted,
|
|
QueuedForStepID: task.QueuedForStepID,
|
|
QueuedAt: task.QueuedAt,
|
|
Repositories: repositories,
|
|
WorkspaceFolders: workspaceFolders,
|
|
Position: task.Position,
|
|
PrimarySessionID: primarySessionID,
|
|
SessionCount: sessionCount,
|
|
ReviewStatus: reviewStatus,
|
|
PrimaryExecutorID: primaryExecutorID,
|
|
PrimaryExecutorType: primaryExecutorType,
|
|
PrimaryExecutorName: primaryExecutorName,
|
|
PrimaryAgentName: primaryAgentName,
|
|
PrimaryAgentProfileID: primaryAgentProfileID,
|
|
PrimaryWorkingDirectory: primaryWorkingDirectory,
|
|
PrimarySessionState: primarySessionState,
|
|
PrimarySessionPendingAction: primarySessionPendingAction,
|
|
IsRemoteExecutor: primaryExecutorType != nil && models.IsRemoteExecutorType(models.ExecutorType(*primaryExecutorType)),
|
|
ParentID: task.ParentID,
|
|
Autopilot: task.Autopilot,
|
|
ArchivedAt: task.ArchivedAt,
|
|
CreatedAt: task.CreatedAt,
|
|
UpdatedAt: task.UpdatedAt,
|
|
Metadata: models.PublicTaskMetadata(task.Metadata),
|
|
Interrupted: task.Metadata[models.MetaKeyInterruptedAt] != nil,
|
|
AutoStartFailed: task.Metadata[models.MetaKeyAutoStartFailed] != nil,
|
|
// Office extensions. AssigneeAgentProfileID is a read-time
|
|
// projection from workflow_step_participants (ADR 0005 Wave F);
|
|
// the repo's task SELECTs hydrate it via a correlated subquery.
|
|
AssigneeAgentProfileID: task.AssigneeAgentProfileID,
|
|
AssigneeUserID: task.AssigneeUserID,
|
|
Origin: task.Origin,
|
|
ProjectID: task.ProjectID,
|
|
Labels: task.Labels,
|
|
Identifier: task.Identifier,
|
|
ExternalID: task.ExternalID,
|
|
IsFromOffice: task.IsFromOffice,
|
|
}
|
|
}
|
|
|
|
// FromTaskSessionSummary converts a session model to a summary DTO (no snapshot fields).
|
|
func FromTaskSessionSummary(session *models.TaskSession) TaskSessionSummaryDTO {
|
|
result := TaskSessionSummaryDTO{
|
|
ID: session.ID,
|
|
TaskID: session.TaskID,
|
|
Name: session.Name,
|
|
AgentExecutionID: session.AgentExecutionID,
|
|
ContainerID: session.ContainerID,
|
|
AgentProfileID: session.AgentProfileID,
|
|
ExecutorID: session.ExecutorID,
|
|
ExecutorProfileID: session.ExecutorProfileID,
|
|
EnvironmentID: session.EnvironmentID,
|
|
RepositoryID: session.RepositoryID,
|
|
BaseBranch: session.BaseBranch,
|
|
BaseCommitSHA: session.BaseCommitSHA,
|
|
WorkspacePath: session.WorkspacePath,
|
|
State: session.State,
|
|
ErrorMessage: session.ErrorMessage,
|
|
Metadata: session.Metadata,
|
|
StartedAt: session.StartedAt,
|
|
CompletedAt: session.CompletedAt,
|
|
UpdatedAt: session.UpdatedAt,
|
|
IsPrimary: session.IsPrimary,
|
|
IsPassthrough: session.IsPassthrough,
|
|
ReviewStatus: session.ReviewStatus,
|
|
TaskEnvironmentID: session.TaskEnvironmentID,
|
|
LastReadMessageID: session.LastReadMessageID,
|
|
}
|
|
if worktrees := session.WorktreesAPI(); len(worktrees) > 0 {
|
|
result.WorktreeID = session.Worktrees[0].WorktreeID
|
|
result.WorktreePath = session.Worktrees[0].WorktreePath
|
|
result.WorktreeBranch = session.Worktrees[0].WorktreeBranch
|
|
result.Worktrees = worktrees
|
|
}
|
|
return result
|
|
}
|
|
|
|
func FromTaskSession(session *models.TaskSession) TaskSessionDTO {
|
|
result := TaskSessionDTO{
|
|
ID: session.ID,
|
|
TaskID: session.TaskID,
|
|
Name: session.Name,
|
|
AgentExecutionID: session.AgentExecutionID,
|
|
ContainerID: session.ContainerID,
|
|
AgentProfileID: session.AgentProfileID,
|
|
ExecutorID: session.ExecutorID,
|
|
ExecutorProfileID: session.ExecutorProfileID,
|
|
EnvironmentID: session.EnvironmentID,
|
|
RepositoryID: session.RepositoryID,
|
|
BaseBranch: session.BaseBranch,
|
|
BaseCommitSHA: session.BaseCommitSHA,
|
|
WorkspacePath: session.WorkspacePath,
|
|
State: session.State,
|
|
ErrorMessage: session.ErrorMessage,
|
|
Metadata: session.Metadata,
|
|
AgentProfileSnapshot: session.AgentProfileSnapshot,
|
|
ExecutorSnapshot: session.ExecutorSnapshot,
|
|
EnvironmentSnapshot: session.EnvironmentSnapshot,
|
|
RepositorySnapshot: session.RepositorySnapshot,
|
|
StartedAt: session.StartedAt,
|
|
CompletedAt: session.CompletedAt,
|
|
UpdatedAt: session.UpdatedAt,
|
|
// Workflow fields
|
|
IsPrimary: session.IsPrimary,
|
|
IsPassthrough: session.IsPassthrough,
|
|
ReviewStatus: session.ReviewStatus,
|
|
TaskEnvironmentID: session.TaskEnvironmentID,
|
|
LastReadMessageID: session.LastReadMessageID,
|
|
CostSubcents: session.CostSubcents,
|
|
TokensIn: session.TokensIn,
|
|
TokensCachedIn: session.TokensCachedIn,
|
|
TokensOut: session.TokensOut,
|
|
}
|
|
if worktrees := session.WorktreesAPI(); len(worktrees) > 0 {
|
|
result.WorktreeID = session.Worktrees[0].WorktreeID
|
|
result.WorktreePath = session.Worktrees[0].WorktreePath
|
|
result.WorktreeBranch = session.Worktrees[0].WorktreeBranch
|
|
result.Worktrees = worktrees
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ForegroundActivityProvider surfaces the in-memory fine-grained busy substate
|
|
// for a session (ADR-0049). It is satisfied by the
|
|
// orchestrator; the serialization layer depends only on this narrow seam so it
|
|
// takes no hard orchestrator dependency and can be faked in tests.
|
|
type ForegroundActivityProvider interface {
|
|
ForegroundActivity(sessionID string) v1.ForegroundActivity
|
|
}
|
|
|
|
// CancellationPendingProvider surfaces the orchestrator's runtime cancellation
|
|
// projection without coupling task serialization to the orchestrator package.
|
|
type CancellationPendingProvider interface {
|
|
CancellationPending(sessionID string) bool
|
|
}
|
|
|
|
// CancellationPendingSnapshotProvider is the atomic form of the cancellation
|
|
// projection used by serialization boundaries that need ordering identity.
|
|
// Implementations must read the boolean and revision from one critical section.
|
|
type CancellationPendingSnapshotProvider interface {
|
|
CancellationPendingSnapshot(sessionID string) (pending bool, revision uint64)
|
|
}
|
|
|
|
type ActiveSubagentCountProvider interface {
|
|
ActiveSubagentCount(sessionID string) int
|
|
}
|
|
|
|
// SteerEligibleProvider reports whether a send to a session right now would be
|
|
// delivered as a mid-turn steer. Optional: a provider that does not implement it
|
|
// simply never advertises steering, which is the conservative default.
|
|
type SteerEligibleProvider interface {
|
|
SteerEligible(sessionID string, state models.TaskSessionState) bool
|
|
}
|
|
|
|
// EnrichForegroundActivity stamps the live fine-grained busy substate onto a full
|
|
// session DTO. Generating is emitted only for RUNNING sessions; detached
|
|
// background activity remains meaningful after the coarse state settles.
|
|
func EnrichForegroundActivity(dto *TaskSessionDTO, provider ForegroundActivityProvider) {
|
|
if dto == nil || provider == nil {
|
|
return
|
|
}
|
|
if activity, ok := sessionForegroundActivity(dto.ID, dto.State, provider); ok {
|
|
dto.ForegroundActivity = activity
|
|
}
|
|
dto.ActiveSubagentCount = activeSubagentCount(dto.ID, provider)
|
|
dto.SupportsSteering = steerEligible(dto.ID, dto.State, provider)
|
|
}
|
|
|
|
// EnrichForegroundActivitySummary is EnrichForegroundActivity for the lightweight
|
|
// summary DTO used by the list endpoints.
|
|
func EnrichForegroundActivitySummary(dto *TaskSessionSummaryDTO, provider ForegroundActivityProvider) {
|
|
if dto == nil || provider == nil {
|
|
return
|
|
}
|
|
if activity, ok := sessionForegroundActivity(dto.ID, dto.State, provider); ok {
|
|
dto.ForegroundActivity = activity
|
|
}
|
|
dto.ActiveSubagentCount = activeSubagentCount(dto.ID, provider)
|
|
dto.SupportsSteering = steerEligible(dto.ID, dto.State, provider)
|
|
}
|
|
|
|
func activeSubagentCount(sessionID string, provider ForegroundActivityProvider) int {
|
|
countProvider, ok := provider.(ActiveSubagentCountProvider)
|
|
if !ok {
|
|
return 0
|
|
}
|
|
return countProvider.ActiveSubagentCount(sessionID)
|
|
}
|
|
|
|
// steerEligible reports whether the live provider would deliver a send to this
|
|
// session as a mid-turn steer. Derived here at the serialization boundary from
|
|
// the live in-memory provider and never persisted, so a restart with no
|
|
// connected execution serializes false.
|
|
func steerEligible(sessionID string, state models.TaskSessionState, provider ForegroundActivityProvider) bool {
|
|
steerProvider, ok := provider.(SteerEligibleProvider)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return steerProvider.SteerEligible(sessionID, state)
|
|
}
|
|
|
|
// WorkflowStepDTO represents a workflow step for API responses
|
|
type WorkflowStepDTO struct {
|
|
ID string `json:"id"`
|
|
WorkflowID string `json:"workflow_id"`
|
|
Name string `json:"name"`
|
|
Position int `json:"position"`
|
|
Color string `json:"color"`
|
|
Prompt string `json:"prompt,omitempty"`
|
|
Events *StepEventsDTO `json:"events,omitempty"`
|
|
AllowManualMove bool `json:"allow_manual_move"`
|
|
IsStartStep bool `json:"is_start_step"`
|
|
ShowInCommandPanel bool `json:"show_in_command_panel"`
|
|
AutoArchiveAfterHours int `json:"auto_archive_after_hours,omitempty"`
|
|
AgentProfileID string `json:"agent_profile_id,omitempty"`
|
|
ProfileSessionStartPolicy models.WorkflowProfileSessionStartPolicy `json:"profile_session_start_policy"`
|
|
ProfileSessionEndPolicy models.WorkflowProfileSessionEndPolicy `json:"profile_session_end_policy"`
|
|
WIPLimit int `json:"wip_limit"`
|
|
PullFromStepID string `json:"pull_from_step_id,omitempty"`
|
|
// StageType is a Phase 2 (ADR-0004) semantic hint for the frontend.
|
|
// Allowed values: "work" | "review" | "approval" | "custom".
|
|
StageType string `json:"stage_type,omitempty"`
|
|
AutoAdvanceRequiresSignal bool `json:"auto_advance_requires_signal"`
|
|
CancelTriggersTurnComplete bool `json:"cancel_triggers_turn_complete"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// StepEventsDTO represents step events for API responses
|
|
type StepEventsDTO struct {
|
|
OnEnter []StepActionDTO `json:"on_enter,omitempty"`
|
|
OnTurnStart []StepActionDTO `json:"on_turn_start,omitempty"`
|
|
OnTurnComplete []StepActionDTO `json:"on_turn_complete,omitempty"`
|
|
OnExit []StepActionDTO `json:"on_exit,omitempty"`
|
|
OnComment []StepActionDTO `json:"on_comment,omitempty"`
|
|
OnBlockerResolved []StepActionDTO `json:"on_blocker_resolved,omitempty"`
|
|
OnChildrenCompleted []StepActionDTO `json:"on_children_completed,omitempty"`
|
|
OnApprovalResolved []StepActionDTO `json:"on_approval_resolved,omitempty"`
|
|
OnHeartbeat []StepActionDTO `json:"on_heartbeat,omitempty"`
|
|
OnBudgetAlert []StepActionDTO `json:"on_budget_alert,omitempty"`
|
|
OnAgentError []StepActionDTO `json:"on_agent_error,omitempty"`
|
|
}
|
|
|
|
// StepActionDTO represents a step action for API responses
|
|
type StepActionDTO struct {
|
|
Type string `json:"type"`
|
|
Config map[string]interface{} `json:"config,omitempty"`
|
|
}
|
|
|
|
// MoveTaskResponse includes the task and the target workflow step info
|
|
type MoveTaskResponse struct {
|
|
Task TaskDTO `json:"task"`
|
|
WorkflowStep WorkflowStepDTO `json:"workflow_step"`
|
|
}
|
|
|
|
// Session Workflow Review DTOs
|
|
|
|
// ApproveSessionRequest is the request to approve a session's current step
|
|
type ApproveSessionRequest struct {
|
|
SessionID string `json:"-"` // From URL path
|
|
}
|
|
|
|
// ApproveSessionResponse is the response after approving a session
|
|
type ApproveSessionResponse struct {
|
|
Success bool `json:"success"`
|
|
Session TaskSessionDTO `json:"session"`
|
|
WorkflowStep WorkflowStepDTO `json:"workflow_step,omitempty"` // New step after approval
|
|
}
|
|
|
|
// TaskPlanDTO represents a task plan for API responses
|
|
type TaskPlanDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content"`
|
|
CreatedBy string `json:"created_by"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
ImplementationStartedAt *time.Time `json:"implementation_started_at,omitempty"`
|
|
ImplementationStartedSessionID *string `json:"implementation_started_session_id,omitempty"`
|
|
ImplementationStartedBy *string `json:"implementation_started_by,omitempty"`
|
|
}
|
|
|
|
// TaskPlanFromModel converts a TaskPlan model to a TaskPlanDTO.
|
|
func TaskPlanFromModel(plan *models.TaskPlan) *TaskPlanDTO {
|
|
if plan == nil {
|
|
return nil
|
|
}
|
|
return &TaskPlanDTO{
|
|
ID: plan.ID,
|
|
TaskID: plan.TaskID,
|
|
Title: plan.Title,
|
|
Content: plan.Content,
|
|
CreatedBy: plan.CreatedBy,
|
|
CreatedAt: plan.CreatedAt,
|
|
UpdatedAt: plan.UpdatedAt,
|
|
ImplementationStartedAt: plan.ImplementationStartedAt,
|
|
ImplementationStartedSessionID: plan.ImplementationStartedSessionID,
|
|
ImplementationStartedBy: plan.ImplementationStartedBy,
|
|
}
|
|
}
|
|
|
|
// TaskPlanRevisionDTO represents a plan revision for API responses.
|
|
// Content is optional so list responses can omit it (fetched on demand).
|
|
type TaskPlanRevisionDTO struct {
|
|
ID string `json:"id"`
|
|
TaskID string `json:"task_id"`
|
|
RevisionNumber int `json:"revision_number"`
|
|
Title string `json:"title"`
|
|
Content string `json:"content,omitempty"`
|
|
// ContentLength is the character (rune) count of Content, computed here
|
|
// before TaskPlanRevisionMetaFromModel blanks Content for list/WS payloads
|
|
// — so list rows can show a size without fetching full content.
|
|
ContentLength int `json:"content_length"`
|
|
AuthorKind string `json:"author_kind"`
|
|
AuthorName string `json:"author_name"`
|
|
RevertOfRevisionID *string `json:"revert_of_revision_id,omitempty"`
|
|
WorkflowStepID string `json:"workflow_step_id,omitempty"`
|
|
WorkflowStepName string `json:"workflow_step_name,omitempty"`
|
|
WorkflowStepColor string `json:"workflow_step_color,omitempty"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// TaskPlanRevisionFromModel converts a TaskPlanRevision model with content.
|
|
func TaskPlanRevisionFromModel(rev *models.TaskPlanRevision) *TaskPlanRevisionDTO {
|
|
if rev == nil {
|
|
return nil
|
|
}
|
|
return &TaskPlanRevisionDTO{
|
|
ID: rev.ID,
|
|
TaskID: rev.TaskID,
|
|
RevisionNumber: rev.RevisionNumber,
|
|
Title: rev.Title,
|
|
Content: rev.Content,
|
|
ContentLength: models.PlanContentLength(rev.Content),
|
|
AuthorKind: rev.AuthorKind,
|
|
AuthorName: rev.AuthorName,
|
|
RevertOfRevisionID: rev.RevertOfRevisionID,
|
|
WorkflowStepID: rev.WorkflowStepID,
|
|
WorkflowStepName: rev.WorkflowStepName,
|
|
WorkflowStepColor: rev.WorkflowStepColor,
|
|
CreatedAt: rev.CreatedAt,
|
|
UpdatedAt: rev.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// TaskPlanRevisionMetaFromModel converts without content (for list payloads/WS broadcasts).
|
|
func TaskPlanRevisionMetaFromModel(rev *models.TaskPlanRevision) *TaskPlanRevisionDTO {
|
|
meta := TaskPlanRevisionFromModel(rev)
|
|
if meta != nil {
|
|
meta.Content = ""
|
|
}
|
|
return meta
|
|
}
|
|
|
|
const turnTimestampLayout = "2006-01-02T15:04:05.000000000Z07:00"
|
|
|
|
// FromTurn converts a Turn model to a TurnDTO.
|
|
func FromTurn(turn *models.Turn) TurnDTO {
|
|
var completedAt *string
|
|
if turn.CompletedAt != nil {
|
|
formatted := turn.CompletedAt.UTC().Format(turnTimestampLayout)
|
|
completedAt = &formatted
|
|
}
|
|
|
|
return TurnDTO{
|
|
ID: turn.ID,
|
|
SessionID: turn.TaskSessionID,
|
|
TaskID: turn.TaskID,
|
|
StartedAt: turn.StartedAt.UTC().Format(turnTimestampLayout),
|
|
CompletedAt: completedAt,
|
|
Metadata: turn.Metadata,
|
|
CreatedAt: turn.CreatedAt.UTC().Format(turnTimestampLayout),
|
|
UpdatedAt: turn.UpdatedAt.UTC().Format(turnTimestampLayout),
|
|
}
|
|
}
|