import { z } from 'zod' import { runtimeChecklistSchema } from './runtime-checklist' import { externalKnowledgeLocatorSchema } from './external-knowledge-contracts' import { runtimeSelectionLayerSchema, type AgentRuntimeSelection } from './runtime-selection-contracts' import { sshHostIdSchema } from './ssh-host-contracts' export const assistantIdSchema = z.string().uuid() export const executionStatsInputSchema = z.union([ z.object({ conversationId: assistantIdSchema }).strict(), z.object({ projectId: assistantIdSchema }).strict() ]) export type ExecutionStatsInput = z.infer export interface ExecutionStats { /** Accumulated running time, including open segments through asOf. */ durationMs: number /** Number of open segments; the UI adds (now - asOf) * runningCount. */ runningCount: number /** Pre-timing history or an interrupted segment has unknown duration. */ incomplete: boolean /** Main epoch milliseconds at snapshot time. */ asOf: number /** Project card totals; empty for conversation queries. */ taskDurations: Array<{ id: string; durationMs: number; runningCount: number; incomplete: boolean }> } export const projectKindSchema = z.enum(['user', 'channel']) export const projectChannels = [ 'weixin', 'wecom', 'dingtalk', 'telegram' ] as const export const projectChannelSchema = z.enum(projectChannels) export const projectChannelLabels: Record = { weixin: '微信 ClawBot', wecom: '企业微信', dingtalk: '钉钉', telegram: 'Telegram' } export type ProjectKind = z.infer export type ProjectChannel = z.infer const projectExecutionSpacePathSchema = z.string().max(4_096) export const activityRecordSchema = z .object({ id: z.string().min(1).max(256), conversationId: z.string().min(1).max(256), requestId: z.string().min(1).max(256), callId: z.string().min(1).max(256).optional(), scope: z.discriminatedUnion('kind', [ z.object({ kind: z.literal('global') }).strict(), z .object({ kind: z.literal('project'), projectId: z.string().min(1).max(256), projectName: z.string().min(1).max(120) }) .strict(), z.object({ kind: z.literal('unavailable') }).strict() ]), kind: z.enum([ 'request', 'tool', 'approval', 'subagent', 'result' ]), title: z.string().min(1).max(240), detail: z.string(), status: z.enum([ 'pending', 'running', 'completed', 'failed', 'denied', 'cancelled', 'interrupted' ]), createdAt: z.number().finite().nonnegative() }) .strict() export const activityHistorySnapshotSchema = z .object({ records: z.array(activityRecordSchema), legacyHistoryMayBeIncomplete: z.boolean() }) .strict() /** * Incremental activity history change (PERF-15). Changes apply in order to the * stored, newest-first list; the result is identical to `replace` with the * same final list. A record is identified by its `id` (first occurrence when * legacy history contains duplicate IDs). * - `upsert` + `position: 'front'` inserts the record, or moves the existing * record to the front with the new content (renderer `upsertActivityRecord`). * - `upsert` + `position: 'in-place'` replaces the existing record where it * is; a record that does not exist yet is inserted at the front. * - `remove` deletes every record with the ID. * The other changes match shown records (first occurrences) by content, so * the renderer does not need to have them loaded: * - `upsert-call`: a tool/subagent record with a callId. The first record * of the same kind, request and call is moved to the front with the new * content but its own id, createdAt and scope (nothing happens when it is * unchanged and already first); without one the record is inserted at the * front (renderer `upsertActivityRecord`). * - `update-request`: every request record of the request gets the status * (and the detail, when given), in place. * - `resolve-approval`: the first pending approval of the conversation gets * the status and `detailLine` appended on a new line, in place. * - `settle-request`: every pending/running non-request record of the * request gets the status and `detailLine` appended, in place. * - `remove-request`: deletes every record of the request. * - `remove-call`: deletes the tool records of the request with the callId. */ const activityIdSchema = z.string().min(1).max(256) const activityStatusSchema = activityRecordSchema.shape.status const activityDetailLineSchema = z.string().max(1_000_000) export const activityHistoryChangeSchema = z.discriminatedUnion('type', [ z.object({ type: z.literal('upsert'), record: activityRecordSchema, position: z.enum(['front', 'in-place']) }).strict(), z.object({ type: z.literal('remove'), id: activityIdSchema }).strict(), z.object({ type: z.literal('remove-duplicates') }).strict(), z.object({ type: z.literal('upsert-call'), record: activityRecordSchema.refine( (record) => (record.kind === 'tool' || record.kind === 'subagent') && record.callId !== undefined, 'upsert-call needs a tool or subagent record with a callId' ) }).strict(), z.object({ type: z.literal('update-request'), requestId: activityIdSchema, status: activityStatusSchema, detail: z.string().optional() }).strict(), z.object({ type: z.literal('resolve-approval'), conversationId: activityIdSchema, status: activityStatusSchema, detailLine: activityDetailLineSchema }).strict(), z.object({ type: z.literal('settle-request'), requestId: activityIdSchema, status: activityStatusSchema, detailLine: activityDetailLineSchema }).strict(), z.object({ type: z.literal('remove-request'), requestId: activityIdSchema }).strict(), z.object({ type: z.literal('remove-call'), requestId: activityIdSchema, callId: activityIdSchema }).strict() ]) export const activityHistoryUpdateSchema = z .object({ changes: z.array(activityHistoryChangeSchema).max(20_000), legacyHistoryMayBeIncomplete: z.boolean().optional() }) .strict() /** * Activity history pages (PERF-15), newest first. A record ID shows once: * legacy duplicates after the first occurrence are left out. `before` is the * `nextBefore` of the previous page. */ export const activityHistoryFilterSchema = z.enum(['all', 'active', 'failed']) export const activityHistoryPageRequestSchema = z .object({ before: z.number().int().optional(), limit: z.number().int().min(1).max(500), filter: activityHistoryFilterSchema.optional(), projectId: z.string().min(1).max(256).optional(), conversationId: z.string().min(1).max(256).optional() }) .strict() export const activityHistorySummaryRequestSchema = z .object({ /** Conversations whose title and status are needed (the loaded ones). */ conversationIds: z.array(z.string().min(1).max(256)).max(5_000) }) .strict() export const activityHistoryReconcileRequestSchema = z .object({ activeRequestIds: z.array(z.string().min(1).max(256)).max(10_000), /** The localized line appended to records marked interrupted. */ interruptedDetail: z.string().max(1_000) }) .strict() export type ActivityHistoryFilter = z.infer export type ActivityHistoryPageRequest = z.infer export type ActivityHistoryPage = { records: ActivityRecord[] /** Set when older records may follow. */ nextBefore?: number } export type ActivityHistorySummaryRequest = z.infer export type ActivityHistorySummary = { /** Record counts per filter, over the whole history. */ counts: Record /** * Per requested conversation: the title of its newest request record and * the conversation status (latest request's result, else latest result, * else latest record), over the whole history. */ conversations: Record legacyHistoryMayBeIncomplete: boolean } export type ActivityHistoryReconcileRequest = z.infer export type ActivityRecord = z.infer export type ActivityHistorySnapshot = z.infer< typeof activityHistorySnapshotSchema > export type ActivityHistoryChange = z.infer export type ActivityHistoryUpdate = z.infer /** The stored record of each upsert-call change, in order. */ export type ActivityHistoryUpdateResult = { calls: ActivityRecord[] } export const projectExecutionSpaceSchema = z.discriminatedUnion( 'kind', [ z .object({ kind: z.literal('local'), rootPath: projectExecutionSpacePathSchema }) .strict(), z .object({ kind: z.literal('ssh'), hostId: sshHostIdSchema, remoteRootPath: projectExecutionSpacePathSchema.refine( (value) => value.trim().length > 0, '远程项目目录不能为空' ) }) .strict() ] ) export const persistedProjectExecutionSpaceSchema = projectExecutionSpaceSchema export type ProjectExecutionSpace = z.output< typeof persistedProjectExecutionSpaceSchema > export const projectUpdateSchema = z .object({ name: z.string().trim().min(1).max(120), description: z.string().trim().max(2_000), rootPath: z.string().trim().max(4_096), /** Project layer; absent means follow the global settings. */ runtimeSelection: runtimeSelectionLayerSchema.optional() }) .strict() export const projectCreateSchema = projectUpdateSchema.extend({ rootPath: z.string().trim().min(1).max(4_096) }) export type ProjectCreateInput = z.infer export const conversationAttachmentSchema = z .object({ id: assistantIdSchema, name: z.string().trim().min(1).max(500), size: z.number().int().nonnegative().max(12 * 1024 * 1024), preview: z.string().max(500), kind: z.enum(['text', 'image']), resourceId: z.string().uuid().optional(), attachmentId: z.string().uuid().optional(), resultId: z.string().uuid().optional(), completeness: z.enum(['complete', 'partial', 'images-only']).optional(), parsingState: z.enum(['parsing', 'failed', 'interrupted']).optional(), parsingError: z.string().max(1000).optional(), originalName: z.string().max(500).optional(), originalSize: z.number().int().nonnegative().max(20 * 1024 * 1024).optional(), originalMime: z.enum(['image/png', 'image/jpeg', 'image/webp']).optional(), imageWidth: z.number().int().positive().optional(), imageHeight: z.number().int().positive().optional(), sendMode: z.enum(['image', 'text']).optional(), provenance: z.object({ resultId: z.string().uuid(), imageId: z.string().uuid(), documentName: z.string().max(500), pageNumber: z.number().int() }).optional(), thumbnailUrl: z .string() .max(2_000_000) .refine( (value) => value.startsWith('data:image/png;base64,') || value.startsWith('data:image/jpeg;base64,'), '会话附件缩略图格式无效' ) .optional(), contentUrl: z .string() .max(400_000) .refine( (value) => value.startsWith('data:image/png;base64,') || value.startsWith('data:image/jpeg;base64,'), '会话附件图片格式无效' ) .optional() }) .strict() export type ConversationAttachment = z.infer< typeof conversationAttachmentSchema > export const conversationQueueItemSchema = z .object({ hasAttachments: z.boolean().optional(), error: z.string().max(1000).optional(), id: assistantIdSchema, conversationId: assistantIdSchema, source: z.enum(['user', 'schedule']), label: z.string().trim().min(1).max(200), createdAt: z.string().datetime({ offset: true }), scheduleRunId: assistantIdSchema.optional(), scheduleId: assistantIdSchema.optional(), taskId: assistantIdSchema.optional() }) .strict() export type ConversationQueueItem = z.infer< typeof conversationQueueItemSchema > export const conversationToolActivitySchema = z .object({ callId: z.string().optional(), name: z.string(), state: z.enum([ 'pending', 'running', 'completed', 'failed', 'recoverable', 'cancelled', 'interrupted' ]), summary: z.string(), input: z.string().optional(), output: z.string().optional(), error: z.string().optional() }) .strict() export const conversationMessageBlockSchema = z.discriminatedUnion('type', [ z .object({ id: assistantIdSchema, type: z.literal('text'), content: z.string().min(1) }) .strict(), z .object({ id: assistantIdSchema, type: z.literal('reasoning'), content: z.string().min(1) }) .strict(), z .object({ id: assistantIdSchema, type: z.literal('tool'), tool: conversationToolActivitySchema }) .strict(), z .object({ id: assistantIdSchema, type: z.literal('subagent'), childTaskId: assistantIdSchema }) .strict(), z .object({ id: assistantIdSchema, type: z.literal('question'), questionId: z.string().min(1) }) .strict() ]) export const conversationMessageBlocksSchema = z.array( conversationMessageBlockSchema ) export type ConversationToolActivity = z.infer< typeof conversationToolActivitySchema > export type ConversationMessageBlock = z.infer< typeof conversationMessageBlockSchema > export const subagentActorSchema = z .object({ kind: z.literal('direct-model'), label: z.literal('编程 Subagent') }) .strict() export type SubagentActor = z.infer const conversationSubagentActivityBaseSchema = z.object({ childTaskId: assistantIdSchema, routingMode: z.enum(['manual', 'smart', 'native']), runtimeCallId: z.string().trim().min(1).max(256).optional(), state: z.enum([ 'queued', 'running', 'completed', 'failed', 'cancelled' ]), reason: z.string().trim().min(1).max(240).optional(), progress: conversationMessageBlocksSchema.optional(), output: z.string().optional(), error: z.string().trim().min(1).max(1_000).optional() }) export const legacyConversationSubagentActivitySchema = conversationSubagentActivityBaseSchema .extend({ expertId: assistantIdSchema, expertName: z.string().trim().min(1).max(80) }) .strict() export const actorConversationSubagentActivitySchema = conversationSubagentActivityBaseSchema .extend({ actor: subagentActorSchema }) .strict() export const conversationSubagentActivitySchema = z.union([ legacyConversationSubagentActivitySchema, actorConversationSubagentActivitySchema ]) export type ConversationSubagentActivity = z.infer< typeof conversationSubagentActivitySchema > export const conversationContextCompressionMarkerSchema = z .object({ state: z.enum(['compressing', 'completed', 'failed']), scope: z.enum(['conversation', 'agent-run']).optional(), estimatedBeforeTokens: z.number().int().nonnegative(), estimatedAfterTokens: z.number().int().nonnegative().optional(), compressionCount: z.number().int().positive().optional() }) .strict() export type ConversationContextCompressionMarker = z.infer< typeof conversationContextCompressionMarkerSchema > export const conversationAnsweredQuestionSchema = z .object({ questionId: z.string().min(1), skipped: z.boolean().optional(), questions: z .array( z .object({ header: z.string(), question: z.string(), options: z .array( z .object({ label: z.string(), description: z.string() }) .strict() ), multiple: z.boolean(), custom: z.boolean(), answer: z.array(z.string()).optional() }) .strict() ) }) .strict() export type ConversationAnsweredQuestion = z.infer< typeof conversationAnsweredQuestionSchema > export const conversationMessageSchema = z .object({ id: assistantIdSchema, queueItemId: assistantIdSchema.optional(), role: z.enum(['user', 'assistant']), content: z.string(), reasoning: z.string().optional(), runtimeChecklist: runtimeChecklistSchema.optional(), blocks: conversationMessageBlocksSchema.optional(), displayCaptureTruncated: z.boolean().optional(), createdAt: z.number().int().nonnegative(), state: z.enum(['streaming', 'complete', 'error']), terminalStatus: z.enum(['failed', 'cancelled']).optional(), status: z.string().max(4_000).optional(), contextCompression: conversationContextCompressionMarkerSchema.optional(), contextCompressions: z .array(conversationContextCompressionMarkerSchema) .max(2) .optional(), tools: z.array(conversationToolActivitySchema).optional(), subagents: z.array(conversationSubagentActivitySchema).optional(), sources: z.array(z.string().max(8_192)).max(100).optional(), sourceReferences: z .array( z .object({ libraryId: assistantIdSchema, libraryName: z.string().max(200), documentId: assistantIdSchema.optional(), external: externalKnowledgeLocatorSchema.optional(), warnings: z.array(z.string().max(500)).max(20).optional(), chunkId: assistantIdSchema.optional(), documentName: z.string().max(500), sourceName: z.string().max(500), sourceLocation: z.string().max(4_096).optional(), locator: z.string().max(1_000).optional(), snippet: z.string().max(16_000), rank: z.number().finite(), score: z.number().finite().optional(), lexicalRank: z.number().int().positive().optional(), vectorRank: z.number().int().positive().optional(), graphRank: z.number().int().positive().optional(), similarity: z.number().min(-1).max(1).optional(), retrievalChannels: z .array(z.enum(['fts', 'cjk', 'vector', 'graph'])) .max(4) .optional(), evidenceIds: z .array(assistantIdSchema) .max(100) .optional() }) .strict() ) .max(20) .optional(), knowledgeRetrieval: z .object({ mode: z.literal('always'), state: z.enum([ 'searching', 'succeeded', 'zero', 'degraded', 'failed', 'cancelled' ]), libraryCount: z.number().int().min(1).max(20), resultCount: z.number().int().nonnegative().max(20), durationMs: z.number().int().nonnegative().optional(), usedChannels: z .array(z.enum(['fts', 'cjk', 'vector', 'graph'])) .max(4), warnings: z.array(z.string().max(500)).max(20) }) .strict() .optional(), artifactIds: z.array(assistantIdSchema).max(8).optional(), imageOperations: z.array(imageOperationSchema).optional(), imageSourceArtifactIds: z.array(assistantIdSchema).max(8).optional(), imageContextNotice: z .enum(['reference-unavailable', 'editing-unavailable']) .optional(), task: z .object({ id: assistantIdSchema, title: z.string().trim().min(1).max(120) }) .strict() .optional(), attachments: z .array(conversationAttachmentSchema) .max(8) .optional(), answeredQuestions: z.array(conversationAnsweredQuestionSchema).optional() }) .strict() export type ConversationMessage = z.infer< typeof conversationMessageSchema > export const conversationHistoryMessageSchema = conversationMessageSchema .pick({ role: true, content: true }) .strict() export const conversationContextMetricsSchema = z .object({ runtimeSelectionKey: z.string().trim().min(1).max(1_000), contextTokens: z.number().int().nonnegative().max(50_000_000), source: z.enum(['provider', 'estimated']), basis: z.enum(['model-call', 'conversation']).optional(), // Accepted only to migrate snapshots saved before display settings // were derived from the current Runtime configuration. effectiveTriggerTokens: z .number() .int() .nonnegative() .max(10_000_000) .optional(), contextWindowTokens: z .number() .int() .nonnegative() .max(10_000_000) .optional(), compressionEnabled: z.boolean().optional(), }) .strict() .transform((metrics) => ({ runtimeSelectionKey: metrics.runtimeSelectionKey, contextTokens: metrics.contextTokens, source: metrics.source, basis: metrics.basis })) export type ConversationContextMetrics = z.infer< typeof conversationContextMetricsSchema > export const conversationContextCompressionStateSchema = z .object({ coveredHistoryDigest: z.string().regex(/^[0-9a-f]{64}$/u), coveredMessageCount: z.number().int().nonnegative(), coveredFromMessageId: assistantIdSchema.optional(), coveredThroughMessageId: assistantIdSchema.optional(), summary: z.string().trim().min(1).max(100_000) }) .strict() export type ConversationContextCompressionState = z.infer< typeof conversationContextCompressionStateSchema > export const conversationBranchInputSchema = z .object({ sourceConversationId: assistantIdSchema, title: z.string().trim().min(1).max(200) }) .strict() export type ConversationBranchInput = z.infer< typeof conversationBranchInputSchema > export const conversationBranchSchema = z .object({ sourceConversationId: assistantIdSchema, sourceTitle: z.string().trim().min(1).max(200) }) .strict() export const liveAgentQuestionSchema = z.object({ requestId: z.string().min(1), type: z.literal('question'), questionId: z.string().min(1), childTaskId: z.string().optional(), questions: z.array(z.object({ header: z.string(), question: z.string(), options: z.array(z.object({ label: z.string(), description: z.string() })), multiple: z.boolean(), custom: z.boolean() })) }) export type LiveAgentQuestion = z.infer export const conversationSnapshotSchema = z .object({ id: assistantIdSchema, projectId: assistantIdSchema.optional(), /** Conversation layer; absent means follow the project. */ runtimeSelection: runtimeSelectionLayerSchema.optional(), knowledgeLibraryIds: z.array(assistantIdSchema).max(20).optional(), knowledgeRetrievalMode: z.enum(['auto', 'always']).optional(), storyGraphEnabled: z.boolean().optional(), contextMetrics: conversationContextMetricsSchema.optional(), contextCompressionState: conversationContextCompressionStateSchema.optional(), remote: z .object({ channel: projectChannelSchema, accountDisplay: z.string().trim().min(1).max(200), conversationType: z.enum(['direct', 'group']) }) .strict() .optional(), branch: conversationBranchSchema.optional(), title: z.string().trim().min(1).max(200), // Absent in older snapshots means unpinned; written only via setPinned. pinned: z.boolean().optional(), updatedAt: z.number().int().nonnegative(), // Main-only live projection. Never part of a saved conversation header. activeRequest: z.object({ requestId: assistantIdSchema, messageId: assistantIdSchema, questions: z.array(liveAgentQuestionSchema) }).optional(), messages: z.array(conversationMessageSchema) }) .strict() export type ConversationSnapshot = z.infer< typeof conversationSnapshotSchema > // Read-only list projection. An absent summary means messages are complete. // Never serialize messageSummary as a persisted conversation/header. export type ConversationListSnapshot = ConversationSnapshot & { messageSummary?: { count: number firstRole?: ConversationMessage['role'] latestMessageAt?: number } } export const conversationListRequestSchema = z.object({ detailIds: z.array(assistantIdSchema).default([]) }).strict() export const conversationSearchRequestSchema = z.object({ query: z.string(), conversationIds: z.array(assistantIdSchema).default([]) }).strict() export const conversationSetPinnedSchema = z.object({ conversationId: assistantIdSchema, pinned: z.boolean() }).strict() export const conversationSetStoryGraphSchema = z.object({ conversationId: assistantIdSchema, enabled: z.boolean() }).strict() export type ConversationSetPinnedInput = z.infer< typeof conversationSetPinnedSchema > export const conversationSnapshotsSchema = z .array(conversationSnapshotSchema) .max(100) export const localConversationHeaderSchema = conversationSnapshotSchema .omit({ messages: true, pinned: true, remote: true, activeRequest: true }) export type LocalConversationHeader = z.infer< typeof localConversationHeaderSchema > export const localConversationSaveSchema = z .object({ header: localConversationHeaderSchema, messages: z.array(conversationMessageSchema) }) .strict() export type LocalConversationSaveInput = z.infer< typeof localConversationSaveSchema > export const localConversationSaveBatchSchema = z .array(localConversationSaveSchema) .max(100) export type LocalConversationSaveBatch = z.infer< typeof localConversationSaveBatchSchema > export type AssistantProject = ProjectCreateInput & { id: string kind: ProjectKind executionSpace: ProjectExecutionSpace channel?: ProjectChannel readonly builtInDefault?: boolean status: 'active' | 'archived' createdAt: string updatedAt: string } export const builtInDefaultProjectSeedName = '默认项目' export const builtInDefaultProjectSeedDescription = 'GoodBuddy 默认工作区' export function isUntouchedBuiltInDefaultProject( project: AssistantProject ): boolean { return ( project.builtInDefault === true && project.name === builtInDefaultProjectSeedName && project.description === builtInDefaultProjectSeedDescription ) } export type WorkspaceChanges = { rootPath: string available: boolean status: string patch: string stagedPatch?: string files: WorkspaceChangedFile[] truncated: boolean error?: string } export type WorkspaceChangedFile = { path: string status: string previousPath?: string } export type WorkspaceDirectoryEntry = { name: string path: string type: 'file' | 'directory' modifiedAt?: string createdAt?: string } export type WorkspaceDirectoryListing = { path: string entries: WorkspaceDirectoryEntry[] truncated: boolean } export type WorkspaceFilePreview = { path: string name: string content: string mimeType: 'text/markdown' | 'text/plain' | 'application/json' size: number offsetBytes: number nextOffsetBytes: number truncated: boolean } export type AssistantTaskStatus = | 'queued' | 'running' | 'waiting_approval' | 'paused' | 'idle' | 'completed' | 'failed' | 'cancelled' | 'interrupted' export type AssistantTask = { id: string projectId?: string conversationId?: string scheduleId?: string parentTaskId?: string expertId?: string routingMode?: 'manual' | 'smart' title: string instructions: string origin: 'user' | 'assistant' | 'schedule' | 'delegation' | 'subagent' status: AssistantTaskStatus progress?: number createdAt: string startedAt?: string completedAt?: string error?: string } export type ModelUsageCallInput = { requestId: string callId: string runtime: string provider: string model: string input: number output: number cacheRead: number cacheWrite: number } export const tokenUsageSystemSources = [ 'heartbeat', 'supervision', 'magic-notes', 'knowledge', 'delegation' ] as const export type TokenUsageSystemSource = (typeof tokenUsageSystemSources)[number] export type SystemModelUsageInput = Omit< ModelUsageCallInput, 'requestId' | 'callId' > & { source: Extract bucket: 'graph-extraction' | 'embedding' } export type TokenUsageTotals = { callCount: number input: number output: number cacheRead: number cacheWrite: number cacheInput?: number totalTokens: number } export type TokenUsageRecord = { requestId: string /** Set when the usage came from background system work instead of a conversation. */ systemSource?: TokenUsageSystemSource projectId?: string projectName?: string conversationId?: string conversationTitle?: string runtime: string provider: string model: string callCount: number input: number output: number cacheRead: number cacheWrite: number cacheInput?: number totalTokens: number } export type TokenUsageSummary = { /** Totals across all recorded usage. */ totals: TokenUsageTotals /** Totals for conversation work only; absent in legacy summaries. */ conversationTotals?: TokenUsageTotals /** Totals for background system work only; absent in legacy summaries. */ systemTotals?: TokenUsageTotals records: TokenUsageRecord[] } export type AssistantArtifact = { id: string projectId?: string taskId?: string kind: 'markdown' | 'text' | 'json' | 'image' | 'file' title: string mimeType: string content?: string byteSize: number createdAt: string updatedAt: string } export const memoryCreateSchema = z .object({ scope: z.enum(['global', 'project', 'conversation']), scopeId: z.string().max(256).optional(), type: z.enum(['preference', 'fact', 'summary', 'procedure']), content: z.string().trim().min(1).max(8_000) }) .strict() export type MemoryCreateInput = z.infer export type AssistantMemory = MemoryCreateInput & { id: string confidence: number salience: number status: 'proposed' | 'confirmed' | 'rejected' createdAt: string updatedAt: string } export const scheduleCreateSchema = z .object({ projectId: z.string().uuid().optional(), conversationId: z.string().uuid().optional(), title: z.string().trim().min(1).max(120), prompt: z.string().trim().min(1).max(100_000), recurrence: z.enum(['once', 'daily', 'weekly']), nextRunAt: z.string().datetime({ offset: true }), runImmediately: z.boolean().optional() }) .strict() .refine((input) => !input.runImmediately || input.recurrence === 'once', { message: '立即执行只支持单次任务', path: ['recurrence'] }) export type ScheduleCreateInput = z.input export type AssistantSchedule = Omit, 'runImmediately'> & { id: string taskId: string conversationId: string /** A concrete selection captured by legacy schedule templates. */ runtimeSelection?: AgentRuntimeSelection enabled: boolean lastRunAt?: string createdAt: string updatedAt: string } export const heartbeatRecurrenceSchema = z.discriminatedUnion('type', [ z .object({ type: z.literal('daily'), localTime: z .string() .regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/) }) .strict(), z .object({ type: z.literal('weekly'), localTime: z .string() .regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/), weekday: z.number().int().min(0).max(6) }) .strict() ]) export const heartbeatScopeSchema = z.discriminatedUnion('kind', [ z .object({ kind: z.literal('global') }) .strict(), z .object({ kind: z.literal('projects'), projectIds: z .array(assistantIdSchema) .min(1) .max(100) .refine( (projectIds) => new Set(projectIds).size === projectIds.length, 'Project IDs must be unique' ) }) .strict() ]) // How the supervisor intervenes after an automatic review. Real-time // conversation intervention is not offered until it is implemented. export const heartbeatInterventionSchema = z.enum(['suggest', 'memory']) export type HeartbeatIntervention = z.infer export const defaultHeartbeatIntervention: HeartbeatIntervention = 'suggest' export const heartbeatCreateSchema = z .object({ scope: heartbeatScopeSchema, name: z.string().trim().min(1).max(120), timezone: z.string().trim().min(1).max(100), recurrence: heartbeatRecurrenceSchema, enabled: z.boolean(), lookbackHours: z.number().int().min(1).max(24 * 30), retentionDays: z.number().int().min(1).max(365), intervention: heartbeatInterventionSchema.optional() }) .strict() export const heartbeatUpdateSchema = heartbeatCreateSchema export const heartbeatListSchema = z .object({ projectId: assistantIdSchema.optional() }) .strict() export const heartbeatHistorySchema = z .object({ configId: assistantIdSchema.optional(), limit: z.number().int().min(1).max(200).default(50) }) .strict() export const heartbeatIdSchema = z .object({ id: assistantIdSchema }) .strict() export const heartbeatUpdateRequestSchema = z .object({ id: assistantIdSchema, config: heartbeatUpdateSchema }) .strict() export const heartbeatPauseSchema = z .object({ id: assistantIdSchema, paused: z.boolean() }) .strict() export const heartbeatRunNowSchema = z .object({ id: assistantIdSchema, idempotencyKey: z.string().trim().min(1).max(200) }) .strict() export const heartbeatSummaryOutputSchema = z .object({ summary: z.string().trim().min(1).max(12_000), highlights: z.array(z.string().trim().min(1).max(1_000)).max(20), proposedMemories: z .array( z.discriminatedUnion('scope', [ z .object({ scope: z.literal('global'), type: z.enum([ 'preference', 'fact', 'summary', 'procedure' ]), content: z.string().trim().min(1).max(8_000), confidence: z.number().min(0).max(1), salience: z.number().min(0).max(1) }) .strict(), z .object({ scope: z.literal('project'), projectId: assistantIdSchema, type: z.enum([ 'preference', 'fact', 'summary', 'procedure' ]), content: z.string().trim().min(1).max(8_000), confidence: z.number().min(0).max(1), salience: z.number().min(0).max(1) }) .strict() ]) ) .max(10), followUpTasks: z .array( z .object({ title: z.string().trim().min(1).max(200), instructions: z.string().trim().min(1).max(8_000), projectId: assistantIdSchema.optional() }) .strict() ) .max(10) }) .strict() export type HeartbeatRecurrence = z.infer< typeof heartbeatRecurrenceSchema > export type HeartbeatScope = z.infer export type HeartbeatCreateInput = z.infer< typeof heartbeatCreateSchema > export type HeartbeatUpdateInput = z.infer< typeof heartbeatUpdateSchema > export type HeartbeatSummaryOutput = z.infer< typeof heartbeatSummaryOutputSchema > export type HeartbeatRunStatus = | 'claimed' | 'no_change' | 'completed' | 'failed' | 'skipped' export type AssistantHeartbeatConfig = HeartbeatCreateInput & { id: string nextRunAt: string lastRunAt?: string lastStatus?: HeartbeatRunStatus createdAt: string updatedAt: string } export type AssistantHeartbeatRun = { id: string configId: string trigger: 'scheduled' | 'manual' scheduledFor: string status: HeartbeatRunStatus attemptCount: number nextAttemptAt?: string startedAt?: string completedAt?: string error?: string entryId?: string createdAt: string updatedAt: string } export type AssistantHeartbeatEntry = { id: string configId: string runId: string scheduledFor: string summary: string highlights: string[] artifactId?: string proposedMemoryIds: string[] followUpTaskIds: string[] createdAt: string } const routingKeywordSchema = z .string() .transform((value) => value.normalize('NFKC').trim().replace(/\s+/gu, ' ').toLowerCase() ) .pipe(z.string().min(2).max(48)) export const expertCreateSchema = z .object({ name: z.string().trim().min(1).max(80), description: z.string().trim().max(500), systemInstructions: z.string().trim().min(1).max(20_000), modelProfileId: assistantIdSchema.optional(), routingKeywords: z .array(routingKeywordSchema) .max(32) .default([]) .transform((keywords) => [...new Set(keywords)]) }) .strict() export type ExpertCreateInput = z.input export type ExpertUpdateInput = ExpertCreateInput export type AssistantExpert = z.output & { id: string enabled: boolean createdAt: string updatedAt: string } import { imageOperationSchema } from './image-generation-contracts'