Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.
- Remove the Main readWithFallback path and the supervision worker's
separate business write connection; manifests are scanned read-only and
committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
directories and reclaim confirmed stale ones; stop writing duplicate
parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
acceptance fixtures, and maintenance/architecture documentation.
Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
1293 lines
37 KiB
TypeScript
1293 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',
|
|
'telegram'
|
|
] as const
|
|
export const projectChannelSchema = z.enum(projectChannels)
|
|
export const projectChannelLabels: Record<ProjectChannel, string> = {
|
|
weixin: '微信 ClawBot',
|
|
wecom: '企业微信',
|
|
dingtalk: '钉钉',
|
|
telegram: 'Telegram'
|
|
}
|
|
|
|
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 toolOutputReferenceSchema = z.object({
|
|
handle: z.string().min(1),
|
|
nextCursor: z.number().int().nonnegative(),
|
|
totalBytes: z.number().int().nonnegative()
|
|
}).strict()
|
|
|
|
export type ToolOutputReference = z.infer<typeof toolOutputReferenceSchema>
|
|
|
|
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(),
|
|
outputReferences: z.array(toolOutputReferenceSchema).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(),
|
|
outputReference: toolOutputReferenceSchema.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'
|