Files
goodbuddy/src/shared/supervision-contracts.ts
T
mesalogo 5116613615 fix: keep supervisor controls usable and prevent overlapping reviews
Allow only one active review across manual starts, automatic runs and resume. Add cancellation that holds admission through runtime cleanup, preserves saved batches and prevents cancelled runs from resuming.

Keep review controls usable during execution, show a dismissible progress notice and remove redundant filter labels. Include focused regressions and lifecycle documentation.

Release note: Supervision reviews can now be cancelled from Activity records. Starting another review shows the active review instead of duplicating work, while scope, time and refresh controls remain available.
2026-09-26 15:55:50 +08:00

222 lines
8.9 KiB
TypeScript

import { z } from 'zod'
import { heartbeatScopeSchema } from './assistant-contracts'
export const supervisionTriggerSchema = z.enum(['manual', 'heartbeat'])
export type SupervisionTrigger = z.infer<typeof supervisionTriggerSchema>
export const supervisionTimeRangeSchema = z
.object({
from: z.string().datetime({ offset: true }),
to: z.string().datetime({ offset: true })
})
.strict()
.refine((value) => Date.parse(value.from) <= Date.parse(value.to), {
message: '监督者时间范围无效'
})
export const supervisionRunRequestSchema = z
.object({
trigger: supervisionTriggerSchema,
scope: heartbeatScopeSchema,
timeRange: supervisionTimeRangeSchema
})
.strict()
export type SupervisionRunRequest = z.infer<typeof supervisionRunRequestSchema>
export const supervisionActivityRequestSchema = z.object({
configId: z.string().min(1).max(256).optional(),
limit: z.number().int().min(1).max(100).default(50),
offset: z.number().int().min(0).max(100_000).default(0)
}).strict()
export type SupervisionActivityRequest = z.input<typeof supervisionActivityRequestSchema>
export type SupervisionActivity = {
id: string
kind: 'supervision' | 'heartbeat'
trigger: 'manual' | 'scheduled' | 'heartbeat'
status: 'running' | 'completed' | 'failed' | 'skipped' | 'no_change' | 'paused' | 'cancelled'
reviewProgress?: import('./supervision-review-contracts').SupervisionReviewProgress
scope: SupervisionRunRequest['scope'] | null
startedAt: string
completedAt: string | null
timeRange: SupervisionRunRequest['timeRange'] | null
error: string | null
summary: string | null
resultId: string | null
heartbeatStatus: 'claimed' | 'completed' | 'failed' | 'skipped' | 'no_change' | null
supervisionStatus: 'running' | 'completed' | 'failed' | 'no_change' | 'paused' | 'cancelled' | null
}
export const supervisionEvidenceSchema = z
.object({
id: z.string().min(1).max(256),
sourceType: z.enum(['conversation', 'task', 'note', 'memory', 'knowledge']),
sourceId: z.string().min(1).max(256),
title: z.string().max(500),
content: z.string().max(8_000),
occurredAt: z.string().datetime({ offset: true }),
locator: z.record(z.string(), z.unknown()).optional()
})
.strict()
export type SupervisionEvidence = z.infer<typeof supervisionEvidenceSchema>
const sourceReferenceIdsSchema = z.array(z.string().min(1).max(256))
export const supervisionEventSchema = z
.object({
title: z.string().min(1),
description: z.string(),
occurredAt: z.string().datetime({ offset: true }),
eventType: z.enum(['decision', 'change', 'discussion', 'milestone']),
entityIds: z.array(z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/)),
sourceReferenceIds: sourceReferenceIdsSchema
})
.strict()
export const supervisionEntitySchema = z
.object({
id: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
persistedId: z.string().uuid().optional(),
label: z.string().min(1),
description: z.string(),
sourceReferenceIds: sourceReferenceIdsSchema
})
.strict()
export const supervisionRelationSchema = z
.object({
fromEntityId: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
toEntityId: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
relationType: z.enum(['supports', 'depends-on', 'contrasts', 'related']),
reason: z.string(),
sourceReferenceIds: sourceReferenceIdsSchema
})
.strict()
export const supervisionEntityChangeSchema = z.object({
entityId: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
changeType: z.enum(['proposed', 'added', 'verified', 'revised']),
description: z.string(),
sourceReferenceIds: sourceReferenceIdsSchema
}).strict()
export const supervisionSummaryOutputSchema = z
.object({
summary: z.string().min(1),
changeDigest: z.string(),
openItems: z.array(z.string().min(1)),
events: z.array(supervisionEventSchema),
entities: z.array(supervisionEntitySchema),
entityChanges: z.array(supervisionEntityChangeSchema),
relations: z.array(supervisionRelationSchema)
})
.strict()
export type SupervisionSummaryOutput = z.infer<
typeof supervisionSummaryOutputSchema
>
// Navigation combines saved summaries; it cannot create or replace leaf facts.
export const supervisionNavigationOutputSchema = supervisionSummaryOutputSchema.pick({
summary: true, changeDigest: true, openItems: true
}).extend({
events: z.array(z.never()).max(0).optional(),
entities: z.array(z.never()).max(0).optional(),
entityChanges: z.array(z.never()).max(0).optional(),
relations: z.array(z.never()).max(0).optional()
}).strict()
export const supervisionEntityActionSchema = z
.object({
resultId: z.string().min(1).optional(),
entityId: z.string().regex(/^[a-zA-Z0-9_-]{1,120}$/),
action: z.enum(['confirm', 'revise', 'revoke']),
label: z.string().trim().min(1).optional(),
description: z.string().optional()
})
.strict()
export const supervisionRelationActionSchema = z
.object({
resultId: z.string().min(1).optional(),
relationId: z.string().uuid(),
action: z.enum(['confirm', 'revoke'])
})
.strict()
export const supervisionSourceRequestSchema = z
.object({ sourceId: z.string().min(1).max(256) })
.strict()
export const supervisionContinueContextRequestSchema = z.object({
sourceId: z.string().min(1).max(256),
resultId: z.string().min(1).max(256)
}).strict()
export type SupervisionContinueContextRequest = z.infer<typeof supervisionContinueContextRequestSchema>
export const supervisionContinueRequestSchema = z.object({
conversationId: z.string().min(1).max(128),
prompt: z.string().trim().min(1).max(12_000),
runtimeSelection: z.unknown().optional(),
projectId: z.string().min(1).max(128).optional()
}).strict()
export type SupervisionContinueRequest = z.infer<typeof supervisionContinueRequestSchema>
export const supervisionKnowledgePreviewRequestSchema = z.object({
operation: z.enum(['create-entity', 'update-entity']),
libraryId: z.string().uuid(),
entityId: z.string().uuid().optional(),
label: z.string().trim().min(1).max(120),
type: z.string().trim().min(1).max(120),
description: z.string().trim().max(2_000),
aliases: z.array(z.string().trim().min(1).max(120)).max(50),
sourceId: z.string().min(1).max(256)
}).strict().superRefine((value, context) => {
if (value.operation === 'update-entity' && !value.entityId) {
context.addIssue({ code: 'custom', path: ['entityId'], message: 'update-entity requires entityId' })
}
})
export type SupervisionKnowledgePreviewRequest = z.infer<typeof supervisionKnowledgePreviewRequestSchema>
export const supervisionKnowledgeCommitRequestSchema = z.object({
previewId: z.string().uuid()
}).strict()
export type SupervisionKnowledgeCommitRequest = z.infer<typeof supervisionKnowledgeCommitRequestSchema>
export type SupervisionEntityAction = z.infer<typeof supervisionEntityActionSchema>
export type SupervisionRelationAction = z.infer<typeof supervisionRelationActionSchema>
export const supervisionResultViewSchema = z.object({
runId: z.string().optional(),
coverage: z.record(z.string(), z.unknown()).optional(),
storyLineId: z.string(),
sourceId: z.string().nullable(),
id: z.string(), summary: z.string(), changeDigest: z.string(), createdAt: z.string(),
scope: heartbeatScopeSchema, timeRange: supervisionTimeRangeSchema,
openItems: z.array(z.string())
})
export type SupervisionResultView = z.infer<typeof supervisionResultViewSchema>
export const supervisionTargetSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('conversation'), conversationId: z.string().min(1).max(128) }).strict(),
z.object({ type: z.literal('task'), taskId: z.string().min(1).max(128) }).strict()
])
export type SupervisionTarget = z.infer<typeof supervisionTargetSchema>
export const supervisionOverviewRequestSchema = z.object({ target: supervisionTargetSchema.optional(), resultId: z.string().min(1).max(256).optional() }).strict()
export const supervisionGraphRequestSchema = z.object({
resultId: z.string().min(1).optional(), storyLineId: z.string().min(1).optional()
}).strict()
export type SupervisionGraphRequest = z.infer<typeof supervisionGraphRequestSchema>
export const supervisionGraphViewSchema = z.object({
storyLine: z.object({ id: z.string(), scope_json: z.string() }).nullable(),
events: z.array(z.object({ id: z.string(), title: z.string(), description: z.string(), occurred_at: z.string() })),
entities: z.array(z.object({ id: z.string(), canonical_label: z.string(), description: z.string(), confirmation_state: z.string() })),
relations: z.array(z.object({ id: z.string(), from_entity_id: z.string(), to_entity_id: z.string(), relation_type: z.string(), reason: z.string(), confirmation_state: z.string() })),
sources: z.array(z.object({ id: z.string(), title: z.string(), occurred_at: z.string() })),
eventEntities: z.array(z.object({ event_id: z.string(), entity_id: z.string() })),
eventSources: z.array(z.object({ event_id: z.string(), source_id: z.string() }))
})
export type SupervisionGraphView = z.infer<typeof supervisionGraphViewSchema>