Files
ttstd-infinite-canvas/backend/internal/app/cloud_agent_task_facts.go

132 lines
6.3 KiB
Go

package app
import (
"encoding/json"
"time"
"infinite-canvas/backend/internal/model"
"infinite-canvas/backend/internal/repository"
)
// cloudAgentTaskFacts is the model-facing projection of durable task state.
// Keep this typed: canvas reads, task_get and the fresh context frame must not
// independently invent string-key contracts for the same execution facts.
type cloudAgentTaskFacts struct {
TaskID string `json:"taskId"`
TaskStatus model.TaskStatus `json:"taskStatus"`
TaskType string `json:"taskType,omitempty"`
Operation string `json:"operation,omitempty"`
AgentRunID string `json:"agentRunId,omitempty"`
GenerationID string `json:"generationId,omitempty"`
ApprovalID string `json:"approvalId,omitempty"`
NodeID string `json:"nodeId,omitempty"`
TaskSubmitted bool `json:"taskSubmitted"`
SubmissionOutcome string `json:"submissionOutcome"`
Phase string `json:"phase,omitempty"`
DiagnosticCode string `json:"diagnosticCode,omitempty"`
DiagnosticID string `json:"diagnosticId,omitempty"`
FieldPath string `json:"fieldPath,omitempty"`
RetryClass string `json:"retryClass,omitempty"`
Error string `json:"error,omitempty"`
WritebackOutcome string `json:"writebackOutcome,omitempty"`
WritebackReason string `json:"writebackReason,omitempty"`
WritebackNodeID string `json:"writebackNodeId,omitempty"`
WritebackError string `json:"writebackError,omitempty"`
CancellationSource string `json:"cancellationSource,omitempty"`
CancellationAt *time.Time `json:"cancellationRequestedAt,omitempty"`
SubmissionReceipt *cloudAgentSubmissionReceipt `json:"submissionReceipt,omitempty"`
Billing *cloudAgentTaskBillingFacts `json:"billing,omitempty"`
}
type cloudAgentSubmissionReceipt struct {
TaskID string `json:"taskId"`
GenerationID string `json:"generationId,omitempty"`
RecordedAt time.Time `json:"recordedAt"`
}
type cloudAgentTaskBillingFacts struct {
OrderID string `json:"orderId"`
Status model.BillingStatus `json:"status,omitempty"`
AuthorizedChargeMicrocredits int64 `json:"authorizedChargeMicrocredits"`
ChargeLimitSet bool `json:"chargeLimitSet"`
ChargeLimitMicrocredits int64 `json:"chargeLimitMicrocredits"`
ReservedAmountMicrocredits int64 `json:"reservedAmountMicrocredits,omitempty"`
ActualAmountMicrocredits int64 `json:"actualAmountMicrocredits,omitempty"`
RefundedAmountMicrocredits int64 `json:"refundedAmountMicrocredits,omitempty"`
StateAvailable bool `json:"stateAvailable"`
}
func cloudAgentTaskDiagnostic(repo *repository.Repository, task *model.Task) map[string]any {
if task == nil {
return map[string]any{"taskStatus": "unavailable", "taskSubmitted": false, "submissionOutcome": "unavailable"}
}
facts := cloudAgentTaskFacts{
TaskID: task.ID, TaskStatus: task.Status, TaskType: task.Type, Operation: task.Operation,
AgentRunID: task.AgentRunID, GenerationID: task.GenerationID, ApprovalID: task.ApprovalID,
TaskSubmitted: true, SubmissionOutcome: cloudAgentTaskSubmissionOutcome(task),
CancellationSource: task.CancellationSource, CancellationAt: task.CancellationRequestedAt,
SubmissionReceipt: &cloudAgentSubmissionReceipt{TaskID: task.ID, GenerationID: task.GenerationID, RecordedAt: task.CreatedAt},
}
if context := taskClientContext(task.InputJSON); context != nil {
facts.NodeID = context.NodeID
}
if diagnostic := taskExecutionDiagnostic(task); diagnostic != nil {
facts.TaskSubmitted = diagnostic.TaskSubmitted
facts.SubmissionOutcome = diagnostic.SubmissionOutcome
facts.Phase = diagnostic.Phase
facts.DiagnosticCode = diagnostic.Code
facts.DiagnosticID = diagnostic.DiagnosticID
facts.FieldPath = diagnostic.FieldPath
facts.RetryClass = diagnostic.RetryClass
facts.Error = diagnostic.SafeMessage
facts.WritebackOutcome = diagnostic.WritebackOutcome
facts.WritebackReason = diagnostic.WritebackReason
facts.WritebackNodeID = diagnostic.WritebackNodeID
facts.WritebackError = diagnostic.WritebackMessage
} else if task.Status == model.TaskStatusFailed || task.Status == model.TaskStatusCancelled {
facts.Error = cloudAgentSafeMediaTaskError(task)
}
if task.BillingOrderID != "" {
facts.Billing = &cloudAgentTaskBillingFacts{
OrderID: task.BillingOrderID, AuthorizedChargeMicrocredits: task.AuthorizedChargeMicrocredits,
}
if repo != nil {
if order, err := repo.BillingOrder(task.BillingOrderID); err == nil && order.UserID == task.UserID && (order.TaskID == "" || order.TaskID == task.ID) {
facts.Billing.Status = order.Status
facts.Billing.ChargeLimitSet = order.ChargeLimitSet
facts.Billing.ChargeLimitMicrocredits = order.ChargeLimitMicrocredits
facts.Billing.ReservedAmountMicrocredits = order.ReservedAmountMicrocredits
facts.Billing.ActualAmountMicrocredits = order.ActualAmountMicrocredits
facts.Billing.RefundedAmountMicrocredits = order.RefundedAmountMicrocredits
facts.Billing.StateAvailable = true
}
}
}
raw, err := json.Marshal(facts)
if err != nil {
return map[string]any{"taskId": task.ID, "taskStatus": task.Status, "taskSubmitted": true, "submissionOutcome": "unknown"}
}
result := map[string]any{}
if json.Unmarshal(raw, &result) != nil {
return map[string]any{"taskId": task.ID, "taskStatus": task.Status, "taskSubmitted": true, "submissionOutcome": "unknown"}
}
return result
}
func cloudAgentTaskSubmissionOutcome(task *model.Task) string {
if task == nil {
return "unavailable"
}
if task.ProviderRequestID != "" {
return "accepted"
}
switch task.Status {
case model.TaskStatusQueued:
return "queued"
case model.TaskStatusRunning:
return "submitted"
default:
return "unknown"
}
}