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.
1281 lines
37 KiB
TypeScript
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'
|