d25d482dc2
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
import { z } from 'zod'
|
|
import { userFileSchema } from '@/lib/api/contracts/primitives'
|
|
import { defineRouteContract } from '@/lib/api/contracts/types'
|
|
|
|
const comparisonOperatorSchema = z.enum(['=', '>', '<', '>=', '<=', '!='])
|
|
|
|
export const logIdParamsSchema = z.object({
|
|
id: z.string().min(1),
|
|
})
|
|
|
|
export const executionIdParamsSchema = z.object({
|
|
executionId: z.string().min(1),
|
|
})
|
|
|
|
export const cancelWorkflowExecutionParamsSchema = z.object({
|
|
id: z.string().min(1, 'Invalid workflow ID'),
|
|
executionId: z.string().min(1, 'Invalid execution ID'),
|
|
})
|
|
|
|
const logFilterQuerySchema = z.object({
|
|
workspaceId: z.string(),
|
|
level: z.string().optional(),
|
|
workflowIds: z.string().optional(),
|
|
folderIds: z.string().optional(),
|
|
triggers: z.string().optional(),
|
|
startDate: z.string().optional(),
|
|
endDate: z.string().optional(),
|
|
search: z.string().optional(),
|
|
workflowName: z.string().optional(),
|
|
folderName: z.string().optional(),
|
|
executionId: z.string().optional(),
|
|
costOperator: comparisonOperatorSchema.optional(),
|
|
costValue: z.coerce.number().optional(),
|
|
durationOperator: comparisonOperatorSchema.optional(),
|
|
durationValue: z.coerce.number().optional(),
|
|
})
|
|
|
|
export const logSortBySchema = z.enum(['date', 'duration', 'cost', 'status']).default('date')
|
|
export const logSortOrderSchema = z.enum(['asc', 'desc']).default('desc')
|
|
|
|
export const listLogsQuerySchema = logFilterQuerySchema.extend({
|
|
cursor: z.string().optional(),
|
|
limit: z.coerce.number().int().min(1).max(200).optional().default(100),
|
|
sortBy: logSortBySchema,
|
|
sortOrder: logSortOrderSchema,
|
|
})
|
|
|
|
export const logDetailQuerySchema = z.object({
|
|
workspaceId: z.string().min(1),
|
|
})
|
|
|
|
export const statsQueryParamsSchema = logFilterQuerySchema.extend({
|
|
segmentCount: z.coerce.number().optional().default(72),
|
|
})
|
|
|
|
const workflowSummarySchema = z
|
|
.object({
|
|
id: z.string(),
|
|
name: z.string().nullable(),
|
|
description: z.string().nullable(),
|
|
folderId: z.string().nullable(),
|
|
userId: z.string().nullable(),
|
|
workspaceId: z.string().nullable(),
|
|
createdAt: z.string().nullable(),
|
|
updatedAt: z.string().nullable(),
|
|
})
|
|
.partial()
|
|
|
|
const tokenBreakdownSchema = z
|
|
.object({
|
|
total: z.number().optional(),
|
|
input: z.number().optional(),
|
|
output: z.number().optional(),
|
|
prompt: z.number().optional(),
|
|
completion: z.number().optional(),
|
|
})
|
|
.partial()
|
|
|
|
const modelCostSchema = z
|
|
.object({
|
|
input: z.number().optional(),
|
|
output: z.number().optional(),
|
|
total: z.number().optional(),
|
|
tokens: tokenBreakdownSchema.optional(),
|
|
})
|
|
.partial()
|
|
|
|
const costSummarySchema = z
|
|
.object({
|
|
total: z.number().optional(),
|
|
input: z.number().optional(),
|
|
output: z.number().optional(),
|
|
tokens: tokenBreakdownSchema.optional(),
|
|
models: z.record(z.string(), modelCostSchema).optional(),
|
|
pricing: z
|
|
.object({
|
|
input: z.number(),
|
|
output: z.number(),
|
|
cachedInput: z.number().optional(),
|
|
updatedAt: z.string(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.partial()
|
|
|
|
/**
|
|
* Itemized cost breakdown derived from the usage_log ledger (the single source
|
|
* of truth) for the detail view. Each item is one billed line (base fee, a
|
|
* model, or a tool/integration); the items reconcile to `total`.
|
|
*/
|
|
const costLedgerItemSchema = z.object({
|
|
category: z.enum(['fixed', 'model', 'tool']),
|
|
description: z.string(),
|
|
cost: z.number(),
|
|
inputTokens: z.number().optional(),
|
|
outputTokens: z.number().optional(),
|
|
})
|
|
|
|
export const costLedgerSchema = z.object({
|
|
total: z.number(),
|
|
items: z.array(costLedgerItemSchema),
|
|
})
|
|
|
|
export type CostLedger = z.output<typeof costLedgerSchema>
|
|
export type CostLedgerItem = z.output<typeof costLedgerItemSchema>
|
|
|
|
const pauseSummarySchema = z.object({
|
|
status: z.string().nullable(),
|
|
total: z.number(),
|
|
resumed: z.number(),
|
|
})
|
|
|
|
const blockExecutionSchema = z.object({
|
|
id: z.string(),
|
|
blockId: z.string(),
|
|
blockName: z.string(),
|
|
blockType: z.string(),
|
|
startedAt: z.string(),
|
|
endedAt: z.string(),
|
|
durationMs: z.number(),
|
|
status: z.enum(['success', 'error', 'skipped']),
|
|
errorMessage: z.string().optional(),
|
|
errorStackTrace: z.string().optional(),
|
|
inputData: z.unknown(),
|
|
outputData: z.unknown(),
|
|
cost: costSummarySchema.optional(),
|
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
})
|
|
|
|
const toolCallSchema = z
|
|
.object({
|
|
id: z.string().optional(),
|
|
name: z.string().optional(),
|
|
arguments: z.unknown().optional(),
|
|
result: z.unknown().optional(),
|
|
error: z.string().optional(),
|
|
startTime: z.string().optional(),
|
|
endTime: z.string().optional(),
|
|
duration: z.number().optional(),
|
|
})
|
|
.passthrough()
|
|
|
|
type TraceSpan = {
|
|
id: string
|
|
name: string
|
|
type: string
|
|
duration?: number
|
|
durationMs?: number
|
|
startTime?: string
|
|
endTime?: string
|
|
status?: string
|
|
blockId?: string
|
|
input?: unknown
|
|
output?: unknown
|
|
tokens?: number | { total?: number; input?: number; output?: number }
|
|
relativeStartMs?: number
|
|
toolCalls?: Array<z.output<typeof toolCallSchema>>
|
|
children?: TraceSpan[]
|
|
}
|
|
|
|
const traceSpanSchema: z.ZodType<TraceSpan> = z.lazy(() =>
|
|
z
|
|
.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
type: z.string(),
|
|
duration: z.number().optional(),
|
|
durationMs: z.number().optional(),
|
|
startTime: z.string().optional(),
|
|
endTime: z.string().optional(),
|
|
status: z.string().optional(),
|
|
blockId: z.string().optional(),
|
|
input: z.unknown().optional(),
|
|
output: z.unknown().optional(),
|
|
tokens: z
|
|
.union([
|
|
z.number(),
|
|
z
|
|
.object({
|
|
total: z.number().optional(),
|
|
input: z.number().optional(),
|
|
output: z.number().optional(),
|
|
})
|
|
.partial(),
|
|
])
|
|
.optional(),
|
|
relativeStartMs: z.number().optional(),
|
|
toolCalls: z.array(toolCallSchema).optional(),
|
|
children: z.array(traceSpanSchema).optional(),
|
|
})
|
|
.passthrough()
|
|
)
|
|
|
|
const executionDataDetailSchema = z
|
|
.object({
|
|
totalDuration: z.number().nullable().optional(),
|
|
enhanced: z.literal(true).optional(),
|
|
traceSpans: z.array(traceSpanSchema).optional(),
|
|
blockExecutions: z.array(blockExecutionSchema).optional(),
|
|
finalOutput: z.unknown().optional(),
|
|
workflowInput: z.unknown().optional(),
|
|
blockInput: z.record(z.string(), z.unknown()).optional(),
|
|
trigger: z.unknown().optional(),
|
|
})
|
|
.passthrough()
|
|
|
|
export const workflowLogSummarySchema = z.object({
|
|
id: z.string(),
|
|
workflowId: z.string().nullable(),
|
|
executionId: z.string().nullable(),
|
|
deploymentVersionId: z.string().nullable(),
|
|
deploymentVersion: z.number().nullable(),
|
|
deploymentVersionName: z.string().nullable(),
|
|
level: z.string(),
|
|
status: z.string().nullable(),
|
|
duration: z.string().nullable(),
|
|
trigger: z.string().nullable(),
|
|
createdAt: z.string(),
|
|
workflow: workflowSummarySchema.nullable(),
|
|
jobTitle: z.string().nullable(),
|
|
// Top-level run cost is the cost_total projection of the usage_log ledger,
|
|
// rendered as { total } (dollars). The itemized breakdown lives in costLedger
|
|
// (detail only); per-block costs use the richer costSummarySchema elsewhere.
|
|
cost: z.object({ total: z.number() }).nullable(),
|
|
pauseSummary: pauseSummarySchema,
|
|
hasPendingPause: z.boolean(),
|
|
})
|
|
|
|
export const workflowLogDetailSchema = workflowLogSummarySchema.extend({
|
|
executionData: executionDataDetailSchema,
|
|
files: z.array(userFileSchema).nullable(),
|
|
// Itemized, ledger-sourced cost breakdown. Null for legacy/pre-ledger runs,
|
|
// where the UI falls back to the (reconciling) cost jsonb.
|
|
costLedger: costLedgerSchema.nullable().optional(),
|
|
})
|
|
|
|
export type WorkflowLogSummary = z.output<typeof workflowLogSummarySchema>
|
|
export type WorkflowLogDetail = z.output<typeof workflowLogDetailSchema>
|
|
|
|
/**
|
|
* A row that may be either a list-view summary or a fully loaded detail. Used by
|
|
* UI surfaces that render the same log before and after its detail query resolves.
|
|
*/
|
|
export type WorkflowLogRow = WorkflowLogSummary &
|
|
Partial<Pick<WorkflowLogDetail, 'executionData' | 'files' | 'costLedger'>>
|
|
|
|
export const listLogsResponseSchema = z.object({
|
|
data: z.array(workflowLogSummarySchema),
|
|
nextCursor: z.string().nullable(),
|
|
})
|
|
|
|
export type ListLogsResponse = z.output<typeof listLogsResponseSchema>
|
|
|
|
export const segmentStatsSchema = z.object({
|
|
timestamp: z.string(),
|
|
totalExecutions: z.number(),
|
|
successfulExecutions: z.number(),
|
|
avgDurationMs: z.number(),
|
|
})
|
|
|
|
export const workflowStatsSchema = z.object({
|
|
workflowId: z.string(),
|
|
workflowName: z.string(),
|
|
segments: z.array(segmentStatsSchema),
|
|
overallSuccessRate: z.number(),
|
|
totalExecutions: z.number(),
|
|
totalSuccessful: z.number(),
|
|
})
|
|
|
|
export const dashboardStatsResponseSchema = z.object({
|
|
workflows: z.array(workflowStatsSchema),
|
|
aggregateSegments: z.array(segmentStatsSchema),
|
|
totalRuns: z.number(),
|
|
totalErrors: z.number(),
|
|
avgLatency: z.number(),
|
|
timeBounds: z.object({
|
|
start: z.string(),
|
|
end: z.string(),
|
|
}),
|
|
segmentMs: z.number(),
|
|
})
|
|
|
|
export const executionSnapshotDataSchema = z.object({
|
|
executionId: z.string(),
|
|
workflowId: z.string().nullable(),
|
|
workflowState: z.record(z.string(), z.unknown()).nullable(),
|
|
childWorkflowSnapshots: z.record(z.string(), z.unknown()).optional(),
|
|
executionMetadata: z.object({
|
|
trigger: z.string().nullable(),
|
|
startedAt: z.string(),
|
|
endedAt: z.string().optional(),
|
|
totalDurationMs: z.number().nullable().optional(),
|
|
cost: z.unknown().nullable(),
|
|
totalTokens: z.number().nullable().optional(),
|
|
}),
|
|
})
|
|
|
|
export const triggersQuerySchema = z.object({
|
|
workspaceId: z.string(),
|
|
})
|
|
export type TriggersQuery = z.output<typeof triggersQuerySchema>
|
|
|
|
export const cancelWorkflowExecutionResponseSchema = z.object({
|
|
success: z.boolean(),
|
|
executionId: z.string(),
|
|
redisAvailable: z.boolean(),
|
|
durablyRecorded: z.boolean(),
|
|
locallyAborted: z.boolean(),
|
|
pausedCancelled: z.boolean(),
|
|
reason: z.enum(['recorded', 'redis_unavailable', 'redis_write_failed']),
|
|
})
|
|
|
|
export type SegmentStats = z.output<typeof segmentStatsSchema>
|
|
export type WorkflowStats = z.output<typeof workflowStatsSchema>
|
|
export type DashboardStatsResponse = z.output<typeof dashboardStatsResponseSchema>
|
|
export type ExecutionSnapshotData = z.output<typeof executionSnapshotDataSchema>
|
|
export type CancelWorkflowExecutionResponse = z.output<typeof cancelWorkflowExecutionResponseSchema>
|
|
|
|
export const listLogsContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/logs',
|
|
query: listLogsQuerySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: listLogsResponseSchema,
|
|
},
|
|
})
|
|
|
|
export const getLogDetailContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/logs/[id]',
|
|
params: logIdParamsSchema,
|
|
query: logDetailQuerySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: z.object({
|
|
data: workflowLogDetailSchema,
|
|
}),
|
|
},
|
|
})
|
|
|
|
export const getLogByExecutionIdContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/logs/by-execution/[executionId]',
|
|
params: executionIdParamsSchema,
|
|
query: logDetailQuerySchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: z.object({
|
|
data: workflowLogDetailSchema,
|
|
}),
|
|
},
|
|
})
|
|
|
|
export const getDashboardStatsContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/logs/stats',
|
|
query: statsQueryParamsSchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: dashboardStatsResponseSchema,
|
|
},
|
|
})
|
|
|
|
export const getExecutionSnapshotContract = defineRouteContract({
|
|
method: 'GET',
|
|
path: '/api/logs/execution/[executionId]',
|
|
params: executionIdParamsSchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: executionSnapshotDataSchema,
|
|
},
|
|
})
|
|
|
|
export const cancelWorkflowExecutionContract = defineRouteContract({
|
|
method: 'POST',
|
|
path: '/api/workflows/[id]/executions/[executionId]/cancel',
|
|
params: cancelWorkflowExecutionParamsSchema,
|
|
response: {
|
|
mode: 'json',
|
|
schema: cancelWorkflowExecutionResponseSchema,
|
|
},
|
|
})
|