@ouraihub/aether-worker 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,234 @@
1
+ import type { ImagePart, ModelRef, RunRequest } from '@ouraihub/aether-contracts'
2
+ import type { HistoryTurn } from '@ouraihub/aether-core/types'
3
+ import { badRequest } from '@ouraihub/aether-core/error'
4
+ import { canonicalJson, charCount } from '@ouraihub/aether-core/guard'
5
+ import {
6
+ checkId,
7
+ checkText,
8
+ conflict,
9
+ historyPath,
10
+ idle,
11
+ imageParts,
12
+ notFound,
13
+ target,
14
+ validateImages,
15
+ visibleText,
16
+ } from './model.ts'
17
+ import type { ConversationDocument, StoredImage, StoredTurn } from './model.ts'
18
+
19
+ export type Action =
20
+ | { kind: 'reply'; parentId: string | null }
21
+ | { kind: 'retry'; sourceId: string }
22
+ | { kind: 'translation'; sourceId: string; targetLanguage: string }
23
+ export type SelectionGuard =
24
+ { kind: 'explicit' } | { kind: 'selected'; turnId: string } | { kind: 'latest'; turnId: string | null }
25
+ export interface ReservationInput {
26
+ input: string
27
+ images: ImagePart[]
28
+ historyPath: StoredTurn[]
29
+ history: HistoryTurn[]
30
+ sourceModel: ModelRef | null
31
+ }
32
+ export interface PreparedAction extends ReservationInput {
33
+ request: RunRequest
34
+ effectiveRequest: RunRequest
35
+ action: Action
36
+ selectionGuard: SelectionGuard
37
+ expected: string
38
+ requiresSourceModel: boolean
39
+ }
40
+ function latest(document: ConversationDocument): string | null {
41
+ return document.turns.findLast((t) => t.status === 'completed')?.id ?? null
42
+ }
43
+ export function authoritative(
44
+ request: RunRequest,
45
+ action: Action,
46
+ guard: SelectionGuard,
47
+ document: ConversationDocument | null,
48
+ ): ReservationInput {
49
+ if ((request.conversationId ?? null) !== (document?.summary.id ?? null)) throw notFound('conversation')
50
+ if (document) {
51
+ idle(document)
52
+ if (guard.kind === 'selected' && document.summary.selectedTurnId !== guard.turnId)
53
+ throw conflict('preparation_changed')
54
+ if (guard.kind === 'latest' && (document.summary.selectedTurnId !== null || latest(document) !== guard.turnId))
55
+ throw conflict('preparation_changed')
56
+ }
57
+ let input = request.input,
58
+ images = request.images ?? [],
59
+ head: string | null,
60
+ sourceModel: ModelRef | null = null
61
+ if (action.kind === 'reply') {
62
+ head = action.parentId
63
+ if (head !== null && (!document || target(document, head).status !== 'completed'))
64
+ throw conflict('turn_not_completed')
65
+ } else {
66
+ if (!document) throw notFound('conversation')
67
+ const source = target(document, action.sourceId)
68
+ if (source.status === 'running') throw conflict('turn_running')
69
+ sourceModel =
70
+ source.endpointId !== null && source.modelId !== null
71
+ ? { endpointId: source.endpointId, modelId: source.modelId }
72
+ : null
73
+ if (action.kind === 'retry') {
74
+ input = source.input
75
+ images = imageParts(source.images)
76
+ head = source.parentId
77
+ } else {
78
+ const text = visibleText(source)
79
+ if (!text.trim()) throw badRequest('translateTurnId', 'empty_reply')
80
+ input = `Translate the following reply into ${action.targetLanguage}. Return only the translation, preserving Markdown formatting.\n\n${text}`
81
+ images = []
82
+ head = source.id
83
+ }
84
+ }
85
+ checkText('input', input, 100_000)
86
+ const path = document ? historyPath(document, head) : []
87
+ return {
88
+ input,
89
+ images,
90
+ historyPath: path,
91
+ history: path.map((turn) => ({
92
+ status: turn.status,
93
+ input: turn.input,
94
+ messages: turn.messages,
95
+ images: imageParts(turn.images),
96
+ })),
97
+ sourceModel,
98
+ }
99
+ }
100
+ async function digest(value: string): Promise<string> {
101
+ const bytes = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))
102
+ return Array.from(new Uint8Array(bytes), (byte) => byte.toString(16).padStart(2, '0')).join('')
103
+ }
104
+ async function imageBasis(turnId: string, kind: string, images: readonly StoredImage[]): Promise<unknown[]> {
105
+ return Promise.all(
106
+ images.map(async (image, index) => ({
107
+ turnId,
108
+ kind,
109
+ index,
110
+ mime: image.mime,
111
+ data: image.data === null ? null : await digest(image.data),
112
+ })),
113
+ )
114
+ }
115
+ export async function fingerprint(
116
+ request: RunRequest,
117
+ action: Action,
118
+ guard: SelectionGuard,
119
+ document: ConversationDocument | null,
120
+ input: ReservationInput,
121
+ ): Promise<string> {
122
+ const source = action.kind !== 'reply' && document ? target(document, action.sourceId) : null
123
+ const sourceBasis = source
124
+ ? {
125
+ id: source.id,
126
+ status: source.status,
127
+ parentId: source.parentId,
128
+ questionId: source.questionId,
129
+ supersedesId: source.supersedesId,
130
+ kind: source.kind,
131
+ modelId: source.modelId,
132
+ endpointId: source.endpointId,
133
+ input: source.input,
134
+ visibleText: action.kind === 'translation' ? visibleText(source) : null,
135
+ images: await imageBasis(source.id, 'input', source.images),
136
+ generated: await imageBasis(source.id, 'generated', source.generated),
137
+ }
138
+ : null
139
+ const path = await Promise.all(
140
+ input.historyPath.map(async (turn) => ({
141
+ id: turn.id,
142
+ parentId: turn.parentId,
143
+ status: turn.status,
144
+ input: turn.input,
145
+ messages: turn.messages,
146
+ endpointId: turn.endpointId,
147
+ modelId: turn.modelId,
148
+ reasoningEffort: turn.reasoningEffort,
149
+ images: await imageBasis(turn.id, 'input', turn.images),
150
+ generated: await imageBasis(turn.id, 'generated', turn.generated),
151
+ })),
152
+ )
153
+ const currentImages = await Promise.all(
154
+ input.images.map(async (image, index) => ({ index, mime: image.mime, data: await digest(image.data) })),
155
+ )
156
+ return digest(
157
+ canonicalJson({
158
+ version: 1,
159
+ conversationId: request.conversationId ?? null,
160
+ scope: document?.summary.scope ?? request.scope ?? null,
161
+ action,
162
+ guard,
163
+ input: input.input,
164
+ images: currentImages,
165
+ source: sourceBasis,
166
+ path,
167
+ }),
168
+ )
169
+ }
170
+ export async function resolveAction(
171
+ request: RunRequest,
172
+ document: ConversationDocument | null,
173
+ ): Promise<PreparedAction> {
174
+ for (const [field, id] of [
175
+ ['conversationId', request.conversationId],
176
+ ['replyToId', request.replyToId],
177
+ ['retryTurnId', request.retryTurnId],
178
+ ['translateTurnId', request.translateTurnId],
179
+ ])
180
+ if (id !== undefined && id !== null) {
181
+ if (typeof id !== 'string') throw badRequest(String(field), 'invalid')
182
+ checkId(String(field), id)
183
+ }
184
+ if (request.scope !== undefined && request.scope !== null) checkText('scope', request.scope, 200)
185
+ checkText('input', request.input, 100_000)
186
+ validateImages(request.images ?? [])
187
+ const sourced = Boolean(request.retryTurnId || request.translateTurnId || request.regenerate)
188
+ if (Number(Boolean(request.retryTurnId)) + Number(Boolean(request.translateTurnId)) + Number(request.regenerate) > 1)
189
+ throw badRequest('action', 'multiple')
190
+ if (sourced && request.replyToId) throw badRequest('replyToId', 'action_conflict')
191
+ if (sourced && (request.images?.length ?? 0) > 0) throw badRequest('images', 'action_conflict')
192
+ if (Boolean(request.translateTurnId) !== (request.targetLanguage !== null && request.targetLanguage !== undefined))
193
+ throw badRequest('targetLanguage', 'translation_required')
194
+ if (!request.conversationId && (sourced || request.replyToId)) throw badRequest('conversationId', 'required')
195
+ if ((request.retryTurnId || request.translateTurnId) && request.webSearch)
196
+ throw badRequest('webSearch', 'action_conflict')
197
+ let guard: SelectionGuard = { kind: 'explicit' },
198
+ head: string | null = request.replyToId ?? null
199
+ if (document && !request.replyToId && !request.retryTurnId && !request.translateTurnId) {
200
+ if (document.summary.selectedTurnId !== null) {
201
+ head = document.summary.selectedTurnId
202
+ guard = { kind: 'selected', turnId: head }
203
+ } else {
204
+ head = latest(document)
205
+ guard = { kind: 'latest', turnId: head }
206
+ }
207
+ }
208
+ let action: Action
209
+ if (request.translateTurnId) {
210
+ const language = request.targetLanguage?.trim() ?? ''
211
+ if (!language || charCount(language) > 80) throw badRequest('targetLanguage', 'invalid')
212
+ action = { kind: 'translation', sourceId: request.translateTurnId, targetLanguage: language }
213
+ } else if (request.retryTurnId) action = { kind: 'retry', sourceId: request.retryTurnId }
214
+ else if (request.regenerate) {
215
+ if (head === null) throw notFound('turn')
216
+ action = { kind: 'retry', sourceId: head }
217
+ } else action = { kind: 'reply', parentId: head }
218
+ const input = authoritative(request, action, guard, document)
219
+ const effectiveRequest: RunRequest = {
220
+ ...request,
221
+ input: input.input,
222
+ images: input.images,
223
+ ...(request.model ? {} : { model: input.sourceModel }),
224
+ }
225
+ return {
226
+ ...input,
227
+ request,
228
+ effectiveRequest,
229
+ action,
230
+ selectionGuard: guard,
231
+ expected: await fingerprint(request, action, guard, document, input),
232
+ requiresSourceModel: action.kind !== 'reply' && !request.model,
233
+ }
234
+ }