Files
goodbuddy/src/shared/assistant-contracts.ts
T
mesalogo 56ebd40bd8 feat: unify execution and add workspace file imports
Remove Ask/Execute modes and general product tool approvals so requests use the selected runtime, enabled capabilities, and execution account permissions. Migrate retired application metadata while preserving user content.

Retire the Results page while retaining conversation images and stored artifacts. Add local and remote workspace file imports, timestamps and sorting, improve image discovery and saving, and refine workspace navigation and supervision responsiveness. Include the accompanying web-search changes, tests, probes, and product documentation.

Remote imports require an updated Agent. Existing artifacts are not automatically copied into project directories. Validation evidence and outstanding failures remain documented in feature progress files; no full test suite or real-model calls were rerun for this commit.

Release note: Run requests without switching work modes, import files into a selected local or remote workspace folder, sort workspace files by date, and keep using conversation images after removal of the Results page.
2026-10-04 20:02:59 +08:00

1281 lines
37 KiB
TypeScript

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<typeof executionStatsInputSchema>
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'
] as const
export const projectChannelSchema = z.enum(projectChannels)
export const projectChannelLabels: Record<ProjectChannel, string> = {
weixin: '微信 ClawBot',
wecom: '企业微信',
dingtalk: '钉钉'
}
export type ProjectKind = z.infer<typeof projectKindSchema>
export type ProjectChannel = z.infer<typeof projectChannelSchema>
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<typeof activityHistoryFilterSchema>
export type ActivityHistoryPageRequest = z.infer<typeof activityHistoryPageRequestSchema>
export type ActivityHistoryPage = {
records: ActivityRecord[]
/** Set when older records may follow. */
nextBefore?: number
}
export type ActivityHistorySummaryRequest = z.infer<typeof activityHistorySummaryRequestSchema>
export type ActivityHistorySummary = {
/** Record counts per filter, over the whole history. */
counts: Record<ActivityHistoryFilter, number>
/**
* 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<string, { title?: string; status: ActivityRecord['status'] }>
legacyHistoryMayBeIncomplete: boolean
}
export type ActivityHistoryReconcileRequest = z.infer<typeof activityHistoryReconcileRequestSchema>
export type ActivityRecord = z.infer<typeof activityRecordSchema>
export type ActivityHistorySnapshot = z.infer<
typeof activityHistorySnapshotSchema
>
export type ActivityHistoryChange = z.infer<typeof activityHistoryChangeSchema>
export type ActivityHistoryUpdate = z.infer<typeof activityHistoryUpdateSchema>
/** 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<typeof projectCreateSchema>
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<typeof subagentActorSchema>
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<typeof liveAgentQuestionSchema>
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<TokenUsageSystemSource, 'knowledge'>
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<typeof memoryCreateSchema>
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<typeof scheduleCreateSchema>
export type AssistantSchedule = Omit<z.output<typeof scheduleCreateSchema>, '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<typeof heartbeatInterventionSchema>
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<typeof heartbeatScopeSchema>
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<typeof expertCreateSchema>
export type ExpertUpdateInput = ExpertCreateInput
export type AssistantExpert = z.output<typeof expertCreateSchema> & {
id: string
enabled: boolean
createdAt: string
updatedAt: string
}
import { imageOperationSchema } from './image-generation-contracts'